IO-836 Server Side Job Costing

This commit is contained in:
Patrick Fic
2021-04-06 13:54:47 -07:00
parent 9af6311d52
commit 4779964449
9 changed files with 502 additions and 231 deletions

View File

@@ -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 (
<div>
<JobCostingStatistics job={job} summaryData={summaryData} />
<JobCostingPartsTable
job={job}
data={costCenterData}
summaryData={summaryData}
/>
<JobCostingStatistics summaryData={summaryData} />
<JobCostingPartsTable data={costCenterData} summaryData={summaryData} />
<div className="imex-flex-row">
<div style={{ flex: 1 }}>
<Typography.Title level={4}>
@@ -204,7 +30,3 @@ export function JobCostingModalComponent({ bodyshop, job }) {
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobCostingModalComponent);

View File

@@ -1,13 +1,11 @@
import { useQuery } from "@apollo/client";
import { Modal } from "antd";
import React from "react";
import axios from "axios";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_JOB_COSTING_DETAILS } from "../../graphql/jobs.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectJobCosting } from "../../redux/modals/modals.selectors";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import JobCostingModalComponent from "./job-costing-modal.component";
@@ -24,14 +22,25 @@ export function JobCostingModalContainer({
toggleModalVisible,
}) {
const { t } = useTranslation();
const [costingData, setCostingData] = useState(null);
const { visible, context } = jobCostingModal;
const { jobId } = context;
const { loading, error, data } = useQuery(QUERY_JOB_COSTING_DETAILS, {
variables: { id: jobId },
skip: !jobId,
});
// const { loading, error, data } = useQuery(QUERY_JOB_COSTING_DETAILS, {
// variables: { id: jobId },
// skip: !jobId,
// });
useEffect(() => {
async function getData() {
if (jobId && visible) {
const { data } = await axios.post("/job/costing", { jobid: jobId });
console.log(data);
setCostingData(data);
}
}
getData();
}, [jobId, visible]);
return (
<Modal
@@ -43,11 +52,13 @@ export function JobCostingModalContainer({
width="90%"
destroyOnClose
>
{error ? <AlertComponent message={error.message} type="error" /> : null}
{loading ? (
<LoadingSpinner loading={loading} />
{!costingData ? (
<LoadingSpinner loading={true} />
) : (
<JobCostingModalComponent job={data && data.jobs_by_pk} />
<JobCostingModalComponent
costCenterData={costingData.costCenterData}
summaryData={costingData.summaryData}
/>
)}
</Modal>
);

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useMemo } from "react";
import { Cell, Pie, PieChart, ResponsiveContainer } from "recharts";
import Dinero from "dinero.js";
export default function JobCostingPieComponent({
type = "sales",
costCenterData,
@@ -11,8 +11,8 @@ export default function JobCostingPieComponent({
return data.reduce((acc, i) => {
const value =
type === "sales"
? i.sales_dinero.getAmount()
: i.costs_dinero.getAmount();
? Dinero(i.sales_dinero).getAmount()
: Dinero(i.costs_dinero).getAmount();
if (value > 0) {
acc.push({
@@ -21,13 +21,13 @@ export default function JobCostingPieComponent({
label: `${i.cost_center} - ${
type === "sales"
? i.sales_dinero.toFormat()
: i.costs_dinero.toFormat()
? Dinero(i.sales_dinero).toFormat()
: Dinero(i.costs_dinero).toFormat()
}`,
value:
type === "sales"
? i.sales_dinero.getAmount()
: i.costs_dinero.getAmount(),
? Dinero(i.sales_dinero).getAmount()
: Dinero(i.costs_dinero).getAmount(),
});
}
return acc;

View File

@@ -1,9 +1,9 @@
import { Input, Table, Typography } from "antd";
import { Input, Space, Table, Typography } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
export default function JobCostingPartsTable({ job, data, summaryData }) {
import Dinero from "dinero.js";
export default function JobCostingPartsTable({ data, summaryData }) {
const [searchText, setSearchText] = useState("");
const [state, setState] = useState({
sortedInfo: {},
@@ -75,18 +75,16 @@ export default function JobCostingPartsTable({ job, data, summaryData }) {
<Table
title={() => {
return (
<div className="imex-table-header">
<div className="imex-table-header__search">
<Input.Search
placeholder={t("general.labels.search")}
value={searchText}
onChange={(e) => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
</div>
</div>
<Space wrap>
<Input.Search
placeholder={t("general.labels.search")}
value={searchText}
onChange={(e) => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
</Space>
);
}}
scroll={{ x: "50%", y: "40rem" }}
@@ -103,13 +101,13 @@ export default function JobCostingPartsTable({ job, data, summaryData }) {
</Typography.Title>
</Table.Summary.Cell>
<Table.Summary.Cell>
{summaryData.totalSales.toFormat()}
{Dinero(summaryData.totalSales).toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell>
{summaryData.totalCost.toFormat()}
{Dinero(summaryData.totalCost).toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell>
{summaryData.gpdollars.toFormat()}
{Dinero(summaryData.gpdollars).toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell></Table.Summary.Cell>
</Table.Summary.Row>

View File

@@ -1,39 +1,39 @@
import { Statistic } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
export default function JobCostingStatistics({ job, summaryData }) {
import Dinero from "dinero.js";
export default function JobCostingStatistics({ summaryData }) {
const { t } = useTranslation();
return (
<div>
<div className="imex-flex-row imex-flex-row__flex-space-around">
<Statistic
value={summaryData.totalLaborSales.toFormat()}
value={Dinero(summaryData.totalLaborSales).toFormat()}
title={t("jobs.labels.sale_labor")}
/>
<Statistic
value={summaryData.totalPartsSales.toFormat()}
value={Dinero(summaryData.totalPartsSales).toFormat()}
title={t("jobs.labels.sale_parts")}
/>
<Statistic
value={summaryData.totalSales.toFormat()}
value={Dinero(summaryData.totalSales).toFormat()}
title={t("jobs.labels.total_sales")}
/>
<Statistic
value={summaryData.totalLaborCost.toFormat()}
value={Dinero(summaryData.totalLaborCost).toFormat()}
title={t("jobs.labels.cost_labor")}
/>
<Statistic
value={summaryData.totalPartsCost.toFormat()}
value={Dinero(summaryData.totalPartsCost).toFormat()}
title={t("jobs.labels.cost_parts")}
/>
<Statistic
value={summaryData.totalCost.toFormat()}
value={Dinero(summaryData.totalCost).toFormat()}
title={t("jobs.labels.total_cost")}
/>
<Statistic
value={summaryData.gpdollars.toFormat()}
value={Dinero(summaryData.gpdollars).toFormat()}
title={t("jobs.labels.gpdollars")}
/>
<Statistic

View File

@@ -94,6 +94,8 @@ app.post(
var job = require("./server/job/job");
app.post("/job/totals", fb.validateFirebaseIdToken, job.totals);
app.post("/job/totalsssu", fb.validateFirebaseIdToken, job.totalsSsu);
app.post("/job/costing", fb.validateFirebaseIdToken, job.costing);
app.post("/job/costingmulti", fb.validateFirebaseIdToken, job.costingmulti);
//Scheduling
var scheduling = require("./server/scheduling/scheduling-job");

View File

@@ -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
View 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;

View File

@@ -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;