Further work to refactor job costing + qb posting. BOD-383
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 +
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -1242,6 +1242,9 @@
|
||||
}
|
||||
},
|
||||
"payments": {
|
||||
"errors": {
|
||||
"exporting": "Error exporting payment(s). {{error}}"
|
||||
},
|
||||
"fields": {
|
||||
"amount": "Amount",
|
||||
"created_at": "Created At",
|
||||
|
||||
@@ -1242,6 +1242,9 @@
|
||||
}
|
||||
},
|
||||
"payments": {
|
||||
"errors": {
|
||||
"exporting": ""
|
||||
},
|
||||
"fields": {
|
||||
"amount": "",
|
||||
"created_at": "",
|
||||
|
||||
@@ -1242,6 +1242,9 @@
|
||||
}
|
||||
},
|
||||
"payments": {
|
||||
"errors": {
|
||||
"exporting": ""
|
||||
},
|
||||
"fields": {
|
||||
"amount": "",
|
||||
"created_at": "",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
// },
|
||||
// };
|
||||
// };
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user