IO-836 Server Side Job Costing
This commit is contained in:
@@ -579,3 +579,198 @@ exports.GET_JOB_BY_PK = ` query GET_JOB_BY_PK($id: uuid!) {
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
exports.QUERY_JOB_COSTING_DETAILS = ` query QUERY_JOB_COSTING_DETAILS($id: uuid!) {
|
||||
jobs_by_pk(id: $id) {
|
||||
ro_number
|
||||
clm_total
|
||||
id
|
||||
ded_amt
|
||||
ded_status
|
||||
depreciation_taxes
|
||||
other_amount_payable
|
||||
towing_payable
|
||||
storage_payable
|
||||
adjustment_bottom_line
|
||||
federal_tax_rate
|
||||
state_tax_rate
|
||||
local_tax_rate
|
||||
tax_tow_rt
|
||||
tax_str_rt
|
||||
tax_paint_mat_rt
|
||||
tax_sub_rt
|
||||
tax_lbr_rt
|
||||
tax_levies_rt
|
||||
parts_tax_rates
|
||||
job_totals
|
||||
labor_rate_desc
|
||||
rate_la1
|
||||
rate_la2
|
||||
rate_la3
|
||||
rate_la4
|
||||
rate_laa
|
||||
rate_lab
|
||||
rate_lad
|
||||
rate_lae
|
||||
rate_laf
|
||||
rate_lag
|
||||
rate_lam
|
||||
rate_lar
|
||||
rate_las
|
||||
rate_lau
|
||||
rate_ma2s
|
||||
rate_ma2t
|
||||
rate_ma3s
|
||||
rate_mabl
|
||||
rate_macs
|
||||
rate_mahw
|
||||
rate_mapa
|
||||
rate_mash
|
||||
rate_matd
|
||||
actual_in
|
||||
status
|
||||
ca_bc_pvrt
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
unq_seq
|
||||
line_ind
|
||||
tax_part
|
||||
line_desc
|
||||
prt_dsmk_p
|
||||
prt_dsmk_m
|
||||
part_type
|
||||
oem_partno
|
||||
db_price
|
||||
act_price
|
||||
part_qty
|
||||
mod_lbr_ty
|
||||
db_hrs
|
||||
mod_lb_hrs
|
||||
lbr_op
|
||||
lbr_amt
|
||||
op_code_desc
|
||||
}
|
||||
bills {
|
||||
id
|
||||
federal_tax_rate
|
||||
local_tax_rate
|
||||
state_tax_rate
|
||||
is_credit_memo
|
||||
billlines {
|
||||
actual_cost
|
||||
cost_center
|
||||
id
|
||||
quantity
|
||||
}
|
||||
}
|
||||
timetickets {
|
||||
id
|
||||
rate
|
||||
cost_center
|
||||
actualhrs
|
||||
productivehrs
|
||||
}
|
||||
bodyshop{
|
||||
id
|
||||
md_responsibility_centers
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULTI($ids: [uuid!]!) {
|
||||
jobs( where: {id: {_in: $ids}}) {
|
||||
ro_number
|
||||
clm_total
|
||||
id
|
||||
ded_amt
|
||||
ded_status
|
||||
depreciation_taxes
|
||||
other_amount_payable
|
||||
towing_payable
|
||||
storage_payable
|
||||
adjustment_bottom_line
|
||||
federal_tax_rate
|
||||
state_tax_rate
|
||||
local_tax_rate
|
||||
tax_tow_rt
|
||||
tax_str_rt
|
||||
tax_paint_mat_rt
|
||||
tax_sub_rt
|
||||
tax_lbr_rt
|
||||
tax_levies_rt
|
||||
parts_tax_rates
|
||||
job_totals
|
||||
labor_rate_desc
|
||||
rate_la1
|
||||
rate_la2
|
||||
rate_la3
|
||||
rate_la4
|
||||
rate_laa
|
||||
rate_lab
|
||||
rate_lad
|
||||
rate_lae
|
||||
rate_laf
|
||||
rate_lag
|
||||
rate_lam
|
||||
rate_lar
|
||||
rate_las
|
||||
rate_lau
|
||||
rate_ma2s
|
||||
rate_ma2t
|
||||
rate_ma3s
|
||||
rate_mabl
|
||||
rate_macs
|
||||
rate_mahw
|
||||
rate_mapa
|
||||
rate_mash
|
||||
rate_matd
|
||||
actual_in
|
||||
status
|
||||
ca_bc_pvrt
|
||||
joblines(where: {removed: {_eq: false}}) {
|
||||
id
|
||||
unq_seq
|
||||
line_ind
|
||||
tax_part
|
||||
line_desc
|
||||
prt_dsmk_p
|
||||
prt_dsmk_m
|
||||
part_type
|
||||
oem_partno
|
||||
db_price
|
||||
act_price
|
||||
part_qty
|
||||
mod_lbr_ty
|
||||
db_hrs
|
||||
mod_lb_hrs
|
||||
lbr_op
|
||||
lbr_amt
|
||||
op_code_desc
|
||||
}
|
||||
bills {
|
||||
id
|
||||
federal_tax_rate
|
||||
local_tax_rate
|
||||
state_tax_rate
|
||||
is_credit_memo
|
||||
billlines {
|
||||
actual_cost
|
||||
cost_center
|
||||
id
|
||||
quantity
|
||||
}
|
||||
}
|
||||
timetickets {
|
||||
id
|
||||
rate
|
||||
cost_center
|
||||
actualhrs
|
||||
productivehrs
|
||||
}
|
||||
bodyshop {
|
||||
id
|
||||
md_responsibility_centers
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
241
server/job/job-costing.js
Normal file
241
server/job/job-costing.js
Normal file
@@ -0,0 +1,241 @@
|
||||
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;
|
||||
const {
|
||||
ExportCustomJobInstance,
|
||||
} = require("twilio/lib/rest/bulkexports/v1/export/exportCustomJob");
|
||||
|
||||
// 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;
|
||||
@@ -1,2 +1,4 @@
|
||||
exports.totals = require("./job-totals").default;
|
||||
exports.totalsSsu = require("./job-totals").totalsSsu;
|
||||
exports.costing = require("./job-costing").JobCosting;
|
||||
exports.costingmulti = require("./job-costing").JobCostingMulti;
|
||||
|
||||
Reference in New Issue
Block a user