feature/IO-3255-simplified-parts-management - DO NOT SPLIT LABOR / PARTS
This commit is contained in:
@@ -16,8 +16,6 @@ const {
|
||||
// Defaults
|
||||
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.
|
||||
* @param {string} shopId - The bodyshop UUID.
|
||||
@@ -327,10 +325,6 @@ const extractJobLines = (rq) => {
|
||||
const refinishInfo = line.RefinishLaborInfo || {};
|
||||
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 = {
|
||||
line_no: parseInt(line.LineNum || 0, 10),
|
||||
unq_seq: parseInt(line.UniqueSequenceNum || 0, 10),
|
||||
@@ -339,134 +333,87 @@ const extractJobLines = (rq) => {
|
||||
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);
|
||||
// Push the part line with ONLY part pricing/fields
|
||||
out.push({
|
||||
...base,
|
||||
part_type: partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null,
|
||||
part_qty: parseFloat(partInfo.Quantity || 0) || 1,
|
||||
oem_partno: partInfo.OEMPartNum || partInfo.PartNum || null,
|
||||
db_price: price,
|
||||
act_price: price,
|
||||
// Tax flag from PartInfo.TaxableInd when provided
|
||||
...(partInfo.TaxableInd !== undefined &&
|
||||
lineOut.part_type = partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null;
|
||||
lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1;
|
||||
lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null;
|
||||
lineOut.db_price = isNaN(price) ? 0 : price;
|
||||
lineOut.act_price = isNaN(price) ? 0 : price;
|
||||
|
||||
// Tax flag from PartInfo.TaxableInd when provided
|
||||
if (
|
||||
partInfo.TaxableInd !== undefined &&
|
||||
(typeof partInfo.TaxableInd === "string" ||
|
||||
typeof partInfo.TaxableInd === "number" ||
|
||||
typeof partInfo.TaxableInd === "boolean")
|
||||
? {
|
||||
tax_part:
|
||||
partInfo.TaxableInd === true ||
|
||||
partInfo.TaxableInd === 1 ||
|
||||
partInfo.TaxableInd === "1" ||
|
||||
(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 })
|
||||
});
|
||||
}
|
||||
) {
|
||||
lineOut.tax_part =
|
||||
partInfo.TaxableInd === true ||
|
||||
partInfo.TaxableInd === 1 ||
|
||||
partInfo.TaxableInd === "1" ||
|
||||
(typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y");
|
||||
}
|
||||
} else if (jobLineType === "SUBLET") {
|
||||
out.push({
|
||||
...base,
|
||||
part_type: "PAS",
|
||||
part_qty: 1,
|
||||
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 })
|
||||
});
|
||||
} else if (hasSublet) {
|
||||
const amt = parseFloat(subletInfo.SubletAmount || 0);
|
||||
lineOut.part_type = "PAS"; // Sublet as parts-as-service
|
||||
lineOut.part_qty = 1;
|
||||
lineOut.act_price = isNaN(amt) ? 0 : amt;
|
||||
}
|
||||
|
||||
// Add a separate refinish labor line if present and enabled
|
||||
if (INCLUDE_LABOR && Object.keys(refinishInfo).length > 0) {
|
||||
const hrs = parseFloat(refinishInfo.LaborHours || 0);
|
||||
const amt = parseFloat(refinishInfo.LaborAmt || 0);
|
||||
if (!isNaN(hrs) || !isNaN(amt)) {
|
||||
out.push({
|
||||
...base,
|
||||
// tweak unq_seq to avoid collisions in later upserts
|
||||
unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 500000,
|
||||
line_desc: base.line_desc || "Refinish",
|
||||
mod_lbr_ty: "LAR",
|
||||
mod_lb_hrs: isNaN(hrs) ? 0 : hrs,
|
||||
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 })
|
||||
});
|
||||
}
|
||||
// Primary labor (if present) recorded on the same line
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -522,7 +469,7 @@ const computeLinesTotal = (joblines = []) => {
|
||||
} else if (!jl.part_type && Number.isFinite(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,11 +56,10 @@ const extractUpdatedJobData = (rq) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts updated job lines from the request payload, mirroring the AddRq splitting rules:
|
||||
* - PART lines carry only part pricing (act_price) and related fields
|
||||
* - If LaborInfo exists on a part line, add a separate LABOR line at unq_seq + 400000
|
||||
* - If RefinishLaborInfo exists, add a separate LABOR line at unq_seq + 500000 with mod_lbr_ty=LAR
|
||||
* - SUBLET lines become PAS part_type with act_price=SubletAmount
|
||||
* Extracts updated job lines from the request payload without splitting parts and labor:
|
||||
* - Keep part and labor on the same jobline
|
||||
* - Aggregate RefinishLabor into secondary labor fields and add its amount to lbr_amt
|
||||
* - SUBLET-only lines become PAS part_type with act_price = SubletAmount
|
||||
*/
|
||||
const extractUpdatedJobLines = (addsChgs = {}, jobId) => {
|
||||
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
|
||||
};
|
||||
|
||||
const lineOut = { ...base };
|
||||
|
||||
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;
|
||||
|
||||
if (hasPart) {
|
||||
const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0);
|
||||
out.push({
|
||||
...base,
|
||||
part_type: partInfo.PartType ? String(partInfo.PartType).toUpperCase() : null,
|
||||
part_qty: parseFloat(partInfo.Quantity || 0) || 1,
|
||||
oem_partno: partInfo.OEMPartNum || partInfo.PartNum || null,
|
||||
db_price: isNaN(price) ? 0 : price,
|
||||
act_price: isNaN(price) ? 0 : price
|
||||
});
|
||||
lineOut.part_type = partInfo.PartType ? String(partInfo.PartType).toUpperCase() : null;
|
||||
lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1;
|
||||
lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null;
|
||||
lineOut.db_price = isNaN(price) ? 0 : price;
|
||||
lineOut.act_price = isNaN(price) ? 0 : price;
|
||||
|
||||
// Split any attached labor on the part line into a derived labor jobline
|
||||
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,
|
||||
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
|
||||
});
|
||||
// Optional: taxability flag for parts
|
||||
if (
|
||||
partInfo.TaxableInd !== undefined &&
|
||||
(typeof partInfo.TaxableInd === "string" ||
|
||||
typeof partInfo.TaxableInd === "number" ||
|
||||
typeof partInfo.TaxableInd === "boolean")
|
||||
) {
|
||||
lineOut.tax_part =
|
||||
partInfo.TaxableInd === true ||
|
||||
partInfo.TaxableInd === 1 ||
|
||||
partInfo.TaxableInd === "1" ||
|
||||
(typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y");
|
||||
}
|
||||
} else if (hasSublet) {
|
||||
out.push({
|
||||
...base,
|
||||
part_type: "PAS",
|
||||
part_qty: 1,
|
||||
act_price: parseFloat(subletInfo.SubletAmount || 0) || 0
|
||||
});
|
||||
const amt = parseFloat(subletInfo.SubletAmount || 0);
|
||||
lineOut.part_type = "PAS";
|
||||
lineOut.part_qty = 1;
|
||||
lineOut.act_price = isNaN(amt) ? 0 : amt;
|
||||
}
|
||||
|
||||
// Labor-only line (no PartInfo): still upsert as a labor entry
|
||||
if (hasLaborOnly) {
|
||||
out.push({
|
||||
...base,
|
||||
mod_lbr_ty: laborInfo.LaborType || null,
|
||||
mod_lb_hrs: parseFloat(laborInfo.LaborHours || 0) || 0,
|
||||
lbr_op: laborInfo.LaborOperation || null,
|
||||
lbr_amt: parseFloat(laborInfo.LaborAmt || 0) || 0
|
||||
});
|
||||
// Primary labor on same line
|
||||
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) {
|
||||
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
|
||||
if (Object.keys(refinishInfo).length > 0) {
|
||||
const rHrs = parseFloat(refinishInfo.LaborHours || 0);
|
||||
const rAmt = parseFloat(refinishInfo.LaborAmt || 0);
|
||||
if (!isNaN(rHrs) || !isNaN(rAmt)) {
|
||||
out.push({
|
||||
...base,
|
||||
unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 500000,
|
||||
line_desc: base.line_desc || "Refinish",
|
||||
mod_lbr_ty: "LAR",
|
||||
mod_lb_hrs: isNaN(rHrs) ? 0 : rHrs,
|
||||
lbr_op: refinishInfo.LaborOperation || null,
|
||||
lbr_amt: isNaN(rAmt) ? 0 : rAmt
|
||||
});
|
||||
// Refinish labor on same line using secondary fields; aggregate amount into lbr_amt
|
||||
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;
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user