diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index e60267dba..7d5d0322f 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -1295,6 +1295,27 @@ + + from + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + other false @@ -17254,6 +17275,158 @@ + + reconciliation + + + billlinestotal + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + byassoc + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + byprice + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + joblinestotal + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + multipleactprices + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + multiplebilllines + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + multiplebillsforactprice + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + reconciliationheader false diff --git a/client/src/components/job-reconciliation-modal/job-reconciliation-modal.component.jsx b/client/src/components/job-reconciliation-modal/job-reconciliation-modal.component.jsx index 1ca03da2c..7b5b8aac8 100644 --- a/client/src/components/job-reconciliation-modal/job-reconciliation-modal.component.jsx +++ b/client/src/components/job-reconciliation-modal/job-reconciliation-modal.component.jsx @@ -40,9 +40,9 @@ export default function JobReconciliationModalComponent({ job, bills }) { diff --git a/client/src/components/job-reconciliation-totals/job-reconciliation-totals.component.jsx b/client/src/components/job-reconciliation-totals/job-reconciliation-totals.component.jsx index adf49953a..0604591d7 100644 --- a/client/src/components/job-reconciliation-totals/job-reconciliation-totals.component.jsx +++ b/client/src/components/job-reconciliation-totals/job-reconciliation-totals.component.jsx @@ -1,16 +1,23 @@ -import React, { useMemo } from "react"; +import { Button, Space, Statistic } from "antd"; import Dinero from "dinero.js"; import _ from "lodash"; -import { Space, Statistic } from "antd"; +import React, { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; +import { + reconcileByAssocLine, + reconcileByPrice, +} from "./job-reconciliation-totals.utility"; export default function JobReconciliationTotals({ billLines, jobLines, - selectedBillLines, - selectedJobLines, + jobLineState, + billLineState, }) { + const [errors, setErrors] = useState([]); const { t } = useTranslation(); + const [selectedBillLines, setSelectedBillLines] = billLineState; + const [selectedJobLines, setSelectedJobLines] = jobLineState; const totals = useMemo(() => { const jlLookup = _.keyBy(selectedJobLines, (i) => i); @@ -20,7 +27,6 @@ export default function JobReconciliationTotals({ joblinesTotal: jobLines .filter((jl) => !!jlLookup[jl.id]) .reduce((acc, val) => { - console.log("acc :>> ", val); return acc.add( Dinero({ amount: val.act_price * 100 }).multiply(val.part_qty || 1) ); @@ -38,15 +44,53 @@ export default function JobReconciliationTotals({ }, [billLines, jobLines, selectedBillLines, selectedJobLines]); return ( - - - - +
+ + + + + + + {errors.length > 0 && ( +
+ {t("general.labels.errors")} +
    + {errors.map((error, idx) => ( +
  • {error}
  • + ))} +
+
+ )} +
); } diff --git a/client/src/components/job-reconciliation-totals/job-reconciliation-totals.utility.js b/client/src/components/job-reconciliation-totals/job-reconciliation-totals.utility.js new file mode 100644 index 000000000..274b35621 --- /dev/null +++ b/client/src/components/job-reconciliation-totals/job-reconciliation-totals.utility.js @@ -0,0 +1,106 @@ +import i18next from "i18next"; +import _ from "lodash"; +export const reconcileByAssocLine = ( + jobLines, + jobLineState, + billLines, + billLineState, + setErrors +) => { + const [selectedBillLines, setSelectedBillLines] = billLineState; + const [selectedJobLines, setSelectedJobLines] = jobLineState; + + const allJoblinesFromBills = billLines + .filter((bl) => !!bl.joblineid) + .map((bl) => bl.joblineid); + + const duplicatedJobLinesbyInvoiceId = _.filter( + allJoblinesFromBills, + (val, i, iteratee) => _.includes(iteratee, val, i + 1) + ); + + if (duplicatedJobLinesbyInvoiceId.length > 0) + setErrors((errors) => [ + ...errors, + ..._.uniqBy(duplicatedJobLinesbyInvoiceId).map((dupedId) => + i18next.t("jobs.labels.reconciliation.multiplebilllines", { + line_desc: jobLines.find((j) => j.id === dupedId).line_desc, + }) + ), + ]); + + setSelectedBillLines( + _.union( + selectedBillLines, + billLines.filter((bl) => !!bl.joblineid).map((bl) => bl.id) + ) + ); + + setSelectedJobLines( + _.union(selectedJobLines, _.uniqBy(allJoblinesFromBills)) + ); +}; + +export const reconcileByPrice = ( + jobLines, + jobLineState, + billLines, + billLineState, + setErrors +) => { + const [selectedBillLines, setSelectedBillLines] = billLineState; + const [selectedJobLines, setSelectedJobLines] = jobLineState; + + const allActPricesFromJobs = jobLines.map((jl) => jl.act_price); + const duplicateActPrices = _.filter( + allActPricesFromJobs, + (val, i, iteratee) => _.includes(iteratee, val, i + 1) + ); + + if (duplicateActPrices.length > 0) + setErrors((errors) => [ + ...errors, + ..._.uniqBy(duplicateActPrices).map((dupeAp) => + i18next.t("jobs.labels.reconciliation.multipleactprices", { + act_price: dupeAp, + }) + ), + ]); + + const foundJobLines = []; + var foundBillLines = []; + const actPricesWithMoreThan1MatchingBill = []; + + jobLines.forEach((jl) => { + const matchingBillLineIds = billLines + .filter((bl) => bl.actual_price === jl.act_price) + .map((bl) => bl.id); + + if (matchingBillLineIds.length > 1) { + actPricesWithMoreThan1MatchingBill.push(jl.act_price); + } + + foundBillLines = [...foundBillLines, ...matchingBillLineIds]; + if (matchingBillLineIds.length > 0) { + foundJobLines.push(jl.id); + } + }); + + setErrors((errors) => [ + ...errors, + ..._.uniqBy(duplicateActPrices).map((dupeAp) => + i18next.t("jobs.labels.reconciliation.multipleactprices", { + act_price: dupeAp, + }) + ), + ..._.uniqBy(actPricesWithMoreThan1MatchingBill).map((act_price) => + i18next.t("jobs.labels.reconciliation.multiplebillsforactprice", { + act_price: act_price, + }) + ), + ]); + + setSelectedBillLines(_.union(selectedBillLines, _.uniqBy(foundBillLines))); + + setSelectedJobLines(_.union(selectedJobLines, _.uniqBy(foundJobLines))); +}; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 047c2eff8..528352609 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -96,6 +96,7 @@ }, "labels": { "entered": "Entered", + "from": "From", "other": "--Not On Estimate--", "reconciled": "Reconciled!", "unreconciled": "Unreconciled" @@ -1048,6 +1049,15 @@ "partstotal": "Parts Total", "rates": "Rates", "rates_subtotal": "Rates Subtotal", + "reconciliation": { + "billlinestotal": "Bill Lines Total", + "byassoc": "By Line Association", + "byprice": "By Price", + "joblinestotal": "Job Lines Total", + "multipleactprices": "${{act_price}} is the price for multiple job lines.", + "multiplebilllines": "{{line_desc}} has 2 or more bill lines associated to it.", + "multiplebillsforactprice": "Found more than 1 bill matching ${{act_price}} retail price." + }, "reconciliationheader": "Parts & Sublet Reconciliation", "sale_labor": "Sales - Labor", "sale_parts": "Sales - Parts", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 4a5977a2c..1af54bb26 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -96,6 +96,7 @@ }, "labels": { "entered": "", + "from": "", "other": "", "reconciled": "", "unreconciled": "" @@ -1048,6 +1049,15 @@ "partstotal": "", "rates": "Tarifas", "rates_subtotal": "", + "reconciliation": { + "billlinestotal": "", + "byassoc": "", + "byprice": "", + "joblinestotal": "", + "multipleactprices": "", + "multiplebilllines": "", + "multiplebillsforactprice": "" + }, "reconciliationheader": "", "sale_labor": "", "sale_parts": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index eed4a568a..c32b0f16e 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -96,6 +96,7 @@ }, "labels": { "entered": "", + "from": "", "other": "", "reconciled": "", "unreconciled": "" @@ -1048,6 +1049,15 @@ "partstotal": "", "rates": "Les taux", "rates_subtotal": "", + "reconciliation": { + "billlinestotal": "", + "byassoc": "", + "byprice": "", + "joblinestotal": "", + "multipleactprices": "", + "multiplebilllines": "", + "multiplebillsforactprice": "" + }, "reconciliationheader": "", "sale_labor": "", "sale_parts": "",