IO-2054 QBO Split AR
This commit is contained in:
@@ -24557,6 +24557,53 @@
|
|||||||
</concept_node>
|
</concept_node>
|
||||||
</children>
|
</children>
|
||||||
</folder_node>
|
</folder_node>
|
||||||
|
<folder_node>
|
||||||
|
<name>qb_multiple_payers</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>amount</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>name</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>es-MX</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
<translation>
|
||||||
|
<language>fr-CA</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
<concept_node>
|
<concept_node>
|
||||||
<name>queued_for_parts</name>
|
<name>queued_for_parts</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
|
|||||||
@@ -4535,6 +4535,24 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
|
|||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
<LayoutFormRow header={<div>Multiple Payers Item</div>}>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bodyshop.fields.responsibilitycenter_accountitem")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
name={[
|
||||||
|
"md_responsibility_centers",
|
||||||
|
"qb_multiple_payers",
|
||||||
|
"accountitem",
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</LayoutFormRow>
|
||||||
<Typography.Title level={4}>
|
<Typography.Title level={4}>
|
||||||
{t("bodyshop.labels.responsibilitycenters.sales_tax_codes")}
|
{t("bodyshop.labels.responsibilitycenters.sales_tax_codes")}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
|
|||||||
@@ -1890,6 +1890,7 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
|
|||||||
actual_in
|
actual_in
|
||||||
kmin
|
kmin
|
||||||
kmout
|
kmout
|
||||||
|
qb_multiple_payers
|
||||||
joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) {
|
joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) {
|
||||||
id
|
id
|
||||||
removed
|
removed
|
||||||
|
|||||||
@@ -1,34 +1,38 @@
|
|||||||
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { useApolloClient, useMutation } from "@apollo/client";
|
import { useApolloClient, useMutation } from "@apollo/client";
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Form,
|
|
||||||
notification,
|
|
||||||
Popconfirm,
|
|
||||||
Space,
|
|
||||||
Alert,
|
Alert,
|
||||||
|
Button,
|
||||||
Divider,
|
Divider,
|
||||||
PageHeader,
|
Form,
|
||||||
InputNumber,
|
|
||||||
Input,
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
notification,
|
||||||
|
PageHeader,
|
||||||
|
Popconfirm,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
//import { useHistory } from "react-router-dom";
|
//import { useHistory } from "react-router-dom";
|
||||||
|
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
import moment from "moment";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import DateTimePicker from "../../components/form-date-time-picker/form-date-time-picker.component";
|
||||||
import FormsFieldChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
|
import FormsFieldChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
|
import CurrencyInput from "../../components/form-items-formatted/currency-form-item.component";
|
||||||
import JobsScoreboardAdd from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component";
|
import JobsScoreboardAdd from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component";
|
||||||
import JobsCloseAutoAllocate from "../../components/jobs-close-auto-allocate/jobs-close-auto-allocate.component";
|
import JobsCloseAutoAllocate from "../../components/jobs-close-auto-allocate/jobs-close-auto-allocate.component";
|
||||||
import JobsCloseLines from "../../components/jobs-close-lines/jobs-close-lines.component";
|
import JobsCloseLines from "../../components/jobs-close-lines/jobs-close-lines.component";
|
||||||
|
import LayoutFormRow from "../../components/layout-form-row/layout-form-row.component";
|
||||||
import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.queries";
|
import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.queries";
|
||||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import LayoutFormRow from "../../components/layout-form-row/layout-form-row.component";
|
|
||||||
import DateTimePicker from "../../components/form-date-time-picker/form-date-time-picker.component";
|
|
||||||
import moment from "moment";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -42,6 +46,11 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
|
|||||||
// const history = useHistory();
|
// const history = useHistory();
|
||||||
const [closeJob] = useMutation(UPDATE_JOB);
|
const [closeJob] = useMutation(UPDATE_JOB);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { Qb_Multi_Ar } = useTreatments(
|
||||||
|
["Qb_Multi_Ar"],
|
||||||
|
{},
|
||||||
|
bodyshop && bodyshop.imexshopid
|
||||||
|
);
|
||||||
|
|
||||||
const handleFinish = async ({ removefromproduction, ...values }) => {
|
const handleFinish = async ({ removefromproduction, ...values }) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -65,6 +74,9 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
|
|||||||
kmout: values.kmout,
|
kmout: values.kmout,
|
||||||
dms_allocation: values.dms_allocation,
|
dms_allocation: values.dms_allocation,
|
||||||
...(removefromproduction ? { inproduction: false } : {}),
|
...(removefromproduction ? { inproduction: false } : {}),
|
||||||
|
...(values.qb_multiple_payers
|
||||||
|
? { qb_multiple_payers: values.qb_multiple_payers }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
refetchQueries: ["QUERY_JOB_CLOSE_DETAILS"],
|
refetchQueries: ["QUERY_JOB_CLOSE_DETAILS"],
|
||||||
@@ -127,6 +139,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
|
|||||||
kmin: job.kmin,
|
kmin: job.kmin,
|
||||||
kmout: job.kmout,
|
kmout: job.kmout,
|
||||||
dms_allocation: job.dms_allocation,
|
dms_allocation: job.dms_allocation,
|
||||||
|
qb_multiple_payers: job.qb_multiple_payers,
|
||||||
}}
|
}}
|
||||||
scrollToFirstError
|
scrollToFirstError
|
||||||
>
|
>
|
||||||
@@ -312,6 +325,76 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
{Qb_Multi_Ar.treatment === "on" && (
|
||||||
|
<>
|
||||||
|
<Form.List name={["qb_multiple_payers"]}>
|
||||||
|
{(fields, { add, remove }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<Form.Item key={field.key}>
|
||||||
|
<Space>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.qb_multiple_payers.name")}
|
||||||
|
key={`${index}name`}
|
||||||
|
name={[field.name, "name"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
style={{ minWidth: "12rem" }}
|
||||||
|
disabled={jobRO}
|
||||||
|
>
|
||||||
|
{bodyshop.md_ins_cos.map((s) => (
|
||||||
|
<Select.Option key={s.name} value={s.name}>
|
||||||
|
{s.name}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.qb_multiple_payers.amount")}
|
||||||
|
key={`${index}amount`}
|
||||||
|
name={[field.name, "amount"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput min={0} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<DeleteFilled
|
||||||
|
disabled={jobRO}
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
disabled={jobRO}
|
||||||
|
onClick={() => {
|
||||||
|
if (fields.length < 3) add();
|
||||||
|
}}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{t("jobs.actions.dms.addpayer")}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.List>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Divider />
|
<Divider />
|
||||||
<JobsCloseLines job={job} />
|
<JobsCloseLines job={job} />
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1468,6 +1468,10 @@
|
|||||||
"production_vars": {
|
"production_vars": {
|
||||||
"note": "Production Note"
|
"note": "Production Note"
|
||||||
},
|
},
|
||||||
|
"qb_multiple_payers": {
|
||||||
|
"amount": "Amount",
|
||||||
|
"name": "Name"
|
||||||
|
},
|
||||||
"queued_for_parts": "Queued for Parts",
|
"queued_for_parts": "Queued for Parts",
|
||||||
"rate_ats": "ATS Rate",
|
"rate_ats": "ATS Rate",
|
||||||
"rate_la1": "LA1",
|
"rate_la1": "LA1",
|
||||||
|
|||||||
@@ -1468,6 +1468,10 @@
|
|||||||
"production_vars": {
|
"production_vars": {
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
|
"qb_multiple_payers": {
|
||||||
|
"amount": "",
|
||||||
|
"name": ""
|
||||||
|
},
|
||||||
"queued_for_parts": "",
|
"queued_for_parts": "",
|
||||||
"rate_ats": "",
|
"rate_ats": "",
|
||||||
"rate_la1": "Tarifa LA1",
|
"rate_la1": "Tarifa LA1",
|
||||||
|
|||||||
@@ -1468,6 +1468,10 @@
|
|||||||
"production_vars": {
|
"production_vars": {
|
||||||
"note": ""
|
"note": ""
|
||||||
},
|
},
|
||||||
|
"qb_multiple_payers": {
|
||||||
|
"amount": "",
|
||||||
|
"name": ""
|
||||||
|
},
|
||||||
"queued_for_parts": "",
|
"queued_for_parts": "",
|
||||||
"rate_ats": "",
|
"rate_ats": "",
|
||||||
"rate_la1": "Taux LA1",
|
"rate_la1": "Taux LA1",
|
||||||
|
|||||||
@@ -3139,6 +3139,7 @@
|
|||||||
- po_number
|
- po_number
|
||||||
- policy_no
|
- policy_no
|
||||||
- production_vars
|
- production_vars
|
||||||
|
- qb_multiple_payers
|
||||||
- queued_for_parts
|
- queued_for_parts
|
||||||
- rate_ats
|
- rate_ats
|
||||||
- rate_la1
|
- rate_la1
|
||||||
@@ -3401,6 +3402,7 @@
|
|||||||
- po_number
|
- po_number
|
||||||
- policy_no
|
- policy_no
|
||||||
- production_vars
|
- production_vars
|
||||||
|
- qb_multiple_payers
|
||||||
- queued_for_parts
|
- queued_for_parts
|
||||||
- rate_ats
|
- rate_ats
|
||||||
- rate_la1
|
- rate_la1
|
||||||
@@ -3673,6 +3675,7 @@
|
|||||||
- po_number
|
- po_number
|
||||||
- policy_no
|
- policy_no
|
||||||
- production_vars
|
- production_vars
|
||||||
|
- qb_multiple_payers
|
||||||
- queued_for_parts
|
- queued_for_parts
|
||||||
- rate_ats
|
- rate_ats
|
||||||
- rate_la1
|
- rate_la1
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."jobs" add column "qb_multiple_payers" jsonb
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."jobs" add column "qb_multiple_payers" jsonb
|
||||||
|
null;
|
||||||
43065
logs/oAuthClient-log.log
43065
logs/oAuthClient-log.log
File diff suppressed because one or more lines are too long
@@ -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;
|
return InvoiceLineAdd;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -667,3 +717,65 @@ const findTaxCode = ({ local, state, federal }, taxcode) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
exports.findTaxCode = findTaxCode;
|
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 GraphQLClient = require("graphql-request").GraphQLClient;
|
||||||
const { generateOwnerTier } = require("../qbxml/qbxml-utils");
|
const { generateOwnerTier } = require("../qbxml/qbxml-utils");
|
||||||
|
const { createMultiQbPayerLines } = require("../qb-receivables-lines");
|
||||||
|
|
||||||
exports.default = async (req, res) => {
|
exports.default = async (req, res) => {
|
||||||
const oauthClient = new OAuthClient({
|
const oauthClient = new OAuthClient({
|
||||||
@@ -115,7 +116,13 @@ exports.default = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Query for the Job or Create it.
|
//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?
|
// Need to validate that the job tier is associated to the right individual?
|
||||||
|
|
||||||
@@ -140,6 +147,65 @@ exports.default = async (req, res) => {
|
|||||||
jobTier
|
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.
|
// //No error. Mark the job exported & insert export log.
|
||||||
if (elgen) {
|
if (elgen) {
|
||||||
const result = await client
|
const result = await client
|
||||||
@@ -212,7 +278,7 @@ async function QueryInsuranceCo(oauthClient, qbo_realmId, req, job) {
|
|||||||
"query",
|
"query",
|
||||||
`select * From Customer where DisplayName = '${StandardizeName(
|
`select * From Customer where DisplayName = '${StandardizeName(
|
||||||
job.ins_co_nm.trim()
|
job.ins_co_nm.trim()
|
||||||
)}'`
|
)}' and Active = true`
|
||||||
),
|
),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -284,7 +350,7 @@ async function QueryOwner(oauthClient, qbo_realmId, req, job) {
|
|||||||
"query",
|
"query",
|
||||||
`select * From Customer where DisplayName = '${StandardizeName(
|
`select * From Customer where DisplayName = '${StandardizeName(
|
||||||
ownerName
|
ownerName
|
||||||
)}'`
|
)}' and Active = true`
|
||||||
),
|
),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -348,12 +414,12 @@ async function InsertOwner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
exports.InsertOwner = 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({
|
const result = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(
|
url: urlBuilder(
|
||||||
qbo_realmId,
|
qbo_realmId,
|
||||||
"query",
|
"query",
|
||||||
`select * From Customer where DisplayName = '${job.ro_number}'`
|
`select * From Customer where DisplayName = '${job.ro_number}' and Active = true`
|
||||||
),
|
),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -365,9 +431,14 @@ async function QueryJob(oauthClient, qbo_realmId, req, job) {
|
|||||||
result.json &&
|
result.json &&
|
||||||
result.json.QueryResponse &&
|
result.json.QueryResponse &&
|
||||||
result.json.QueryResponse.Customer &&
|
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;
|
exports.QueryJob = QueryJob;
|
||||||
async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
||||||
const Customer = {
|
const Customer = {
|
||||||
@@ -602,3 +673,137 @@ async function InsertInvoice(
|
|||||||
throw error;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
|
|||||||
storage_payable
|
storage_payable
|
||||||
adjustment_bottom_line
|
adjustment_bottom_line
|
||||||
state_tax_rate
|
state_tax_rate
|
||||||
|
qb_multiple_payers
|
||||||
owner {
|
owner {
|
||||||
accountingid
|
accountingid
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user