RrScratch3 - Tax / Extras Improvements
This commit is contained in:
@@ -63,8 +63,10 @@ function emptyCenterBucket() {
|
||||
laborTaxableSale: zero, // labor that should be taxed in RR
|
||||
laborNonTaxableSale: zero, // labor that should NOT be taxed in RR
|
||||
|
||||
// Extras
|
||||
extrasSale: zero // MAPA/MASH/towing/storage/PAO/etc
|
||||
// Extras (MAPA/MASH/towing/PAO/etc)
|
||||
extrasSale: zero, // total extras (taxable + non-taxable)
|
||||
extrasTaxableSale: zero,
|
||||
extrasNonTaxableSale: zero
|
||||
};
|
||||
}
|
||||
|
||||
@@ -162,26 +164,228 @@ function buildTaxAllocations(bodyshop, job) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide if a labor line is taxable vs non-taxable for RR.
|
||||
* ============================
|
||||
* Tax Context & Helpers
|
||||
* ============================
|
||||
*/
|
||||
function isLaborTaxable(line) {
|
||||
return line.tax_part;
|
||||
|
||||
/**
|
||||
* Build a small "tax context" object from the job + current instance.
|
||||
*
|
||||
* This centralises all of the "is this category taxable?" logic so that
|
||||
* the rest of the allocation code just asks simple yes/no questions.
|
||||
*
|
||||
* IMPORTANT: we are **not** calculating any tax **amounts** here – that is
|
||||
* still handled by job-costing. We only need to know if a given sale bucket
|
||||
* should be treated as taxable vs non-taxable for RR (CustTxblNTxblFlag).
|
||||
*/
|
||||
function buildTaxContext(job = {}) {
|
||||
const isImex = !!InstanceManager({ imex: true }); // Canada
|
||||
const isRome = !!InstanceManager({ rome: true }); // US
|
||||
|
||||
const toNumber = (v) => (v == null ? 0 : Number(v) || 0);
|
||||
|
||||
const federalTaxRate = toNumber(job.federal_tax_rate);
|
||||
const stateTaxRate = toNumber(job.state_tax_rate);
|
||||
const localTaxRate = toNumber(job.local_tax_rate);
|
||||
|
||||
const hasFederalRate = federalTaxRate > 0;
|
||||
const hasState = stateTaxRate > 0;
|
||||
const hasLocal = localTaxRate > 0;
|
||||
|
||||
// "hasFederal" kept for backwards compatibility / logging (Canada only)
|
||||
const hasFederal = isImex && hasFederalRate;
|
||||
|
||||
// Canada: if ANY of federal / state / local > 0, treat the job as
|
||||
// "everything taxable by default", then let line-level flags override
|
||||
// for parts where applicable.
|
||||
const globalAllTaxCanada = isImex && (hasFederalRate || hasState || hasLocal);
|
||||
|
||||
const hasAnySalesTax = hasFederalRate || hasState || hasLocal;
|
||||
|
||||
// Parts tax rate map (PAA/PAC/…)
|
||||
let partTaxRates = job.part_tax_rates || job.parts_tax_rates || {};
|
||||
if (typeof partTaxRates === "string") {
|
||||
try {
|
||||
partTaxRates = JSON.parse(partTaxRates);
|
||||
} catch {
|
||||
partTaxRates = {};
|
||||
}
|
||||
}
|
||||
if (!partTaxRates || typeof partTaxRates !== "object") {
|
||||
partTaxRates = {};
|
||||
}
|
||||
|
||||
const tax_lbr_rt = toNumber(job.tax_lbr_rt); // labour
|
||||
const tax_paint_mat_rt = toNumber(job.tax_paint_mat_rt || job.tax_paint_mt_rate); // MAPA
|
||||
const tax_shop_mat_rt = toNumber(job.tax_shop_mat_rt); // MASH
|
||||
const tax_tow_rt = toNumber(job.tax_tow_rt); // towing
|
||||
const tax_sub_rt = toNumber(job.tax_sub_rt); // sublet (rarely used directly)
|
||||
|
||||
const hasAnyPartsWithTax = Object.values(partTaxRates).some(
|
||||
(entry) => entry && entry.prt_tax_in && toNumber(entry.prt_tax_rt) > 0
|
||||
);
|
||||
|
||||
const hasAnyTax =
|
||||
hasAnySalesTax ||
|
||||
tax_lbr_rt > 0 ||
|
||||
tax_paint_mat_rt > 0 ||
|
||||
tax_shop_mat_rt > 0 ||
|
||||
tax_tow_rt > 0 ||
|
||||
tax_sub_rt > 0 ||
|
||||
hasAnyPartsWithTax;
|
||||
|
||||
return {
|
||||
isImex,
|
||||
isRome,
|
||||
|
||||
federalTaxRate,
|
||||
stateTaxRate,
|
||||
localTaxRate,
|
||||
|
||||
hasFederal,
|
||||
hasState,
|
||||
hasLocal,
|
||||
hasAnySalesTax,
|
||||
globalAllTaxCanada,
|
||||
|
||||
partTaxRates,
|
||||
tax_lbr_rt,
|
||||
tax_paint_mat_rt,
|
||||
tax_shop_mat_rt,
|
||||
tax_tow_rt,
|
||||
tax_sub_rt,
|
||||
hasAnyPartsWithTax,
|
||||
hasAnyTax
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the "PA" / part-type code (PAA/PAC/…) from a job line.
|
||||
*/
|
||||
function resolvePartType(line = {}) {
|
||||
return line.part_type || line.partType || line.pa_code || line.pa || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide if a *part* line is taxable vs non-taxable for RR.
|
||||
* For now we mirror the same flag; this can be extended with CAD-specific
|
||||
* logic (federal_tax_rate, parts_tax_rate, prt_tax_in, etc.) later.
|
||||
*
|
||||
* Rules:
|
||||
* - Canada (IMEX):
|
||||
* - If ANY of federal_tax_rate / state_tax_rate / local_tax_rate > 0
|
||||
* => everything is taxable by default (globalAllTaxCanada),
|
||||
* unless tax_part is explicitly false.
|
||||
* - Otherwise, use part_tax_rates[part_type] (prt_tax_in && prt_tax_rt > 0),
|
||||
* with tax_part as final override.
|
||||
* - US (ROME):
|
||||
* - Use part_tax_rates[part_type] (prt_tax_in && prt_tax_rt > 0),
|
||||
* with tax_part as final override.
|
||||
*
|
||||
* - line.tax_part is treated as the *final* check:
|
||||
* - tax_part === false => always non-taxable.
|
||||
* - tax_part === true => always taxable, even if we have no table entry.
|
||||
*/
|
||||
function isPartTaxable(line) {
|
||||
return line.tax_part;
|
||||
function isPartTaxable(line = {}, taxCtx) {
|
||||
if (!taxCtx) return !!line.tax_part;
|
||||
|
||||
const { globalAllTaxCanada, partTaxRates } = taxCtx;
|
||||
|
||||
// Explicit per-line override to *not* tax.
|
||||
if (typeof line.tax_part === "boolean" && line.tax_part === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Canada: any federal/state/local tax rate set => all parts taxable,
|
||||
// unless explicitly turned off above.
|
||||
if (globalAllTaxCanada) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let taxable = false;
|
||||
|
||||
const partType = resolvePartType(line);
|
||||
if (partType && partTaxRates && partTaxRates[partType]) {
|
||||
const entry = partTaxRates[partType];
|
||||
const rate = Number(entry?.prt_tax_rt || 0);
|
||||
const indicator = !!entry?.prt_tax_in;
|
||||
taxable = indicator && rate > 0;
|
||||
}
|
||||
|
||||
// tax_part === true is treated as "final yes" even if we didn't find
|
||||
// a matching part_tax_rate entry.
|
||||
if (typeof line.tax_part === "boolean" && line.tax_part === true) {
|
||||
taxable = true;
|
||||
}
|
||||
|
||||
return taxable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide if *labour* for this job is taxable.
|
||||
*
|
||||
* - Canada (IMEX):
|
||||
* - If ANY of federal_tax_rate / state_tax_rate / local_tax_rate > 0
|
||||
* (globalAllTaxCanada) => all labour is taxable.
|
||||
* - Else if tax_lbr_rt > 0 => labour taxable.
|
||||
* - Else => non-taxable.
|
||||
* - US (ROME):
|
||||
* - tax_lbr_rt > 0 => labour taxable, otherwise not.
|
||||
*/
|
||||
function isLaborTaxable(_line, taxCtx) {
|
||||
if (!taxCtx) return false;
|
||||
const { isImex, globalAllTaxCanada, tax_lbr_rt } = taxCtx;
|
||||
|
||||
if (isImex && globalAllTaxCanada) return true;
|
||||
return tax_lbr_rt > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Taxability helpers for "extras" buckets.
|
||||
* These are all job-level decisions; there are no per-line flags for them
|
||||
* in the data we currently work with.
|
||||
*
|
||||
* Canada: if globalAllTaxCanada is true, we treat these as taxable.
|
||||
*/
|
||||
function isMapaTaxable(taxCtx) {
|
||||
if (!taxCtx) return false;
|
||||
const { isImex, globalAllTaxCanada, tax_paint_mat_rt } = taxCtx;
|
||||
if (isImex && globalAllTaxCanada) return true;
|
||||
return tax_paint_mat_rt > 0;
|
||||
}
|
||||
|
||||
function isMashTaxable(taxCtx) {
|
||||
if (!taxCtx) return false;
|
||||
const { isImex, globalAllTaxCanada, tax_shop_mat_rt } = taxCtx;
|
||||
if (isImex && globalAllTaxCanada) return true;
|
||||
return tax_shop_mat_rt > 0;
|
||||
}
|
||||
|
||||
function isTowTaxable(taxCtx) {
|
||||
if (!taxCtx) return false;
|
||||
const { isImex, globalAllTaxCanada, tax_tow_rt } = taxCtx;
|
||||
if (isImex && globalAllTaxCanada) return true;
|
||||
return tax_tow_rt > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to push an "extra" (MAPA/MASH/towing/PAO/etc) amount into the
|
||||
* appropriate taxable / non-taxable buckets for a given center.
|
||||
*/
|
||||
function addExtras(bucket, dineroAmount, isTaxable) {
|
||||
if (!bucket || !dineroAmount || typeof dineroAmount.add !== "function") return;
|
||||
bucket.extrasSale = bucket.extrasSale.add(dineroAmount);
|
||||
if (isTaxable) {
|
||||
bucket.extrasTaxableSale = bucket.extrasTaxableSale.add(dineroAmount);
|
||||
} else {
|
||||
bucket.extrasNonTaxableSale = bucket.extrasNonTaxableSale.add(dineroAmount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build profitCenterHash from joblines (parts + labor) and detect MAPA/MASH presence.
|
||||
* Now stores *buckets* instead of a single Dinero per center.
|
||||
*/
|
||||
function buildProfitCenterHash(job, debugLog) {
|
||||
function buildProfitCenterHash(job, debugLog, taxContext) {
|
||||
let hasMapaLine = false;
|
||||
let hasMashLine = false;
|
||||
|
||||
@@ -231,7 +435,7 @@ function buildProfitCenterHash(job, debugLog) {
|
||||
amount = amount.add(discount);
|
||||
}
|
||||
|
||||
const taxable = isPartTaxable(val);
|
||||
const taxable = isPartTaxable(val, taxContext);
|
||||
|
||||
if (taxable) {
|
||||
bucket.partsTaxableSale = bucket.partsTaxableSale.add(amount);
|
||||
@@ -254,7 +458,7 @@ function buildProfitCenterHash(job, debugLog) {
|
||||
amount: Math.round(rate * 100)
|
||||
}).multiply(val.mod_lb_hrs);
|
||||
|
||||
if (isLaborTaxable(val)) {
|
||||
if (isLaborTaxable(val, taxContext)) {
|
||||
bucket.laborTaxableSale = bucket.laborTaxableSale.add(laborAmount);
|
||||
} else {
|
||||
bucket.laborNonTaxableSale = bucket.laborNonTaxableSale.add(laborAmount);
|
||||
@@ -274,7 +478,9 @@ function buildProfitCenterHash(job, debugLog) {
|
||||
partsNonTaxable: summarizeMoney(b.partsNonTaxableSale),
|
||||
laborTaxable: summarizeMoney(b.laborTaxableSale),
|
||||
laborNonTaxable: summarizeMoney(b.laborNonTaxableSale),
|
||||
extras: summarizeMoney(b.extrasSale)
|
||||
extras: summarizeMoney(b.extrasSale),
|
||||
extrasTaxable: summarizeMoney(b.extrasTaxableSale),
|
||||
extrasNonTaxable: summarizeMoney(b.extrasNonTaxableSale)
|
||||
}))
|
||||
});
|
||||
|
||||
@@ -353,39 +559,48 @@ function applyMapaMashManualLines({
|
||||
profitCenterHash,
|
||||
hasMapaLine,
|
||||
hasMashLine,
|
||||
debugLog
|
||||
debugLog,
|
||||
taxContext
|
||||
}) {
|
||||
// MAPA
|
||||
// MAPA (paint materials)
|
||||
if (!hasMapaLine && job.job_totals.rates.mapa.total.amount > 0) {
|
||||
const mapaAccountName = selectedDmsAllocationConfig.profits.MAPA;
|
||||
const mapaAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === mapaAccountName);
|
||||
|
||||
if (mapaAccount) {
|
||||
const amount = Dinero(job.job_totals.rates.mapa.total);
|
||||
const taxable = isMapaTaxable(taxContext);
|
||||
|
||||
debugLog("Adding MAPA Line Manually", {
|
||||
mapaAccountName,
|
||||
amount: summarizeMoney(Dinero(job.job_totals.rates.mapa.total))
|
||||
amount: summarizeMoney(amount),
|
||||
taxable
|
||||
});
|
||||
|
||||
const bucket = ensureCenterBucket(profitCenterHash, mapaAccountName);
|
||||
bucket.extrasSale = bucket.extrasSale.add(Dinero(job.job_totals.rates.mapa.total));
|
||||
addExtras(bucket, amount, taxable);
|
||||
} else {
|
||||
debugLog("NO MAPA ACCOUNT FOUND!!", { mapaAccountName });
|
||||
}
|
||||
}
|
||||
|
||||
// MASH
|
||||
// MASH (shop materials)
|
||||
if (!hasMashLine && job.job_totals.rates.mash.total.amount > 0) {
|
||||
const mashAccountName = selectedDmsAllocationConfig.profits.MASH;
|
||||
const mashAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === mashAccountName);
|
||||
|
||||
if (mashAccount) {
|
||||
const amount = Dinero(job.job_totals.rates.mash.total);
|
||||
const taxable = isMashTaxable(taxContext);
|
||||
|
||||
debugLog("Adding MASH Line Manually", {
|
||||
mashAccountName,
|
||||
amount: summarizeMoney(Dinero(job.job_totals.rates.mash.total))
|
||||
amount: summarizeMoney(amount),
|
||||
taxable
|
||||
});
|
||||
|
||||
const bucket = ensureCenterBucket(profitCenterHash, mashAccountName);
|
||||
bucket.extrasSale = bucket.extrasSale.add(Dinero(job.job_totals.rates.mash.total));
|
||||
addExtras(bucket, amount, taxable);
|
||||
} else {
|
||||
debugLog("NO MASH ACCOUNT FOUND!!", { mashAccountName });
|
||||
}
|
||||
@@ -467,9 +682,17 @@ function applyMaterialsCosting({ job, bodyshop, selectedDmsAllocationConfig, cos
|
||||
|
||||
/**
|
||||
* Apply non-tax extras (PVRT, towing, storage, PAO).
|
||||
* Extras go into the extrasSale bucket.
|
||||
* Extras go into the extrasSale bucket (split taxable / non-taxable).
|
||||
*/
|
||||
function applyExtras({ job, bodyshop, selectedDmsAllocationConfig, profitCenterHash, taxAllocations, debugLog }) {
|
||||
function applyExtras({
|
||||
job,
|
||||
bodyshop,
|
||||
selectedDmsAllocationConfig,
|
||||
profitCenterHash,
|
||||
taxAllocations,
|
||||
debugLog,
|
||||
taxContext
|
||||
}) {
|
||||
const { ca_bc_pvrt } = job;
|
||||
|
||||
// BC PVRT -> state tax
|
||||
@@ -485,17 +708,19 @@ function applyExtras({ job, bodyshop, selectedDmsAllocationConfig, profitCenterH
|
||||
const towAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === towAccountName);
|
||||
|
||||
if (towAccount) {
|
||||
const amount = Dinero({
|
||||
amount: Math.round((job.towing_payable || 0) * 100)
|
||||
});
|
||||
const taxable = isTowTaxable(taxContext);
|
||||
|
||||
debugLog("Adding towing_payable to TOW account", {
|
||||
towAccountName,
|
||||
towing_payable: job.towing_payable
|
||||
towing_payable: job.towing_payable,
|
||||
taxable
|
||||
});
|
||||
|
||||
const bucket = ensureCenterBucket(profitCenterHash, towAccountName);
|
||||
bucket.extrasSale = bucket.extrasSale.add(
|
||||
Dinero({
|
||||
amount: Math.round((job.towing_payable || 0) * 100)
|
||||
})
|
||||
);
|
||||
addExtras(bucket, amount, taxable);
|
||||
} else {
|
||||
debugLog("NO TOW ACCOUNT FOUND!!", { towAccountName });
|
||||
}
|
||||
@@ -507,17 +732,19 @@ function applyExtras({ job, bodyshop, selectedDmsAllocationConfig, profitCenterH
|
||||
const towAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === storageAccountName);
|
||||
|
||||
if (towAccount) {
|
||||
const amount = Dinero({
|
||||
amount: Math.round((job.storage_payable || 0) * 100)
|
||||
});
|
||||
const taxable = isTowTaxable(taxContext);
|
||||
|
||||
debugLog("Adding storage_payable to TOW account", {
|
||||
storageAccountName,
|
||||
storage_payable: job.storage_payable
|
||||
storage_payable: job.storage_payable,
|
||||
taxable
|
||||
});
|
||||
|
||||
const bucket = ensureCenterBucket(profitCenterHash, storageAccountName);
|
||||
bucket.extrasSale = bucket.extrasSale.add(
|
||||
Dinero({
|
||||
amount: Math.round((job.storage_payable || 0) * 100)
|
||||
})
|
||||
);
|
||||
addExtras(bucket, amount, taxable);
|
||||
} else {
|
||||
debugLog("NO STORAGE/TOW ACCOUNT FOUND!!", { storageAccountName });
|
||||
}
|
||||
@@ -529,17 +756,19 @@ function applyExtras({ job, bodyshop, selectedDmsAllocationConfig, profitCenterH
|
||||
const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === otherAccountName);
|
||||
|
||||
if (otherAccount) {
|
||||
const amount = Dinero({
|
||||
amount: Math.round((job.adjustment_bottom_line || 0) * 100)
|
||||
});
|
||||
const taxable = !!(taxContext && taxContext.hasAnyTax);
|
||||
|
||||
debugLog("Adding adjustment_bottom_line to PAO", {
|
||||
otherAccountName,
|
||||
adjustment_bottom_line: job.adjustment_bottom_line
|
||||
adjustment_bottom_line: job.adjustment_bottom_line,
|
||||
taxable
|
||||
});
|
||||
|
||||
const bucket = ensureCenterBucket(profitCenterHash, otherAccountName);
|
||||
bucket.extrasSale = bucket.extrasSale.add(
|
||||
Dinero({
|
||||
amount: Math.round((job.adjustment_bottom_line || 0) * 100)
|
||||
})
|
||||
);
|
||||
addExtras(bucket, amount, taxable);
|
||||
} else {
|
||||
debugLog("NO PAO ACCOUNT FOUND!!", { otherAccountName });
|
||||
}
|
||||
@@ -558,7 +787,8 @@ function applyRomeProfileAdjustments({
|
||||
selectedDmsAllocationConfig,
|
||||
profitCenterHash,
|
||||
debugLog,
|
||||
connectionData
|
||||
connectionData,
|
||||
taxContext
|
||||
}) {
|
||||
// Only relevant for Rome instances
|
||||
if (!InstanceManager({ rome: true })) return profitCenterHash;
|
||||
@@ -576,6 +806,8 @@ function applyRomeProfileAdjustments({
|
||||
rateKeys: Object.keys(rateMap)
|
||||
});
|
||||
|
||||
const extrasTaxable = !!(taxContext && taxContext.hasAnyTax);
|
||||
|
||||
// Parts adjustments
|
||||
Object.keys(partsAdjustments).forEach((key) => {
|
||||
const accountName = selectedDmsAllocationConfig.profits[key];
|
||||
@@ -585,12 +817,13 @@ function applyRomeProfileAdjustments({
|
||||
const bucket = ensureCenterBucket(profitCenterHash, accountName);
|
||||
|
||||
const adjMoney = Dinero(partsAdjustments[key]);
|
||||
bucket.extrasSale = bucket.extrasSale.add(adjMoney);
|
||||
addExtras(bucket, adjMoney, extrasTaxable);
|
||||
|
||||
debugLog("Added parts adjustment", {
|
||||
key,
|
||||
accountName,
|
||||
adjustment: summarizeMoney(adjMoney)
|
||||
adjustment: summarizeMoney(adjMoney),
|
||||
taxable: extrasTaxable
|
||||
});
|
||||
} else {
|
||||
CreateRRLogEvent(
|
||||
@@ -619,12 +852,13 @@ function applyRomeProfileAdjustments({
|
||||
|
||||
// Note: we intentionally use `rate.adjustments` here to mirror CDK behaviour
|
||||
const adjMoney = Dinero(rate.adjustments);
|
||||
bucket.extrasSale = bucket.extrasSale.add(adjMoney);
|
||||
addExtras(bucket, adjMoney, extrasTaxable);
|
||||
|
||||
debugLog("Added rate adjustment", {
|
||||
key,
|
||||
accountName,
|
||||
adjustment: summarizeMoney(adjMoney)
|
||||
adjustment: summarizeMoney(adjMoney),
|
||||
taxable: extrasTaxable
|
||||
});
|
||||
} else {
|
||||
CreateRRLogEvent(
|
||||
@@ -651,6 +885,8 @@ function applyRomeProfileAdjustments({
|
||||
* laborTaxableSale,
|
||||
* laborNonTaxableSale,
|
||||
* extrasSale,
|
||||
* extrasTaxableSale,
|
||||
* extrasNonTaxableSale,
|
||||
* totalSale,
|
||||
* cost,
|
||||
* profitCenter,
|
||||
@@ -663,10 +899,8 @@ function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLo
|
||||
const jobAllocations = centers.map((center) => {
|
||||
const bucket = profitCenterHash[center] || emptyCenterBucket();
|
||||
|
||||
const totalSale = bucket.partsSale
|
||||
.add(bucket.laborTaxableSale)
|
||||
.add(bucket.laborNonTaxableSale)
|
||||
.add(bucket.extrasSale);
|
||||
const extrasSale = bucket.extrasSale;
|
||||
const totalSale = bucket.partsSale.add(bucket.laborTaxableSale).add(bucket.laborNonTaxableSale).add(extrasSale);
|
||||
|
||||
const profitCenter = bodyshop.md_responsibility_centers.profits.find((c) => c.name === center);
|
||||
const costCenter = bodyshop.md_responsibility_centers.costs.find((c) => c.name === center);
|
||||
@@ -684,7 +918,9 @@ function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLo
|
||||
laborNonTaxableSale: bucket.laborNonTaxableSale,
|
||||
|
||||
// Extras
|
||||
extrasSale: bucket.extrasSale,
|
||||
extrasSale,
|
||||
extrasTaxableSale: bucket.extrasTaxableSale,
|
||||
extrasNonTaxableSale: bucket.extrasNonTaxableSale,
|
||||
|
||||
totalSale,
|
||||
|
||||
@@ -705,6 +941,8 @@ function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLo
|
||||
laborTaxable: summarizeMoney(row.laborTaxableSale),
|
||||
laborNonTaxable: summarizeMoney(row.laborNonTaxableSale),
|
||||
extras: summarizeMoney(row.extrasSale),
|
||||
extrasTaxable: summarizeMoney(row.extrasTaxableSale),
|
||||
extrasNonTaxable: summarizeMoney(row.extrasNonTaxableSale),
|
||||
totalSale: summarizeMoney(row.totalSale),
|
||||
cost: summarizeMoney(row.cost)
|
||||
}))
|
||||
@@ -812,12 +1050,35 @@ function calculateAllocations(connectionData, job) {
|
||||
timetickets: Array.isArray(job.timetickets) ? job.timetickets.length : 0
|
||||
});
|
||||
|
||||
const taxContext = buildTaxContext(job);
|
||||
debugLog("Tax context initialised", {
|
||||
isImex: taxContext.isImex,
|
||||
isRome: taxContext.isRome,
|
||||
federalTaxRate: taxContext.federalTaxRate,
|
||||
stateTaxRate: taxContext.stateTaxRate,
|
||||
localTaxRate: taxContext.localTaxRate,
|
||||
hasFederal: taxContext.hasFederal,
|
||||
hasState: taxContext.hasState,
|
||||
hasLocal: taxContext.hasLocal,
|
||||
globalAllTaxCanada: taxContext.globalAllTaxCanada,
|
||||
tax_lbr_rt: taxContext.tax_lbr_rt,
|
||||
tax_paint_mat_rt: taxContext.tax_paint_mat_rt,
|
||||
tax_shop_mat_rt: taxContext.tax_shop_mat_rt,
|
||||
tax_tow_rt: taxContext.tax_tow_rt,
|
||||
hasAnyPartsWithTax: taxContext.hasAnyPartsWithTax,
|
||||
hasAnyTax: taxContext.hasAnyTax
|
||||
});
|
||||
|
||||
// 1) Tax allocations
|
||||
let taxAllocations = buildTaxAllocations(bodyshop, job);
|
||||
debugLog("Initial taxAllocations", summarizeTaxAllocations(taxAllocations));
|
||||
|
||||
// 2) Profit centers from job lines + MAPA/MASH detection
|
||||
const { profitCenterHash: initialProfitHash, hasMapaLine, hasMashLine } = buildProfitCenterHash(job, debugLog);
|
||||
const {
|
||||
profitCenterHash: initialProfitHash,
|
||||
hasMapaLine,
|
||||
hasMashLine
|
||||
} = buildProfitCenterHash(job, debugLog, taxContext);
|
||||
|
||||
// 3) DMS allocation config
|
||||
const selectedDmsAllocationConfig =
|
||||
@@ -842,7 +1103,8 @@ function calculateAllocations(connectionData, job) {
|
||||
profitCenterHash: initialProfitHash,
|
||||
hasMapaLine,
|
||||
hasMashLine,
|
||||
debugLog
|
||||
debugLog,
|
||||
taxContext
|
||||
});
|
||||
|
||||
// 6) Materials costing (MAPA/MASH cost side)
|
||||
@@ -861,7 +1123,8 @@ function calculateAllocations(connectionData, job) {
|
||||
selectedDmsAllocationConfig,
|
||||
profitCenterHash,
|
||||
taxAllocations,
|
||||
debugLog
|
||||
debugLog,
|
||||
taxContext
|
||||
}));
|
||||
|
||||
// 8) Rome-only profile-level adjustments
|
||||
@@ -871,7 +1134,8 @@ function calculateAllocations(connectionData, job) {
|
||||
selectedDmsAllocationConfig,
|
||||
profitCenterHash,
|
||||
debugLog,
|
||||
connectionData
|
||||
connectionData,
|
||||
taxContext
|
||||
});
|
||||
|
||||
debugLog("profitCenterHash before jobAllocations build", {
|
||||
@@ -882,7 +1146,9 @@ function calculateAllocations(connectionData, job) {
|
||||
partsNonTaxable: summarizeMoney(b.partsNonTaxableSale),
|
||||
laborTaxable: summarizeMoney(b.laborTaxableSale),
|
||||
laborNonTaxable: summarizeMoney(b.laborNonTaxableSale),
|
||||
extras: summarizeMoney(b.extrasSale)
|
||||
extras: summarizeMoney(b.extrasSale),
|
||||
extrasTaxable: summarizeMoney(b.extrasTaxableSale),
|
||||
extrasNonTaxable: summarizeMoney(b.extrasNonTaxableSale)
|
||||
}))
|
||||
});
|
||||
debugLog("costCenterHash before jobAllocations build", {
|
||||
|
||||
Reference in New Issue
Block a user