const DineroQbFormat = require("./accounting-constants").DineroQbFormat; const Dinero = require("dinero.js"); const { DiscountNotAlreadyCounted } = require("../job/job-totals"); const logger = require("../utils/logger"); exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes, classes, }) { 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. jobs_by_pk.joblines.map((jobline) => { 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; } //Parts Lines Mappings. if (jobline.profitcenter_part) { let DineroAmount = Dinero({ amount: Math.round((jobline.act_price || 0) * 100), }).multiply(jobline.part_qty || 1); // console.log("Have a part discount", jobline); DineroAmount = DineroAmount.add( ((jobline.prt_dsmk_m && jobline.prt_dsmk_m !== 0) || (jobline.prt_dsmk_p && jobline.prt_dsmk_p !== 0)) && DiscountNotAlreadyCounted(jobline, jobs_by_pk.joblines) ? jobline.prt_dsmk_m ? Dinero({ amount: Math.round(jobline.prt_dsmk_m * 100) }) : Dinero({ amount: Math.round(jobline.act_price * 100), }) .multiply(jobline.part_qty || 0) .percentage(Math.abs(jobline.prt_dsmk_p || 0)) .multiply(jobline.prt_dsmk_p > 0 ? 1 : -1) : Dinero() ); const account = responsibilityCenters.profits.find( (i) => jobline.profitcenter_part.toLowerCase() === i.name.toLowerCase() ); if (!account) { logger.log( "qbxml-receivables-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 (qbo) { //Do the mapping as per QBO. //Determine the Tax code grouping. //Going to always assume that we need to apply GST. const taxAccountCode = findTaxCode( { local: false, federal: true, state: jobs_by_pk.state_tax_rate === 0 ? false : jobline.db_ref === "900511" || jobline.db_ref === "900510" ? true : jobline.tax_part, }, bodyshop.md_responsibility_centers.sales_tax_codes ); const QboTaxId = taxCodes[taxAccountCode]; if (!invoiceLineHash[account.name]) invoiceLineHash[account.name] = {}; if (!invoiceLineHash[account.name][QboTaxId]) { invoiceLineHash[account.name][QboTaxId] = { DetailType: "SalesItemLineDetail", Amount: DineroAmount, SalesItemLineDetail: { ...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } : {}), ItemRef: { value: items[account.accountitem], }, TaxCodeRef: { value: QboTaxId, }, Qty: 1, }, }; } else { invoiceLineHash[account.name][QboTaxId].Amount = invoiceLineHash[account.name][QboTaxId].Amount.add(DineroAmount); } } else { 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); } } } //End Parts line mappings. // Labor Lines if (jobline.profitcenter_labor && jobline.mod_lb_hrs) { const DineroAmount = Dinero({ amount: Math.round( jobs_by_pk[`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 (qbo) { //Going to always assume that we need to apply GST and PST for labor. const taxAccountCode = findTaxCode( { local: false, federal: true, state: jobs_by_pk.state_tax_rate === 0 ? false : true, }, bodyshop.md_responsibility_centers.sales_tax_codes ); const QboTaxId = taxCodes[taxAccountCode]; if (!invoiceLineHash[account.name]) invoiceLineHash[account.name] = {}; if (!invoiceLineHash[account.name][QboTaxId]) { invoiceLineHash[account.name][QboTaxId] = { DetailType: "SalesItemLineDetail", Amount: DineroAmount, SalesItemLineDetail: { ...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } : {}), ItemRef: { value: items[account.accountitem], }, TaxCodeRef: { value: QboTaxId, }, Qty: 1, }, }; } else { invoiceLineHash[account.name][QboTaxId].Amount = invoiceLineHash[account.name][QboTaxId].Amount.add(DineroAmount); } } else { 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); } } } }); if (!hasMapaLine && jobs_by_pk.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) { if (qbo) { //Add QBO MAPA //Going to always assume that we need to apply GST and PST for labor. const taxAccountCode = findTaxCode( { local: false, federal: true, state: jobs_by_pk.state_tax_rate === 0 ? false : true, }, bodyshop.md_responsibility_centers.sales_tax_codes ); const QboTaxId = taxCodes[taxAccountCode]; if (!invoiceLineHash[mapaAccount.name]) invoiceLineHash[mapaAccount.name] = {}; if (!invoiceLineHash[mapaAccount.name][QboTaxId]) { invoiceLineHash[mapaAccount.name][QboTaxId] = { DetailType: "SalesItemLineDetail", Amount: Dinero(jobs_by_pk.job_totals.rates.mapa.total), SalesItemLineDetail: { ItemRef: { value: items[mapaAccount.accountitem], }, ...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } : {}), TaxCodeRef: { value: QboTaxId, }, Qty: 1, }, }; } else { invoiceLineHash[mapaAccount.name][QboTaxId].Amount = invoiceLineHash[ mapaAccount.name ][QboTaxId].Amount.add( Dinero(jobs_by_pk.job_totals.rates.mapa.total) ); } } else { InvoiceLineAdd.push({ ItemRef: { FullName: mapaAccount.accountitem }, Desc: mapaAccount.accountdesc, Quantity: 1, Amount: Dinero(jobs_by_pk.job_totals.rates.mapa.total).toFormat( DineroQbFormat ), SalesTaxCodeRef: { FullName: "E", }, }); } } else { //console.log("NO MAPA ACCOUNT FOUND!!"); } } if (!hasMashLine && jobs_by_pk.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) { if (qbo) { //Going to always assume that we need to apply GST and PST for labor. const taxAccountCode = findTaxCode( { local: false, federal: true, state: jobs_by_pk.state_tax_rate === 0 ? false : true, }, bodyshop.md_responsibility_centers.sales_tax_codes ); const QboTaxId = taxCodes[taxAccountCode]; if (!invoiceLineHash[mashAccount.name]) invoiceLineHash[mashAccount.name] = {}; if (!invoiceLineHash[mashAccount.name][QboTaxId]) { invoiceLineHash[mashAccount.name][QboTaxId] = { DetailType: "SalesItemLineDetail", Amount: Dinero(jobs_by_pk.job_totals.rates.mash.total), SalesItemLineDetail: { ItemRef: { value: items[mashAccount.accountitem], }, ...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } : {}), TaxCodeRef: { value: QboTaxId, }, Qty: 1, }, }; } else { invoiceLineHash[mashAccount.name][QboTaxId].Amount = invoiceLineHash[ mashAccount.name ][QboTaxId].Amount.add( Dinero(jobs_by_pk.job_totals.rates.mash.total) ); } } else { InvoiceLineAdd.push({ ItemRef: { FullName: mashAccount.accountitem }, Desc: mashAccount.accountdesc, Quantity: 1, Amount: Dinero(jobs_by_pk.job_totals.rates.mash.total).toFormat( DineroQbFormat ), SalesTaxCodeRef: { FullName: "E", }, }); } } else { // console.log("NO MASH ACCOUNT FOUND!!"); } } if (qbo) { Object.keys(invoiceLineHash).forEach((key) => { Object.keys(invoiceLineHash[key]).forEach((key2) => { const account = responsibilityCenters.profits.find( (p) => p.name === key ); InvoiceLineAdd.push({ ...invoiceLineHash[key][key2], ...(account ? { Description: account.accountdesc } : {}), Amount: invoiceLineHash[key][key2].Amount.toFormat(DineroQbFormat), }); }); }); } else { Object.keys(invoiceLineHash).forEach((key) => { InvoiceLineAdd.push({ ...invoiceLineHash[key], Amount: invoiceLineHash[key].Amount.toFormat(DineroQbFormat), }); }); } //Convert the hash to an array. //Add Towing, storage and adjustment lines. if (jobs_by_pk.towing_payable && jobs_by_pk.towing_payable !== 0) { if (qbo) { //Going to always assume that we need to apply GST and PST for labor. const taxAccountCode = findTaxCode( { local: false, federal: true, state: jobs_by_pk.state_tax_rate === 0 ? false : true, }, bodyshop.md_responsibility_centers.sales_tax_codes ); const account = responsibilityCenters.profits.find( (c) => c.name === responsibilityCenters.defaults.profits["TOW"] ); const QboTaxId = taxCodes[taxAccountCode]; InvoiceLineAdd.push({ DetailType: "SalesItemLineDetail", Amount: Dinero({ amount: Math.round((jobs_by_pk.towing_payable || 0) * 100), }).toFormat(DineroQbFormat), SalesItemLineDetail: { ...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } : {}), ItemRef: { value: items[account.accountitem], }, TaxCodeRef: { value: QboTaxId, }, Qty: 1, }, }); } else { InvoiceLineAdd.push({ ItemRef: { FullName: responsibilityCenters.profits.find( (c) => c.name === responsibilityCenters.defaults.profits["TOW"] ).accountitem, }, Desc: "Towing", Quantity: 1, Amount: Dinero({ amount: Math.round((jobs_by_pk.towing_payable || 0) * 100), }).toFormat(DineroQbFormat), SalesTaxCodeRef: { FullName: "E", }, }); } } if (jobs_by_pk.storage_payable && jobs_by_pk.storage_payable !== 0) { if (qbo) { //Going to always assume that we need to apply GST and PST for labor. const taxAccountCode = findTaxCode( { local: false, federal: true, state: jobs_by_pk.state_tax_rate === 0 ? false : true, }, bodyshop.md_responsibility_centers.sales_tax_codes ); const account = responsibilityCenters.profits.find( (c) => c.name === responsibilityCenters.defaults.profits["TOW"] ); const QboTaxId = taxCodes[taxAccountCode]; InvoiceLineAdd.push({ DetailType: "SalesItemLineDetail", Amount: Dinero({ amount: Math.round((jobs_by_pk.storage_payable || 0) * 100), }).toFormat(DineroQbFormat), SalesItemLineDetail: { ...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } : {}), ItemRef: { value: items[account.accountitem], }, TaxCodeRef: { value: QboTaxId, }, Qty: 1, }, }); } else { InvoiceLineAdd.push({ ItemRef: { FullName: responsibilityCenters.profits.find( (c) => c.name === responsibilityCenters.defaults.profits["TOW"] ).accountitem, }, Desc: "Storage", Quantity: 1, Amount: Dinero({ amount: Math.round((jobs_by_pk.storage_payable || 0) * 100), }).toFormat(DineroQbFormat), SalesTaxCodeRef: { FullName: "E", }, }); } } if ( jobs_by_pk.adjustment_bottom_line && jobs_by_pk.adjustment_bottom_line !== 0 ) { if (qbo) { //Going to always assume that we need to apply GST and PST for labor. const taxAccountCode = findTaxCode( { local: false, federal: true, state: jobs_by_pk.state_tax_rate === 0 ? false : true, }, bodyshop.md_responsibility_centers.sales_tax_codes ); const account = responsibilityCenters.profits.find( (c) => c.name === responsibilityCenters.defaults.profits["PAO"] ); const QboTaxId = taxCodes[taxAccountCode]; InvoiceLineAdd.push({ DetailType: "SalesItemLineDetail", Amount: Dinero({ amount: Math.round((jobs_by_pk.adjustment_bottom_line || 0) * 100), }).toFormat(DineroQbFormat), SalesItemLineDetail: { ...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } : {}), ItemRef: { value: items[account.accountitem], }, TaxCodeRef: { value: QboTaxId, }, Qty: 1, }, }); } else { InvoiceLineAdd.push({ ItemRef: { FullName: responsibilityCenters.profits.find( (c) => c.name === responsibilityCenters.defaults.profits["PAO"] ).accountitem, }, Desc: "Adjustment", Quantity: 1, Amount: Dinero({ amount: Math.round((jobs_by_pk.adjustment_bottom_line || 0) * 100), }).toFormat(DineroQbFormat), SalesTaxCodeRef: { FullName: "E", }, }); } } //Add tax lines const job_totals = jobs_by_pk.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) { if (qbo) { // do qbo } else { 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) { if (qbo) { // do qbo } else { 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) { if (qbo) { // do qbo } else { 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 } = jobs_by_pk; if (ca_bc_pvrt) { if (qbo) { InvoiceLineAdd.push({ DetailType: "SalesItemLineDetail", Amount: Dinero({ amount: (ca_bc_pvrt || 0) * 100 }).toFormat( DineroQbFormat ), SalesItemLineDetail: { ...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } : {}), ItemRef: { value: items["PVRT"], }, Qty: 1, TaxCodeRef: { value: taxCodes[ findTaxCode( { local: false, federal: true, state: false, }, bodyshop.md_responsibility_centers.sales_tax_codes ) ], }, }, }); } else { InvoiceLineAdd.push({ ItemRef: { FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem, }, Desc: "PVRT", Amount: Dinero({ amount: (ca_bc_pvrt || 0) * 100 }).toFormat( DineroQbFormat ), }); } } //QB USA with GST //This was required for the No. 1 Collision Group. if ( bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && bodyshop.accountingconfig.qbo_usa && bodyshop.region_config.includes("CA_") ) { InvoiceLineAdd.push({ DetailType: "SalesItemLineDetail", Amount: Dinero(jobs_by_pk.job_totals.totals.federal_tax).toFormat( DineroQbFormat ), SalesItemLineDetail: { ...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } : {}), ItemRef: { value: items[bodyshop.md_responsibility_centers.taxes.federal.accountitem], }, Qty: 1, }, }); } if (!qbo && InvoiceLineAdd.length === 0) { //Handle the scenario where there is a $0 sale invoice. InvoiceLineAdd.push({ Desc: "No estimate lines.", }); } return InvoiceLineAdd; }; const findTaxCode = ({ local, state, federal }, taxcode) => { const t = taxcode.filter( (t) => !!t.local === !!local && !!t.state === !!state && !!t.federal === !!federal ); if (t.length === 1) { return t[0].code; } else if (t.length > 1) { return "Multiple Tax Codes Match"; } else { return ""; } }; exports.findTaxCode = findTaxCode;