Further work to refactor job costing + qb posting. BOD-383

This commit is contained in:
Patrick Fic
2020-09-14 16:57:40 -07:00
parent eff49e3d25
commit 379c12c7bb
13 changed files with 239 additions and 96 deletions

View File

@@ -20162,6 +20162,32 @@
<folder_node> <folder_node>
<name>payments</name> <name>payments</name>
<children> <children>
<folder_node>
<name>errors</name>
<children>
<concept_node>
<name>exporting</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>
<folder_node> <folder_node>
<name>fields</name> <name>fields</name>
<children> <children>

View File

@@ -153,19 +153,20 @@ export default function AccountingPayablesTableComponent({
loading={loading} loading={loading}
title={() => { title={() => {
return ( return (
<div> <div className="imex-table-header">
<PaymentsExportAllButton
paymentIds={selectedPayments}
disabled={transInProgress || selectedPayments.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedPayments}
/>
<Input <Input
className="imex-table-header__search"
value={state.search} value={state.search}
onChange={handleSearch} onChange={handleSearch}
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
allowClear allowClear
/> />
<PaymentsExportAllButton
paymentIds={selectedPayments}
disabled={transInProgress}
loadingCallback={setTransInProgress}
completedCallback={setSelectedPayments}
/>
</div> </div>
); );
}} }}

View File

@@ -38,6 +38,7 @@ function JobLinesUpsertModalContainer({
.then((r) => { .then((r) => {
if (jobLineEditModal.actions.refetch) if (jobLineEditModal.actions.refetch)
jobLineEditModal.actions.refetch(); jobLineEditModal.actions.refetch();
//Need to recalcuate totals.
toggleModalVisible(); toggleModalVisible();
notification["success"]({ notification["success"]({
message: t("joblines.successes.created"), message: t("joblines.successes.created"),

View File

@@ -80,27 +80,32 @@ export function JobsCloseExportButton({ bodyshop, jobId, disabled }) {
}) })
); );
} else { } else {
const jobUpdateResponse = await updateJob({
variables: {
jobId: jobId,
job: {
status: bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: new Date(),
},
},
});
if (!!!jobUpdateResponse.errors) {
notification["success"]({ // const jobUpdateResponse = await updateJob({
message: t("jobs.successes.exported"), // variables: {
}); // jobId: jobId,
} else { // job: {
notification["error"]({ // status: bodyshop.md_ro_statuses.default_exported || "Exported*",
message: t("jobs.errors.exporting", { // date_exported: new Date(),
error: JSON.stringify(jobUpdateResponse.error), // },
}), // },
}); // });
}
// if (!!!jobUpdateResponse.errors) {
// notification["success"]({
// message: t("jobs.successes.exported"),
// });
// } else {
// notification["error"]({
// message: t("jobs.errors.exporting", {
// error: JSON.stringify(jobUpdateResponse.error),
// }),
// });
// }
} }
setLoading(false); setLoading(false);

View File

@@ -73,14 +73,21 @@ export function JobsCloseLines({ bodyshop, joblines }) {
name={[field.name, "profitcenter_part"]} name={[field.name, "profitcenter_part"]}
rules={[ rules={[
{ {
required: required: !!joblines[index].act_price,
!!joblines[index].part_type &&
!!joblines[index].act_price,
message: t("general.validation.required"), message: t("general.validation.required"),
}, },
]} ]}
> >
<Select allowClear> <Select
allowClear
optionFilterProp="children"
showSearch
filterOption={(input, option) =>
option.children
.toLowerCase()
.indexOf(input.toLowerCase()) >= 0
}
>
{bodyshop.md_responsibility_centers.profits.map((p) => ( {bodyshop.md_responsibility_centers.profits.map((p) => (
<Select.Option key={p.name} value={p.name}> <Select.Option key={p.name} value={p.name}>
{p.name} {p.name}
@@ -99,7 +106,16 @@ export function JobsCloseLines({ bodyshop, joblines }) {
}, },
]} ]}
> >
<Select allowClear> <Select
allowClear
optionFilterProp="children"
showSearch
filterOption={(input, option) =>
option.children
.toLowerCase()
.indexOf(input.toLowerCase()) >= 0
}
>
{bodyshop.md_responsibility_centers.profits.map((p) => ( {bodyshop.md_responsibility_centers.profits.map((p) => (
<Select.Option key={p.name} value={p.name}> <Select.Option key={p.name} value={p.name}>
{p.name} {p.name}

View File

@@ -151,7 +151,6 @@ export const GET_JOB_LINES_TO_ENTER_INVOICE = gql`
// } // }
export const generateJobLinesUpdatesForInvoicing = (joblines) => { export const generateJobLinesUpdatesForInvoicing = (joblines) => {
console.log("generateJobLinesUpdatesForInvoicing -> joblines", joblines);
const updates = joblines.reduce((acc, jl, idx) => { const updates = joblines.reduce((acc, jl, idx) => {
return ( return (
acc + acc +

View File

@@ -1,5 +1,5 @@
import { Button, Form, Space, notification, Popconfirm } from "antd"; import { Button, Form, Space, notification, Popconfirm } from "antd";
import React 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 { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -24,22 +24,33 @@ export function JobsCloseComponent({ job, bodyshop }) {
const client = useApolloClient(); const client = useApolloClient();
const history = useHistory(); const history = useHistory();
const [closeJob] = useMutation(UPDATE_JOB); const [closeJob] = useMutation(UPDATE_JOB);
const [loading, setLoading] = useState(false);
// useEffect(() => { // useEffect(() => {
// //if (job && form) form.setFields({ joblines: job.joblines }); // //if (job && form) form.setFields({ joblines: job.joblines });
// }, [job, form]); // }, [job, form]);
const handleFinish = async (values) => { const handleFinish = async (values) => {
console.log(values); setLoading(true);
const result = await client.mutate({ const result = await client.mutate({
mutation: generateJobLinesUpdatesForInvoicing(values.joblines), mutation: generateJobLinesUpdatesForInvoicing(values.joblines),
}); });
console.log("result.data", result.data); if (!!!result.errors) {
notification["success"]({ message: t("job.successes.saved") });
form.resetFields();
} else {
notification["error"]({
message: t("job.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
form.resetFields(); form.resetFields();
form.resetFields(); form.resetFields();
setLoading(false);
}; };
const handleClose = async () => { const handleClose = async () => {
setLoading(true);
const result = await closeJob({ const result = await closeJob({
variables: { variables: {
jobId: job.id, jobId: job.id,
@@ -54,6 +65,7 @@ export function JobsCloseComponent({ job, bodyshop }) {
notification["success"]({ message: t("job.successes.closed") }); notification["success"]({ message: t("job.successes.closed") });
history.push(`/manage/jobs/${job.id}`); history.push(`/manage/jobs/${job.id}`);
} else { } else {
setLoading(false);
notification["error"]({ notification["error"]({
message: t("job.errors.closing", { message: t("job.errors.closing", {
error: JSON.stringify(result.errors), error: JSON.stringify(result.errors),
@@ -69,6 +81,7 @@ export function JobsCloseComponent({ job, bodyshop }) {
form={form} form={form}
onFinish={handleFinish} onFinish={handleFinish}
initialValues={{ joblines: job.joblines }} initialValues={{ joblines: job.joblines }}
scrollToFirstError
> >
<Space> <Space>
<JobsCloseAutoAllocate <JobsCloseAutoAllocate
@@ -77,7 +90,7 @@ export function JobsCloseComponent({ job, bodyshop }) {
disabled={!!job.date_exported} disabled={!!job.date_exported}
/> />
<Button onClick={() => form.submit()}> <Button loading={loading} onClick={() => form.submit()}>
{t("general.actions.save")} {t("general.actions.save")}
</Button> </Button>
@@ -87,7 +100,9 @@ export function JobsCloseComponent({ job, bodyshop }) {
cancelText={t("general.labels.no")} cancelText={t("general.labels.no")}
title={t("jobs.labels.closeconfirm")} title={t("jobs.labels.closeconfirm")}
> >
<Button type="danger">{t("general.actions.close")}</Button> <Button loading={loading} type="danger">
{t("general.actions.close")}
</Button>
</Popconfirm> </Popconfirm>
<JobsScoreboardAdd job={job} disabled={false} /> <JobsScoreboardAdd job={job} disabled={false} />

View File

@@ -1242,6 +1242,9 @@
} }
}, },
"payments": { "payments": {
"errors": {
"exporting": "Error exporting payment(s). {{error}}"
},
"fields": { "fields": {
"amount": "Amount", "amount": "Amount",
"created_at": "Created At", "created_at": "Created At",

View File

@@ -1242,6 +1242,9 @@
} }
}, },
"payments": { "payments": {
"errors": {
"exporting": ""
},
"fields": { "fields": {
"amount": "", "amount": "",
"created_at": "", "created_at": "",

View File

@@ -1242,6 +1242,9 @@
} }
}, },
"payments": { "payments": {
"errors": {
"exporting": ""
},
"fields": { "fields": {
"amount": "", "amount": "",
"created_at": "", "created_at": "",

View File

@@ -76,11 +76,14 @@ const generatePayment = (payment) => {
TotalAmount: Dinero({ TotalAmount: Dinero({
amount: Math.round(payment.amount * 100), amount: Math.round(payment.amount * 100),
}).toFormat(DineroQbFormat), }).toFormat(DineroQbFormat),
PaymentMethodRef: {
FullName: payment.type,
},
Memo: `RO ${payment.job.ro_number || ""} OWNER ${ Memo: `RO ${payment.job.ro_number || ""} OWNER ${
payment.job.ownr_fn || "" payment.job.ownr_fn || ""
} ${payment.job.ownr_ln || ""} ${payment.job.ownr_co_nm || ""} ${ } ${payment.job.ownr_ln || ""} ${payment.job.ownr_co_nm || ""} ${
payment.stripeid payment.stripeid || ""
}`, } ${payment.payer ? ` PAID BY ${payment.payer}` : ""}`,
IsAutoApply: true, IsAutoApply: true,
// AppliedToTxnAdd:{ // AppliedToTxnAdd:{
// T // T

View File

@@ -28,7 +28,6 @@ exports.default = async (req, res) => {
const result = await client const result = await client
.setHeaders({ Authorization: BearerToken }) .setHeaders({ Authorization: BearerToken })
.request(queries.QUERY_JOBS_FOR_RECEIVABLES_EXPORT, { ids: jobIds }); .request(queries.QUERY_JOBS_FOR_RECEIVABLES_EXPORT, { ids: jobIds });
console.log("result", result);
const { jobs } = result; const { jobs } = result;
const { bodyshops } = result; const { bodyshops } = result;
const QbXmlToExecute = []; const QbXmlToExecute = [];
@@ -174,44 +173,74 @@ const generateJobQbxml = (
const generateInvoiceQbxml = (jobs_by_pk, bodyshop) => { const generateInvoiceQbxml = (jobs_by_pk, bodyshop) => {
//Build the Invoice XML file. //Build the Invoice XML file.
const InvoiceLineAdd = []; const InvoiceLineAdd = [];
const invoice_allocation = jobs_by_pk.invoice_allocation; const responsibilityCenters = bodyshop.md_responsibility_centers;
Object.keys(invoice_allocation.partsAllocations).forEach(
(partsAllocationKey) => { jobs_by_pk.joblines.map((jobline) => {
if ( if (jobline.profitcenter_part && jobline.act_price) {
!!!invoice_allocation.partsAllocations[partsAllocationKey].allocations const DineroAmount = Dinero({
) amount: Math.round(jobline.act_price * 100),
return;
invoice_allocation.partsAllocations[
partsAllocationKey
].allocations.forEach((alloc) => {
InvoiceLineAdd.push(
generateInvoiceLine(
jobs_by_pk,
alloc,
bodyshop.md_responsibility_centers
)
);
}); });
} const account = responsibilityCenters.profits.find(
); (i) => jobline.profitcenter_part.toLowerCase() === i.name.toLowerCase()
Object.keys(invoice_allocation.labMatAllocations).forEach((AllocationKey) => { );
if (!!!invoice_allocation.labMatAllocations[AllocationKey].allocations)
return; if (!!!account) {
invoice_allocation.labMatAllocations[AllocationKey].allocations.forEach( throw new Error(
(alloc) => { `A matching account does not exist for the allocation. Center: ${center}`
InvoiceLineAdd.push(
generateInvoiceLine(
jobs_by_pk,
alloc,
bodyshop.md_responsibility_centers
)
); );
} }
);
InvoiceLineAdd.push({
ItemRef: { FullName: account.accountitem },
Desc: `${account.accountdesc} - ${jobline.line_desc}`,
Quantity: jobline.part_qty,
Rate: DineroAmount.toFormat(DineroQbFormat),
//Amount: DineroAmount.toFormat(DineroQbFormat),
SalesTaxCodeRef: {
FullName: "E",
},
});
}
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
),
});
console.log(
"Rate",
jobline.mod_lbr_ty,
jobs_by_pk[`rate_${jobline.mod_lbr_ty.toLowerCase()}`]
);
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 allocation. Center: ${center}`
);
}
InvoiceLineAdd.push({
ItemRef: { FullName: account.accountitem },
Desc: `${account.accountdesc} - ${jobline.op_code_desc} ${jobline.line_desc}`,
Quantity: jobline.mod_lb_hrs,
Rate: DineroAmount.toFormat(DineroQbFormat),
//Amount: DineroAmount.toFormat(DineroQbFormat),
SalesTaxCodeRef: {
FullName: "E",
},
});
}
}); });
//Add tax lines //Add tax lines
const job_totals = JSON.parse(jobs_by_pk.job_totals); const job_totals = jobs_by_pk.job_totals;
const federal_tax = Dinero(job_totals.totals.federal_tax); const federal_tax = Dinero(job_totals.totals.federal_tax);
const state_tax = Dinero(job_totals.totals.state_tax); const state_tax = Dinero(job_totals.totals.state_tax);
@@ -288,31 +317,32 @@ const generateInvoiceQbxml = (jobs_by_pk, bodyshop) => {
.end({ pretty: true }); .end({ pretty: true });
const invoiceQbxml_Full = QbXmlUtils.addQbxmlHeader(invoiceQbxml_partial); const invoiceQbxml_Full = QbXmlUtils.addQbxmlHeader(invoiceQbxml_partial);
console.log("invoiceQbxml_Full", invoiceQbxml_Full);
return invoiceQbxml_Full; return invoiceQbxml_Full;
}; };
const generateInvoiceLine = (job, allocation, responsibilityCenters) => { // const generateInvoiceLine = (job, allocation, responsibilityCenters) => {
const { amount, center } = allocation; // const { amount, center } = allocation;
const DineroAmount = Dinero(amount); // const DineroAmount = Dinero(amount);
const account = responsibilityCenters.profits.find( // const account = responsibilityCenters.profits.find(
(i) => i.name.toLowerCase() === center.toLowerCase() // (i) => i.name.toLowerCase() === center.toLowerCase()
); // );
if (!!!account) { // if (!!!account) {
throw new Error( // throw new Error(
`A matching account does not exist for the allocation. Center: ${center}` // `A matching account does not exist for the allocation. Center: ${center}`
); // );
} // }
return { // return {
ItemRef: { FullName: account.accountitem }, // ItemRef: { FullName: account.accountitem },
Desc: account.accountdesc, // Desc: account.accountdesc,
Quantity: 1, // Quantity: 1,
//Rate: 100, // //Rate: 100,
Amount: DineroAmount.toFormat(DineroQbFormat), // Amount: DineroAmount.toFormat(DineroQbFormat),
SalesTaxCodeRef: { // SalesTaxCodeRef: {
FullName: "E", // FullName: "E",
}, // },
}; // };
}; // };

View File

@@ -59,9 +59,45 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
ownr_city ownr_city
ownr_st ownr_st
ins_co_nm ins_co_nm
job_totals
rate_la1
rate_la2
rate_la3
rate_la4
rate_laa
rate_lab
rate_lad
rate_lae
rate_laf
rate_lag
rate_lam
rate_lar
rate_las
rate_lau
rate_ma2s
rate_ma2t
rate_ma3s
rate_mabl
rate_macs
rate_mahw
rate_mapa
rate_mash
rate_matd
owner { owner {
accountingid accountingid
} }
joblines{
id
line_desc
part_type
act_price
mod_lb_hrs
mod_lbr_ty
part_qty
op_code_desc
profitcenter_labor
profitcenter_part
}
} }
bodyshops(where: {associations: {active: {_eq: true}}}) { bodyshops(where: {associations: {active: {_eq: true}}}) {
id id
@@ -136,6 +172,8 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = `
stripeid stripeid
exportedat exportedat
stripeid stripeid
type
payer
} }
} }
`; `;