From 2637538d9a2e7d4867fe01241164a82d2a15fc60 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Wed, 29 Sep 2021 08:43:15 -0700 Subject: [PATCH 1/2] IO-256 QBO Payables --- .../accounting-payables-table.component.jsx | 22 +- .../jobs-available-table.container.jsx | 2 +- .../payable-export-all-button.component.jsx | 85 +-- .../payable-export-button.component.jsx | 90 ++-- server/accounting/qb-receivables-lines.js | 3 +- server/accounting/qbo/qbo-payables.js | 500 +++++------------- server/accounting/qbo/qbo-receivables.js | 21 +- server/accounting/qbxml/qbxml-receivables.js | 4 +- 8 files changed, 266 insertions(+), 461 deletions(-) diff --git a/client/src/components/accounting-payables-table/accounting-payables-table.component.jsx b/client/src/components/accounting-payables-table/accounting-payables-table.component.jsx index 64e335833..8b017991a 100644 --- a/client/src/components/accounting-payables-table/accounting-payables-table.component.jsx +++ b/client/src/components/accounting-payables-table/accounting-payables-table.component.jsx @@ -9,8 +9,25 @@ import PayableExportAll from "../payable-export-all-button/payable-export-all-bu import { DateFormatter } from "../../utils/DateFormatter"; import queryString from "query-string"; import { logImEXEvent } from "../../firebase/firebase.utils"; +import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; -export default function AccountingPayablesTableComponent({ loading, bills }) { +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); + +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(AccountingPayablesTableComponent); + +export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) { const { t } = useTranslation(); const [selectedBills, setSelectedBills] = useState([]); const [transInProgress, setTransInProgress] = useState(false); @@ -166,6 +183,9 @@ export default function AccountingPayablesTableComponent({ loading, bills }) { loadingCallback={setTransInProgress} completedCallback={setSelectedBills} /> + {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( + + )} { logImEXEvent("accounting_payables_export_all"); + let PartnerResponse; setLoading(true); if (!!loadingCallback) loadingCallback(true); - - let QbXmlResponse; - try { - QbXmlResponse = await axios.post( - "/accounting/qbxml/payables", - { bills: billids }, - { - headers: { - Authorization: `Bearer ${await auth.currentUser.getIdToken()}`, - }, - } - ); - } catch (error) { - console.log("Error getting QBXML from Server.", error); - notification["error"]({ - message: t("bills.errors.exporting", { - error: "Unable to retrieve QBXML. " + JSON.stringify(error.message), - }), + if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { + PartnerResponse = await axios.post(`/qbo/receivables`, { + withCredentials: true, + bills: billids, }); - if (loadingCallback) loadingCallback(false); - setLoading(false); - return; - } + } else { + let QbXmlResponse; + try { + QbXmlResponse = await axios.post( + "/accounting/qbxml/payables", + { bills: billids }, + { + headers: { + Authorization: `Bearer ${await auth.currentUser.getIdToken()}`, + }, + } + ); + } catch (error) { + console.log("Error getting QBXML from Server.", error); + notification["error"]({ + message: t("bills.errors.exporting", { + error: "Unable to retrieve QBXML. " + JSON.stringify(error.message), + }), + }); + if (loadingCallback) loadingCallback(false); + setLoading(false); + return; + } - let PartnerResponse; - - try { - PartnerResponse = await axios.post( - "http://localhost:1337/qb/", - QbXmlResponse.data - ); - } catch (error) { - console.log("Error connecting to quickbooks or partner.", error); - notification["error"]({ - message: t("bills.errors.exporting-partner"), - }); - if (!!loadingCallback) loadingCallback(false); - setLoading(false); - return; + try { + PartnerResponse = await axios.post( + "http://localhost:1337/qb/", + QbXmlResponse.data + ); + } catch (error) { + console.log("Error connecting to quickbooks or partner.", error); + notification["error"]({ + message: t("bills.errors.exporting-partner"), + }); + if (!!loadingCallback) loadingCallback(false); + setLoading(false); + return; + } } console.log("handleQbxml -> PartnerResponse", PartnerResponse); - const groupedData = _.groupBy(PartnerResponse.data, "id"); + const groupedData = _.groupBy( + PartnerResponse.data, + bodyshop.accountingconfig.qbo ? "billid" : "id" + ); + const proms = []; Object.keys(groupedData).forEach((key) => { proms.push( diff --git a/client/src/components/payable-export-button/payable-export-button.component.jsx b/client/src/components/payable-export-button/payable-export-button.component.jsx index d531b308f..6a9fd2494 100644 --- a/client/src/components/payable-export-button/payable-export-button.component.jsx +++ b/client/src/components/payable-export-button/payable-export-button.component.jsx @@ -38,44 +38,53 @@ export function PayableExportButton({ setLoading(true); if (!!loadingCallback) loadingCallback(true); - let QbXmlResponse; - try { - QbXmlResponse = await axios.post( - "/accounting/qbxml/payables", - { bills: [billId] }, - { - headers: { - Authorization: `Bearer ${await auth.currentUser.getIdToken()}`, - }, - } - ); - } catch (error) { - console.log("Error getting QBXML from Server.", error); - notification["error"]({ - message: t("bills.errors.exporting", { - error: "Unable to retrieve QBXML. " + JSON.stringify(error.message), - }), - }); - if (loadingCallback) loadingCallback(false); - setLoading(false); - return; - } - + //Check if it's a QBO Setup. let PartnerResponse; - - try { - PartnerResponse = await axios.post( - "http://localhost:1337/qb/", - QbXmlResponse.data - ); - } catch (error) { - console.log("Error connecting to quickbooks or partner.", error); - notification["error"]({ - message: t("bills.errors.exporting-partner"), + if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { + PartnerResponse = await axios.post(`/qbo/payables`, { + withCredentials: true, + bills: [billId], }); - if (!!loadingCallback) loadingCallback(false); - setLoading(false); - return; + } else { + //Default is QBD + + let QbXmlResponse; + try { + QbXmlResponse = await axios.post( + "/accounting/qbxml/payables", + { bills: [billId] }, + { + headers: { + Authorization: `Bearer ${await auth.currentUser.getIdToken()}`, + }, + } + ); + } catch (error) { + console.log("Error getting QBXML from Server.", error); + notification["error"]({ + message: t("bills.errors.exporting", { + error: "Unable to retrieve QBXML. " + JSON.stringify(error.message), + }), + }); + if (loadingCallback) loadingCallback(false); + setLoading(false); + return; + } + + try { + PartnerResponse = await axios.post( + "http://localhost:1337/qb/", + QbXmlResponse.data + ); + } catch (error) { + console.log("Error connecting to quickbooks or partner.", error); + notification["error"]({ + message: t("bills.errors.exporting-partner"), + }); + if (!!loadingCallback) loadingCallback(false); + setLoading(false); + return; + } } console.log("handleQbxml -> PartnerResponse", PartnerResponse); @@ -123,7 +132,14 @@ export function PayableExportButton({ }); const billUpdateResponse = await updateBill({ variables: { - billIdList: successfulTransactions.map((st) => st.id), + billIdList: successfulTransactions.map( + (st) => + st[ + bodyshop.accountingconfig && bodyshop.accountingconfig.qbo + ? "billid" + : "id" + ] + ), bill: { exported: true, exported_at: new Date(), diff --git a/server/accounting/qb-receivables-lines.js b/server/accounting/qb-receivables-lines.js index 1302998a7..909473f13 100644 --- a/server/accounting/qb-receivables-lines.js +++ b/server/accounting/qb-receivables-lines.js @@ -3,7 +3,7 @@ const DineroQbFormat = require("./accounting-constants").DineroQbFormat; const Dinero = require("dinero.js"); const logger = require("../utils/logger"); -module.exports = function ({ +exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, @@ -591,3 +591,4 @@ const findTaxCode = ({ local, state, federal }, taxcode) => { return "No Tax Code Matches"; } }; +exports.findTaxCode = findTaxCode; diff --git a/server/accounting/qbo/qbo-payables.js b/server/accounting/qbo/qbo-payables.js index 4a42c9e87..ffe60b373 100644 --- a/server/accounting/qbo/qbo-payables.js +++ b/server/accounting/qbo/qbo-payables.js @@ -16,9 +16,9 @@ const { setNewRefreshToken, } = require("./qbo-callback"); const OAuthClient = require("intuit-oauth"); - +const moment = require("moment"); const GraphQLClient = require("graphql-request").GraphQLClient; -const { generateOwnerTier } = require("../qbxml/qbxml-utils"); +const findTaxCode = require("../qb-receivables-lines").findTaxCode; exports.default = async (req, res) => { const oauthClient = new OAuthClient({ @@ -53,20 +53,36 @@ exports.default = async (req, res) => { .request(queries.QUERY_BILLS_FOR_PAYABLES_EXPORT, { bills: billsToQuery, }); + const { bills } = result; + const ret = []; for (const bill of bills) { - let vendorRecord; - vendorRecord = await QueryVendorRecord(oauthClient, req, bill); + try { + let vendorRecord; + vendorRecord = await QueryVendorRecord(oauthClient, req, bill); - if (!vendorRecord) { - vendorRecord = await InsertVendorRecord(oauthClient, req, bill); + if (!vendorRecord) { + vendorRecord = await InsertVendorRecord(oauthClient, req, bill); + } + + const insertResults = await InsertBill( + oauthClient, + req, + bill, + vendorRecord + ); + ret.push({ billid: bill.id, success: true }); + } catch (error) { + ret.push({ + billid: bill.id, + success: false, + errorMessage: error.message, + }); } - - const insertResults = await InsertBill(oauthClient, req, bill); } - res.json({}); + res.status(200).json(ret); } catch (error) { console.log(error); logger.log("qbo-payable-create-error", "ERROR", req.user.email, { error }); @@ -126,54 +142,108 @@ async function InsertVendorRecord(oauthClient, req, bill) { } } -async function InsertBill(oauthClient, req, bill) { - const vendor = { - DisplayName: job.ro_number, - BillAddr: { - City: job.ownr_city, - Line1: job.ownr_addr1, - Line2: job.ownr_addr2, - PostalCode: job.ownr_zip, - CountrySubDivisionCode: job.ownr_st, +async function InsertBill(oauthClient, req, bill, vendor) { + const { accounts, taxCodes, classes } = await QueryMetaData(oauthClient, req); + + const billQbo = { + VendorRef: { + value: vendor.Id, }, - ...(isThreeTier - ? { - Job: true, - ParentRef: { - value: parentTierRef.Id, - }, - } - : {}), + TxnDate: moment(bill.date).format("YYYY-MM-DD"), + DueDate: bill.due_date && moment(bill.due_date).format("YYYY-MM-DD"), + DocNumber: bill.invoice_number, + ...(bill.job.class ? { ClassRef: { Id: classes[bill.job.class] } } : {}), + + Memo: `RO ${bill.job.ro_number || ""} OWNER ${bill.job.ownr_fn || ""} ${ + bill.job.ownr_ln || "" + } ${bill.job.ownr_co_nm || ""}`, + Line: bill.billlines.map((il) => + generateBillLine( + il, + accounts, + bill.job.class, + bill.job.bodyshop.md_responsibility_centers.sales_tax_codes, + classes, + taxCodes + ) + ), }; try { const result = await oauthClient.makeApiCall({ - url: urlBuilder(req.cookies.qbo_realmId, "vendor"), + url: urlBuilder(req.cookies.qbo_realmId, "bill"), method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify(vendor), + body: JSON.stringify(billQbo), }); setNewRefreshToken(req.user.email, result); - return result && result.Customer; + return result && result.Bill; } catch (error) { logger.log("qbo-payables-error", "DEBUG", req.user.email, bill.id, { error, - method: "InsertOwner", + method: "InsertBill", }); throw error; } } +// [ +// { +// DetailType: "AccountBasedExpenseLineDetail", +// Amount: 200.0, +// Id: "1", +// AccountBasedExpenseLineDetail: { +// AccountRef: { +// value: "7", +// }, +// }, +// }, +// ], +const generateBillLine = ( + billLine, + accounts, + jobClass, + responsibilityCenters, + classes, + taxCodes +) => { + return { + DetailType: "AccountBasedExpenseLineDetail", + + AccountBasedExpenseLineDetail: { + ...(jobClass ? { ClassRef: { Id: classes[jobClass] } } : {}), + TaxCodeRef: { + value: + taxCodes[ + findTaxCode(billLine.applicable_taxes, responsibilityCenters) + ], + }, + AccountRef: { + value: accounts[billLine.cost_center], + }, + }, + + Amount: Dinero({ + amount: Math.round(billLine.actual_cost * 100), + }) + .multiply(billLine.quantity || 1) + .toFormat(DineroQbFormat), + }; +}; async function QueryMetaData(oauthClient, req) { - const items = await oauthClient.makeApiCall({ - url: urlBuilder(req.cookies.qbo_realmId, "query", `select * From Item`), + const accounts = await oauthClient.makeApiCall({ + url: urlBuilder( + req.cookies.qbo_realmId, + "query", + `select * From Account where AccountType = 'Cost of Goods Sold'` + ), method: "POST", headers: { "Content-Type": "application/json", }, }); - setNewRefreshToken(req.user.email, items); + setNewRefreshToken(req.user.email, accounts); const taxCodes = await oauthClient.makeApiCall({ url: urlBuilder(req.cookies.qbo_realmId, "query", `select * From TaxCode`), method: "POST", @@ -182,368 +252,40 @@ async function QueryMetaData(oauthClient, req) { }, }); - const taxCodeMapping = {}; - - const accounts = await oauthClient.makeApiCall({ - url: urlBuilder(req.cookies.qbo_realmId, "query", `select * From Account`), + const classes = await oauthClient.makeApiCall({ + url: urlBuilder(req.cookies.qbo_realmId, "query", `select * From Class`), method: "POST", headers: { "Content-Type": "application/json", }, }); + const taxCodeMapping = {}; + taxCodes.json && taxCodes.json.QueryResponse && taxCodes.json.QueryResponse.TaxCode.forEach((t) => { taxCodeMapping[t.Name] = t.Id; }); - const itemMapping = {}; + const accountMapping = {}; - items.json && - items.json.QueryResponse && - items.json.QueryResponse.Item.forEach((t) => { - itemMapping[t.Name] = t.Id; + accounts.json && + accounts.json.QueryResponse && + accounts.json.QueryResponse.Account.forEach((t) => { + accountMapping[t.Name] = t.Id; + }); + + const classMapping = {}; + classes.json && + classes.json.QueryResponse && + classes.json.QueryResponse.Class.forEach((t) => { + accountMapping[t.Name] = t.Id; }); return { - items: itemMapping, + accounts: accountMapping, taxCodes: taxCodeMapping, + classes: classMapping, }; } - -async function InsertInvoice(oauthClient, req, job, bodyshop, parentTierRef) { - const { items, taxCodes } = await QueryMetaData(oauthClient, req); - const InvoiceLineAdd = []; - const responsibilityCenters = bodyshop.md_responsibility_centers; - - const invoiceLineHash = {}; //The hash of cost and profit centers based on the center name. - - //Determine if there are MAPA and MASH lines already on the estimate. - //If there are, don't do anything extra (mitchell estimate) - //Otherwise, calculate them and add them to the default MAPA and MASH centers. - let hasMapaLine = false; - let hasMashLine = false; - - //Create the invoice lines mapping. - job.joblines.map((jobline) => { - //Parts Lines - if (jobline.db_ref === "936008") { - //If either of these DB REFs change, they also need to change in job-totals/job-costing calculations. - hasMapaLine = true; - } - if (jobline.db_ref === "936007") { - hasMashLine = true; - } - - if (jobline.profitcenter_part && jobline.act_price) { - let DineroAmount = Dinero({ - amount: Math.round(jobline.act_price * 100), - }).multiply(jobline.part_qty || 1); - - if (jobline.prt_dsmk_p && jobline.prt_dsmk_p !== 0) { - // console.log("Have a part discount", jobline); - DineroAmount = DineroAmount.add( - DineroAmount.percentage(jobline.prt_dsmk_p || 0) - ); - } - const account = responsibilityCenters.profits.find( - (i) => jobline.profitcenter_part.toLowerCase() === i.name.toLowerCase() - ); - - if (!account) { - logger.log("qbxml-payables-no-account", "warn", null, jobline.id, null); - throw new Error( - `A matching account does not exist for the part allocation. Center: ${jobline.profitcenter_part}` - ); - } - if (!invoiceLineHash[account.name]) { - invoiceLineHash[account.name] = { - ItemRef: { FullName: account.accountitem }, - Desc: account.accountdesc, - Quantity: 1, //jobline.part_qty, - Amount: DineroAmount, //.toFormat(DineroQbFormat), - SalesTaxCodeRef: { - FullName: "E", - }, - }; - } else { - invoiceLineHash[account.name].Amount = - invoiceLineHash[account.name].Amount.add(DineroAmount); - } - } - // Labor Lines - if (jobline.profitcenter_labor && jobline.mod_lb_hrs) { - const DineroAmount = Dinero({ - amount: Math.round( - job[`rate_${jobline.mod_lbr_ty.toLowerCase()}`] * 100 - ), - }).multiply(jobline.mod_lb_hrs); - const account = responsibilityCenters.profits.find( - (i) => jobline.profitcenter_labor.toLowerCase() === i.name.toLowerCase() - ); - - if (!account) { - throw new Error( - `A matching account does not exist for the labor allocation. Center: ${jobline.profitcenter_labor}` - ); - } - if (!invoiceLineHash[account.name]) { - invoiceLineHash[account.name] = { - ItemRef: { FullName: account.accountitem }, - Desc: account.accountdesc, - Quantity: 1, // jobline.mod_lb_hrs, - Amount: DineroAmount, - //Amount: DineroAmount.toFormat(DineroQbFormat), - SalesTaxCodeRef: { - FullName: "E", - }, - }; - } else { - invoiceLineHash[account.name].Amount = - invoiceLineHash[account.name].Amount.add(DineroAmount); - } - } - }); - // console.log("Done creating hash", JSON.stringify(invoiceLineHash)); - - if (!hasMapaLine && job.job_totals.rates.mapa.total.amount > 0) { - // console.log("Adding MAPA Line Manually."); - const mapaAccountName = responsibilityCenters.defaults.profits.MAPA; - - const mapaAccount = responsibilityCenters.profits.find( - (c) => c.name === mapaAccountName - ); - - if (mapaAccount) { - InvoiceLineAdd.push({ - ItemRef: { FullName: mapaAccount.accountitem }, - Desc: mapaAccount.accountdesc, - Quantity: 1, - Amount: Dinero(job.job_totals.rates.mapa.total).toFormat( - DineroQbFormat - ), - SalesTaxCodeRef: { - FullName: "E", - }, - }); - } else { - //console.log("NO MAPA ACCOUNT FOUND!!"); - } - } - - if (!hasMashLine && job.job_totals.rates.mash.total.amount > 0) { - // console.log("Adding MASH Line Manually."); - - const mashAccountName = responsibilityCenters.defaults.profits.MASH; - - const mashAccount = responsibilityCenters.profits.find( - (c) => c.name === mashAccountName - ); - - if (mashAccount) { - InvoiceLineAdd.push({ - ItemRef: { FullName: mashAccount.accountitem }, - Desc: mashAccount.accountdesc, - Quantity: 1, - Amount: Dinero(job.job_totals.rates.mash.total).toFormat( - DineroQbFormat - ), - SalesTaxCodeRef: { - FullName: "E", - }, - }); - } else { - // console.log("NO MASH ACCOUNT FOUND!!"); - } - } - - //Convert the hash to an array. - Object.keys(invoiceLineHash).forEach((key) => { - InvoiceLineAdd.push({ - ...invoiceLineHash[key], - Amount: invoiceLineHash[key].Amount.toFormat(DineroQbFormat), - }); - }); - - //Add Towing, storage and adjustment lines. - - if (job.towing_payable && job.towing_payable !== 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: responsibilityCenters.profits.find( - (c) => c.name === responsibilityCenters.defaults.profits["TOW"] - ).accountitem, - }, - Desc: "Towing", - Quantity: 1, - Amount: Dinero({ - amount: Math.round((job.towing_payable || 0) * 100), - }).toFormat(DineroQbFormat), - SalesTaxCodeRef: { - FullName: "E", - }, - }); - } - if (job.storage_payable && job.storage_payable !== 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: responsibilityCenters.profits.find( - (c) => c.name === responsibilityCenters.defaults.profits["TOW"] - ).accountitem, - }, - Desc: "Storage", - Quantity: 1, - Amount: Dinero({ - amount: Math.round((job.storage_payable || 0) * 100), - }).toFormat(DineroQbFormat), - SalesTaxCodeRef: { - FullName: "E", - }, - }); - } - if (job.adjustment_bottom_line && job.adjustment_bottom_line !== 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: responsibilityCenters.profits.find( - (c) => c.name === responsibilityCenters.defaults.profits["PAO"] - ).accountitem, - }, - Desc: "Adjustment", - Quantity: 1, - Amount: Dinero({ - amount: Math.round((job.adjustment_bottom_line || 0) * 100), - }).toFormat(DineroQbFormat), - SalesTaxCodeRef: { - FullName: "E", - }, - }); - } - - //Add tax lines - const job_totals = job.job_totals; - - const federal_tax = Dinero(job_totals.totals.federal_tax); - const state_tax = Dinero(job_totals.totals.state_tax); - const local_tax = Dinero(job_totals.totals.local_tax); - - if (federal_tax.getAmount() > 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: bodyshop.md_responsibility_centers.taxes.federal.accountitem, - }, - Desc: bodyshop.md_responsibility_centers.taxes.federal.accountdesc, - Amount: federal_tax.toFormat(DineroQbFormat), - }); - } - - if (state_tax.getAmount() > 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem, - }, - Desc: bodyshop.md_responsibility_centers.taxes.state.accountdesc, - Amount: state_tax.toFormat(DineroQbFormat), - }); - } - - if (local_tax.getAmount() > 0) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: bodyshop.md_responsibility_centers.taxes.local.accountitem, - }, - Desc: bodyshop.md_responsibility_centers.taxes.local.accountdesc, - Amount: local_tax.toFormat(DineroQbFormat), - }); - } - - //Region Specific - const { ca_bc_pvrt } = job; - if (ca_bc_pvrt) { - InvoiceLineAdd.push({ - ItemRef: { - FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem, - }, - Desc: "PVRT", - Amount: Dinero({ amount: (ca_bc_pvrt || 0) * 100 }).toFormat( - DineroQbFormat - ), - }); - } - - //map each invoice line to the correct style for QBO. - - const invoiceObj = { - Line: [ - { - DetailType: "SalesItemLineDetail", - Amount: 100, - SalesItemLineDetail: { - // ItemRef: { - // // name: "Services", - // value: "16", - // }, - TaxCodeRef: { - value: "2", - }, - Qty: 1, - UnitPrice: 100, - }, - }, - ], - // Line: InvoiceLineAdd.map((i) => { - // return { - // DetailType: "SalesItemLineDetail", - // Amount: i.Amount, - // SalesItemLineDetail: { - // ItemRef: { - // //name: "Services", - // value: items[i.ItemRef.FullName], - // }, - // // TaxCodeRef: { - // // value: "2", - // // }, - // Qty: 1, - // }, - // }; - // }), - TxnTaxDetail: { - TaxLine: [ - { - DetailType: "TaxLineDetail", - - TaxLineDetail: { - NetAmountTaxable: 100, - TaxPercent: 7, - TaxRateRef: { - value: "16", - }, - PercentBased: true, - }, - }, - ], - }, - CustomerRef: { - value: parentTierRef.Id, - }, - }; - - try { - const result = await oauthClient.makeApiCall({ - url: urlBuilder(req.cookies.qbo_realmId, "invoice"), - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(invoiceObj), - }); - setNewRefreshToken(req.user.email, result); - return result && result.Invoice; - } catch (error) { - logger.log("qbo-payables-error", "DEBUG", req.user.email, job.id, { - error, - method: "InsertOwner", - }); - throw error; - } -} diff --git a/server/accounting/qbo/qbo-receivables.js b/server/accounting/qbo/qbo-receivables.js index 010266812..f23873544 100644 --- a/server/accounting/qbo/qbo-receivables.js +++ b/server/accounting/qbo/qbo-receivables.js @@ -14,7 +14,7 @@ const { setNewRefreshToken, } = require("./qbo-callback"); const OAuthClient = require("intuit-oauth"); -const CreateInvoiceLines = require("../qb-receivables-lines"); +const CreateInvoiceLines = require("../qb-receivables-lines").default; const moment = require("moment"); const GraphQLClient = require("graphql-request").GraphQLClient; @@ -332,6 +332,14 @@ async function QueryMetaData(oauthClient, req) { }, }); + const classes = await oauthClient.makeApiCall({ + url: urlBuilder(req.cookies.qbo_realmId, "query", `select * From Class`), + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + const taxCodeMapping = {}; taxCodes.json && @@ -348,14 +356,22 @@ async function QueryMetaData(oauthClient, req) { itemMapping[t.Name] = t.Id; }); + const classMapping = {}; + classes.json && + classes.json.QueryResponse && + classes.json.QueryResponse.Class.forEach((t) => { + itemMapping[t.Name] = t.Id; + }); + return { items: itemMapping, taxCodes: taxCodeMapping, + classes: classMapping, }; } async function InsertInvoice(oauthClient, req, job, bodyshop, parentTierRef) { - const { items, taxCodes } = await QueryMetaData(oauthClient, req); + const { items, taxCodes, classes } = await QueryMetaData(oauthClient, req); const InvoiceLineAdd = CreateInvoiceLines({ bodyshop, jobs_by_pk: job, @@ -368,6 +384,7 @@ async function InsertInvoice(oauthClient, req, job, bodyshop, parentTierRef) { Line: InvoiceLineAdd, TxnDate: moment(job.date_invoiced).format("YYYY-MM-DD"), DocNumber: job.ro_number, + ...(job.class ? { ClassRef: { Id: classes[job.class] } } : {}), CustomerRef: { value: parentTierRef.Id, }, diff --git a/server/accounting/qbxml/qbxml-receivables.js b/server/accounting/qbxml/qbxml-receivables.js index 352088cba..c683f45a9 100644 --- a/server/accounting/qbxml/qbxml-receivables.js +++ b/server/accounting/qbxml/qbxml-receivables.js @@ -7,7 +7,7 @@ const moment = require("moment"); var builder = require("xmlbuilder2"); const QbXmlUtils = require("./qbxml-utils"); const logger = require("../../utils/logger"); -const CreateInvoiceLines = require("../qb-receivables-lines"); +const CreateInvoiceLines = require("../qb-receivables-lines").default; require("dotenv").config({ path: path.resolve( @@ -109,7 +109,7 @@ exports.default = async (req, res) => { "error", req.user.email, req.body.jobIds, - {error: (error)} + { error: error } ); res.status(400).send(JSON.stringify(error)); } From e73d082eab8926da6920a61a1574f7d06dd5f762 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Wed, 29 Sep 2021 08:49:55 -0700 Subject: [PATCH 2/2] IO-1349 Resolve manual job creation. --- client/src/pages/jobs-create/jobs-create.container.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/src/pages/jobs-create/jobs-create.container.jsx b/client/src/pages/jobs-create/jobs-create.container.jsx index 079a109c9..b7ffad7b2 100644 --- a/client/src/pages/jobs-create/jobs-create.container.jsx +++ b/client/src/pages/jobs-create/jobs-create.container.jsx @@ -85,7 +85,6 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { }); }; - console.log("Manual State", state); const handleFinish = (values) => { let job = Object.assign( {}, @@ -142,6 +141,10 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { } job = { ...job, ...ownerData }; + + if (job.owner === null) delete job.owner; + if (job.vehicle === null) delete job.vehicle; + runInsertJob(job); };