Files
bodyshop/server/job/job-costing.js
2021-04-06 14:29:44 -07:00

239 lines
7.5 KiB
JavaScript

const Dinero = require("dinero.js");
const queries = require("../graphql-client/queries");
//const client = require("../graphql-client/graphql-client").client;
const _ = require("lodash");
const GraphQLClient = require("graphql-request").GraphQLClient;
// Dinero.defaultCurrency = "USD";
// Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN";
async function JobCosting(req, res) {
const { jobid } = req.body;
console.log("🚀 ~ file: job-costing.js ~ line 13 ~ jobid", jobid);
const BearerToken = req.headers.authorization;
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
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) {
console.log("error", error);
res.status(400).send(JSON.stringify(error));
}
}
async function JobCostingMulti(req, res) {
const { jobids } = req.body;
console.log("🚀 ~ file: job-costing.js ~ line 13 ~ jobids", jobids);
const BearerToken = req.headers.authorization;
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
try {
const resp = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QUERY_JOB_COSTING_DETAILS_MULTI, {
ids: jobids,
});
//for Each!***************
const ret = {};
resp.jobs.map((job) => {
ret[job.id] = GenerateCostingData(job);
});
res.status(200).json(ret);
} catch (error) {
console.log("error", error);
res.status(400).send(JSON.stringify(error));
}
}
function GenerateCostingData(job) {
const defaultProfits =
job.bodyshop.md_responsibility_centers.defaults.profits;
const allProfitCenters = _.union(
job.bodyshop.md_responsibility_centers.profits.map((p) => p.name),
job.bodyshop.md_responsibility_centers.costs.map((p) => p.name)
);
//Massage the data.
const jobLineTotalsByProfitCenter =
job &&
job.joblines.reduce(
(acc, val) => {
const laborProfitCenter = defaultProfits[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
);
const partsProfitCenter = defaultProfits[val.part_type] || "?";
if (!partsProfitCenter)
console.log(
"Unknown cost/profit center mapping for parts.",
val.part_type
);
const partsAmount = Dinero({
amount: Math.round((val.act_price || 0) * 100),
}).multiply(val.part_qty || 1);
if (!acc.parts[partsProfitCenter])
acc.parts[partsProfitCenter] = Dinero();
acc.parts[partsProfitCenter] = acc.parts[partsProfitCenter].add(
partsAmount
);
return acc;
},
{ parts: {}, labor: {} }
);
const billTotalsByProfitCenter = job.bills.reduce((bill_acc, bill_val) => {
//At the invoice level.
bill_val.billlines.map((line_val) => {
//At the invoice line level.
//console.log("JobCostingPartsTable -> line_val", line_val);
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;
}, {});
const ticketTotalsByProfitCenter = job.timetickets.reduce(
(ticket_acc, ticket_val) => {
//At the invoice level.
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.actualhrs || ticket_val.productivehrs || 0)
);
return ticket_acc;
},
{}
);
const summaryData = {
totalLaborSales: Dinero({ amount: 0 }),
totalPartsSales: Dinero({ amount: 0 }),
totalSales: Dinero({ amount: 0 }),
totalLaborCost: Dinero({ amount: 0 }),
totalPartsCost: Dinero({ amount: 0 }),
totalCost: Dinero({ amount: 0 }),
gpdollars: Dinero({ amount: 0 }),
gppercent: null,
gppercentFormatted: null,
};
const costCenterData = allProfitCenters.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 cost_labor =
ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
const cost_parts = billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 });
const costs = (
billTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 })
).add(ticketTotalsByProfitCenter[ccVal] || Dinero({ amount: 0 }));
const totalSales = sale_labor.add(sale_parts);
const gpdollars = totalSales.subtract(costs);
const gppercent = (
(gpdollars.getAmount() / totalSales.getAmount()) *
100
).toFixed(2);
let gppercentFormatted;
if (isNaN(gppercent)) gppercentFormatted = "0%";
else if (!isFinite(gppercent)) gppercentFormatted = "- ∞";
else {
gppercentFormatted = `${gppercent}%`;
}
//Push summary data to avoid extra loop.
summaryData.totalLaborSales = summaryData.totalLaborSales.add(sale_labor);
summaryData.totalPartsSales = summaryData.totalPartsSales.add(sale_parts);
summaryData.totalSales = summaryData.totalSales
.add(sale_labor)
.add(sale_parts);
summaryData.totalLaborCost = summaryData.totalLaborCost.add(cost_labor);
summaryData.totalPartsCost = summaryData.totalPartsCost.add(cost_parts);
summaryData.totalCost = summaryData.totalCost.add(costs);
return {
id: idx,
cost_center: ccVal,
sale_labor: sale_labor && sale_labor.toFormat(),
sale_parts: sale_parts && sale_parts.toFormat(),
sales: sale_labor.add(sale_parts).toFormat(),
sales_dinero: sale_labor.add(sale_parts),
cost_parts: cost_parts && cost_parts.toFormat(),
cost_labor: cost_labor && cost_labor.toFormat(),
costs: cost_parts.add(cost_labor).toFormat(),
costs_dinero: cost_parts.add(cost_labor),
gpdollars: gpdollars.toFormat(),
gppercent: gppercentFormatted,
};
});
//Final summary data massaging.
summaryData.gpdollars = summaryData.totalSales.subtract(
summaryData.totalCost
);
summaryData.gppercent = (
(summaryData.gpdollars.getAmount() / summaryData.totalSales.getAmount()) *
100
).toFixed(2);
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;