Merge branch 'release/2022-09-23' of https://bitbucket.org/snaptsoft/bodyshop into release/2022-09-23

This commit is contained in:
Patrick Fic
2022-09-22 14:46:12 -07:00
13 changed files with 43567 additions and 17 deletions

View File

@@ -24578,6 +24578,53 @@
</concept_node>
</children>
</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>
<name>queued_for_parts</name>
<definition_loaded>false</definition_loaded>

View File

@@ -4535,6 +4535,24 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Input />
</Form.Item>
</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}>
{t("bodyshop.labels.responsibilitycenters.sales_tax_codes")}
</Typography.Title>

View File

@@ -1890,6 +1890,7 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
actual_in
kmin
kmout
qb_multiple_payers
joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) {
id
removed

View File

@@ -1,34 +1,38 @@
import { DeleteFilled } from "@ant-design/icons";
import { useApolloClient, useMutation } from "@apollo/client";
import {
Button,
Form,
notification,
Popconfirm,
Space,
Alert,
Button,
Divider,
PageHeader,
InputNumber,
Form,
Input,
InputNumber,
notification,
PageHeader,
Popconfirm,
Select,
Space,
Switch,
} from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
//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 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 CurrencyInput from "../../components/form-items-formatted/currency-form-item.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 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 { UPDATE_JOB } from "../../graphql/jobs.queries";
import { selectJobReadOnly } from "../../redux/application/application.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({
bodyshop: selectBodyshop,
@@ -42,6 +46,11 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
// const history = useHistory();
const [closeJob] = useMutation(UPDATE_JOB);
const [loading, setLoading] = useState(false);
const { Qb_Multi_Ar } = useTreatments(
["Qb_Multi_Ar"],
{},
bodyshop && bodyshop.imexshopid
);
const handleFinish = async ({ removefromproduction, ...values }) => {
setLoading(true);
@@ -65,6 +74,9 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
kmout: values.kmout,
dms_allocation: values.dms_allocation,
...(removefromproduction ? { inproduction: false } : {}),
...(values.qb_multiple_payers
? { qb_multiple_payers: values.qb_multiple_payers }
: {}),
},
},
refetchQueries: ["QUERY_JOB_CLOSE_DETAILS"],
@@ -127,6 +139,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
kmin: job.kmin,
kmout: job.kmout,
dms_allocation: job.dms_allocation,
qb_multiple_payers: job.qb_multiple_payers,
}}
scrollToFirstError
>
@@ -312,6 +325,76 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
</Form.Item>
)}
</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 />
<JobsCloseLines job={job} />
</Form>

View File

@@ -1469,6 +1469,10 @@
"production_vars": {
"note": "Production Note"
},
"qb_multiple_payers": {
"amount": "Amount",
"name": "Name"
},
"queued_for_parts": "Queued for Parts",
"rate_ats": "ATS Rate",
"rate_la1": "LA1",

View File

@@ -1469,6 +1469,10 @@
"production_vars": {
"note": ""
},
"qb_multiple_payers": {
"amount": "",
"name": ""
},
"queued_for_parts": "",
"rate_ats": "",
"rate_la1": "Tarifa LA1",

View File

@@ -1469,6 +1469,10 @@
"production_vars": {
"note": ""
},
"qb_multiple_payers": {
"amount": "",
"name": ""
},
"queued_for_parts": "",
"rate_ats": "",
"rate_la1": "Taux LA1",

View File

@@ -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;

View File

@@ -0,0 +1,2 @@
alter table "public"."jobs" add column "qb_multiple_payers" jsonb
null;

File diff suppressed because one or more lines are too long

View File

@@ -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;
};

View File

@@ -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;
}
}

View File

@@ -191,6 +191,7 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
storage_payable
adjustment_bottom_line
state_tax_rate
qb_multiple_payers
owner {
accountingid
}