Successful 2tier and 3 tier export of invoices. BOD-83 BOD-131

This commit is contained in:
Patrick Fic
2020-06-02 09:19:46 -07:00
parent 5564b5dc4a
commit 73040064d4
16 changed files with 265 additions and 112 deletions

View File

@@ -1,4 +1,4 @@
<babeledit_project version="1.2" be_version="2.6.1">
<babeledit_project be_version="2.6.1" version="1.2">
<!--
BabelEdit project file
@@ -7374,6 +7374,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>export</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>gotojob</name>
<definition_loaded>false</definition_loaded>

View File

@@ -68,7 +68,7 @@ export default connect(
try {
const response2 = await axios.post(
"http://localhost:1337/qb/receivables",
"http://e9c5a8ed9079.ngrok.io/qb/receivables",
response.data,
{
headers: {

View File

@@ -19,6 +19,7 @@ export function JobsCloseLabmatAllocationButton({
allocation,
setAllocations,
bodyshop,
invoiced
}) {
const [visible, setVisible] = useState(false);
const [state, setState] = useState({ center: "", amount: 0 });
@@ -87,7 +88,7 @@ export function JobsCloseLabmatAllocationButton({
<Button
onClick={handleAllocate}
disabled={
state.amount === 0 || state.center === "" || remainingAmount === 0
state.amount === 0 || state.center === "" || remainingAmount === 0 || invoiced
}
>
{t("jobs.actions.allocate")}
@@ -95,9 +96,9 @@ export function JobsCloseLabmatAllocationButton({
</div>
<div>
{visible ? (
<CloseCircleFilled onClick={() => setVisible(false)} />
<CloseCircleFilled onClick={() => setVisible(false)} disabled={invoiced} />
) : (
<PlusCircleFilled onClick={() => setVisible(true)} />
<PlusCircleFilled onClick={() => setVisible(true)} disabled={invoiced}/>
)}
</div>
</div>

View File

@@ -4,14 +4,15 @@ export default function JobsCloseLabMatAllocationTags({
allocationKey,
allocation,
setAllocations,
invoiced,
}) {
return (
<div>
{allocation.allocations.map((a, idx) => (
<Tag
closable
closable={!invoiced} //Value is whether it is invoiced.
visible
color='green'
color="green"
onClose={() => {
setAllocations((state) => {
return {

View File

@@ -14,6 +14,7 @@ export function JobsCloseAutoAllocate({
setLabmatAllocations,
partsAllocations,
setPartsAllocations,
invoiced
}) {
const { t } = useTranslation();
const handleAllocate = () => {
@@ -62,6 +63,6 @@ export function JobsCloseAutoAllocate({
});
};
return <Button onClick={handleAllocate}>{t("jobs.actions.autoallocate")}</Button>;
return <Button onClick={handleAllocate} disabled={invoiced}>{t("jobs.actions.autoallocate")}</Button>;
}
export default connect(mapStateToProps, null)(JobsCloseAutoAllocate);

View File

@@ -0,0 +1,43 @@
import { Button } from "antd";
import axios from "axios";
import React from "react";
import { useTranslation } from "react-i18next";
import { auth } from "../../firebase/firebase.utils";
export default function JobsCloseExportButton({ jobId, disabled }) {
const { t } = useTranslation();
const handleQbxml = async () => {
const response = await axios.post(
"/accounting/qbxml/receivables",
{ jobId: jobId },
{
headers: {
Authorization: `Bearer ${await auth.currentUser.getIdToken(true)}`,
},
}
);
console.log("handle -> XML", response);
try {
const response2 = await axios.post(
"http://e9c5a8ed9079.ngrok.io/qb/receivables",
response.data,
{
headers: {
Authorization: `Bearer ${await auth.currentUser.getIdToken(true)}`,
},
}
);
console.log("handle -> result", response2);
} catch (error) {
console.log("error", error, JSON.stringify(error));
}
};
return (
<Button onClick={handleQbxml} disabled={disabled} type="dashed">
{t("jobs.actions.export")}
</Button>
);
}

View File

@@ -8,6 +8,7 @@ export default function JobCloseLabMatAllocation({
labmatAllocations,
setLabmatAllocations,
labMatTotalAllocation,
invoiced
}) {
const { t } = useTranslation();
@@ -50,6 +51,7 @@ export default function JobCloseLabMatAllocation({
<td>
<AllocationButton
allocationKey={alloc}
invoiced={invoiced}
remainingAmount={labmatAllocations[alloc].total
.subtract(
Dinero({
@@ -69,6 +71,7 @@ export default function JobCloseLabMatAllocation({
<td>
<AllocationTags
allocationKey={alloc}
invoiced={invoiced}
allocation={labmatAllocations[alloc]}
setAllocations={setLabmatAllocations}
/>

View File

@@ -8,6 +8,7 @@ export default function JobsClosePartsAllocation({
partsAllocations,
setPartsAllocations,
partsAllocatedTotal,
invoiced,
}) {
const { t } = useTranslation();
@@ -50,6 +51,7 @@ export default function JobsClosePartsAllocation({
<td>
<AllocationButton
allocationKey={alloc}
invoiced={invoiced}
remainingAmount={partsAllocations[alloc].total
.subtract(
Dinero({
@@ -68,6 +70,7 @@ export default function JobsClosePartsAllocation({
</td>
<td>
<AllocationTags
invoiced={invoiced}
allocationKey={alloc}
allocation={partsAllocations[alloc]}
setAllocations={setPartsAllocations}

View File

@@ -18,6 +18,8 @@ export function JobsCloseSaveButton({
jobTotals,
labMatAllocations,
partsAllocations,
setInvoicedState,
disabled,
}) {
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
@@ -38,9 +40,23 @@ export function JobsCloseSaveButton({
},
},
},
optimisticResponse: {
update_jobs: {
returning: {
id: jobId,
date_invoiced: new Date(),
status: bodyshop.md_ro_statuses.default_invoiced || "Invoiced*",
invoice_allocation: {
labMatAllocations,
partsAllocations,
},
},
},
},
});
if (!!!result.errors) {
notification["success"]({ message: t("jobs.successes.invoiced") });
setInvoicedState(true);
} else {
notification["error"]({
message: t("jobs.errors.invoicing", {
@@ -50,14 +66,14 @@ export function JobsCloseSaveButton({
}
setLoading(false);
};
return (
<Button
onClick={handleSave}
disabled={suspenseAmount > 0}
disabled={suspenseAmount > 0 || disabled}
loading={loading}
>
{t("general.actions.close")}
{t("general.actions.save")}
</Button>
);
}

View File

@@ -755,11 +755,6 @@ export const QUERY_ALL_JOBS_PAGINATED = gql`
export const QUERY_JOB_CLOSE_DETAILS = gql`
query QUERY_JOB_CLOSE_DETAILS($id: uuid!) {
jobs_by_pk(id: $id) {
po_number
special_coverage_policy
scheduled_delivery
converted
est_number
ro_number
clm_total
inproduction
@@ -769,26 +764,12 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
v_model_desc
v_make_desc
v_color
invoice_allocation
ins_co_id
policy_no
loss_date
clm_no
area_of_damage
ins_co_nm
ins_addr1
ins_city
ins_ct_ln
ins_ct_fn
ins_ea
ins_ph1
est_co_nm
est_ct_fn
est_ct_ln
pay_date
est_ph1
est_ea
regie_number
scheduled_completion
id
ded_amt
ded_status
@@ -819,20 +800,6 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
ownr_zip
ownr_ctry
ownr_ph1
owner {
id
ownr_fn
ownr_ln
ownr_ea
ownr_addr1
ownr_addr2
ownr_city
ownr_st
ownr_zip
ownr_ctry
ownr_ph1
}
labor_rate_desc
rate_atp
rate_la1
rate_la2
@@ -857,18 +824,10 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
rate_mapa
rate_mash
rate_matd
actual_completion
scheduled_delivery
actual_delivery
date_invoiced
date_closed
date_exported
status
status
owner_owing
joblines {
id
unq_seq
line_ind
tax_part
line_desc
prt_dsmk_p
@@ -885,7 +844,6 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
lbr_amt
op_code_desc
}
cieca_ttl
}
}
`;

View File

@@ -8,33 +8,76 @@ import Dinero from "dinero.js";
import JobsCloseTotals from "../../components/jobs-close-totals/jobs-close-totals.component";
import JobsCloseAutoAllocate from "../../components/jobs-close-auto-allocate/jobs-close-auto-allocate.component";
import JobsCloseSaveButton from "../../components/jobs-close-save-button/jobs-close-save-button.component";
import JobsCloseExportButton from "../../components/jobs-close-export-button/jobs-close-export-button.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export function JobsCloseComponent({ job, bodyshop, jobTotals }) {
const [invoiced, setInvoiced] = useState(!!job.invoice_allocation);
const [labmatAllocations, setLabmatAllocations] = useState(
Object.keys(jobTotals.rates).reduce((acc, val) => {
acc[val] = jobTotals.rates[val];
if (val.includes("subtotal")) return acc;
//Not a subtotal - therefore can be allocated.
acc[val].allocations = [];
return acc;
}, {})
!!job.invoice_allocation && !!job.invoice_allocation.labMatAllocations
? Object.keys(job.invoice_allocation.labMatAllocations).reduce(
(acc, val) => {
if (val.includes("subtotal")) {
acc[val] = Dinero(job.invoice_allocation.labMatAllocations[val]);
} else {
acc[val] = {
...job.invoice_allocation.labMatAllocations[val],
total: Dinero(
job.invoice_allocation.labMatAllocations[val].total
),
allocations: job.invoice_allocation.labMatAllocations[
val
].allocations.map((item) => {
return { ...item, amount: Dinero(item.amount) };
}),
};
}
return acc;
},
{}
)
: Object.keys(jobTotals.rates).reduce((acc, val) => {
acc[val] = jobTotals.rates[val];
if (val.includes("subtotal")) return acc;
//Not a subtotal - therefore can be allocated.
acc[val].allocations = [];
return acc;
}, {})
);
const [partsAllocations, setPartsAllocations] = useState({
...Object.keys(jobTotals.parts.parts.list).reduce((acc, val) => {
acc[val] = { ...jobTotals.parts.parts.list[val], allocations: [] };
const [partsAllocations, setPartsAllocations] = useState(
!!job.invoice_allocation && !!job.invoice_allocation.partsAllocations
? Object.keys(job.invoice_allocation.partsAllocations).reduce(
(acc, val) => {
acc[val] = {
...job.invoice_allocation.partsAllocations[val],
total: Dinero(job.invoice_allocation.partsAllocations[val].total),
allocations: job.invoice_allocation.partsAllocations[
val
].allocations.map((item) => {
return { ...item, amount: Dinero(item.amount) };
}),
};
return acc;
},
{}
)
: {
...Object.keys(jobTotals.parts.parts.list).reduce((acc, val) => {
acc[val] = { ...jobTotals.parts.parts.list[val], allocations: [] };
return acc;
}, {}),
pas: {
...jobTotals.parts.sublets,
allocations: [],
},
});
return acc;
}, {}),
pas: {
...jobTotals.parts.sublets,
allocations: [],
},
}
);
const labmatAllocatedTotalsArray = Object.keys(labmatAllocations)
.filter((i) => !i.includes("subtotal"))
@@ -61,6 +104,8 @@ export function JobsCloseComponent({ job, bodyshop, jobTotals }) {
<div>
<JobsCloseSaveButton
jobId={job.id}
invoiced={invoiced}
setInvoicedState={setInvoiced}
partsAllocations={partsAllocations}
labMatAllocations={labmatAllocations}
jobTotals={jobTotals}
@@ -69,6 +114,7 @@ export function JobsCloseComponent({ job, bodyshop, jobTotals }) {
.subtract(partsAllocatedTotal)
.getAmount()}
/>
<JobsCloseExportButton jobId={job.id} disabled={!invoiced} />
<JobsCloseTotals
jobTotals={jobTotals}
labMatTotal={labmatAllocatedTotal}
@@ -79,16 +125,19 @@ export function JobsCloseComponent({ job, bodyshop, jobTotals }) {
setLabmatAllocations={setLabmatAllocations}
partsAllocations={partsAllocations}
setPartsAllocations={setPartsAllocations}
invoiced={invoiced}
/>
<JobsCloseLaborMaterialAllocation
labmatAllocations={labmatAllocations}
setLabmatAllocations={setLabmatAllocations}
labMatTotalAllocation={labmatAllocatedTotal}
invoiced={invoiced}
/>
<JobsClosePartsAllocation
partsAllocations={partsAllocations}
setPartsAllocations={setPartsAllocations}
partsAllocatedTotal={partsAllocatedTotal}
invoiced={invoiced}
/>
</div>
);

View File

@@ -500,6 +500,7 @@
"autoallocate": "Auto Allocate",
"changestatus": "Change Status",
"convert": "Convert",
"export": "Export",
"gotojob": "Go to Job",
"manualnew": "Create New Job Manually",
"postInvoices": "Post Invoices",

View File

@@ -500,6 +500,7 @@
"autoallocate": "",
"changestatus": "Cambiar Estado",
"convert": "Convertir",
"export": "",
"gotojob": "",
"manualnew": "",
"postInvoices": "Contabilizar facturas",

View File

@@ -500,6 +500,7 @@
"autoallocate": "",
"changestatus": "Changer le statut",
"convert": "Convertir",
"export": "",
"gotojob": "",
"manualnew": "",
"postInvoices": "Poster des factures",

View File

@@ -31,17 +31,16 @@ exports.default = async (req, res) => {
//Is this a two tier, or 3 tier setup?
const isThreeTier = bodyshop.accountingconfig.tiers === 3;
QbXmlToExecute.push(
generateCustomerQbxml(jobs_by_pk, bodyshop, isThreeTier)
);
if (isThreeTier) {
QbXmlToExecute.push(generateJobQbxml(jobs_by_pk, bodyshop, 2));
QbXmlToExecute.push(generateJobQbxml(jobs_by_pk, bodyshop, 3));
QbXmlToExecute.push(
generateSourceCustomerQbxml(jobs_by_pk, bodyshop) // Create the source customer.
);
}
QbXmlToExecute.push(generateJobQbxml(jobs_by_pk, bodyshop, isThreeTier, 2));
QbXmlToExecute.push(generateJobQbxml(jobs_by_pk, bodyshop, isThreeTier, 3));
//Generate the actual invoice.
QbXmlToExecute.push(generateInvoiceQbxml(jobs_by_pk, bodyshop));
console.log(QbXmlToExecute);
res.status(200).json(QbXmlToExecute);
} catch (error) {
console.log("error", error);
@@ -49,31 +48,21 @@ exports.default = async (req, res) => {
}
};
const generateCustomerQbxml = (jobs_by_pk, bodyshop, isThreeTier) => {
const generateSourceCustomerQbxml = (jobs_by_pk, bodyshop) => {
const customerQbxmlObj = {
QBXML: {
QBXMLMsgsRq: {
"@onError": "continueOnError",
CustomerAddRq: {
CustomerAdd: {
Name: isThreeTier
? jobs_by_pk.ins_co_nm
: jobs_by_pk.ownr_co_nm
? `${jobs_by_pk.ownr_co_nm} - ${jobs_by_pk.ownr_ln || ""} ${
jobs_by_pk.ownr_fn || ""
} #${jobs_by_pk.owner.accountingid || ""}`
: `${jobs_by_pk.ownr_ln || ""} ${jobs_by_pk.ownr_fn || ""} #${
jobs_by_pk.owner.accountingid || ""
}`,
BillAddress: isThreeTier
? null
: {
Addr1: jobs_by_pk.ownr_addr1,
Addr2: jobs_by_pk.ownr_addr2,
City: jobs_by_pk.ownr_city,
State: jobs_by_pk.ownr_st,
PostalCode: jobs_by_pk.ownrzip,
},
Name: jobs_by_pk.ins_co_nm,
BillAddress: {
Addr1: jobs_by_pk.ownr_addr1,
Addr2: jobs_by_pk.ownr_addr2,
City: jobs_by_pk.ownr_city,
State: jobs_by_pk.ownr_st,
PostalCode: jobs_by_pk.ownrzip,
},
},
},
},
@@ -93,15 +82,35 @@ const generateCustomerQbxml = (jobs_by_pk, bodyshop, isThreeTier) => {
return customerQbxml_Full;
};
const generateJobQbxml = (jobs_by_pk, bodyshop, isThreeTier, tierLevel) => {
const tier1Name = jobs_by_pk.ownr_co_nm;
const tier2Name = jobs_by_pk.ownr_co_nm
const generateSourceTier = (jobs_by_pk) => {
return jobs_by_pk.ins_co_nm;
};
const generateJobTier = (jobs_by_pk) => {
return jobs_by_pk.ro_number;
};
const generateOwnerTier = (jobs_by_pk) => {
return jobs_by_pk.ownr_co_nm
? `${jobs_by_pk.ownr_co_nm} - ${jobs_by_pk.ownr_ln || ""} ${
jobs_by_pk.ownr_fn || ""
} #${jobs_by_pk.owner.accountingid || ""}`
: `${jobs_by_pk.ownr_ln || ""} ${jobs_by_pk.ownr_fn || ""} #${
jobs_by_pk.owner.accountingid || ""
}`;
};
const generateJobQbxml = (jobs_by_pk, bodyshop, isThreeTier, tierLevel) => {
let Name;
let ParentRefName;
if (tierLevel === 2) {
Name = generateOwnerTier(jobs_by_pk);
ParentRefName = isThreeTier ? generateSourceTier(jobs_by_pk) : null;
} else if (tierLevel === 3) {
Name = generateJobTier(jobs_by_pk);
ParentRefName = isThreeTier
? `${jobs_by_pk.ins_co_nm}:${generateOwnerTier(jobs_by_pk)}`
: generateOwnerTier(jobs_by_pk);
}
const jobQbxmlObj = {
QBXML: {
@@ -109,10 +118,12 @@ const generateJobQbxml = (jobs_by_pk, bodyshop, isThreeTier, tierLevel) => {
"@onError": "continueOnError",
CustomerAddRq: {
CustomerAdd: {
Name: tierLevel === 2 ? null : null,
ParentRef: {
FullName: tierLevel === 2 ? null : null,
},
Name: Name,
ParentRef: ParentRefName
? {
FullName: ParentRefName,
}
: null,
},
},
},
@@ -128,6 +139,7 @@ const generateJobQbxml = (jobs_by_pk, bodyshop, isThreeTier, tierLevel) => {
.end({ pretty: true });
const jobQbxml_Full = addQbxmlHeader(jobQbxml_partial);
console.log("jobQbxml_Full", jobQbxml_Full);
return jobQbxml_Full;
};
@@ -170,6 +182,46 @@ const generateInvoiceQbxml = (jobs_by_pk, bodyshop) => {
);
});
//Add tax lines
const job_totals = JSON.parse(jobs_by_pk.job_totals);
const federal_tax = Dinero(job_totals.totals.federal_tax);
const state_tax = Dinero(job_totals.totals.state_tax);
const local_tax = Dinero(job_totals.totals.local_tax);
if (federal_tax.getAmount() > 0) {
InvoiceLineAdd.push({
ItemRef: {
FullName: bodyshop.md_responsibility_centers.taxes.federal.accountitem,
},
Desc: bodyshop.md_responsibility_centers.taxes.federal.accountdesc,
//Quantity: 1,
Amount: federal_tax.toFormat(DineroQbFormat),
});
}
if (state_tax.getAmount() > 0) {
InvoiceLineAdd.push({
ItemRef: {
FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem,
},
Desc: bodyshop.md_responsibility_centers.taxes.state.accountdesc,
//Quantity: 1,
Amount: state_tax.toFormat(DineroQbFormat),
});
}
if (local_tax.getAmount() > 0) {
InvoiceLineAdd.push({
ItemRef: {
FullName: bodyshop.md_responsibility_centers.taxes.local.accountitem,
},
Desc: bodyshop.md_responsibility_centers.taxes.local.accountdesc,
//Quantity: 1,
Amount: local_tax.toFormat(DineroQbFormat),
});
}
const invoiceQbxmlObj = {
QBXML: {
QBXMLMsgsRq: {
@@ -177,13 +229,14 @@ const generateInvoiceQbxml = (jobs_by_pk, bodyshop) => {
InvoiceAddRq: {
InvoiceAdd: {
CustomerRef: {
//This can equal the Customer or the Customer Job.
FullName:
bodyshop.accountingconfig.tiers === 3
? "3tier"
: bodyshop.accountingconfig.twotierpref === "name"
? "2tiername"
: "2tiersource",
? `${generateSourceTier(jobs_by_pk)}:${generateOwnerTier(
jobs_by_pk
)}:${generateJobTier(jobs_by_pk)}`
: `${generateOwnerTier(jobs_by_pk)}:${generateJobTier(
jobs_by_pk
)}`,
},
TxnDate: new Date(),
RefNumber: jobs_by_pk.ro_number,
@@ -234,7 +287,7 @@ const generateInvoiceLine = (job, allocation, responsibilityCenters) => {
//Rate: 100,
Amount: DineroAmount.toFormat(DineroQbFormat),
SalesTaxCodeRef: {
FullName: "Z",
FullName: "E",
},
};
};

View File

@@ -48,6 +48,7 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($id: uuid!) {
date_invoiced
ro_number
clm_total
clm_no
invoice_allocation
ownerid
ownr_ln