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>
<name>payments</name>
<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>
<name>fields</name>
<children>

View File

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

View File

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

View File

@@ -80,27 +80,32 @@ export function JobsCloseExportButton({ bodyshop, jobId, disabled }) {
})
);
} 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"]({
message: t("jobs.successes.exported"),
});
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
});
}
// 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"]({
// message: t("jobs.successes.exported"),
// });
// } else {
// notification["error"]({
// message: t("jobs.errors.exporting", {
// error: JSON.stringify(jobUpdateResponse.error),
// }),
// });
// }
}
setLoading(false);

View File

@@ -73,14 +73,21 @@ export function JobsCloseLines({ bodyshop, joblines }) {
name={[field.name, "profitcenter_part"]}
rules={[
{
required:
!!joblines[index].part_type &&
!!joblines[index].act_price,
required: !!joblines[index].act_price,
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) => (
<Select.Option key={p.name} value={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) => (
<Select.Option key={p.name} value={p.name}>
{p.name}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,9 +59,45 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
ownr_city
ownr_st
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 {
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}}}) {
id
@@ -136,6 +172,8 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = `
stripeid
exportedat
stripeid
type
payer
}
}
`;