Files
bodyshop/server/accounting/qbxml/qbxml-payables.js
Allan Carr ac4fcf1694 IO-3368 QB Bill Accumulator
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-10-02 21:22:35 -07:00

152 lines
4.4 KiB
JavaScript

const DineroQbFormat = require("../accounting-constants").DineroQbFormat;
const queries = require("../../graphql-client/queries");
const Dinero = require("dinero.js");
const builder = require("xmlbuilder2");
const QbXmlUtils = require("./qbxml-utils");
const moment = require("moment-timezone");
const logger = require("../../utils/logger");
const InstanceManager = require("../../utils/instanceMgr").default;
exports.default = async (req, res) => {
const { bills: billsToQuery } = req.body;
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
try {
logger.log("qbxml-payable-create", "DEBUG", req.user.email, req.body.billsToQuery);
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QUERY_BILLS_FOR_PAYABLES_EXPORT, {
bills: billsToQuery
});
const { bills, bodyshops } = result;
const bodyshop = bodyshops[0];
const QbXmlToExecute = [];
bills.map((i) => {
QbXmlToExecute.push({
id: i.id,
okStatusCodes: ["0"],
qbxml: generateBill(i, bodyshop)
});
});
//For each invoice.
res.status(200).json(QbXmlToExecute);
} catch (error) {
logger.log("qbxml-payable-error", "ERROR", req?.user?.email, null, {
billsToQuery: req?.body?.billsToQuery,
error: error?.message,
stack: error?.stack
});
res.status(400).send(JSON.stringify(error));
}
};
const generateBill = (bill, bodyshop) => {
let lines;
if (bodyshop.accountingconfig.accumulatePayableLines === true) {
lines = Object.values(
bill.billlines.reduce((acc, il) => {
const { cost_center, actual_cost, quantity = 1 } = il;
if (!acc[cost_center]) {
acc[cost_center] = { ...il, actual_cost: 0, quantity: 1 };
}
acc[cost_center].actual_cost += Math.round(actual_cost * quantity * 100);
return acc;
}, {})
).map((il) => {
il.actual_cost /= 100;
return generateBillLine(il, bodyshop.md_responsibility_centers, bill.job.class);
});
} else {
lines = bill.billlines.map((il) => generateBillLine(il, bodyshop.md_responsibility_centers, bill.job.class));
}
const billQbxmlObj = {
QBXML: {
QBXMLMsgsRq: {
"@onError": "continueOnError",
[`${bill.is_credit_memo ? "VendorCreditAddRq" : "BillAddRq"}`]: {
[`${bill.is_credit_memo ? "VendorCreditAdd" : "BillAdd"}`]: {
VendorRef: {
FullName: bill.vendor.name
},
TxnDate: moment(bill.date)
//.tz(bill.job.bodyshop.timezone)
.format("YYYY-MM-DD"),
...(!bill.is_credit_memo &&
bill.vendor.due_date && {
DueDate: moment(bill.date)
// .tz(bill.job.bodyshop.timezone)
.add(bill.vendor.due_date, "days")
.format("YYYY-MM-DD")
}),
RefNumber: bill.invoice_number,
Memo: `RO ${bill.job.ro_number || ""}`,
ExpenseLineAdd: lines
}
}
}
}
};
var billQbxml_partial = builder
.create(billQbxmlObj, {
version: "1.30",
encoding: "UTF-8",
headless: true
})
.end({ pretty: true });
const billQbxml_Full = QbXmlUtils.addQbxmlHeader(billQbxml_partial);
return billQbxml_Full;
};
const generateBillLine = (billLine, responsibilityCenters, jobClass) => {
return {
AccountRef: {
FullName: responsibilityCenters.costs.find((c) => c.name === billLine.cost_center).accountname
},
Amount: Dinero({
amount: Math.round(billLine.actual_cost * 100)
})
.multiply(billLine.quantity || 1)
.toFormat(DineroQbFormat),
...(jobClass ? { ClassRef: { FullName: jobClass } } : {}),
...InstanceManager({
imex: {
SalesTaxCodeRef: {
FullName: findTaxCode(billLine, responsibilityCenters.sales_tax_codes)
}
}
})
};
};
const findTaxCode = (billLine, taxcode) => {
const {
applicable_taxes: { local, state, federal }
} =
billLine.applicable_taxes === null
? {
...billLine,
applicable_taxes: { local: false, state: false, federal: false }
}
: billLine;
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 "No Tax Code Matches";
}
};