Compare commits

...

17 Commits

Author SHA1 Message Date
Allan Carr
78770ed54e IO-3503 Job Costing Corrections
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-30 16:45:25 -08:00
Allan Carr
e1666baddd IO-3503 Job Costing Fixes for CAD
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-23 14:17:03 -08:00
Allan Carr
c29ac5f711 IO-3503 Job Costing Fixes
Correction to handle CAD

Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-22 15:52:20 -08:00
Allan Carr
1716c3e6b2 IO-3503 Job Costing Fixes
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-20 11:55:12 -08:00
Allan Carr
f3513a80c5 Merged in hotfix/2026-01-15 (pull request #2848)
IO-3498 No SalesTerms for Credits
2026-01-19 18:57:06 +00:00
Allan Carr
3c38d9daeb Merged in feature/IO-3498-QBO-Auth-Token (pull request #2847)
IO-3498 No SalesTerms for Credits

Approved-by: Dave Richer
2026-01-19 18:54:15 +00:00
Dave Richer
27b9c3f342 Merged in hotfix/2026-01-15 (pull request #2844)
Hotfix/2026 01 15

Approved-by: Allan Carr
2026-01-17 01:48:21 +00:00
Allan Carr
23ce1c42d1 Merged in feature/IO-3498-QBO-Auth-Token (pull request #2843)
IO-3498 QBO Fix for Returned Data from oauthClient

Approved-by: Dave Richer
2026-01-17 01:07:25 +00:00
Dave Richer
55dd0c6e14 Merged in hotfix/2026-01-15 (pull request #2833)
IO-3498 QBO Changes due to oauthClient response change

Approved-by: Allan Carr
2026-01-16 00:55:49 +00:00
Dave
d30e03a184 Merge remote-tracking branch 'origin/feature/IO-3498-QBO-Auth-Token' into hotfix/2026-01-15 2026-01-15 19:35:44 -05:00
Allan Carr
aef04ec29e Merged in hotfix/2026-01-15 (pull request #2828)
Hotfix/2026 01 15
2026-01-15 16:57:04 +00:00
Allan Carr
69e57195d3 Merged in feature/IO-3503-Job-Costing-Bug-Fix (pull request #2824)
IO-3503 Job Costing Bug Fix

Approved-by: Dave Richer
2026-01-15 16:40:47 +00:00
Allan Carr
883c7257db Merged in feature/IO-3500-PROFIT-CENTER-ITEM (pull request #2825)
IO-3500 Profit Center Item

Approved-by: Dave Richer
2026-01-15 16:39:02 +00:00
Allan Carr
92fd5b0315 IO-3503 Job Costing Bug Fix
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-14 18:02:26 -08:00
Allan Carr
54ce0e1802 IO-3500 Profit Center Item
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2026-01-13 16:40:24 -08:00
Dave Richer
be2df79555 Merged in hotfix/2026-01-13 (pull request #2813)
Hotfix/2026 01 13 into master-AIO
2026-01-13 20:52:00 +00:00
Allan Carr
9c733702e4 Merged in feature/IO-3498-QBO-Auth-Token (pull request #2810)
IO-3498 QBO Auth Token

Approved-by: Dave Richer
2026-01-13 20:45:42 +00:00
4 changed files with 153 additions and 61 deletions

View File

@@ -695,25 +695,25 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
>
<Input onBlur={handleBlur} />
</Form.Item>
{!hasDMSKey && (
<Form.Item
label={t("bodyshop.fields.responsibilitycenter_accountitem")}
key={`${index}accountitem`}
name={[field.name, "accountitem"]}
rules={[{ required: true }]}
>
<Input onBlur={handleBlur} />
</Form.Item>
)}
{hasDMSKey && !bodyshop.rr_dealerid && (
<>
<Form.Item
label={t("bodyshop.fields.responsibilitycenter_accountitem")}
key={`${index}accountitem`}
name={[field.name, "accountitem"]}
rules={[{ required: true }]}
>
<Input onBlur={handleBlur} />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.dms.dms_acctnumber")}
key={`${index}dms_acctnumber`}
name={[field.name, "dms_acctnumber"]}
rules={[{ required: true }]}
>
<Input onBlur={handleBlur} />
</Form.Item>
</>
<Form.Item
label={t("bodyshop.fields.dms.dms_acctnumber")}
key={`${index}dms_acctnumber`}
name={[field.name, "dms_acctnumber"]}
rules={[{ required: true }]}
>
<Input onBlur={handleBlur} />
</Form.Item>
)}
{bodyshop.cdk_dealerid && (
<Form.Item

View File

@@ -1725,6 +1725,7 @@ query QUERY_JOB_COSTING_DETAILS($id: uuid!) {
profitcenter_part
profitcenter_labor
act_price_before_ppc
manual_line
}
bills {
id
@@ -1842,6 +1843,7 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT
op_code_desc
profitcenter_part
profitcenter_labor
manual_line
}
bills {
id

View File

@@ -2,6 +2,7 @@ const _ = require("lodash");
const Dinero = require("dinero.js");
const queries = require("../graphql-client/queries");
const logger = require("../utils/logger");
const { ParseCalopCode } = require("../job/job-totals-USA");
const InstanceManager = require("../utils/instanceMgr").default;
const { DiscountNotAlreadyCounted } = InstanceManager({
imex: require("../job/job-totals"),
@@ -265,6 +266,9 @@ function GenerateCostingData(job) {
);
const materialsHours = { mapaHrs: 0, mashHrs: 0 };
let mashOpCodes = InstanceManager({
rome: ParseCalopCode(job.materials["MASH"]?.cal_opcode)
});
let hasMapaLine = false;
let hasMashLine = false;
@@ -304,6 +308,7 @@ function GenerateCostingData(job) {
if (
job.cieca_pfl &&
job.cieca_pfl[val.mod_lbr_ty.toUpperCase()] &&
typeof job.cieca_pfl[val.mod_lbr_ty.toUpperCase()].lbr_adjp === "number" &&
job.cieca_pfl[val.mod_lbr_ty.toUpperCase()].lbr_adjp !== 0
) {
let adjp = 0;
@@ -338,7 +343,7 @@ function GenerateCostingData(job) {
if (!acc.labor[laborProfitCenter]) acc.labor[laborProfitCenter] = Dinero();
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(laborAmount);
if (val.mod_lb_hrs === 0 && val.act_price > 0 && val.lbr_op === "OP14") {
if (val.act_price > 0 && val.lbr_op === "OP14" && !val.part_type) {
//Scenario where SGI may pay out hours using a part price.
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(
Dinero({
@@ -350,8 +355,17 @@ function GenerateCostingData(job) {
if (val.mod_lbr_ty === "LAR") {
materialsHours.mapaHrs += val.mod_lb_hrs || 0;
}
if (val.mod_lbr_ty !== "LAR") {
materialsHours.mashHrs += val.mod_lb_hrs || 0;
if (InstanceManager({ imex: true, rome: false })) {
if (val.mod_lbr_ty !== "LAR") {
materialsHours.mashHrs += val.mod_lb_hrs || 0;
}
} else {
if (val.mod_lbr_ty !== "LAR" && mashOpCodes.includes(val.lbr_op)) {
materialsHours.mashHrs += val.mod_lb_hrs || 0;
}
if (val.manual_line === true && !mashOpCodes.includes(val.lbr_op) && val.mod_lbr_ty !== "LAR" ) {
materialsHours.mashHrs += val.mod_lb_hrs || 0;
}
}
}
@@ -397,32 +411,6 @@ function GenerateCostingData(job) {
: Dinero()
);
// Profile Discount for Parts
if (job.parts_tax_rates && job.parts_tax_rates[val.part_type.toUpperCase()]) {
if (
job.parts_tax_rates[val.part_type.toUpperCase()].prt_discp !== undefined &&
job.parts_tax_rates[val.part_type.toUpperCase()].prt_discp >= 0
) {
const discountRate =
Math.abs(job.parts_tax_rates[val.part_type.toUpperCase()].prt_discp) > 1
? job.parts_tax_rates[val.part_type.toUpperCase()].prt_discp
: job.parts_tax_rates[val.part_type.toUpperCase()].prt_discp * 100;
const disc = partsAmount.percentage(discountRate).multiply(-1);
partsAmount = partsAmount.add(disc);
}
if (
job.parts_tax_rates[val.part_type.toUpperCase()].prt_mkupp !== undefined &&
job.parts_tax_rates[val.part_type.toUpperCase()].prt_mkupp >= 0
) {
const markupRate =
Math.abs(job.parts_tax_rates[val.part_type.toUpperCase()].prt_mkupp) > 1
? job.parts_tax_rates[val.part_type.toUpperCase()].prt_mkupp
: job.parts_tax_rates[val.part_type.toUpperCase()].prt_mkupp * 100;
const markup = partsAmount.percentage(markupRate);
partsAmount = partsAmount.add(markup);
}
}
if (!acc.parts[partsProfitCenter]) acc.parts[partsProfitCenter] = Dinero();
acc.parts[partsProfitCenter] = acc.parts[partsProfitCenter].add(partsAmount);
}
@@ -469,10 +457,7 @@ function GenerateCostingData(job) {
}
//Additional Profit Center
if (
(!val.part_type && !val.mod_lbr_ty) ||
(!val.part_type && val.mod_lbr_ty && val.act_price > 0 && val.lbr_op !== "OP14")
) {
if ((!val.part_type && !val.mod_lbr_ty) || (!val.part_type && val.mod_lbr_ty && val.lbr_op !== "OP14")) {
//Does it already have a defined profit center?
//If so, use it, otherwise try to use the same from the auto-allocate logic in IO app jobs-close-auto-allocate.
const partsProfitCenter = val.profitcenter_part || getAdditionalCostCenter(val, defaultProfits) || "Unknown";
@@ -512,10 +497,67 @@ function GenerateCostingData(job) {
{ parts: {}, labor: {}, additional: {}, sublet: {} }
);
// Profile Discount for Parts
Object.keys(jobLineTotalsByProfitCenter.parts).forEach((key) => {
let disc = Dinero(),
markup = Dinero();
const convertedKey = Object.keys(defaultProfits).find((k) => defaultProfits[k] === key);
if (convertedKey && job.parts_tax_rates && job.parts_tax_rates[convertedKey.toUpperCase()]) {
if (
job.parts_tax_rates[convertedKey.toUpperCase()].prt_discp !== undefined &&
job.parts_tax_rates[convertedKey.toUpperCase()].prt_discp >= 0
) {
const discountRate =
Math.abs(job.parts_tax_rates[convertedKey.toUpperCase()].prt_discp) > 1
? job.parts_tax_rates[convertedKey.toUpperCase()].prt_discp
: job.parts_tax_rates[convertedKey.toUpperCase()].prt_discp * 100;
disc = jobLineTotalsByProfitCenter.parts[key].percentage(discountRate).multiply(-1);
}
if (
job.parts_tax_rates[convertedKey.toUpperCase()].prt_mkupp !== undefined &&
job.parts_tax_rates[convertedKey.toUpperCase()].prt_mkupp >= 0
) {
const markupRate =
Math.abs(job.parts_tax_rates[convertedKey.toUpperCase()].prt_mkupp) > 1
? job.parts_tax_rates[convertedKey.toUpperCase()].prt_mkupp
: job.parts_tax_rates[convertedKey.toUpperCase()].prt_mkupp * 100;
markup = jobLineTotalsByProfitCenter.parts[key].percentage(markupRate);
}
}
if (InstanceManager({ rome: true })) {
if (convertedKey) {
const correspondingCiecaStlTotalLine = job.cieca_stl?.data.find(
(c) => c.ttl_typecd === convertedKey.toUpperCase()
);
if (
correspondingCiecaStlTotalLine &&
Math.abs(jobLineTotalsByProfitCenter.parts[key].getAmount() - correspondingCiecaStlTotalLine.ttl_amt * 100) > 1
) {
jobLineTotalsByProfitCenter.parts[key] = jobLineTotalsByProfitCenter.parts[key].add(disc).add(markup);
}
}
}
});
if (!hasMapaLine) {
let threshold;
if (
job.materials["MAPA"] &&
job.materials["MAPA"].cal_maxdlr !== undefined &&
job.materials["MAPA"].cal_maxdlr >= 0
) {
//It has an upper threshhold.
threshold = Dinero({
amount: Math.round(job.materials["MAPA"].cal_maxdlr * 100)
});
}
if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]])
jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = Dinero();
const origMAPAAmount = jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]];
jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = jobLineTotalsByProfitCenter.additional[
defaultProfits["MAPA"]
].add(
@@ -524,7 +566,12 @@ function GenerateCostingData(job) {
}).multiply(materialsHours.mapaHrs || 0)
);
let adjp = 0;
if (job.materials["MAPA"] && job.materials["MAPA"].mat_adjp) {
if (
job.materials["MAPA"] &&
job.materials["MAPA"].mat_adjp &&
typeof job.materials["MAPA"].mat_adjp === "number" &&
job.materials["MAPA"].mat_adjp !== 0
) {
adjp =
Math.abs(job.materials["MAPA"].mat_adjp) > 1
? job.materials["MAPA"].mat_adjp
@@ -538,11 +585,29 @@ function GenerateCostingData(job) {
.percentage(adjp < 0 ? adjp * -1 : adjp)
.multiply(adjp < 0 ? -1 : 1)
);
if (threshold && jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]].greaterThanOrEqual(threshold)) {
jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = threshold.add(origMAPAAmount);
}
}
if (!hasMashLine) {
let threshold;
if (
job.materials["MASH"] &&
job.materials["MASH"].cal_maxdlr !== undefined &&
job.materials["MASH"].cal_maxdlr >= 0
) {
//It has an upper threshhold.
threshold = Dinero({
amount: Math.round(job.materials["MASH"].cal_maxdlr * 100)
});
}
if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]])
jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = Dinero();
const origMASHAmount = jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]];
jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = jobLineTotalsByProfitCenter.additional[
defaultProfits["MASH"]
].add(
@@ -551,7 +616,12 @@ function GenerateCostingData(job) {
}).multiply(materialsHours.mashHrs || 0)
);
let adjp = 0;
if (job.materials["MASH"] && job.materials["MASH"].mat_adjp) {
if (
job.materials["MASH"] &&
job.materials["MASH"].mat_adjp &&
typeof job.materials["MASH"].mat_adjp === "number" &&
job.materials["MASH"].mat_adjp !== 0
) {
adjp =
Math.abs(job.materials["MASH"].mat_adjp) > 1
? job.materials["MASH"].mat_adjp
@@ -565,6 +635,10 @@ function GenerateCostingData(job) {
.percentage(adjp < 0 ? adjp * -1 : adjp)
.multiply(adjp < 0 ? -1 : 1)
);
if (threshold && jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]].greaterThanOrEqual(threshold)) {
jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = threshold.add(origMASHAmount);
}
}
if (InstanceManager({ imex: false, rome: true })) {
@@ -574,20 +648,34 @@ function GenerateCostingData(job) {
if (!jobLineTotalsByProfitCenter.additional[defaultProfits["TOW"]])
jobLineTotalsByProfitCenter.additional[defaultProfits["TOW"]] = Dinero();
jobLineTotalsByProfitCenter.additional[defaultProfits["TOW"]] = stlTowing
? Dinero({ amount: Math.round(stlTowing.ttl_amt * 100) })
: Dinero({
if (stlTowing)
jobLineTotalsByProfitCenter.additional[defaultProfits["TOW"]] = Dinero({
amount: Math.round((stlTowing.ttl_amt || 0) * 100)
});
if (!stlTowing && job.towing_payable && job.towing_payable > 0)
jobLineTotalsByProfitCenter.additional[defaultProfits["TOW"]] = jobLineTotalsByProfitCenter.additional[
defaultProfits["TOW"]
].add(
Dinero({
amount: Math.round((job.towing_payable || 0) * 100)
});
})
);
if (!jobLineTotalsByProfitCenter.additional[defaultProfits["STO"]])
jobLineTotalsByProfitCenter.additional[defaultProfits["STO"]] = Dinero();
jobLineTotalsByProfitCenter.additional[defaultProfits["STO"]] = stlStorage
? Dinero({ amount: Math.round(stlStorage.ttl_amt * 100) })
: Dinero({
if (stlStorage)
jobLineTotalsByProfitCenter.additional[defaultProfits["TOW"]] = Dinero({
amount: Math.round((stlStorage.ttl_amt || 0) * 100)
});
if (!stlStorage && job.storage_payable && job.storage_payable > 0)
jobLineTotalsByProfitCenter.additional[defaultProfits["STO"]] = jobLineTotalsByProfitCenter.additional[
defaultProfits["STO"]
].add(
Dinero({
amount: Math.round((job.storage_payable || 0) * 100)
});
})
);
}
//Is it a DMS Setup?

View File

@@ -1227,6 +1227,8 @@ function ParseCalopCode(opcode) {
return opcode.trim().split(" ");
}
exports.ParseCalopCode = ParseCalopCode;
function IsTrueOrYes(value) {
return value === true || value === "Y" || value === "y";
}