From 4779964449c17659522cf9494035ce314c139291 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Tue, 6 Apr 2021 13:54:47 -0700 Subject: [PATCH] IO-836 Server Side Job Costing --- .../job-costing-modal.component.jsx | 190 +------------- .../job-costing-modal.container.jsx | 37 ++- .../job-costing-modal.pie.component.jsx | 14 +- .../job-costing-parts-table.component.jsx | 34 ++- .../job-costing-statistics.component.jsx | 18 +- server.js | 2 + server/graphql-client/queries.js | 195 ++++++++++++++ server/job/job-costing.js | 241 ++++++++++++++++++ server/job/job.js | 2 + 9 files changed, 502 insertions(+), 231 deletions(-) create mode 100644 server/job/job-costing.js diff --git a/client/src/components/job-costing-modal/job-costing-modal.component.jsx b/client/src/components/job-costing-modal/job-costing-modal.component.jsx index 24d277ca2..499f0d253 100644 --- a/client/src/components/job-costing-modal/job-costing-modal.component.jsx +++ b/client/src/components/job-costing-modal/job-costing-modal.component.jsx @@ -1,194 +1,20 @@ import { Typography } from "antd"; -import Dinero from "dinero.js"; import React from "react"; import { useTranslation } from "react-i18next"; -import { connect } from "react-redux"; -import { createStructuredSelector } from "reselect"; -import { selectBodyshop } from "../../redux/user/user.selectors"; import JobCostingPartsTable from "../job-costing-parts-table/job-costing-parts-table.component"; import JobCostingStatistics from "../job-costing-statistics/job-costing-statistics.component"; import JobCostingPie from "./job-costing-modal.pie.component"; -import _ from "lodash"; -const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, -}); -const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) -}); -export function JobCostingModalComponent({ bodyshop, job }) { - const defaultProfits = bodyshop.md_responsibility_centers.defaults.profits; - const allProfitCenters = _.union( - bodyshop.md_responsibility_centers.profits.map((p) => p.name), - bodyshop.md_responsibility_centers.costs.map((p) => p.name) - ); - - // const defaultCosts = bodyshop.md_responsibility_centers.defaults.costs; +export default function JobCostingModalComponent({ + summaryData, + costCenterData, +}) { const { t } = useTranslation(); - 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 (