Files
bodyshop/server/job/job-costing.js
Dave Richer 6a26fb413c - refactor on scheduled-out-today
- routine package updates

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-02-23 11:46:53 -05:00

974 lines
41 KiB
JavaScript

const _ = require("lodash");
const Dinero = require("dinero.js");
const queries = require("../graphql-client/queries");
const logger = require('../utils/logger');
const {DiscountNotAlreadyCounted} = require("./job-totals");
// Dinero.defaultCurrency = "USD";
// Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN";
async function JobCosting(req, res) {
const {jobid} = req.body;
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null);
try {
const resp = await client
.setHeaders({Authorization: BearerToken})
.request(queries.QUERY_JOB_COSTING_DETAILS, {
id: jobid,
});
const ret = GenerateCostingData(resp.jobs_by_pk);
res.status(200).json(ret);
} catch (error) {
logger.log("job-costing-error", "ERROR", req.user.email, jobid, {
message: error.message,
stack: error.stack,
});
res.status(400).send(JSON.stringify(error));
}
}
async function JobCostingMulti(req, res) {
const {jobids} = req.body;
const logger = req.logger;
const BearerToken = req.BearerToken
const client = req.userGraphQLClient;
logger.log("job-costing-multi-start", "DEBUG", req.user.email, jobids, null);
try {
const resp = await client
.setHeaders({Authorization: BearerToken})
.request(queries.QUERY_JOB_COSTING_DETAILS_MULTI, {
ids: jobids,
});
const multiSummary = {
costCenterData: [],
summaryData: {
totalLaborSales: Dinero({amount: 0}),
totalPartsSales: Dinero({amount: 0}),
totalAdditionalSales: Dinero({amount: 0}),
totalSubletSales: Dinero({amount: 0}),
totalSales: Dinero({amount: 0}),
totalLaborCost: Dinero({amount: 0}),
totalPartsCost: Dinero({amount: 0}),
totalAdditionalCost: Dinero({amount: 0}),
totalSubletCost: Dinero({amount: 0}),
totalCost: Dinero({amount: 0}),
gpdollars: Dinero({amount: 0}),
gppercent: null,
gppercentFormatted: null,
totalLaborGp: Dinero({amount: 0}),
totalPartsGp: Dinero({amount: 0}),
totalAdditionalGp: Dinero({amount: 0}),
totalSubletGp: Dinero({amount: 0}),
totalLaborGppercent: null,
totalLaborGppercentFormatted: null,
totalPartsGppercent: null,
totalPartsGppercentFormatted: null,
totalAdditionalGppercent: null,
totalAdditionalGppercentFormatted: null,
totalSubletGppercent: null,
totalSubletGppercentFormatted: null,
},
};
const ret = {};
resp.jobs.map((job) => {
const costingData = GenerateCostingData(job);
ret[job.id] = costingData;
//Merge on a cost center basis.
costingData.costCenterData.forEach((c) => {
//Find the Cost Center if it exists.
const CostCenterIndex = multiSummary.costCenterData.findIndex(
(x) => x.cost_center === c.cost_center
);
if (CostCenterIndex >= 0) {
//Add it in place
multiSummary.costCenterData[CostCenterIndex] = {
...multiSummary.costCenterData[CostCenterIndex],
sale_labor_dinero: multiSummary.costCenterData[
CostCenterIndex
].sale_labor_dinero.add(c.sale_labor_dinero),
sale_parts_dinero: multiSummary.costCenterData[
CostCenterIndex
].sale_parts_dinero.add(c.sale_parts_dinero),
sale_additional_dinero: multiSummary.costCenterData[
CostCenterIndex
].sale_additional_dinero.add(c.sale_additional_dinero),
sale_sublet_dinero: multiSummary.costCenterData[
CostCenterIndex
].sale_sublet_dinero.add(c.sale_sublet_dinero),
cost_labor_dinero: multiSummary.costCenterData[
CostCenterIndex
].cost_labor_dinero.add(c.cost_labor_dinero),
cost_parts_dinero: multiSummary.costCenterData[
CostCenterIndex
].cost_parts_dinero.add(c.cost_parts_dinero),
cost_additional_dinero: multiSummary.costCenterData[
CostCenterIndex
].cost_additional_dinero.add(c.cost_additional_dinero),
cost_sublet_dinero: multiSummary.costCenterData[
CostCenterIndex
].cost_sublet_dinero.add(c.cost_sublet_dinero),
gpdollars_dinero: multiSummary.costCenterData[
CostCenterIndex
].gpdollars_dinero.add(c.gpdollars_dinero),
costs_dinero: multiSummary.costCenterData[
CostCenterIndex
].costs_dinero.add(c.costs_dinero),
sales_dinero: multiSummary.costCenterData[
CostCenterIndex
].sales_dinero.add(c.sales_dinero),
};
} else {
//Add it to the list instead.
multiSummary.costCenterData.push(c);
}
});
//Add all summary data.
multiSummary.summaryData.totalPartsSales =
multiSummary.summaryData.totalPartsSales.add(
costingData.summaryData.totalPartsSales
);
multiSummary.summaryData.totalAdditionalSales =
multiSummary.summaryData.totalAdditionalSales.add(
costingData.summaryData.totalAdditionalSales
);
multiSummary.summaryData.totalSubletSales =
multiSummary.summaryData.totalSubletSales.add(
costingData.summaryData.totalSubletSales
);
multiSummary.summaryData.totalSales =
multiSummary.summaryData.totalSales.add(
costingData.summaryData.totalSales
);
multiSummary.summaryData.totalLaborCost =
multiSummary.summaryData.totalLaborCost.add(
costingData.summaryData.totalLaborCost
);
multiSummary.summaryData.totalLaborSales =
multiSummary.summaryData.totalLaborSales.add(
costingData.summaryData.totalLaborSales
);
multiSummary.summaryData.totalPartsCost =
multiSummary.summaryData.totalPartsCost.add(
costingData.summaryData.totalPartsCost
);
multiSummary.summaryData.totalAdditionalCost =
multiSummary.summaryData.totalAdditionalCost.add(
costingData.summaryData.totalAdditionalCost
);
multiSummary.summaryData.totalSubletCost =
multiSummary.summaryData.totalSubletCost.add(
costingData.summaryData.totalSubletCost
);
multiSummary.summaryData.totalCost =
multiSummary.summaryData.totalCost.add(
costingData.summaryData.totalCost
);
multiSummary.summaryData.gpdollars =
multiSummary.summaryData.gpdollars.add(
costingData.summaryData.gpdollars
);
multiSummary.summaryData.totalLaborGp =
multiSummary.summaryData.totalLaborGp.add(
costingData.summaryData.totalLaborGp
);
multiSummary.summaryData.totalPartsGp =
multiSummary.summaryData.totalPartsGp.add(
costingData.summaryData.totalPartsGp
);
multiSummary.summaryData.totalAdditionalGp =
multiSummary.summaryData.totalAdditionalGp.add(
costingData.summaryData.totalAdditionalGp
);
multiSummary.summaryData.totalSubletGp =
multiSummary.summaryData.totalSubletGp.add(
costingData.summaryData.totalSubletGp
);
//Take the summary data & add it to total summary data.
});
//For each center, recalculate and toFormat() the values.
multiSummary.summaryData.totalLaborGppercent = (
(multiSummary.summaryData.totalLaborGp.getAmount() /
multiSummary.summaryData.totalLaborSales.getAmount()) *
100
).toFixed(1);
multiSummary.summaryData.totalLaborGppercentFormatted = formatGpPercent(
multiSummary.summaryData.totalLaborGppercent
);
multiSummary.summaryData.totalPartsGppercent = (
(multiSummary.summaryData.totalPartsGp.getAmount() /
multiSummary.summaryData.totalPartsSales.getAmount()) *
100
).toFixed(1);
multiSummary.summaryData.totalPartsGppercentFormatted = formatGpPercent(
multiSummary.summaryData.totalPartsGppercent
);
multiSummary.summaryData.totalAdditionalGppercent = (
(multiSummary.summaryData.totalAdditionalGp.getAmount() /
multiSummary.summaryData.totalAdditionalSales.getAmount()) *
100
).toFixed(1);
multiSummary.summaryData.totalAdditionalGppercentFormatted =
formatGpPercent(multiSummary.summaryData.totalAdditionalGppercent);
multiSummary.summaryData.totalSubletGppercent = (
(multiSummary.summaryData.totalSubletGp.getAmount() /
multiSummary.summaryData.totalSubletSales.getAmount()) *
100
).toFixed(1);
multiSummary.summaryData.totalSubletGppercentFormatted = formatGpPercent(
multiSummary.summaryData.totalSubletGppercent
);
multiSummary.summaryData.gppercent = (
(multiSummary.summaryData.gpdollars.getAmount() /
multiSummary.summaryData.totalSales.getAmount()) *
100
).toFixed(1);
multiSummary.summaryData.gppercentFormatted = formatGpPercent(
multiSummary.summaryData.gppercent
);
const finalCostingdata = multiSummary.costCenterData.map((c) => {
return {
...c,
sale_labor: c.sale_labor_dinero && c.sale_labor_dinero.toFormat(),
sale_parts: c.sale_parts_dinero && c.sale_parts_dinero.toFormat(),
sale_additional:
c.sale_additional_dinero && c.sale_additional_dinero.toFormat(),
sale_sublet: c.sale_sublet_dinero && c.sale_sublet_dinero.toFormat(),
sales: c.sales_dinero.toFormat(),
cost_parts: c.cost_parts_dinero && c.cost_parts_dinero.toFormat(),
cost_labor: c.cost_labor_dinero && c.cost_labor_dinero.toFormat(),
cost_additional:
c.cost_additional_dinero && c.cost_additional_dinero.toFormat(),
cost_sublet: c.cost_sublet_dinero && c.cost_sublet_dinero.toFormat(),
costs: c.costs_dinero.toFormat(),
gpdollars: c.gpdollars_dinero.toFormat(),
gppercent: formatGpPercent(
(
(c.gpdollars_dinero.getAmount() / c.sales_dinero.getAmount()) *
100
).toFixed(1)
),
};
});
//Calculate thte total gross profit percentages.
res.status(200).json({
allCostCenterData: finalCostingdata,
allSummaryData: multiSummary.summaryData,
data: ret,
});
} catch (error) {
logger.log("job-costing-multi-error", "ERROR", req.user.email, [jobids], {
message: error.message,
stack: error.stack,
});
res.status(400).send(error);
}
}
function GenerateCostingData(job) {
const defaultProfits =
job.bodyshop.md_responsibility_centers.defaults.profits;
const allCenters = _.union(
job.bodyshop.md_responsibility_centers.profits.map((p) => p.name),
job.bodyshop.md_responsibility_centers.costs.map((p) => p.name),
["Unknown"]
);
const materialsHours = {mapaHrs: 0, mashHrs: 0};
let hasMapaLine = false;
let hasMashLine = false;
//Massage the data.
const jobLineTotalsByProfitCenter =
job &&
job.joblines.reduce(
(acc, val) => {
//Parts Lines
if (val.db_ref === "936008") {
//If either of these DB REFs change, they also need to change in job-totals/job-costing calculations.
hasMapaLine = true;
}
if (val.db_ref === "936007") {
hasMashLine = true;
}
if (val.mod_lbr_ty) {
const laborProfitCenter =
val.profitcenter_labor ||
defaultProfits[val.mod_lbr_ty] ||
"Unknown";
if (laborProfitCenter === "Unknown")
console.log("Unknown type", val.line_desc, val.mod_lbr_ty);
const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`;
const laborAmount = Dinero({
amount: Math.round((job[rateName] || 0) * 100),
}).multiply(val.mod_lb_hrs || 0);
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"
) {
//Scenario where SGI may pay out hours using a part price.
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(
Dinero({
amount: Math.round((val.act_price || 0) * 100),
}).multiply(val.part_qty)
);
}
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 (
val.part_type &&
val.part_type !== "PAE" &&
val.part_type !== "PAS" &&
val.part_type !== "PASL"
) {
const partsProfitCenter =
val.profitcenter_part || defaultProfits[val.part_type] || "Unknown";
if (partsProfitCenter === "Unknown")
console.log("Unknown type", val.line_desc, val.part_type);
if (!partsProfitCenter)
console.log(
"Unknown cost/profit center mapping for parts.",
val.line_desc,
val.part_type
);
const partsAmount = Dinero({
amount: Math.round((val.act_price || 0) * 100),
})
.multiply(val.part_qty || 1)
.add(
((val.prt_dsmk_m && val.prt_dsmk_m !== 0) ||
(val.prt_dsmk_p && val.prt_dsmk_p !== 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 (!acc.parts[partsProfitCenter])
acc.parts[partsProfitCenter] = Dinero();
acc.parts[partsProfitCenter] =
acc.parts[partsProfitCenter].add(partsAmount);
}
if (
val.part_type &&
val.part_type !== "PAE" &&
(val.part_type === "PAS" || val.part_type === "PASL")
) {
const partsProfitCenter =
val.profitcenter_part || defaultProfits[val.part_type] || "Unknown";
if (partsProfitCenter === "Unknown")
console.log("Unknown type", val.line_desc, val.part_type);
if (!partsProfitCenter)
console.log(
"Unknown cost/profit center mapping for sublet.",
val.line_desc,
val.part_type
);
const partsAmount = Dinero({
amount: Math.round((val.act_price || 0) * 100),
})
.multiply(val.part_qty || 1)
.add(
((val.prt_dsmk_m && val.prt_dsmk_m !== 0) ||
(val.prt_dsmk_p && val.prt_dsmk_p !== 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 (!acc.sublet[partsProfitCenter])
acc.sublet[partsProfitCenter] = Dinero();
acc.sublet[partsProfitCenter] =
acc.sublet[partsProfitCenter].add(partsAmount);
}
//To deal with additional costs.
if (!val.part_type && !val.mod_lbr_ty) {
//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";
if (partsProfitCenter === "Unknown") {
console.log("Unknown type", val.line_desc, val.part_type);
}
const partsAmount = Dinero({
amount: Math.round((val.act_price || 0) * 100),
})
.multiply(val.part_qty || 1)
.add(
((val.prt_dsmk_m && val.prt_dsmk_m !== 0) ||
(val.prt_dsmk_p && val.prt_dsmk_p !== 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 (!acc.additional[partsProfitCenter])
acc.additional[partsProfitCenter] = Dinero();
acc.additional[partsProfitCenter] =
acc.additional[partsProfitCenter].add(partsAmount);
}
return acc;
},
{parts: {}, labor: {}, additional: {}, sublet: {}}
);
if (!hasMapaLine) {
if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]])
jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = Dinero();
jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] =
jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]].add(
Dinero({
amount: Math.round((job.rate_mapa || 0) * 100),
}).multiply(materialsHours.mapaHrs || 0)
);
}
if (!hasMashLine) {
if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]])
jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = Dinero();
jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] =
jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]].add(
Dinero({
amount: Math.round((job.rate_mash || 0) * 100),
}).multiply(materialsHours.mashHrs || 0)
);
}
//Is it a DMS Setup?
const selectedDmsAllocationConfig =
(job.bodyshop.md_responsibility_centers.dms_defaults &&
job.bodyshop.md_responsibility_centers.dms_defaults.find(
(d) => d.name === job.dms_allocation
)) ||
job.bodyshop.md_responsibility_centers.defaults;
const billTotalsByCostCenters = job.bills.reduce(
(bill_acc, bill_val) => {
//At the bill level.
bill_val.billlines.map((line_val) => {
//At the bill line level.
if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid) {
if (
!bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]]
)
bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] =
Dinero();
bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] =
bill_acc[
selectedDmsAllocationConfig.costs[line_val.cost_center]
].add(
Dinero({
amount: Math.round((line_val.actual_cost || 0) * 100),
})
.multiply(line_val.quantity)
.multiply(bill_val.is_credit_memo ? -1 : 1)
);
} else {
const isSubletCostCenter =
line_val.cost_center ===
job.bodyshop.md_responsibility_centers.defaults.costs.PAS ||
line_val.cost_center ===
job.bodyshop.md_responsibility_centers.defaults.costs.PASL;
const isAdditionalCostCenter =
// line_val.cost_center ===
// job.bodyshop.md_responsibility_centers.defaults.costs.PAS ||
// line_val.cost_center ===
// job.bodyshop.md_responsibility_centers.defaults.costs.PASL ||
line_val.cost_center ===
job.bodyshop.md_responsibility_centers.defaults.costs.TOW ||
line_val.cost_center ===
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA ||
line_val.cost_center ===
job.bodyshop.md_responsibility_centers.defaults.costs.MASH;
if (isAdditionalCostCenter) {
if (!bill_acc.additionalCosts[line_val.cost_center])
bill_acc.additionalCosts[line_val.cost_center] = Dinero();
bill_acc.additionalCosts[line_val.cost_center] =
bill_acc.additionalCosts[line_val.cost_center].add(
Dinero({
amount: Math.round((line_val.actual_cost || 0) * 100),
})
.multiply(line_val.quantity)
.multiply(bill_val.is_credit_memo ? -1 : 1)
);
} else if (isSubletCostCenter) {
if (!bill_acc.subletCosts[line_val.cost_center])
bill_acc.subletCosts[line_val.cost_center] = Dinero();
bill_acc.subletCosts[line_val.cost_center] = bill_acc.subletCosts[
line_val.cost_center
].add(
Dinero({
amount: Math.round((line_val.actual_cost || 0) * 100),
})
.multiply(line_val.quantity)
.multiply(bill_val.is_credit_memo ? -1 : 1)
);
} else {
if (!bill_acc[line_val.cost_center])
bill_acc[line_val.cost_center] = Dinero();
bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add(
Dinero({
amount: Math.round((line_val.actual_cost || 0) * 100),
})
.multiply(line_val.quantity)
.multiply(bill_val.is_credit_memo ? -1 : 1)
);
}
}
return null;
});
return bill_acc;
},
{additionalCosts: {}, subletCosts: {}}
);
//If the hourly rates for job costing are set, add them in.
if (
job.bodyshop.jc_hourly_rates &&
(job.bodyshop.jc_hourly_rates.mapa ||
typeof job.bodyshop.jc_hourly_rates.mapa === "number" ||
isNaN(job.bodyshop.jc_hourly_rates.mapa) === false)
) {
if (
!billTotalsByCostCenters.additionalCosts[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
]
)
billTotalsByCostCenters.additionalCosts[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = Dinero();
if (job.bodyshop.use_paint_scale_data === true) {
if (job.mixdata.length > 0) {
billTotalsByCostCenters.additionalCosts[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = Dinero({
amount: Math.round(
((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100
),
});
} else {
billTotalsByCostCenters.additionalCosts[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = billTotalsByCostCenters.additionalCosts[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
].add(
Dinero({
amount: Math.round(
(job.bodyshop.jc_hourly_rates &&
job.bodyshop.jc_hourly_rates.mapa * 100) ||
0
),
}).multiply(materialsHours.mapaHrs)
);
}
} else {
billTotalsByCostCenters.additionalCosts[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = billTotalsByCostCenters.additionalCosts[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
].add(
Dinero({
amount: Math.round(
(job.bodyshop.jc_hourly_rates &&
job.bodyshop.jc_hourly_rates.mapa * 100) ||
0
),
}).multiply(materialsHours.mapaHrs)
);
}
}
if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash) {
if (
!billTotalsByCostCenters.additionalCosts[
job.bodyshop.md_responsibility_centers.defaults.costs.MASH
]
)
billTotalsByCostCenters.additionalCosts[
job.bodyshop.md_responsibility_centers.defaults.costs.MASH
] = Dinero();
billTotalsByCostCenters.additionalCosts[
job.bodyshop.md_responsibility_centers.defaults.costs.MASH
] = billTotalsByCostCenters.additionalCosts[
job.bodyshop.md_responsibility_centers.defaults.costs.MASH
].add(
Dinero({
amount: Math.round(
(job.bodyshop.jc_hourly_rates &&
job.bodyshop.jc_hourly_rates.mash * 100) ||
0
),
}).multiply(materialsHours.mashHrs)
);
}
const ticketTotalsByCostCenter = job.timetickets.reduce(
(ticket_acc, ticket_val) => {
//At the invoice level.
if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid) {
if (
!ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]]
)
ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] =
Dinero();
ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] =
ticket_acc[
selectedDmsAllocationConfig.costs[ticket_val.ciecacode]
].add(
Dinero({
amount: Math.round((ticket_val.rate || 0) * 100),
}).multiply(
ticket_val.flat_rate
? ticket_val.productivehrs || ticket_val.actualhrs || 0
: ticket_val.actualhrs || ticket_val.productivehrs || 0
) //Should base this on the employee.
);
} else {
if (!ticket_acc[ticket_val.cost_center])
ticket_acc[ticket_val.cost_center] = Dinero();
ticket_acc[ticket_val.cost_center] = ticket_acc[
ticket_val.cost_center
].add(
Dinero({
amount: Math.round((ticket_val.rate || 0) * 100),
}).multiply(
ticket_val.flat_rate
? ticket_val.productivehrs || ticket_val.actualhrs || 0
: ticket_val.actualhrs || ticket_val.productivehrs || 0
) //Should base this on the employee.
);
}
return ticket_acc;
},
{}
);
const summaryData = {
totalLaborSales: Dinero({amount: 0}),
totalPartsSales: Dinero({amount: 0}),
totalAdditionalSales: Dinero({amount: 0}),
totalSubletSales: Dinero({amount: 0}),
totalSales: Dinero({amount: 0}),
totalLaborCost: Dinero({amount: 0}),
totalPartsCost: Dinero({amount: 0}),
totalAdditionalCost: Dinero({amount: 0}),
totalSubletCost: Dinero({amount: 0}),
totalCost: Dinero({amount: 0}),
totalLaborGp: Dinero({amount: 0}),
totalPartsGp: Dinero({amount: 0}),
totalAdditionalGp: Dinero({amount: 0}),
totalSubletGp: Dinero({amount: 0}),
gpdollars: Dinero({amount: 0}),
totalLaborGppercent: null,
totalLaborGppercentFormatted: null,
totalPartsGppercent: null,
totalPartsGppercentFormatted: null,
totalAdditionalGppercent: null,
totalAdditionalGppercentFormatted: null,
totalSubletGppercent: null,
totalSubletGppercentFormatted: null,
gppercent: null,
gppercentFormatted: null,
};
const costCenterData = allCenters.map((key, idx) => {
const ccVal = key; // defaultProfits[key];
const sale_labor =
jobLineTotalsByProfitCenter.labor[ccVal] || Dinero({amount: 0});
const sale_parts =
jobLineTotalsByProfitCenter.parts[ccVal] || Dinero({amount: 0});
const sale_additional =
jobLineTotalsByProfitCenter.additional[ccVal] || Dinero({amount: 0});
const sale_sublet =
jobLineTotalsByProfitCenter.sublet[ccVal] || Dinero({amount: 0});
const cost_labor = ticketTotalsByCostCenter[ccVal] || Dinero({amount: 0});
const cost_parts = billTotalsByCostCenters[ccVal] || Dinero({amount: 0});
const cost_additional =
billTotalsByCostCenters.additionalCosts[ccVal] || Dinero({amount: 0});
const cost_sublet =
billTotalsByCostCenters.subletCosts[ccVal] || Dinero({amount: 0});
const costs = cost_labor
.add(cost_parts)
.add(cost_additional)
.add(cost_sublet);
const totalSales = sale_labor
.add(sale_parts)
.add(sale_additional)
.add(sale_sublet);
const gpdollars = totalSales.subtract(costs);
const gppercent = (
(gpdollars.getAmount() / Math.abs(totalSales.getAmount())) *
100
).toFixed(1);
//Push summary data to avoid extra loop.
summaryData.totalLaborSales = summaryData.totalLaborSales.add(sale_labor);
summaryData.totalPartsSales = summaryData.totalPartsSales.add(sale_parts);
summaryData.totalAdditionalSales =
summaryData.totalAdditionalSales.add(sale_additional);
summaryData.totalSubletSales =
summaryData.totalSubletSales.add(sale_sublet);
summaryData.totalSales = summaryData.totalSales.add(totalSales);
summaryData.totalLaborCost = summaryData.totalLaborCost.add(cost_labor);
summaryData.totalPartsCost = summaryData.totalPartsCost.add(cost_parts);
summaryData.totalAdditionalCost =
summaryData.totalAdditionalCost.add(cost_additional);
summaryData.totalSubletCost = summaryData.totalSubletCost.add(cost_sublet);
summaryData.totalCost = summaryData.totalCost.add(costs);
return {
id: idx,
cost_center: ccVal,
sale_labor: sale_labor && sale_labor.toFormat(),
sale_labor_dinero: sale_labor,
sale_parts: sale_parts && sale_parts.toFormat(),
sale_parts_dinero: sale_parts,
sale_additional: sale_additional && sale_additional.toFormat(),
sale_additional_dinero: sale_additional,
sale_sublet: sale_sublet && sale_sublet.toFormat(),
sale_sublet_dinero: sale_sublet,
sales: totalSales.toFormat(),
sales_dinero: totalSales,
cost_parts: cost_parts && cost_parts.toFormat(),
cost_parts_dinero: cost_parts,
cost_labor: cost_labor && cost_labor.toFormat(),
cost_labor_dinero: cost_labor,
cost_additional: cost_additional && cost_additional.toFormat(),
cost_additional_dinero: cost_additional,
cost_sublet: cost_sublet && cost_sublet.toFormat(),
cost_sublet_dinero: cost_sublet,
costs: costs.toFormat(),
costs_dinero: costs,
gpdollars_dinero: gpdollars,
gpdollars: gpdollars.toFormat(),
gppercent: formatGpPercent(gppercent),
};
});
//Push adjustments to bottom line.
if (job.adjustment_bottom_line) {
//Add to totals.
const Adjustment = Dinero({
amount: Math.round(job.adjustment_bottom_line * 100),
}); //Need to invert, since this is being assigned as a cost.
summaryData.totalLaborSales = summaryData.totalLaborSales.add(Adjustment);
summaryData.totalSales = summaryData.totalSales.add(Adjustment);
//Add to lines.
costCenterData.push({
id: "Adj",
cost_center: "Adjustment",
sale_labor: Adjustment.toFormat(),
sale_labor_dinero: Adjustment,
sale_parts: Dinero().toFormat(),
sale_parts_dinero: Dinero(),
sale_additional: Dinero(),
sale_additional_dinero: Dinero(),
sale_sublet: Dinero(),
sale_sublet_dinero: Dinero(),
sales: Adjustment.toFormat(),
sales_dinero: Adjustment,
cost_parts: Dinero().toFormat(),
cost_parts_dinero: Dinero(),
cost_labor: Dinero().toFormat(), //Adjustment.toFormat(),
cost_labor_dinero: Dinero(), // Adjustment,
cost_additional: Dinero(),
cost_additional_dinero: Dinero(),
cost_sublet: Dinero(),
cost_sublet_dinero: Dinero(),
costs: Dinero().toFormat(),
costs_dinero: Dinero(),
gpdollars_dinero: Dinero(),
gpdollars: Dinero().toFormat(),
gppercent: formatGpPercent(0),
});
}
//Final summary data massaging.
summaryData.totalLaborGp = summaryData.totalLaborSales.subtract(
summaryData.totalLaborCost
);
summaryData.totalLaborGppercent = (
(summaryData.totalLaborGp.getAmount() /
summaryData.totalLaborSales.getAmount()) *
100
).toFixed(1);
summaryData.totalLaborGppercentFormatted = formatGpPercent(
summaryData.totalLaborGppercent
);
summaryData.totalPartsGp = summaryData.totalPartsSales.subtract(
summaryData.totalPartsCost
);
summaryData.totalPartsGppercent = (
(summaryData.totalPartsGp.getAmount() /
summaryData.totalPartsSales.getAmount()) *
100
).toFixed(1);
summaryData.totalPartsGppercentFormatted = formatGpPercent(
summaryData.totalPartsGppercent
);
summaryData.totalAdditionalGp = summaryData.totalAdditionalSales.subtract(
summaryData.totalAdditionalCost
);
summaryData.totalAdditionalGppercent = (
(summaryData.totalAdditionalGp.getAmount() /
summaryData.totalAdditionalSales.getAmount()) *
100
).toFixed(1);
summaryData.totalAdditionalGppercentFormatted = formatGpPercent(
summaryData.totalAdditionalGppercent
);
summaryData.totalSubletGp = summaryData.totalSubletSales.subtract(
summaryData.totalSubletCost
);
summaryData.totalSubletGppercent = (
(summaryData.totalSubletGp.getAmount() /
summaryData.totalSubletSales.getAmount()) *
100
).toFixed(1);
summaryData.totalSubletGppercentFormatted = formatGpPercent(
summaryData.totalSubletGppercent
);
summaryData.gpdollars = summaryData.totalSales.subtract(
summaryData.totalCost
);
summaryData.gppercent = (
(summaryData.gpdollars.getAmount() /
Math.abs(summaryData.totalSales.getAmount())) *
100
).toFixed(1);
if (isNaN(summaryData.gppercent)) summaryData.gppercentFormatted = 0;
else if (!isFinite(summaryData.gppercent))
summaryData.gppercentFormatted = "- ∞";
else {
summaryData.gppercentFormatted = `${summaryData.gppercent}%`;
}
return {summaryData, costCenterData};
}
exports.JobCosting = JobCosting;
exports.JobCostingMulti = JobCostingMulti;
const formatGpPercent = (gppercent) => {
let gppercentFormatted;
if (isNaN(gppercent)) gppercentFormatted = "0%";
else if (!isFinite(gppercent)) gppercentFormatted = "- ∞";
else {
gppercentFormatted = `${gppercent}%`;
}
return gppercentFormatted;
};
//Verify that this stays in line with jobs-close-auto-allocate logic from the application.
const getAdditionalCostCenter = (jl, profitCenters) => {
if (!jl.part_type && !jl.mod_lbr_ty) {
const lineDesc = jl.line_desc ? jl.line_desc.toLowerCase() : "";
if (lineDesc.includes("shop mat")) {
return profitCenters["MASH"];
} else if (lineDesc.includes("paint/mat")) {
return profitCenters["MAPA"];
} else if (lineDesc.includes("ats amount")) {
return profitCenters["ATS"];
} else if (lineDesc.includes("towing")) {
return profitCenters["TOW"];
} else {
return null;
}
}
};