From 6a9e871b0846794492ee0dfe0ebece46afe84b74 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 11 Sep 2023 20:44:12 -0700 Subject: [PATCH 01/11] Add parts tax calculations. --- job-totals-testing-util.js | 4 +- server/graphql-client/queries.js | 1 + server/job/job-totals.js | 192 +++++++++++++++++++------------ setadmin.js | 2 +- 4 files changed, 121 insertions(+), 78 deletions(-) diff --git a/job-totals-testing-util.js b/job-totals-testing-util.js index 9fd3550d4..9e5220a87 100644 --- a/job-totals-testing-util.js +++ b/job-totals-testing-util.js @@ -19,8 +19,8 @@ require("dotenv").config({ }); async function RunTheTest() { - const bodyshopids = ["6c63a820-542c-497e-8c82-0cc38fb2bbca"]; - const bearerToken = `Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImM2MGI5ZGUwODBmZmFmYmZjMTgzMzllY2Q0NGFjNzdmN2ZhNGU4ZDMiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiUm9tZSBEZXZlbG9wbWVudCIsImh0dHBzOi8vaGFzdXJhLmlvL2p3dC9jbGFpbXMiOnsieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoiYWRtaW4iLCJ4LWhhc3VyYS1hbGxvd2VkLXJvbGVzIjpbImFkbWluIl0sIngtaGFzdXJhLXVzZXItaWQiOiJ0NlltMU5EbENET1BacjNGOWJndVdINExoU1gyIn0sImlvYWRtaW4iOnRydWUsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9yb21lLXByb2QtMSIsImF1ZCI6InJvbWUtcHJvZC0xIiwiYXV0aF90aW1lIjoxNjkyODk5ODE2LCJ1c2VyX2lkIjoidDZZbTFORGxDRE9QWnIzRjliZ3VXSDRMaFNYMiIsInN1YiI6InQ2WW0xTkRsQ0RPUFpyM0Y5Ymd1V0g0TGhTWDIiLCJpYXQiOjE2OTMyNTA1NjIsImV4cCI6MTY5MzI1NDE2MiwiZW1haWwiOiJwYXRyaWNrQHJvbWUuZGV2IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbInBhdHJpY2tAcm9tZS5kZXYiXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9fQ.POr8U2pP4XtTJEDRJ_BveRkCs92CIfDDdfU24OYe_aZh6LFPN0bQukNHXrLt3gaD30SUcg5mgmI2VUphgmwviMEGY-zizPC9o6GUKEEppZWQXfrfTyJNa1VKKH9h5zZPPFFW8UJRMi131pCc0ev26GS8Do-FJAgwHLJd6Jp2RbbqiCIeafNMhQCEoXohOk-VArNe7tPAb6-IjxqGVyNqvVyIo6niSXYvmgNjyF1WnnIw0CsnPoJlc5kVMtRdYeshJI7V117MOlUwZicF62vsm32eCunjn3qhN5XsujI7gy9us3vzwhdR1lxISZCLhLOXEYHPL373HJh7I_KN1C3NuA`; + 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 { jobs } = await client.request( gql` query GET_JOBS($bodyshopids: [uuid!]!) { diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 3bba58cf4..149184329 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -1168,6 +1168,7 @@ exports.GET_JOB_BY_PK = `query GET_JOB_BY_PK($id: uuid!) { shopid est_ct_ln cieca_pfl + cieca_pft vehicle{ id notes diff --git a/server/job/job-totals.js b/server/job/job-totals.js index 9c825ada4..86a9c1bea 100644 --- a/server/job/job-totals.js +++ b/server/job/job-totals.js @@ -429,55 +429,6 @@ async function CalculateRatesTotals({ job, client }) { function CalculatePartsTotals(jobLines, parts_tax_rates) { const jl = jobLines.filter((jl) => !jl.removed); - // jl.forEach((line) => { - // //Some profile based estimates don't automatically add the discount to the line, some do. - // //Clean up the ones that don't to add it in. - - // //Apply a discount to the line if there is a profile discount, but it isn't added to the line itself. - // const partTax = parts_tax_rates[line.part_type]; - // if ( - // line.act_price > 0 && - // partTax && - // partTax.prt_discp && - // partTax.prt_discp > 0 - // ) { - // //apply a discount - // const discount = Dinero({ - // amount: Math.round(line.act_price * 100), - // }).percentage( - // Math.abs(partTax.prt_discp) > 1 - // ? partTax.prt_discp - // : partTax.prt_discp * 100 - // ); - // line.prt_dsmk_m = discount.toFormat("0.0"); - // line.prt_dsmk_p = partTax.prt_discp; - // line.act_price = Dinero({ - // amount: Math.round(line.act_price * 100), - // }) - // .subtract(discount) - // .toFormat("0.0"); - // } else if ( - // line.act_price > 0 && - // partTax && - // partTax.prt_mkupp && - // partTax.prt_mkupp > 0 - // ) { - // //apply a mark up - // const markup = Dinero({ - // amount: Math.round(line.act_price * 100), - // }).percentage( - // Math.abs(partTax.prt_mkupp) > 1 - // ? partTax.prt_mkupp - // : partTax.prt_mkupp * 100 - // ); - // line.prt_dsmk_m = markup.toFormat("0.0"); - // line.prt_dsmk_p = partTax.prt_mkupp; - // line.act_price = Dinero({ amount: Math.round(line.act_price * 100) }) - // .add(markup) - // .toFormat("0.0"); - // } - // }); - const ret = jl.reduce( (acc, value) => { switch (value.part_type) { @@ -738,7 +689,6 @@ function CalculateAdditional(job) { .add(ret.adjustments) //IO-813 Adjustment takes care of GST & PST at labor rate. .add(ret.towing) .add(ret.storage); - //.add(ret.pvrt); return ret; } @@ -767,23 +717,35 @@ function CalculateTaxesTotals(job, otherTotals) { job.parts_tax_rates.PAGQ || job.parts_tax_rates.PAGR); + const taxableAmounts = { + PAA: Dinero(), + PAN: Dinero(), + PAL: Dinero(), + PAR: Dinero(), + PAC: Dinero(), + PAG: Dinero(), + PAO: Dinero(), + PAS: Dinero(), + PAP: Dinero(), + PAM: Dinero(), + }; + + //For each line, determine if it's taxable, and if it is, add the line amount to the taxable amounts total. job.joblines .filter((jl) => !jl.removed) .forEach((val) => { if (!val.tax_part) return; if (!val.part_type && IsAdditionalCost(val)) { - additionalItemsTax = additionalItemsTax.add( - Dinero({ amount: Math.round((val.act_price || 0) * 100) }) - .multiply(val.part_qty || 0) - .percentage( - ((job.parts_tax_rates && - job.parts_tax_rates["PAN"] && - job.parts_tax_rates["PAN"].prt_tax_rt) || - 0) * 100 - ) + taxableAmounts.PAO = taxableAmounts.PAO.add( + Dinero({ amount: Math.round((val.act_price || 0) * 100) }).multiply( + val.part_qty || 0 + ) ); + } else if (!val.part_type) { + //Do nothing for now. } else { - statePartsTax = statePartsTax.add( + const typeOfPart = val.part_type === "PAM" ? "PAC" : val.part_type; + taxableAmounts[typeOfPart] = taxableAmounts[typeOfPart].add( Dinero({ amount: Math.round((val.act_price || 0) * 100) }) .multiply(val.part_qty || 0) .add( @@ -800,24 +762,98 @@ function CalculateTaxesTotals(job, otherTotals) { .multiply(val.prt_dsmk_p > 0 ? 1 : -1) : Dinero() ) - .percentage( - ((job.parts_tax_rates && - job.parts_tax_rates[val.part_type] && - job.parts_tax_rates[val.part_type].prt_tax_rt) || - (val.part_type && - val.part_type.startsWith("PAG") && - BackupGlassTax && - BackupGlassTax.prt_tax_rt) || - (!val.part_type && - val.db_ref === "900510" && - job.parts_tax_rates["PAN"] && - job.parts_tax_rates["PAN"].prt_tax_rt) || - 0) * 100 - ) ); } }); + //Taxable amounts should match with what is in the STL file. + //Now that we have the taxable amounts, apply the tax. + + const tieredTaxAmounts = { + ty1Tax: Dinero(), + ty2Tax: Dinero(), + ty3Tax: Dinero(), + ty4Tax: Dinero(), + ty5Tax: Dinero(), + ty6Tax: Dinero(), + }; + + const remainingTaxableAmounts = taxableAmounts; + console.log("Taxable Parts Totals", JSON.stringify(taxableAmounts, null, 2)); + + Object.keys(taxableAmounts).forEach((part_type) => { + //Check it's taxability in the PFP + try { + const pfp = job.parts_tax_rates; + + const typeOfPart = part_type === "PAM" ? "PAC" : part_type; + if (IsTrueOrYes(pfp[typeOfPart].prt_tax_in)) { + //At least one of these scenarios must be taxable. + for (let tyCounter = 1; tyCounter <= 5; tyCounter++) { + 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}` + // ); + + let taxableAmountInThisThreshold; + if (thresholdAmount === 9999.99) { + // THis is the last threshold. Tax the entire remaining amount. + taxableAmountInThisThreshold = + remainingTaxableAmounts[typeOfPart]; + remainingTaxableAmounts[typeOfPart] = Dinero(); + } else { + if ( + thresholdAmount >= + remainingTaxableAmounts[typeOfPart].getAmount() / 100 + ) { + //This threshold is bigger than the remaining taxable balance. Add it all. + taxableAmountInThisThreshold = + remainingTaxableAmounts[typeOfPart]; + remainingTaxableAmounts[typeOfPart] = Dinero(); + } 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), + }); + remainingTaxableAmounts[typeOfPart] = remainingTaxableAmounts[ + typeOfPart + ].subtract( + Dinero({ + amount: Math.round(taxableAmountInThisThreshold * 100), + }) + ); + } + } + + const taxAmountToAdd = + taxableAmountInThisThreshold.percentage(thresholdTaxRate); + + tieredTaxAmounts[`ty${tyCounter}Tax`] = + tieredTaxAmounts[`ty${tyCounter}Tax`].add(taxAmountToAdd); + } + } + } + } + } catch (error) { + console.log("Shit the bed."); + } + }); + + statePartsTax = statePartsTax + .add(tieredTaxAmounts.ty1Tax) + .add(tieredTaxAmounts.ty2Tax) + .add(tieredTaxAmounts.ty3Tax) + .add(tieredTaxAmounts.ty4Tax) + .add(tieredTaxAmounts.ty5Tax) + .add(tieredTaxAmounts.ty6Tax); + console.log("Tiered Taxes Total for Parts", statePartsTax.toFormat()); let laborTaxTotal = Dinero(); if (Object.keys(job.cieca_pfl).length > 0) { @@ -853,6 +889,8 @@ function CalculateTaxesTotals(job, otherTotals) { (job.tax_lbr_rt || 0) * 100 ); // THis is currently using the lbr tax rate from PFH not PFL. } + + console.log("Labor Tax Total", laborTaxTotal.toFormat()); let ret = { subtotal: subtotal, federal_tax: subtotal @@ -876,7 +914,7 @@ function CalculateTaxesTotals(job, otherTotals) { .add( otherTotals.additional.storage.percentage((job.tax_str_rt || 0) * 100) ) - .add(additionalItemsTax) + // .add(additionalItemsTax) .add( otherTotals.rates.mapa.hasMapaLine === false //If parts and materials were not added as lines, we must calculate the taxes on them. ? otherTotals.rates.mapa.total.percentage( @@ -964,3 +1002,7 @@ function ParseCalopCode(opcode) { if (!opcode) return []; return opcode.trim().split(" "); } + +function IsTrueOrYes(value) { + return value === true || value === "Y" || value === "y"; +} diff --git a/setadmin.js b/setadmin.js index c62f8c0a2..1f4f9dc0b 100644 --- a/setadmin.js +++ b/setadmin.js @@ -1,6 +1,6 @@ var { admin } = require("./server/firebase/firebase-handler"); -const uidToMakeAdmin = "yTvpfkcNnGckLd1JnoXC7bTdvtu1"; +const uidToMakeAdmin = "fIaZcVQQfUR12Fu14I2fyA5vXbp1"; admin .auth() From 536f8d9cb977803ea4c37510c1f9dc17ccbfc3b6 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 12 Sep 2023 14:16:01 -0700 Subject: [PATCH 02/11] 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 + } + }`; +}; From d1ba90408d89e15254be40a37df4b5d53a415f94 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Wed, 13 Sep 2023 14:39:58 -0700 Subject: [PATCH 03/11] Update hazard waste calculation. --- server/job/job-totals.js | 65 ++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/server/job/job-totals.js b/server/job/job-totals.js index c2ec10132..675ea942c 100644 --- a/server/job/job-totals.js +++ b/server/job/job-totals.js @@ -378,28 +378,41 @@ async function CalculateRatesTotals({ job, client }) { (!hasMahwLine || hasMahwLine.act_price !== stlMahw.ttl_amt) ) { //Add a hazardous waste material line in case there isn't one on the estimate. - const newMahwLine = { - line_desc: "Hazardous Waste Removal*", - part_type: "PAS", - oem_partno: null, - db_price: 0, - act_price: stlMahw.ttl_amt, - part_qty: 1, - //mod_lbr_ty: "LAB", - db_hrs: 0, - mod_lb_hrs: 0, - lbr_op: "OP11", - lbr_amt: 0, - op_code_desc: "REMOVE / REPLACE", - tax_part: stlMahw.tax_amt > 0 ? true : false, - db_ref: null, - manual_line: true, - jobid: job.id, - }; - job.joblines.push(newMahwLine); - await client.request(queries.INSERT_NEW_JOB_LINE, { - lineInput: [newMahwLine], - }); + if (hasMahwLine) { + //Update it + job.joblines.forEach((jl) => { + if (jl.id === hasMahwLine.id) { + jl.act_price = stlMahw.ttl_amt; + } + }); + await client.request(queries.UPDATE_JOB_LINE, { + lineId: hasMahwLine.id, + line: { act_price: stlMahw.ttl_amt }, + }); + } else { + const newMahwLine = { + line_desc: "Hazardous Waste Removal*", + part_type: "PAS", + oem_partno: null, + db_price: 0, + act_price: stlMahw.ttl_amt, + part_qty: 1, + //mod_lbr_ty: "LAB", + db_hrs: 0, + mod_lb_hrs: 0, + lbr_op: "OP11", + lbr_amt: 0, + op_code_desc: "REMOVE / REPLACE", + tax_part: stlMahw.tax_amt > 0 ? true : false, + db_ref: null, + manual_line: true, + jobid: job.id, + }; + job.joblines.push(newMahwLine); + await client.request(queries.INSERT_NEW_JOB_LINE, { + lineInput: [newMahwLine], + }); + } } //Materials Scrubbing as required by CCC. @@ -1061,10 +1074,10 @@ async function UpdateJobLines(joblinesToUpdate) { generateUpdateQuery(_.pick(line, ["id", "prt_dsmk_m", "prt_dsmk_p"]), index) ); const query = ` -mutation UPDATE_EST_LINES{ - ${updateQueries} -} -`; + mutation UPDATE_EST_LINES{ + ${updateQueries} + } + `; const result = await adminClient.request(query); } From ff318599f5ecd45b3313a07f65d13bb74731c8f7 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Wed, 13 Sep 2023 14:55:20 -0700 Subject: [PATCH 04/11] Add regression check. --- .../jobs-detail-rates.parts.component.jsx | 9 +- server/job/job-totals.js | 272 +++++++++++------- 2 files changed, 174 insertions(+), 107 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 2cd2b8917..71f2103c7 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 @@ -1,4 +1,4 @@ -import { Collapse, Form, InputNumber, Switch } from "antd"; +import { Collapse, Form, Input, InputNumber, Switch } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -563,6 +563,13 @@ export function JobsDetailRatesParts({ > + + + !jl.removed) - .forEach((val) => { - if (!val.tax_part) return; - if (!val.part_type && IsAdditionalCost(val)) { - taxableAmounts.PAO = taxableAmounts.PAO.add( - Dinero({ amount: Math.round((val.act_price || 0) * 100) }).multiply( - val.part_qty || 0 - ) - ); - } else if (!val.part_type) { - //Do nothing for now. - } else { - const typeOfPart = val.part_type === "PAM" ? "PAC" : val.part_type; - taxableAmounts[typeOfPart] = taxableAmounts[typeOfPart].add( - Dinero({ amount: Math.round((val.act_price || 0) * 100) }) - .multiply(val.part_qty || 0) - .add( - val.prt_dsmk_m && - val.prt_dsmk_m !== 0 && - DiscountNotAlreadyCounted(val, job.joblines) - ? val.prt_dsmk_m - ? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) }) - : Dinero({ - amount: Math.round(val.act_price * 100), - }) - .multiply(val.part_qty || 0) - .percentage(Math.abs(val.prt_dsmk_p || 0)) - .multiply(val.prt_dsmk_p > 0 ? 1 : -1) - : Dinero() + if ( + job.parts_tax_rates.PAN.prt_tx_ty1 && + job.parts_tax_rates.PAN.prt_tx_ty1 !== "" + ) { + //For each line, determine if it's taxable, and if it is, add the line amount to the taxable amounts total. + job.joblines + .filter((jl) => !jl.removed) + .forEach((val) => { + if (!val.tax_part) return; + if (!val.part_type && IsAdditionalCost(val)) { + taxableAmounts.PAO = taxableAmounts.PAO.add( + Dinero({ amount: Math.round((val.act_price || 0) * 100) }).multiply( + val.part_qty || 0 ) - ); - } - }); + ); + } else if (!val.part_type) { + //Do nothing for now. + } else { + const typeOfPart = val.part_type === "PAM" ? "PAC" : val.part_type; + taxableAmounts[typeOfPart] = taxableAmounts[typeOfPart].add( + Dinero({ amount: Math.round((val.act_price || 0) * 100) }) + .multiply(val.part_qty || 0) + .add( + val.prt_dsmk_m && + val.prt_dsmk_m !== 0 && + DiscountNotAlreadyCounted(val, job.joblines) + ? val.prt_dsmk_m + ? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) }) + : Dinero({ + amount: Math.round(val.act_price * 100), + }) + .multiply(val.part_qty || 0) + .percentage(Math.abs(val.prt_dsmk_p || 0)) + .multiply(val.prt_dsmk_p > 0 ? 1 : -1) + : Dinero() + ) + ); + } + }); - //Taxable amounts should match with what is in the STL file. - //Now that we have the taxable amounts, apply the tax. + //Taxable amounts should match with what is in the STL file. + //Now that we have the taxable amounts, apply the tax. - const tieredTaxAmounts = { - ty1Tax: Dinero(), - ty2Tax: Dinero(), - ty3Tax: Dinero(), - ty4Tax: Dinero(), - ty5Tax: Dinero(), - ty6Tax: Dinero(), - }; + const tieredTaxAmounts = { + ty1Tax: Dinero(), + ty2Tax: Dinero(), + ty3Tax: Dinero(), + ty4Tax: Dinero(), + ty5Tax: Dinero(), + ty6Tax: Dinero(), + }; - const remainingTaxableAmounts = taxableAmounts; - 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 { - const pfp = job.parts_tax_rates; + const remainingTaxableAmounts = taxableAmounts; + 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 { + const pfp = job.parts_tax_rates; - const typeOfPart = part_type === "PAM" ? "PAC" : part_type; - if (IsTrueOrYes(pfp[typeOfPart].prt_tax_in)) { - //At least one of these scenarios must be taxable. - for (let tyCounter = 1; tyCounter <= 5; tyCounter++) { - 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 = parseFloat( - job.cieca_pft[`ty${tyCounter}_thres${threshCounter}`] - ); - const thresholdTaxRate = parseFloat( - job.cieca_pft[`ty${tyCounter}_rate${threshCounter}`] - ); + const typeOfPart = part_type === "PAM" ? "PAC" : part_type; + if (IsTrueOrYes(pfp[typeOfPart].prt_tax_in)) { + //At least one of these scenarios must be taxable. + for (let tyCounter = 1; tyCounter <= 5; tyCounter++) { + 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 = parseFloat( + job.cieca_pft[`ty${tyCounter}_thres${threshCounter}`] + ); + const thresholdTaxRate = parseFloat( + job.cieca_pft[`ty${tyCounter}_rate${threshCounter}`] + ); - let taxableAmountInThisThreshold; - if (thresholdAmount === 9999.99) { - // THis is the last threshold. Tax the entire remaining amount. - taxableAmountInThisThreshold = - remainingTaxableAmounts[typeOfPart]; - remainingTaxableAmounts[typeOfPart] = Dinero(); - } else { - if ( - thresholdAmount >= - remainingTaxableAmounts[typeOfPart].getAmount() / 100 - ) { - //This threshold is bigger than the remaining taxable balance. Add it all. + let taxableAmountInThisThreshold; + if (thresholdAmount === 9999.99) { + // THis is the last threshold. Tax the entire remaining amount. taxableAmountInThisThreshold = remainingTaxableAmounts[typeOfPart]; remainingTaxableAmounts[typeOfPart] = Dinero(); } else { - //Take the size of the threshold from the remaining amount, tax it, and do it all over. - taxableAmountInThisThreshold = Dinero({ - amount: Math.round(thresholdAmount * 100), - }); - remainingTaxableAmounts[typeOfPart] = remainingTaxableAmounts[ - typeOfPart - ].subtract( - Dinero({ - amount: Math.round(taxableAmountInThisThreshold * 100), - }) - ); + if ( + thresholdAmount >= + remainingTaxableAmounts[typeOfPart].getAmount() / 100 + ) { + //This threshold is bigger than the remaining taxable balance. Add it all. + taxableAmountInThisThreshold = + remainingTaxableAmounts[typeOfPart]; + remainingTaxableAmounts[typeOfPart] = Dinero(); + } else { + //Take the size of the threshold from the remaining amount, tax it, and do it all over. + taxableAmountInThisThreshold = Dinero({ + amount: Math.round(thresholdAmount * 100), + }); + remainingTaxableAmounts[typeOfPart] = + remainingTaxableAmounts[typeOfPart].subtract( + Dinero({ + amount: Math.round( + taxableAmountInThisThreshold * 100 + ), + }) + ); + } } + + const taxAmountToAdd = + taxableAmountInThisThreshold.percentage(thresholdTaxRate); + + tieredTaxAmounts[`ty${tyCounter}Tax`] = + tieredTaxAmounts[`ty${tyCounter}Tax`].add(taxAmountToAdd); } - - const taxAmountToAdd = - taxableAmountInThisThreshold.percentage(thresholdTaxRate); - - tieredTaxAmounts[`ty${tyCounter}Tax`] = - tieredTaxAmounts[`ty${tyCounter}Tax`].add(taxAmountToAdd); } } } + } catch (error) { + console.error("Shit the bed."); } - } catch (error) { - console.error("Shit the bed."); - } - }); + }); + + statePartsTax = statePartsTax + .add(tieredTaxAmounts.ty1Tax) + .add(tieredTaxAmounts.ty2Tax) + .add(tieredTaxAmounts.ty3Tax) + .add(tieredTaxAmounts.ty4Tax) + .add(tieredTaxAmounts.ty5Tax) + .add(tieredTaxAmounts.ty6Tax); + console.log("Tiered Taxes Total for Parts", statePartsTax.toFormat()); + } else { + //Use the old thing. + job.joblines + .filter((jl) => !jl.removed) + .forEach((val) => { + if (!val.tax_part) return; + if (!val.part_type && IsAdditionalCost(val)) { + additionalItemsTax = additionalItemsTax.add( + Dinero({ amount: Math.round((val.act_price || 0) * 100) }) + .multiply(val.part_qty || 0) + .percentage( + ((job.parts_tax_rates && + job.parts_tax_rates["PAN"] && + job.parts_tax_rates["PAN"].prt_tax_rt) || + 0) * 100 + ) + ); + } else { + statePartsTax = statePartsTax.add( + Dinero({ amount: Math.round((val.act_price || 0) * 100) }) + .multiply(val.part_qty || 0) + .add( + val.prt_dsmk_m && + val.prt_dsmk_m !== 0 && + DiscountNotAlreadyCounted(val, job.joblines) + ? val.prt_dsmk_m + ? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) }) + : Dinero({ + amount: Math.round(val.act_price * 100), + }) + .multiply(val.part_qty || 0) + .percentage(Math.abs(val.prt_dsmk_p || 0)) + .multiply(val.prt_dsmk_p > 0 ? 1 : -1) + : Dinero() + ) + .percentage( + ((job.parts_tax_rates && + job.parts_tax_rates[val.part_type] && + job.parts_tax_rates[val.part_type].prt_tax_rt) || + (val.part_type && + val.part_type.startsWith("PAG") && + BackupGlassTax && + BackupGlassTax.prt_tax_rt) || + (!val.part_type && + val.db_ref === "900510" && + job.parts_tax_rates["PAN"] && + job.parts_tax_rates["PAN"].prt_tax_rt) || + 0) * 100 + ) + ); + } + console.log(statePartsTax.toFormat(), val.line_desc); + }); + } - statePartsTax = statePartsTax - .add(tieredTaxAmounts.ty1Tax) - .add(tieredTaxAmounts.ty2Tax) - .add(tieredTaxAmounts.ty3Tax) - .add(tieredTaxAmounts.ty4Tax) - .add(tieredTaxAmounts.ty5Tax) - .add(tieredTaxAmounts.ty6Tax); - console.log("Tiered Taxes Total for Parts", statePartsTax.toFormat()); let laborTaxTotal = Dinero(); if (Object.keys(job.cieca_pfl).length > 0) { @@ -975,7 +1035,7 @@ function CalculateTaxesTotals(job, otherTotals) { .add( otherTotals.additional.storage.percentage((job.tax_str_rt || 0) * 100) ) - // .add(additionalItemsTax) + .add(additionalItemsTax) // 0 if using PFP method. .add( otherTotals.rates.mapa.hasMapaLine === false //If parts and materials were not added as lines, we must calculate the taxes on them. ? otherTotals.rates.mapa.total.percentage( From fef680fb995dc253ec4c70dabdb823c59e0b48ca Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 14 Sep 2023 15:18:59 -0700 Subject: [PATCH 05/11] Add labor to tax thresholds. --- job-totals-testing-util.js | 2 +- server/job/job-totals.js | 223 +++++++++++++++++++++---------------- 2 files changed, 129 insertions(+), 96 deletions(-) diff --git a/job-totals-testing-util.js b/job-totals-testing-util.js index 53eea2552..b69ec111b 100644 --- a/job-totals-testing-util.js +++ b/job-totals-testing-util.js @@ -99,7 +99,7 @@ async function RunTheTest() { } } - console.table(results); + console.table(results.filter((r) => r.result !== "PASS")); const summary = results.reduce( (acc, val) => { if (val.result === "PASS") { diff --git a/server/job/job-totals.js b/server/job/job-totals.js index d80805e4e..16b5ca5bd 100644 --- a/server/job/job-totals.js +++ b/server/job/job-totals.js @@ -762,9 +762,6 @@ function CalculateTaxesTotals(job, otherTotals) { .add(otherTotals.rates.subtotal) //No longer using just rates subtotal to include mapa/mash. .add(otherTotals.additional.total); - // .add(Dinero({ amount: (job.towing_payable || 0) * 100 })) - // .add(Dinero({ amount: (job.storage_payable || 0) * 100 })); - //Potential issue here with Sublet Calculation. Sublets are calculated under labor in Mitchell, but it's done in IO //Under the parts rates. @@ -791,6 +788,21 @@ function CalculateTaxesTotals(job, otherTotals) { PAS: Dinero(), PAP: Dinero(), PAM: Dinero(), + + LA1: Dinero(), + LA2: Dinero(), + LA3: Dinero(), + LA4: Dinero(), + LAU: Dinero(), + LAA: Dinero(), + LAB: Dinero(), + LAD: Dinero(), + LAE: Dinero(), + LAF: Dinero(), + LAG: Dinero(), + LAM: Dinero(), + LAR: Dinero(), + LAS: Dinero(), }; if ( @@ -811,7 +823,7 @@ function CalculateTaxesTotals(job, otherTotals) { } else if (!val.part_type) { //Do nothing for now. } else { - const typeOfPart = val.part_type === "PAM" ? "PAC" : val.part_type; + const typeOfPart = val.part_type; // === "PAM" ? "PAC" : val.part_type; taxableAmounts[typeOfPart] = taxableAmounts[typeOfPart].add( Dinero({ amount: Math.round((val.act_price || 0) * 100) }) .multiply(val.part_qty || 0) @@ -833,10 +845,33 @@ function CalculateTaxesTotals(job, otherTotals) { } }); - //Taxable amounts should match with what is in the STL file. - //Now that we have the taxable amounts, apply the tax. + //Check in the PFL file which types of labor are taxable. Add the amount that is considered taxable to the taxable amounts total. + Object.keys(taxableAmounts) + .filter((key) => key.startsWith("LA")) + .map((key) => { + const isLaborTypeTaxable = job.cieca_pfl[key]?.lbr_tax_in; + if (isLaborTypeTaxable) { + taxableAmounts[key] = taxableAmounts[key].add( + otherTotals.rates[key.toLowerCase()].total + ); + } + }); - const tieredTaxAmounts = { + console.log("*** Taxable Amounts***"); + console.table(JSON.parse(JSON.stringify(taxableAmounts))); + + //For the taxable amounts, figure out which tax type applies. + //Then sum up the total of that tax type and then calculate the thresholds. + + const taxableAmountsByTier = { + ty1Tax: Dinero(), + ty2Tax: Dinero(), + ty3Tax: Dinero(), + ty4Tax: Dinero(), + ty5Tax: Dinero(), + ty6Tax: Dinero(), + }; + const totalTaxByTier = { ty1Tax: Dinero(), ty2Tax: Dinero(), ty3Tax: Dinero(), @@ -845,80 +880,104 @@ function CalculateTaxesTotals(job, otherTotals) { ty6Tax: Dinero(), }; - const remainingTaxableAmounts = taxableAmounts; - console.log("Taxable Amounts"); - console.table(JSON.parse(JSON.stringify(taxableAmounts))); - Object.keys(taxableAmounts).forEach((part_type) => { - //Check it's taxability in the PFP + const pfp = job.parts_tax_rates; + const pfl = job.cieca_pfl; + Object.keys(taxableAmounts).map((key) => { try { - const pfp = job.parts_tax_rates; - - const typeOfPart = part_type === "PAM" ? "PAC" : part_type; - if (IsTrueOrYes(pfp[typeOfPart].prt_tax_in)) { + if (key.startsWith("PA")) { + const typeOfPart = key; // === "PAM" ? "PAC" : key; //At least one of these scenarios must be taxable. for (let tyCounter = 1; tyCounter <= 5; tyCounter++) { 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 = parseFloat( - job.cieca_pft[`ty${tyCounter}_thres${threshCounter}`] - ); - const thresholdTaxRate = parseFloat( - job.cieca_pft[`ty${tyCounter}_rate${threshCounter}`] - ); - - let taxableAmountInThisThreshold; - if (thresholdAmount === 9999.99) { - // THis is the last threshold. Tax the entire remaining amount. - taxableAmountInThisThreshold = - remainingTaxableAmounts[typeOfPart]; - remainingTaxableAmounts[typeOfPart] = Dinero(); - } else { - if ( - thresholdAmount >= - remainingTaxableAmounts[typeOfPart].getAmount() / 100 - ) { - //This threshold is bigger than the remaining taxable balance. Add it all. - taxableAmountInThisThreshold = - remainingTaxableAmounts[typeOfPart]; - remainingTaxableAmounts[typeOfPart] = Dinero(); - } else { - //Take the size of the threshold from the remaining amount, tax it, and do it all over. - taxableAmountInThisThreshold = Dinero({ - amount: Math.round(thresholdAmount * 100), - }); - remainingTaxableAmounts[typeOfPart] = - remainingTaxableAmounts[typeOfPart].subtract( - Dinero({ - amount: Math.round( - taxableAmountInThisThreshold * 100 - ), - }) - ); - } - } - - const taxAmountToAdd = - taxableAmountInThisThreshold.percentage(thresholdTaxRate); - - tieredTaxAmounts[`ty${tyCounter}Tax`] = - tieredTaxAmounts[`ty${tyCounter}Tax`].add(taxAmountToAdd); - } + //This amount is taxable for this type. + taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[ + `ty${tyCounter}Tax` + ].add(taxableAmounts[typeOfPart]); + } + } + } else { + //Labor. + for (let tyCounter = 1; tyCounter <= 5; tyCounter++) { + if (IsTrueOrYes(pfl[key][`lbr_tx_in${tyCounter}`])) { + //This amount is taxable for this type. + taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[ + `ty${tyCounter}Tax` + ].add(taxableAmounts[key]); } } } } catch (error) { - console.error("Shit the bed."); + console.error("Key with issue", key); } }); + const remainingTaxableAmounts = taxableAmountsByTier; + console.log("*** Taxable Amounts by Tier***"); + console.table(JSON.parse(JSON.stringify(taxableAmountsByTier))); + + Object.keys(taxableAmountsByTier).forEach((taxTierKey) => { + try { + let tyCounter = taxTierKey[2]; //Get the number from the key. + //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 = parseFloat( + job.cieca_pft[`ty${tyCounter}_thres${threshCounter}`] + ); + const thresholdTaxRate = parseFloat( + job.cieca_pft[`ty${tyCounter}_rate${threshCounter}`] + ); + + let taxableAmountInThisThreshold; + if (thresholdAmount === 9999.99) { + // THis is the last threshold. Tax the entire remaining amount. + taxableAmountInThisThreshold = remainingTaxableAmounts[taxTierKey]; + remainingTaxableAmounts[taxTierKey] = Dinero(); + } else { + if ( + thresholdAmount >= + remainingTaxableAmounts[taxTierKey].getAmount() / 100 + ) { + //This threshold is bigger than the remaining taxable balance. Add it all. + taxableAmountInThisThreshold = + remainingTaxableAmounts[taxTierKey]; + remainingTaxableAmounts[taxTierKey] = Dinero(); + } else { + //Take the size of the threshold from the remaining amount, tax it, and do it all over. + taxableAmountInThisThreshold = Dinero({ + amount: Math.round(thresholdAmount * 100), + }); + remainingTaxableAmounts[taxTierKey] = remainingTaxableAmounts[ + taxTierKey + ].subtract( + Dinero({ + amount: Math.round(taxableAmountInThisThreshold * 100), + }) + ); + } + } + + const taxAmountToAdd = + taxableAmountInThisThreshold.percentage(thresholdTaxRate); + + totalTaxByTier[taxTierKey] = + totalTaxByTier[taxTierKey].add(taxAmountToAdd); + } + } catch (error) { + console.error("PFP Calculation error", error); + } + }); + + console.log("*** Total Tax by Tier Amounts***"); + console.table(JSON.parse(JSON.stringify(totalTaxByTier))); + statePartsTax = statePartsTax - .add(tieredTaxAmounts.ty1Tax) - .add(tieredTaxAmounts.ty2Tax) - .add(tieredTaxAmounts.ty3Tax) - .add(tieredTaxAmounts.ty4Tax) - .add(tieredTaxAmounts.ty5Tax) - .add(tieredTaxAmounts.ty6Tax); + .add(totalTaxByTier.ty1Tax) + .add(totalTaxByTier.ty2Tax) + .add(totalTaxByTier.ty3Tax) + .add(totalTaxByTier.ty4Tax) + .add(totalTaxByTier.ty5Tax) + .add(totalTaxByTier.ty6Tax); + console.log("Tiered Taxes Total for Parts", statePartsTax.toFormat()); } else { //Use the old thing. @@ -978,32 +1037,6 @@ function CalculateTaxesTotals(job, otherTotals) { let laborTaxTotal = Dinero(); if (Object.keys(job.cieca_pfl).length > 0) { - //Do it by labor type - const types = [ - "la1", - "la2", - "la3", - "la4", - "lau", - "laa", - "lab", - "lad", - "lae", - "laf", - "lag", - "lam", - "lar", - "las", - ]; - types.forEach((type) => { - laborTaxTotal = laborTaxTotal.add( - otherTotals.rates[type].total.percentage( - job.cieca_pfl[type.toUpperCase()] - ? job.cieca_pfl[type.toUpperCase()].lbr_taxp - : (job.tax_lbr_rt || 0) * 100 - ) - ); - }); } else { //We don't have it, just add in how it was before. laborTaxTotal = otherTotals.rates.subtotal.percentage( From eff4f82ad7360b431f11d3c8a10e9f44dc635166 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 19 Sep 2023 15:23:47 -0700 Subject: [PATCH 06/11] Add tax rates to shop config, resolve rate population on job detail. --- bodyshop_translations.babel | 380 +++++++++++++++++- .../job-totals.table.totals.component.jsx | 44 +- ...p-info.responsibilitycenters.component.jsx | 2 + ....responsibilitycenters.taxes.component.jsx | 232 +++++++++++ .../jobs-detail.page.component.jsx | 2 +- client/src/translations/en_us/common.json | 7 +- client/src/translations/es/common.json | 6 + client/src/translations/fr/common.json | 6 + server/job/job-totals.js | 9 +- 9 files changed, 665 insertions(+), 23 deletions(-) create mode 100644 client/src/components/shop-info/shop-info.responsibilitycenters.taxes.component.jsx diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 3039e4986..7cfb277dd 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -1,4 +1,4 @@ - +