IO-2054 QBO Split AR
This commit is contained in:
@@ -648,6 +648,56 @@ exports.default = function ({
|
||||
});
|
||||
}
|
||||
|
||||
//Check if there are multiple payers. If there are, add a deduction line and make sure we create new invoices.
|
||||
|
||||
if (
|
||||
jobs_by_pk.qb_multiple_payers &&
|
||||
jobs_by_pk.qb_multiple_payers.length > 0
|
||||
) {
|
||||
jobs_by_pk.qb_multiple_payers.forEach((payer) => {
|
||||
if (qbo) {
|
||||
InvoiceLineAdd.push({
|
||||
DetailType: "SalesItemLineDetail",
|
||||
Amount: Dinero({ amount: (payer.amount || 0) * 100 * -1 }).toFormat(
|
||||
DineroQbFormat
|
||||
),
|
||||
SalesItemLineDetail: {
|
||||
...(jobs_by_pk.class
|
||||
? { ClassRef: { value: classes[jobs_by_pk.class] } }
|
||||
: {}),
|
||||
ItemRef: {
|
||||
value:
|
||||
items[responsibilityCenters.qb_multiple_payers?.accountitem],
|
||||
},
|
||||
Qty: 1,
|
||||
TaxCodeRef: {
|
||||
value:
|
||||
taxCodes[
|
||||
findTaxCode(
|
||||
{
|
||||
local: false,
|
||||
federal: false,
|
||||
state: false,
|
||||
},
|
||||
bodyshop.md_responsibility_centers.sales_tax_codes
|
||||
)
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
InvoiceLineAdd.push({
|
||||
ItemRef: {
|
||||
FullName: responsibilityCenters.qb_multiple_payers?.accountitem,
|
||||
},
|
||||
Desc: `${payer.name} Liability`,
|
||||
Amount: Dinero({ amount: (payer.amount || 0) * 100 * -1 }).toFormat(
|
||||
DineroQbFormat
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return InvoiceLineAdd;
|
||||
};
|
||||
|
||||
@@ -667,3 +717,65 @@ const findTaxCode = ({ local, state, federal }, taxcode) => {
|
||||
}
|
||||
};
|
||||
exports.findTaxCode = findTaxCode;
|
||||
|
||||
exports.createMultiQbPayerLines = function ({
|
||||
bodyshop,
|
||||
jobs_by_pk,
|
||||
qbo = false,
|
||||
items,
|
||||
taxCodes,
|
||||
classes,
|
||||
payer,
|
||||
}) {
|
||||
const InvoiceLineAdd = [];
|
||||
const responsibilityCenters = bodyshop.md_responsibility_centers;
|
||||
|
||||
const invoiceLineHash = {}; //The hash of cost and profit centers based on the center name.
|
||||
|
||||
if (qbo) {
|
||||
//Going to always assume that we need to apply GST and PST for labor.
|
||||
const taxAccountCode = findTaxCode(
|
||||
{
|
||||
local: false,
|
||||
federal: false,
|
||||
state: false,
|
||||
},
|
||||
bodyshop.md_responsibility_centers.sales_tax_codes
|
||||
);
|
||||
const QboTaxId = taxCodes[taxAccountCode];
|
||||
InvoiceLineAdd.push({
|
||||
DetailType: "SalesItemLineDetail",
|
||||
Amount: Dinero({
|
||||
amount: Math.round((payer.amount || 0) * 100),
|
||||
}).toFormat(DineroQbFormat),
|
||||
SalesItemLineDetail: {
|
||||
...(jobs_by_pk.class
|
||||
? { ClassRef: { value: classes[jobs_by_pk.class] } }
|
||||
: {}),
|
||||
ItemRef: {
|
||||
value: items[responsibilityCenters.qb_multiple_payers?.accountitem],
|
||||
},
|
||||
TaxCodeRef: {
|
||||
value: QboTaxId,
|
||||
},
|
||||
Qty: 1,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
InvoiceLineAdd.push({
|
||||
ItemRef: {
|
||||
FullName: responsibilityCenters.qb_multiple_payers?.accountitem,
|
||||
},
|
||||
Desc: `${payer.name} Liability`,
|
||||
Quantity: 1,
|
||||
Amount: Dinero({
|
||||
amount: Math.round((payer.amount || 0) * 100),
|
||||
}).toFormat(DineroQbFormat),
|
||||
SalesTaxCodeRef: {
|
||||
FullName: "E",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return InvoiceLineAdd;
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const moment = require("moment-timezone");
|
||||
|
||||
const GraphQLClient = require("graphql-request").GraphQLClient;
|
||||
const { generateOwnerTier } = require("../qbxml/qbxml-utils");
|
||||
const { createMultiQbPayerLines } = require("../qb-receivables-lines");
|
||||
|
||||
exports.default = async (req, res) => {
|
||||
const oauthClient = new OAuthClient({
|
||||
@@ -115,7 +116,13 @@ exports.default = async (req, res) => {
|
||||
}
|
||||
|
||||
//Query for the Job or Create it.
|
||||
jobTier = await QueryJob(oauthClient, qbo_realmId, req, job);
|
||||
jobTier = await QueryJob(
|
||||
oauthClient,
|
||||
qbo_realmId,
|
||||
req,
|
||||
job,
|
||||
isThreeTier ? ownerCustomerTier : null // ownerCustomerTier || insCoCustomerTier
|
||||
);
|
||||
|
||||
// Need to validate that the job tier is associated to the right individual?
|
||||
|
||||
@@ -140,6 +147,65 @@ exports.default = async (req, res) => {
|
||||
jobTier
|
||||
);
|
||||
|
||||
if (job.qb_multiple_payers && job.qb_multiple_payers.length > 0) {
|
||||
for (const [index, payer] of job.qb_multiple_payers.entries()) {
|
||||
//do the thing.
|
||||
|
||||
//Create the source level.
|
||||
let insCoCustomerTier, ownerCustomerTier, jobTier;
|
||||
|
||||
//Insert the insurance company tier.
|
||||
//Query for top level customer, the insurance company name.
|
||||
insCoCustomerTier = await QueryInsuranceCo(
|
||||
oauthClient,
|
||||
qbo_realmId,
|
||||
req,
|
||||
{ ...job, ins_co_nm: payer.name }
|
||||
);
|
||||
if (!insCoCustomerTier) {
|
||||
//Creating the Insurance Customer.
|
||||
insCoCustomerTier = await InsertInsuranceCo(
|
||||
oauthClient,
|
||||
qbo_realmId,
|
||||
req,
|
||||
{ ...job, ins_co_nm: payer.name },
|
||||
bodyshop
|
||||
);
|
||||
}
|
||||
//Query for the Job or Create it.
|
||||
jobTier = await QueryJob(
|
||||
oauthClient,
|
||||
qbo_realmId,
|
||||
req,
|
||||
job,
|
||||
insCoCustomerTier
|
||||
);
|
||||
// Need to validate that the job tier is associated to the right individual?
|
||||
if (!jobTier) {
|
||||
jobTier = await InsertJob(
|
||||
oauthClient,
|
||||
qbo_realmId,
|
||||
req,
|
||||
job,
|
||||
insCoCustomerTier
|
||||
);
|
||||
}
|
||||
|
||||
//Create the RO level
|
||||
|
||||
await InsertInvoiceMultiPayerInvoice(
|
||||
oauthClient,
|
||||
qbo_realmId,
|
||||
req,
|
||||
job,
|
||||
bodyshop,
|
||||
jobTier,
|
||||
payer,
|
||||
`-${index + 1}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// //No error. Mark the job exported & insert export log.
|
||||
if (elgen) {
|
||||
const result = await client
|
||||
@@ -212,7 +278,7 @@ async function QueryInsuranceCo(oauthClient, qbo_realmId, req, job) {
|
||||
"query",
|
||||
`select * From Customer where DisplayName = '${StandardizeName(
|
||||
job.ins_co_nm.trim()
|
||||
)}'`
|
||||
)}' and Active = true`
|
||||
),
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -284,7 +350,7 @@ async function QueryOwner(oauthClient, qbo_realmId, req, job) {
|
||||
"query",
|
||||
`select * From Customer where DisplayName = '${StandardizeName(
|
||||
ownerName
|
||||
)}'`
|
||||
)}' and Active = true`
|
||||
),
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -348,12 +414,12 @@ async function InsertOwner(
|
||||
}
|
||||
}
|
||||
exports.InsertOwner = InsertOwner;
|
||||
async function QueryJob(oauthClient, qbo_realmId, req, job) {
|
||||
async function QueryJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
||||
const result = await oauthClient.makeApiCall({
|
||||
url: urlBuilder(
|
||||
qbo_realmId,
|
||||
"query",
|
||||
`select * From Customer where DisplayName = '${job.ro_number}'`
|
||||
`select * From Customer where DisplayName = '${job.ro_number}' and Active = true`
|
||||
),
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -365,9 +431,14 @@ async function QueryJob(oauthClient, qbo_realmId, req, job) {
|
||||
result.json &&
|
||||
result.json.QueryResponse &&
|
||||
result.json.QueryResponse.Customer &&
|
||||
result.json.QueryResponse.Customer[0]
|
||||
(parentTierRef
|
||||
? result.json.QueryResponse.Customer.find(
|
||||
(x) => x.ParentRef.value === parentTierRef.Id
|
||||
)
|
||||
: result.json.QueryResponse.Customer[0])
|
||||
);
|
||||
}
|
||||
|
||||
exports.QueryJob = QueryJob;
|
||||
async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
||||
const Customer = {
|
||||
@@ -602,3 +673,137 @@ async function InsertInvoice(
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async function InsertInvoiceMultiPayerInvoice(
|
||||
oauthClient,
|
||||
qbo_realmId,
|
||||
req,
|
||||
job,
|
||||
bodyshop,
|
||||
parentTierRef,
|
||||
payer,
|
||||
suffix
|
||||
) {
|
||||
const { items, taxCodes, classes } = await QueryMetaData(
|
||||
oauthClient,
|
||||
qbo_realmId,
|
||||
req
|
||||
);
|
||||
const InvoiceLineAdd = createMultiQbPayerLines({
|
||||
bodyshop,
|
||||
jobs_by_pk: job,
|
||||
qbo: true,
|
||||
items,
|
||||
taxCodes,
|
||||
classes,
|
||||
payer,
|
||||
suffix,
|
||||
});
|
||||
|
||||
const invoiceObj = {
|
||||
Line: InvoiceLineAdd,
|
||||
TxnDate: moment(job.date_invoiced)
|
||||
.tz(bodyshop.timezone)
|
||||
.format("YYYY-MM-DD"),
|
||||
DocNumber: job.ro_number + suffix,
|
||||
...(job.class ? { ClassRef: { value: classes[job.class] } } : {}),
|
||||
CustomerMemo: {
|
||||
value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${
|
||||
job.po_number ? `PO No: ${job.po_number}` : ``
|
||||
} Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
|
||||
job.v_model_desc || ""
|
||||
} ${job.v_vin || ""} ${job.plate_no || ""} `.trim(),
|
||||
},
|
||||
CustomerRef: {
|
||||
value: parentTierRef.Id,
|
||||
},
|
||||
...(bodyshop.accountingconfig.qbo_departmentid &&
|
||||
bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && {
|
||||
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid },
|
||||
}),
|
||||
CustomField: [
|
||||
...(bodyshop.accountingconfig.ReceivableCustomField1
|
||||
? [
|
||||
{
|
||||
DefinitionId: "1",
|
||||
StringValue:
|
||||
job[bodyshop.accountingconfig.ReceivableCustomField1],
|
||||
Type: "StringType",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(bodyshop.accountingconfig.ReceivableCustomField2
|
||||
? [
|
||||
{
|
||||
DefinitionId: "2",
|
||||
StringValue:
|
||||
job[bodyshop.accountingconfig.ReceivableCustomField2],
|
||||
Type: "StringType",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(bodyshop.accountingconfig.ReceivableCustomField3
|
||||
? [
|
||||
{
|
||||
DefinitionId: "3",
|
||||
StringValue:
|
||||
job[bodyshop.accountingconfig.ReceivableCustomField3],
|
||||
Type: "StringType",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
...(bodyshop.accountingconfig &&
|
||||
bodyshop.accountingconfig.qbo &&
|
||||
bodyshop.accountingconfig.qbo_usa &&
|
||||
bodyshop.region_config.includes("CA_") && {
|
||||
TxnTaxDetail: {
|
||||
TxnTaxCodeRef: {
|
||||
value:
|
||||
taxCodes[
|
||||
bodyshop.md_responsibility_centers.taxes.state.accountitem
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
...(bodyshop.accountingconfig.printlater
|
||||
? { PrintStatus: "NeedToPrint" }
|
||||
: {}),
|
||||
...(bodyshop.accountingconfig.emaillater && job.ownr_ea
|
||||
? { EmailStatus: "NeedToSend" }
|
||||
: {}),
|
||||
BillAddr: {
|
||||
Line3: `${job.ownr_city || ""}, ${job.ownr_st || ""} ${
|
||||
job.ownr_zip || ""
|
||||
}`.trim(),
|
||||
Line2: job.ownr_addr1 || "",
|
||||
Line1: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
|
||||
job.ownr_co_nm || ""
|
||||
}`,
|
||||
},
|
||||
};
|
||||
|
||||
logger.log("qbo-receivable-objectlog", "DEBUG", req.user.email, job.id, {
|
||||
invoiceObj,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await oauthClient.makeApiCall({
|
||||
url: urlBuilder(qbo_realmId, "invoice"),
|
||||
method: "POST",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(invoiceObj),
|
||||
});
|
||||
setNewRefreshToken(req.user.email, result);
|
||||
return result && result.json && result.json.Invoice;
|
||||
} catch (error) {
|
||||
logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, {
|
||||
error,
|
||||
method: "InsertOwner",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user