From 536f8d9cb977803ea4c37510c1f9dc17ccbfc3b6 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 12 Sep 2023 14:16:01 -0700 Subject: [PATCH] Add UI elements for part tax and improve calculations. --- .../jobs-detail-rates.parts.component.jsx | 462 +++++++++++++++++- .../jobs-detail.page.component.jsx | 12 + client/src/translations/en_us/common.json | 2 +- job-totals-testing-util.js | 8 +- server/job/job-totals.js | 160 ++++-- 5 files changed, 587 insertions(+), 57 deletions(-) diff --git a/client/src/components/jobs-detail-rates/jobs-detail-rates.parts.component.jsx b/client/src/components/jobs-detail-rates/jobs-detail-rates.parts.component.jsx index fcd56740d..2cd2b8917 100644 --- a/client/src/components/jobs-detail-rates/jobs-detail-rates.parts.component.jsx +++ b/client/src/components/jobs-detail-rates/jobs-detail-rates.parts.component.jsx @@ -68,11 +68,51 @@ export function JobsDetailRatesParts({ }, ]} > - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + { + Object.keys(job.parts_tax_rates).forEach((parttype) => { + Object.keys(job.parts_tax_rates[parttype]).forEach((key) => { + if (key.includes("tx_in")) { + if (job.parts_tax_rates[parttype][key] === "Y") { + job.parts_tax_rates[parttype][key] = true; + } else { + job.parts_tax_rates[parttype][key] = false; + } + } + }); + }); + return { ...job, loss_date: job.loss_date ? moment(job.loss_date) : null, diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index b2a0bd895..b48315846 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1798,7 +1798,7 @@ "totalreturns": "The total retail amount of returns created for this job." }, "ppc": "This line contains a part price change.", - "profileadjustments": "Profile Disc./Mkup (Already included above)", + "profileadjustments": "Profile Disc./Mkup", "prt_dsmk_total": "Line Item Adjustment", "rates": "Rates", "rates_subtotal": "All Rates Subtotal", diff --git a/job-totals-testing-util.js b/job-totals-testing-util.js index 9e5220a87..53eea2552 100644 --- a/job-totals-testing-util.js +++ b/job-totals-testing-util.js @@ -20,7 +20,7 @@ require("dotenv").config({ async function RunTheTest() { const bodyshopids = ["a7ee1503-ee05-4a02-b80e-bdb11d1cc8ac"]; - const bearerToken = `Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MGFkMTE4YTk0MGFkYzlmMmY1Mzc2YjM1MjkyZmVkZThjMmQwZWUiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiUm9tZSBEZXZlbG9wbWVudCIsImh0dHBzOi8vaGFzdXJhLmlvL2p3dC9jbGFpbXMiOnsieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoidXNlciIsIngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsidXNlciJdLCJ4LWhhc3VyYS11c2VyLWlkIjoidDZZbTFORGxDRE9QWnIzRjliZ3VXSDRMaFNYMiJ9LCJpb2FkbWluIjp0cnVlLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vcm9tZS1wcm9kLTEiLCJhdWQiOiJyb21lLXByb2QtMSIsImF1dGhfdGltZSI6MTY5NDQ2NjM3OCwidXNlcl9pZCI6InQ2WW0xTkRsQ0RPUFpyM0Y5Ymd1V0g0TGhTWDIiLCJzdWIiOiJ0NlltMU5EbENET1BacjNGOWJndVdINExoU1gyIiwiaWF0IjoxNjk0NDY5OTY3LCJleHAiOjE2OTQ0NzM1NjcsImVtYWlsIjoicGF0cmlja0Byb21lLmRldiIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJwYXRyaWNrQHJvbWUuZGV2Il19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0.cL1tTMmFwnyg-mXqRRaFLGSn92pwCDrbTGZviVFKTPqwejDDe_OTla6zizy5w5XBk1_S2CA4SrsH6-8uXkrRSLECd1QMsYK1XKUj0zkfe-o3Z1dMva9gPpVrs-fiCXzjcfs-P12hPfvubNZgqpj1EwZmQ6imq8MidkAo1c4V-IWGpxsmCTNBLN_09N0071spsabwtMosXHJVnkG44StXEM2w-FRak6dE0uZZkbPGgg_2uDwFQb3um6MJ8FbK398pLMs3cUrdQiB-5YozYoIuEI-L00s7dDTvjOKUzOvcDme5UCAS6RmwWoZYkTWrMJU3jf5ivAvjHNIhy3aMGyQxzg`; + const bearerToken = `Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MGFkMTE4YTk0MGFkYzlmMmY1Mzc2YjM1MjkyZmVkZThjMmQwZWUiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiUm9tZSBEZXZlbG9wbWVudCIsImh0dHBzOi8vaGFzdXJhLmlvL2p3dC9jbGFpbXMiOnsieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoidXNlciIsIngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsidXNlciJdLCJ4LWhhc3VyYS11c2VyLWlkIjoidDZZbTFORGxDRE9QWnIzRjliZ3VXSDRMaFNYMiJ9LCJpb2FkbWluIjp0cnVlLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vcm9tZS1wcm9kLTEiLCJhdWQiOiJyb21lLXByb2QtMSIsImF1dGhfdGltZSI6MTY5NDQ2NjM3OCwidXNlcl9pZCI6InQ2WW0xTkRsQ0RPUFpyM0Y5Ymd1V0g0TGhTWDIiLCJzdWIiOiJ0NlltMU5EbENET1BacjNGOWJndVdINExoU1gyIiwiaWF0IjoxNjk0NTQ4MTIwLCJleHAiOjE2OTQ1NTE3MjAsImVtYWlsIjoicGF0cmlja0Byb21lLmRldiIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJwYXRyaWNrQHJvbWUuZGV2Il19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0.hIR3tAIOZFydmuCC5OS1KmIAG3Rq3mB1ZJAcnWUrJRuioNw1DeezAiUvXlb2iQ3Pow3zpyTkZgNMrLhWNpZmywyjPQQaTU7krw2gNhmRfrILmIWjODepvRcp4mcwvdf65WkqXm88S82b-8nkgvPhogvXmYWHrrDFl9EP4nMXdhjJ8rZ-euBcH9wz9o4BehsW4x91JJxeTU_jk4Fa0h6ppG6XdTmPyTlQb79g-WgLbqtyXEIjQr9q_ZbE4br_PLLhFd7SnUV0e-raw3FcK9m4Mc-n37M4KtKEpDbhXM_2MtGSCWbKZ7m3lfydFaV8LlgnCTiX_gSCvoAmCeRyH5w1yQ`; const { jobs } = await client.request( gql` query GET_JOBS($bodyshopids: [uuid!]!) { @@ -61,6 +61,7 @@ async function RunTheTest() { ownr_ln ownr_co_nm ins_co_nm + comment } } `, @@ -73,19 +74,20 @@ async function RunTheTest() { id: newjob.id, owner: `${newjob.ownr_fn} ${newjob.ownr_ln} ${job.ownr_co_nm || ""}`, ins_co: newjob.ins_co_nm, + comment: newjob.comment, }; const calcTotal = newjob.job_totals.totals.total_repairs.amount; const ttlTotal = newjob.cieca_ttl.data.g_ttl_amt * 100; result.difference = (calcTotal - ttlTotal) / 100; - if (Math.abs(calcTotal - ttlTotal) > 5) { + if (Math.abs(calcTotal - ttlTotal) > 3) { //Diff is greater than 5 cents. Fail it. result.result = "***FAIL***"; } else { result.result = "PASS"; } - console.log(`${result.result} => RO ${job.ro_number}`); + console.log(`${result.result} => RO ${job.ro_number} - ${job.id} `); results.push(result); } catch (error) { diff --git a/server/job/job-totals.js b/server/job/job-totals.js index 86a9c1bea..c2ec10132 100644 --- a/server/job/job-totals.js +++ b/server/job/job-totals.js @@ -1,6 +1,8 @@ const Dinero = require("dinero.js"); const queries = require("../graphql-client/queries"); const GraphQLClient = require("graphql-request").GraphQLClient; +const adminClient = require("../graphql-client/graphql-client").client; +const _ = require("lodash"); const logger = require("../utils/logger"); // Dinero.defaultCurrency = "USD"; // Dinero.globalLocale = "en-CA"; @@ -59,7 +61,7 @@ async function TotalsServerSide(req, res) { try { let ret = { rates: await CalculateRatesTotals({ job, client }), - parts: CalculatePartsTotals(job.joblines, job.parts_tax_rates), + parts: CalculatePartsTotals(job.joblines, job.parts_tax_rates, job), additional: CalculateAdditional(job), }; ret.totals = CalculateTaxesTotals(job, ret); @@ -93,7 +95,7 @@ async function Totals(req, res) { try { let ret = { rates: await CalculateRatesTotals({ job, client }), - parts: CalculatePartsTotals(job.joblines, job.parts_tax_rates), + parts: CalculatePartsTotals(job.joblines, job.parts_tax_rates, job), additional: CalculateAdditional(job), }; ret.totals = CalculateTaxesTotals(job, ret); @@ -389,7 +391,7 @@ async function CalculateRatesTotals({ job, client }) { lbr_op: "OP11", lbr_amt: 0, op_code_desc: "REMOVE / REPLACE", - tax_part: hasMahwLine.tax_amt > 0 ? true : false, + tax_part: stlMahw.tax_amt > 0 ? true : false, db_ref: null, manual_line: true, jobid: job.id, @@ -427,7 +429,7 @@ async function CalculateRatesTotals({ job, client }) { return ret; } -function CalculatePartsTotals(jobLines, parts_tax_rates) { +function CalculatePartsTotals(jobLines, parts_tax_rates, job) { const jl = jobLines.filter((jl) => !jl.removed); const ret = jl.reduce( (acc, value) => { @@ -564,23 +566,27 @@ function CalculatePartsTotals(jobLines, parts_tax_rates) { PAS: Dinero(), PAT: Dinero(), }; + //Track all adjustments that need to be made. + + const linesToAdjustForDiscount = []; Object.keys(parts_tax_rates).forEach((key) => { //Check if there's a discount or a mark up. let disc = Dinero(), markup = Dinero(); + + let discountRate, markupRate; if ( parts_tax_rates[key].prt_discp !== undefined && parts_tax_rates[key].prt_discp >= 0 ) { //Check if there's any parts in this part type. if (ret.parts.list[key] !== undefined) { - disc = ret.parts.list[key].total - .percentage( - Math.abs(parts_tax_rates[key].prt_discp) > 1 - ? parts_tax_rates[key].prt_discp - : parts_tax_rates[key].prt_discp * 100 - ) - .multiply(-1); + discountRate = + Math.abs(parts_tax_rates[key].prt_discp) > 1 + ? parts_tax_rates[key].prt_discp + : parts_tax_rates[key].prt_discp * 100; + + disc = ret.parts.list[key].total.percentage(discountRate).multiply(-1); } } if ( @@ -589,26 +595,70 @@ function CalculatePartsTotals(jobLines, parts_tax_rates) { ) { //Check if there's any parts in this part type. if (ret.parts.list[key] !== undefined) { - markup = ret.parts.list[key].total.percentage( + markupRate = Math.abs(parts_tax_rates[key].prt_mkupp) > 1 ? parts_tax_rates[key].prt_mkupp - : parts_tax_rates[key].prt_mkupp * 100 //Seems that mark up is written as decimal not %. - ); + : parts_tax_rates[key].prt_mkupp * 100; //Seems that mark up is written as decimal not %. + + markup = ret.parts.list[key].total.percentage(markupRate); } } let adjustment = disc.add(markup); adjustments[key] = adjustment; + + const correspondingCiecaStlTotalLine = job.cieca_stl?.data.find( + (c) => c.ttl_typecd === key + ); + + //If the difference is greater than a penny, fix it. + + if ( + correspondingCiecaStlTotalLine && + Math.abs( + ret.parts.list[key]?.total.getAmount() - + correspondingCiecaStlTotalLine.ttl_amt * 100 + ) > 1 + ) { + // Update the total. + console.log( + key, + ret.parts.list[key]?.total.getAmount(), + correspondingCiecaStlTotalLine?.ttl_amt + ); + //Find the corresponding lines. Update the discount/markup for them. + + console.warn("There's a difference! Type: ", key); + let totalDiscountToAdjustBy = Dinero(); + job.joblines.forEach((jobline) => { + //Modify the line in place to add the mark up/discount. + if (jobline.part_type === key) { + const discountAmountDinero = Dinero({ + amount: Math.round(jobline.act_price * 100), + }).percentage(discountRate); + + const discountAmount = parseFloat( + discountAmountDinero.toFormat("0.00") + ); + totalDiscountToAdjustBy = + totalDiscountToAdjustBy.add(discountAmountDinero); + jobline.prt_dsmk_m = discountAmount * -1; + jobline.prt_dsmk_p = discountRate * -1; + + linesToAdjustForDiscount.push(jobline); + } + }); + // ret.parts.list[key].total = ret.parts.list[key]?.total.subtract( + // totalDiscountToAdjustBy + // ); + ret.parts.prt_dsmk_total = ret.parts.prt_dsmk_total.add( + totalDiscountToAdjustBy + ); + ret.parts.subtotal = ret.parts.subtotal.subtract(totalDiscountToAdjustBy); + ret.parts.total = ret.parts.total.subtract(totalDiscountToAdjustBy); + } }); - //Temporarily commenting this out since these totals appear to be already included in the calculation. - // Object.keys(adjustments).forEach((key) => { - // if (ret.parts.list[key] !== undefined) { - // ret.parts.list[key].total = ret.parts.list[key].total.add( - // adjustments[key] - // ); - // ret.parts.subtotal = ret.parts.subtotal.add(adjustments[key]); - // } - // }); + //UpdateJobLines(linesToAdjustForDiscount.filter((l) => l.prt_dsmk_m !== 0)); return { adjustments, @@ -779,8 +829,8 @@ function CalculateTaxesTotals(job, otherTotals) { }; const remainingTaxableAmounts = taxableAmounts; - console.log("Taxable Parts Totals", JSON.stringify(taxableAmounts, null, 2)); - + console.log("Taxable Amounts"); + console.table(JSON.parse(JSON.stringify(taxableAmounts))); Object.keys(taxableAmounts).forEach((part_type) => { //Check it's taxability in the PFP try { @@ -793,14 +843,12 @@ function CalculateTaxesTotals(job, otherTotals) { if (IsTrueOrYes(pfp[typeOfPart][`prt_tx_in${tyCounter}`])) { //i represents the tax number. If we got here, this type of tax is applicable. Now we need to add based on the thresholds. for (let threshCounter = 1; threshCounter <= 5; threshCounter++) { - const thresholdAmount = - job.cieca_pft[`ty${tyCounter}_thres${threshCounter}`]; - const thresholdTaxRate = - job.cieca_pft[`ty${tyCounter}_rate${threshCounter}`]; - - // console.log( - // `P: ${typeOfPart} tyCount: ${tyCounter} theshCount: ${threshCounter} TaxRt: ${thresholdTaxRate}` - // ); + const thresholdAmount = parseFloat( + job.cieca_pft[`ty${tyCounter}_thres${threshCounter}`] + ); + const thresholdTaxRate = parseFloat( + job.cieca_pft[`ty${tyCounter}_rate${threshCounter}`] + ); let taxableAmountInThisThreshold; if (thresholdAmount === 9999.99) { @@ -820,7 +868,7 @@ function CalculateTaxesTotals(job, otherTotals) { } else { //Take the size of the threshold from the remaining amount, tax it, and do it all over. taxableAmountInThisThreshold = Dinero({ - amount: Math.round(taxableAmountInThisThreshold * 100), + amount: Math.round(thresholdAmount * 100), }); remainingTaxableAmounts[typeOfPart] = remainingTaxableAmounts[ typeOfPart @@ -842,7 +890,7 @@ function CalculateTaxesTotals(job, otherTotals) { } } } catch (error) { - console.log("Shit the bed."); + console.error("Shit the bed."); } }); @@ -976,13 +1024,13 @@ function DiscountNotAlreadyCounted(jobline, joblines) { } //Check it against the database price too? If it's an OE part. - if ( - Math.abs(jobline.db_price - jobline.act_price) - - Math.abs(jobline.prt_dsmk_m) < - 0.01 - ) { - return false; - } + // if ( + // Math.abs(jobline.db_price - jobline.act_price) - + // Math.abs(jobline.prt_dsmk_m) < + // 0.01 + // ) { + // return false; + // } if ( //If it's not a discount line, then it definitely hasn't been counted yet. @@ -1006,3 +1054,31 @@ function ParseCalopCode(opcode) { function IsTrueOrYes(value) { return value === true || value === "Y" || value === "y"; } + +async function UpdateJobLines(joblinesToUpdate) { + if (joblinesToUpdate.length === 0) return; + const updateQueries = joblinesToUpdate.map((line, index) => + generateUpdateQuery(_.pick(line, ["id", "prt_dsmk_m", "prt_dsmk_p"]), index) + ); + const query = ` +mutation UPDATE_EST_LINES{ + ${updateQueries} +} +`; + + const result = await adminClient.request(query); +} + +const generateUpdateQuery = (lineToUpdate, index) => { + return ` + update_joblines${index}: update_joblines(where: { id: { _eq: "${ + lineToUpdate.id + }" } }, _set: ${JSON.stringify(lineToUpdate).replace( + /"(\w+)"\s*:/g, + "$1:" + )}) { + returning { + id + } + }`; +};