const GraphQLClient = require("graphql-request").GraphQLClient; const path = require("path"); const DineroQbFormat = require("../accounting-constants").DineroQbFormat; const queries = require("../../graphql-client/queries"); const Dinero = require("dinero.js"); const moment = require("moment"); var builder = require("xmlbuilder2"); const QbXmlUtils = require("./qbxml-utils"); require("dotenv").config({ path: path.resolve( process.cwd(), `.env.${process.env.NODE_ENV || "development"}` ), }); Dinero.globalRoundingMode = "HALF_EVEN"; const { generateJobTier, generateOwnerTier, generateSourceTier } = QbXmlUtils; exports.default = async (req, res) => { const BearerToken = req.headers.authorization; const { jobIds } = req.body; const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { headers: { Authorization: BearerToken, }, }); try { const result = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_JOBS_FOR_RECEIVABLES_EXPORT, { ids: jobIds }); const { jobs, bodyshops } = result; const QbXmlToExecute = []; const bodyshop = bodyshops[0]; jobs.map((jobs_by_pk) => { //Is this a two tier, or 3 tier setup? const isThreeTier = bodyshop.accountingconfig.tiers === 3; const twoTierPref = bodyshop.accountingconfig.twotierpref; //This is the Insurance Company tier IF 3 tier is selected. if (isThreeTier) { QbXmlToExecute.push({ id: jobs_by_pk.id, okStatusCodes: ["0", "3100"], qbxml: generateSourceCustomerQbxml(jobs_by_pk, bodyshop), // Create the source customer. }); } //If 3 tier, this should be the customer. //If 2 tier, this should be based on the pref. QbXmlToExecute.push({ id: jobs_by_pk.id, okStatusCodes: ["0", "3100"], qbxml: generateJobQbxml( jobs_by_pk, bodyshop, isThreeTier, 2, twoTierPref ), }); //This is always going to be the job. QbXmlToExecute.push({ id: jobs_by_pk.id, okStatusCodes: ["0", "3100"], qbxml: generateJobQbxml( jobs_by_pk, bodyshop, isThreeTier, 3, twoTierPref ), }); //Generate the actual invoice. QbXmlToExecute.push({ id: jobs_by_pk.id, okStatusCodes: ["0"], qbxml: generateInvoiceQbxml( jobs_by_pk, bodyshop, isThreeTier, twoTierPref ), }); }); res.status(200).json(QbXmlToExecute); } catch (error) { console.log("error", error); res.status(400).send(JSON.stringify(error)); } }; const generateSourceCustomerQbxml = (jobs_by_pk, bodyshop) => { const customerQbxmlObj = { QBXML: { QBXMLMsgsRq: { "@onError": "continueOnError", CustomerAddRq: { CustomerAdd: { Name: jobs_by_pk.ins_co_nm, BillAddress: { Addr1: jobs_by_pk.ownr_addr1, Addr2: jobs_by_pk.ownr_addr2, City: jobs_by_pk.ownr_city, State: jobs_by_pk.ownr_st, PostalCode: jobs_by_pk.ownrzip, }, }, }, }, }, }; var customerQbxml_partial = builder .create(customerQbxmlObj, { version: "1.30", encoding: "UTF-8", headless: true, }) .end({ pretty: true }); const customerQbxml_Full = QbXmlUtils.addQbxmlHeader(customerQbxml_partial); return customerQbxml_Full; }; exports.generateSourceCustomerQbxml = generateSourceCustomerQbxml; const generateJobQbxml = ( jobs_by_pk, bodyshop, isThreeTier, tierLevel, twoTierPref ) => { let Name; let ParentRefName; if (tierLevel === 2) { Name = generateOwnerTier(jobs_by_pk, isThreeTier, twoTierPref); ParentRefName = isThreeTier ? generateSourceTier(jobs_by_pk) : null; } else if (tierLevel === 3) { Name = generateJobTier(jobs_by_pk); ParentRefName = isThreeTier ? `${generateSourceTier(jobs_by_pk)}:${generateOwnerTier(jobs_by_pk)}` : generateOwnerTier(jobs_by_pk, isThreeTier, twoTierPref); } const jobQbxmlObj = { QBXML: { QBXMLMsgsRq: { "@onError": "continueOnError", CustomerAddRq: { CustomerAdd: { Name: Name, ParentRef: ParentRefName ? { FullName: ParentRefName, } : null, }, }, }, }, }; var jobQbxml_partial = builder .create(jobQbxmlObj, { version: "1.30", encoding: "UTF-8", headless: true, }) .end({ pretty: true }); const jobQbxml_Full = QbXmlUtils.addQbxmlHeader(jobQbxml_partial); return jobQbxml_Full; }; exports.generateJobQbxml = generateJobQbxml; const generateInvoiceQbxml = ( jobs_by_pk, bodyshop, isThreeTier, twoTierPref ) => { //Build the Invoice XML file. 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) => { //Parts Lines if (jobline.db_ref === "936008") { //If either of these DB REFs change, they also need to change in job-totals calculations. hasMapaLine = true; } if (jobline.db_ref === "936007") { hasMashLine = true; } if (jobline.profitcenter_part && jobline.act_price) { const DineroAmount = Dinero({ amount: Math.round(jobline.act_price * 100), }).multiply(jobline.part_qty || 1); const account = responsibilityCenters.profits.find( (i) => jobline.profitcenter_part.toLowerCase() === i.name.toLowerCase() ); if (!account) { 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 && jobline.mod_lb_hrs > 0 ) { 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 (!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) { 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(jobs_by_pk.job_totals.rates.mapa.total).toFormat( DineroQbFormat ), SalesTaxCodeRef: { FullName: "E", }, }); } else { console.log("NO MAPA ACCOUNT FOUND!!"); } } if (!hasMashLine) { 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(jobs_by_pk.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 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) { 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 } = jobs_by_pk; 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 ), }); } const invoiceQbxmlObj = { QBXML: { QBXMLMsgsRq: { "@onError": "stopOnError", InvoiceAddRq: { InvoiceAdd: { CustomerRef: { FullName: bodyshop.accountingconfig.tiers === 3 ? `${generateSourceTier(jobs_by_pk)}:${generateOwnerTier( jobs_by_pk )}:${generateJobTier(jobs_by_pk)}` : `${generateOwnerTier( jobs_by_pk, isThreeTier, twoTierPref )}:${generateJobTier(jobs_by_pk)}`, }, ...(jobs_by_pk.class ? { ClassRef: { FullName: jobs_by_pk.class } } : {}), TxnDate: moment(jobs_by_pk.date_invoiced).format("YYYY-MM-DD"), RefNumber: jobs_by_pk.ro_number, ShipAddress: { Addr1: `${jobs_by_pk.ownr_fn || ""} ${jobs_by_pk.ownr_ln || ""} ${ jobs_by_pk.ownr_co_nm || "" }`, Addr2: jobs_by_pk.ownr_addr1, Addr3: jobs_by_pk.ownr_addr2, City: jobs_by_pk.ownr_city, State: jobs_by_pk.ownr_st, PostalCode: jobs_by_pk.ownrzip, }, PONumber: jobs_by_pk.clm_no, InvoiceLineAdd: InvoiceLineAdd, }, }, }, }, }; var invoiceQbxml_partial = builder .create(invoiceQbxmlObj, { version: "1.30", encoding: "UTF-8", headless: true, }) .end({ pretty: true }); const invoiceQbxml_Full = QbXmlUtils.addQbxmlHeader(invoiceQbxml_partial); return invoiceQbxml_Full; }; // const generateInvoiceLine = (job, allocation, responsibilityCenters) => { // const { amount, center } = allocation; // const DineroAmount = Dinero(amount); // const account = responsibilityCenters.profits.find( // (i) => i.name.toLowerCase() === center.toLowerCase() // ); // if (!!!account) { // throw new Error( // `A matching account does not exist for the allocation. Center: ${center}` // ); // } // return { // ItemRef: { FullName: account.accountitem }, // Desc: account.accountdesc, // Quantity: 1, // //Rate: 100, // Amount: DineroAmount.toFormat(DineroQbFormat), // SalesTaxCodeRef: { // FullName: "E", // }, // }; // };