feature/IO-3255-simplified-parts-management - DO NOT SPLIT LABOR / PARTS

This commit is contained in:
Dave
2025-08-21 18:35:10 -04:00
parent 2c8f3a173e
commit 364813193f
2 changed files with 132 additions and 186 deletions

View File

@@ -16,8 +16,6 @@ const {
// Defaults // Defaults
const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN"; const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN";
// Config: include labor lines and labor in totals (default true)
const INCLUDE_LABOR = true;
/** /**
* Fetches the default order status for a bodyshop. * Fetches the default order status for a bodyshop.
* @param {string} shopId - The bodyshop UUID. * @param {string} shopId - The bodyshop UUID.
@@ -327,10 +325,6 @@ const extractJobLines = (rq) => {
const refinishInfo = line.RefinishLaborInfo || {}; const refinishInfo = line.RefinishLaborInfo || {};
const subletInfo = line.SubletInfo || {}; const subletInfo = line.SubletInfo || {};
let jobLineType = "PART";
if (Object.keys(subletInfo).length > 0) jobLineType = "SUBLET";
else if (Object.keys(laborInfo).length > 0 && Object.keys(partInfo).length === 0) jobLineType = "LABOR";
const base = { const base = {
line_no: parseInt(line.LineNum || 0, 10), line_no: parseInt(line.LineNum || 0, 10),
unq_seq: parseInt(line.UniqueSequenceNum || 0, 10), unq_seq: parseInt(line.UniqueSequenceNum || 0, 10),
@@ -339,134 +333,87 @@ const extractJobLines = (rq) => {
notes: line.LineMemo || null notes: line.LineMemo || null
}; };
if (jobLineType === "PART") { const lineOut = { ...base };
// Manual line flag coercion
if (line.ManualLineInd !== undefined) {
lineOut.manual_line =
line.ManualLineInd === true ||
line.ManualLineInd === 1 ||
line.ManualLineInd === "1" ||
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y");
} else {
lineOut.manual_line = null;
}
// Parts (preferred) or Sublet (fallback when no PartInfo)
const hasPart = Object.keys(partInfo).length > 0;
const hasSublet = Object.keys(subletInfo).length > 0;
if (hasPart) {
const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0); const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0);
// Push the part line with ONLY part pricing/fields lineOut.part_type = partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null;
out.push({ lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1;
...base, lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null;
part_type: partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null, lineOut.db_price = isNaN(price) ? 0 : price;
part_qty: parseFloat(partInfo.Quantity || 0) || 1, lineOut.act_price = isNaN(price) ? 0 : price;
oem_partno: partInfo.OEMPartNum || partInfo.PartNum || null,
db_price: price, // Tax flag from PartInfo.TaxableInd when provided
act_price: price, if (
// Tax flag from PartInfo.TaxableInd when provided partInfo.TaxableInd !== undefined &&
...(partInfo.TaxableInd !== undefined &&
(typeof partInfo.TaxableInd === "string" || (typeof partInfo.TaxableInd === "string" ||
typeof partInfo.TaxableInd === "number" || typeof partInfo.TaxableInd === "number" ||
typeof partInfo.TaxableInd === "boolean") typeof partInfo.TaxableInd === "boolean")
? { ) {
tax_part: lineOut.tax_part =
partInfo.TaxableInd === true || partInfo.TaxableInd === true ||
partInfo.TaxableInd === 1 || partInfo.TaxableInd === 1 ||
partInfo.TaxableInd === "1" || partInfo.TaxableInd === "1" ||
(typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y") (typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y");
}
: {}),
// Manual line flag coercion
...(line.ManualLineInd !== undefined
? {
manual_line:
line.ManualLineInd === true ||
line.ManualLineInd === 1 ||
line.ManualLineInd === "1" ||
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y")
}
: { manual_line: null })
});
// If labor is present on the same damage line, split it to a separate LABOR jobline
// TODO: Verify with patrick this is desired.
if (INCLUDE_LABOR) {
const hrs = parseFloat(laborInfo.LaborHours || 0);
const amt = parseFloat(laborInfo.LaborAmt || 0);
const hasLabor =
(!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) ||
(!isNaN(hrs) && hrs !== 0) ||
(!isNaN(amt) && amt !== 0);
if (hasLabor) {
out.push({
...base,
// tweak unq_seq to avoid collisions in later upserts
unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 400000,
mod_lbr_ty: laborInfo.LaborType || null,
mod_lb_hrs: isNaN(hrs) ? 0 : hrs,
lbr_op: laborInfo.LaborOperation || null,
lbr_amt: isNaN(amt) ? 0 : amt,
...(line.ManualLineInd !== undefined
? {
manual_line:
line.ManualLineInd === true ||
line.ManualLineInd === 1 ||
line.ManualLineInd === "1" ||
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y")
}
: { manual_line: null })
});
}
} }
} else if (jobLineType === "SUBLET") { } else if (hasSublet) {
out.push({ const amt = parseFloat(subletInfo.SubletAmount || 0);
...base, lineOut.part_type = "PAS"; // Sublet as parts-as-service
part_type: "PAS", lineOut.part_qty = 1;
part_qty: 1, lineOut.act_price = isNaN(amt) ? 0 : amt;
act_price: parseFloat(subletInfo.SubletAmount || 0),
// Manual line flag
...(line.ManualLineInd !== undefined
? {
manual_line:
line.ManualLineInd === true ||
line.ManualLineInd === 1 ||
line.ManualLineInd === "1" ||
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y")
}
: { manual_line: null })
});
} else if (INCLUDE_LABOR) {
// Labor-only line (only when enabled)
out.push({
...base,
mod_lbr_ty: laborInfo.LaborType || null,
mod_lb_hrs: parseFloat(laborInfo.LaborHours || 0),
lbr_op: laborInfo.LaborOperation || null,
lbr_amt: parseFloat(laborInfo.LaborAmt || 0),
...(line.ManualLineInd !== undefined
? {
manual_line:
line.ManualLineInd === true ||
line.ManualLineInd === 1 ||
line.ManualLineInd === "1" ||
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y")
}
: { manual_line: null })
});
} }
// Add a separate refinish labor line if present and enabled // Primary labor (if present) recorded on the same line
if (INCLUDE_LABOR && Object.keys(refinishInfo).length > 0) { const hrs = parseFloat(laborInfo.LaborHours || 0);
const hrs = parseFloat(refinishInfo.LaborHours || 0); const amt = parseFloat(laborInfo.LaborAmt || 0);
const amt = parseFloat(refinishInfo.LaborAmt || 0); const hasLabor =
if (!isNaN(hrs) || !isNaN(amt)) { (!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) ||
out.push({ (!isNaN(hrs) && hrs !== 0) ||
...base, (!isNaN(amt) && amt !== 0);
// tweak unq_seq to avoid collisions in later upserts if (hasLabor) {
unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 500000, lineOut.mod_lbr_ty = laborInfo.LaborType || null;
line_desc: base.line_desc || "Refinish", lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs;
mod_lbr_ty: "LAR", lineOut.lbr_op = laborInfo.LaborOperation || null;
mod_lb_hrs: isNaN(hrs) ? 0 : hrs, lineOut.lbr_amt = isNaN(amt) ? 0 : amt;
lbr_op: refinishInfo.LaborOperation || null,
lbr_amt: isNaN(amt) ? 0 : amt,
...(line.ManualLineInd !== undefined
? {
manual_line:
line.ManualLineInd === true ||
line.ManualLineInd === 1 ||
line.ManualLineInd === "1" ||
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y")
}
: { manual_line: null })
});
}
} }
// Refinish labor (if present) recorded on the same line using secondary labor fields
const rHrs = parseFloat(refinishInfo.LaborHours || 0);
const rAmt = parseFloat(refinishInfo.LaborAmt || 0);
const hasRefinish =
Object.keys(refinishInfo).length > 0 &&
((refinishInfo.LaborType && String(refinishInfo.LaborType).length > 0) ||
!isNaN(rHrs) ||
!isNaN(rAmt) ||
!!refinishInfo.LaborOperation);
if (hasRefinish) {
lineOut.lbr_typ_j = refinishInfo.LaborType || "LAR";
lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs;
lineOut.lbr_op_j = refinishInfo.LaborOperation || null;
// Aggregate refinish labor amount into the total labor amount for the line
if (!isNaN(rAmt)) {
lineOut.lbr_amt = (Number.isFinite(lineOut.lbr_amt) ? lineOut.lbr_amt : 0) + rAmt;
}
if (refinishInfo.PaintStagesNum !== undefined) lineOut.paint_stg = refinishInfo.PaintStagesNum;
if (refinishInfo.PaintTonesNum !== undefined) lineOut.paint_tone = refinishInfo.PaintTonesNum;
}
out.push(lineOut);
} }
return out; return out;
@@ -522,7 +469,7 @@ const computeLinesTotal = (joblines = []) => {
} else if (!jl.part_type && Number.isFinite(jl.act_price)) { } else if (!jl.part_type && Number.isFinite(jl.act_price)) {
parts += jl.act_price; parts += jl.act_price;
} }
if (INCLUDE_LABOR && Number.isFinite(jl.lbr_amt)) { if (Number.isFinite(jl.lbr_amt)) {
labor += jl.lbr_amt; labor += jl.lbr_amt;
} }
} }

View File

@@ -56,11 +56,10 @@ const extractUpdatedJobData = (rq) => {
}; };
/** /**
* Extracts updated job lines from the request payload, mirroring the AddRq splitting rules: * Extracts updated job lines from the request payload without splitting parts and labor:
* - PART lines carry only part pricing (act_price) and related fields * - Keep part and labor on the same jobline
* - If LaborInfo exists on a part line, add a separate LABOR line at unq_seq + 400000 * - Aggregate RefinishLabor into secondary labor fields and add its amount to lbr_amt
* - If RefinishLaborInfo exists, add a separate LABOR line at unq_seq + 500000 with mod_lbr_ty=LAR * - SUBLET-only lines become PAS part_type with act_price = SubletAmount
* - SUBLET lines become PAS part_type with act_price=SubletAmount
*/ */
const extractUpdatedJobLines = (addsChgs = {}, jobId) => { const extractUpdatedJobLines = (addsChgs = {}, jobId) => {
const linesIn = Array.isArray(addsChgs.DamageLineInfo) ? addsChgs.DamageLineInfo : [addsChgs.DamageLineInfo || {}]; const linesIn = Array.isArray(addsChgs.DamageLineInfo) ? addsChgs.DamageLineInfo : [addsChgs.DamageLineInfo || {}];
@@ -88,74 +87,74 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId) => {
manual_line: line.ManualLineInd !== undefined ? coerceManual(line.ManualLineInd) : null manual_line: line.ManualLineInd !== undefined ? coerceManual(line.ManualLineInd) : null
}; };
const lineOut = { ...base };
const hasPart = Object.keys(partInfo).length > 0; const hasPart = Object.keys(partInfo).length > 0;
const hasLaborOnly = Object.keys(laborInfo).length > 0 && !hasPart && Object.keys(subletInfo).length === 0;
const hasSublet = Object.keys(subletInfo).length > 0; const hasSublet = Object.keys(subletInfo).length > 0;
if (hasPart) { if (hasPart) {
const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0); const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0);
out.push({ lineOut.part_type = partInfo.PartType ? String(partInfo.PartType).toUpperCase() : null;
...base, lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1;
part_type: partInfo.PartType ? String(partInfo.PartType).toUpperCase() : null, lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null;
part_qty: parseFloat(partInfo.Quantity || 0) || 1, lineOut.db_price = isNaN(price) ? 0 : price;
oem_partno: partInfo.OEMPartNum || partInfo.PartNum || null, lineOut.act_price = isNaN(price) ? 0 : price;
db_price: isNaN(price) ? 0 : price,
act_price: isNaN(price) ? 0 : price
});
// Split any attached labor on the part line into a derived labor jobline // Optional: taxability flag for parts
const hrs = parseFloat(laborInfo.LaborHours || 0); if (
const amt = parseFloat(laborInfo.LaborAmt || 0); partInfo.TaxableInd !== undefined &&
const hasLabor = (typeof partInfo.TaxableInd === "string" ||
(!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) || typeof partInfo.TaxableInd === "number" ||
(!isNaN(hrs) && hrs !== 0) || typeof partInfo.TaxableInd === "boolean")
(!isNaN(amt) && amt !== 0); ) {
if (hasLabor) { lineOut.tax_part =
out.push({ partInfo.TaxableInd === true ||
...base, partInfo.TaxableInd === 1 ||
unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 400000, partInfo.TaxableInd === "1" ||
mod_lbr_ty: laborInfo.LaborType || null, (typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y");
mod_lb_hrs: isNaN(hrs) ? 0 : hrs,
lbr_op: laborInfo.LaborOperation || null,
lbr_amt: isNaN(amt) ? 0 : amt
});
} }
} else if (hasSublet) { } else if (hasSublet) {
out.push({ const amt = parseFloat(subletInfo.SubletAmount || 0);
...base, lineOut.part_type = "PAS";
part_type: "PAS", lineOut.part_qty = 1;
part_qty: 1, lineOut.act_price = isNaN(amt) ? 0 : amt;
act_price: parseFloat(subletInfo.SubletAmount || 0) || 0
});
} }
// Labor-only line (no PartInfo): still upsert as a labor entry // Primary labor on same line
if (hasLaborOnly) { const hrs = parseFloat(laborInfo.LaborHours || 0);
out.push({ const amt = parseFloat(laborInfo.LaborAmt || 0);
...base, const hasLabor =
mod_lbr_ty: laborInfo.LaborType || null, (!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) ||
mod_lb_hrs: parseFloat(laborInfo.LaborHours || 0) || 0, (!isNaN(hrs) && hrs !== 0) ||
lbr_op: laborInfo.LaborOperation || null, (!isNaN(amt) && amt !== 0);
lbr_amt: parseFloat(laborInfo.LaborAmt || 0) || 0 if (hasLabor) {
}); lineOut.mod_lbr_ty = laborInfo.LaborType || null;
lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs;
lineOut.lbr_op = laborInfo.LaborOperation || null;
lineOut.lbr_amt = isNaN(amt) ? 0 : amt;
} }
// Separate refinish labor line // Refinish labor on same line using secondary fields; aggregate amount into lbr_amt
if (Object.keys(refinishInfo).length > 0) { const rHrs = parseFloat(refinishInfo.LaborHours || 0);
const rHrs = parseFloat(refinishInfo.LaborHours || 0); const rAmt = parseFloat(refinishInfo.LaborAmt || 0);
const rAmt = parseFloat(refinishInfo.LaborAmt || 0); const hasRefinish =
if (!isNaN(rHrs) || !isNaN(rAmt)) { Object.keys(refinishInfo).length > 0 &&
out.push({ ((refinishInfo.LaborType && String(refinishInfo.LaborType).length > 0) ||
...base, !isNaN(rHrs) ||
unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 500000, !isNaN(rAmt) ||
line_desc: base.line_desc || "Refinish", !!refinishInfo.LaborOperation);
mod_lbr_ty: "LAR", if (hasRefinish) {
mod_lb_hrs: isNaN(rHrs) ? 0 : rHrs, lineOut.lbr_typ_j = refinishInfo.LaborType || "LAR";
lbr_op: refinishInfo.LaborOperation || null, lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs;
lbr_amt: isNaN(rAmt) ? 0 : rAmt lineOut.lbr_op_j = refinishInfo.LaborOperation || null;
}); if (!isNaN(rAmt)) {
lineOut.lbr_amt = (Number.isFinite(lineOut.lbr_amt) ? lineOut.lbr_amt : 0) + rAmt;
} }
if (refinishInfo.PaintStagesNum !== undefined) lineOut.paint_stg = refinishInfo.PaintStagesNum;
if (refinishInfo.PaintTonesNum !== undefined) lineOut.paint_tone = refinishInfo.PaintTonesNum;
} }
out.push(lineOut);
} }
return out; return out;