@@ -204,7 +30,3 @@ export function JobCostingModalComponent({ bodyshop, job }) {
);
}
-export default connect(
- mapStateToProps,
- mapDispatchToProps
-)(JobCostingModalComponent);
diff --git a/client/src/components/job-costing-modal/job-costing-modal.container.jsx b/client/src/components/job-costing-modal/job-costing-modal.container.jsx
index ace532ba4..8a1c1dc40 100644
--- a/client/src/components/job-costing-modal/job-costing-modal.container.jsx
+++ b/client/src/components/job-costing-modal/job-costing-modal.container.jsx
@@ -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 (
- {error ? : null}
- {loading ? (
-
+ {!costingData ? (
+
) : (
-
+
)}
);
diff --git a/client/src/components/job-costing-modal/job-costing-modal.pie.component.jsx b/client/src/components/job-costing-modal/job-costing-modal.pie.component.jsx
index d4dbe2c30..8b483d113 100644
--- a/client/src/components/job-costing-modal/job-costing-modal.pie.component.jsx
+++ b/client/src/components/job-costing-modal/job-costing-modal.pie.component.jsx
@@ -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;
diff --git a/client/src/components/job-costing-parts-table/job-costing-parts-table.component.jsx b/client/src/components/job-costing-parts-table/job-costing-parts-table.component.jsx
index 3f7e576ec..acc81ef91 100644
--- a/client/src/components/job-costing-parts-table/job-costing-parts-table.component.jsx
+++ b/client/src/components/job-costing-parts-table/job-costing-parts-table.component.jsx
@@ -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 }) {
{
return (
-
-
- {
- e.preventDefault();
- setSearchText(e.target.value);
- }}
- />
-
-
+
+ {
+ e.preventDefault();
+ setSearchText(e.target.value);
+ }}
+ />
+
);
}}
scroll={{ x: "50%", y: "40rem" }}
@@ -103,13 +101,13 @@ export default function JobCostingPartsTable({ job, data, summaryData }) {
- {summaryData.totalSales.toFormat()}
+ {Dinero(summaryData.totalSales).toFormat()}
- {summaryData.totalCost.toFormat()}
+ {Dinero(summaryData.totalCost).toFormat()}
- {summaryData.gpdollars.toFormat()}
+ {Dinero(summaryData.gpdollars).toFormat()}
diff --git a/client/src/components/job-costing-statistics/job-costing-statistics.component.jsx b/client/src/components/job-costing-statistics/job-costing-statistics.component.jsx
index 730fc3d76..2eac049a1 100644
--- a/client/src/components/job-costing-statistics/job-costing-statistics.component.jsx
+++ b/client/src/components/job-costing-statistics/job-costing-statistics.component.jsx
@@ -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 (
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
diff --git a/client/src/components/job-totals-table/job-totals.table.labor.component.jsx b/client/src/components/job-totals-table/job-totals.table.labor.component.jsx
index 47a4a9969..55b41aaf6 100644
--- a/client/src/components/job-totals-table/job-totals.table.labor.component.jsx
+++ b/client/src/components/job-totals-table/job-totals.table.labor.component.jsx
@@ -103,11 +103,11 @@ export default function JobTotalsTableLabor({ job }) {
<>
- {t("jobs.labels.labor_rates_subtotal")}
+ {t("jobs.labels.labor_rates_subtotal")}
-
+
{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}
@@ -121,7 +121,7 @@ export default function JobTotalsTableLabor({ job }) {
{job.job_totals.rates.mapa.hours.toFixed(2)}
-
+
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
@@ -133,19 +133,19 @@ export default function JobTotalsTableLabor({ job }) {
{job.job_totals.rates.mash.hours.toFixed(2)}
-
+
{Dinero(job.job_totals.rates.mash.total).toFormat()}
- {t("jobs.labels.labor_rates_subtotal")}
+ {t("jobs.labels.rates_subtotal")}
-
+
- {Dinero(job.job_totals.rates.rates_subtotal).toFormat()}
+ {Dinero(job.job_totals.rates.subtotal).toFormat()}
diff --git a/client/src/components/job-totals-table/job-totals.table.other.component.jsx b/client/src/components/job-totals-table/job-totals.table.other.component.jsx
index 00925007c..c06cd7232 100644
--- a/client/src/components/job-totals-table/job-totals.table.other.component.jsx
+++ b/client/src/components/job-totals-table/job-totals.table.other.component.jsx
@@ -79,7 +79,7 @@ export default function JobTotalsTableOther({ job }) {
<>
- {t("jobs.labels.additionaltotal")}
+ {t("jobs.labels.additionaltotal")}
@@ -90,7 +90,7 @@ export default function JobTotalsTableOther({ job }) {
- {t("jobs.labels.subletstotal")}
+ {t("jobs.labels.subletstotal")}
diff --git a/client/src/components/job-totals-table/job-totals.table.parts.component.jsx b/client/src/components/job-totals-table/job-totals.table.parts.component.jsx
index ca1eef5b3..d71e033b4 100644
--- a/client/src/components/job-totals-table/job-totals.table.parts.component.jsx
+++ b/client/src/components/job-totals-table/job-totals.table.parts.component.jsx
@@ -70,7 +70,9 @@ export default function JobTotalsTableParts({ job }) {
}}
summary={() => (
- {t("jobs.labels.partstotal")}
+
+ {t("jobs.labels.partstotal")}
+
diff --git a/client/src/components/jobs-documents-gallery/jobs-document-gallery.reassign.component.jsx b/client/src/components/jobs-documents-gallery/jobs-document-gallery.reassign.component.jsx
index 58c847cdf..d56c733c9 100644
--- a/client/src/components/jobs-documents-gallery/jobs-document-gallery.reassign.component.jsx
+++ b/client/src/components/jobs-documents-gallery/jobs-document-gallery.reassign.component.jsx
@@ -1,12 +1,29 @@
-import { Button, Form, Popover, notification, Space } from "antd";
-import React, { useState, useMemo } from "react";
-import { useTranslation } from "react-i18next";
-import JobSearchSelect from "../job-search-select/job-search-select.component";
-import { useMutation } from "@apollo/client";
-import { UPDATE_DOCUMENT } from "../../graphql/documents.queries";
+import { useApolloClient, useMutation } from "@apollo/client";
+import { Button, Form, notification, Popover, Space } from "antd";
import axios from "axios";
+import React, { useMemo, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import {
+ GET_DOC_SIZE_BY_JOB,
+ UPDATE_DOCUMENT,
+} from "../../graphql/documents.queries";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+import JobSearchSelect from "../job-search-select/job-search-select.component";
-export default function JobsDocumentsGalleryReassign({ galleryImages }) {
+const mapStateToProps = createStructuredSelector({
+ bodyshop: selectBodyshop,
+});
+const mapDispatchToProps = (dispatch) => ({
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
+});
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(JobsDocumentsGalleryReassign);
+
+export function JobsDocumentsGalleryReassign({ bodyshop, galleryImages }) {
const { t } = useTranslation();
const [form] = Form.useForm();
@@ -16,7 +33,7 @@ export default function JobsDocumentsGalleryReassign({ galleryImages }) {
...galleryImages.other.filter((image) => image.isSelected),
];
}, [galleryImages]);
-
+ const client = useApolloClient();
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [updateDocument] = useMutation(UPDATE_DOCUMENT);
@@ -48,10 +65,37 @@ export default function JobsDocumentsGalleryReassign({ galleryImages }) {
});
}
};
-
+ console.log("selectedImages :>> ", selectedImages);
const handleFinish = async ({ jobid }) => {
setLoading(true);
+ //Check to see if the space remaining on the new job is sufficient. If it isn't cancel this.
+ const newJobData = await client.query({
+ query: GET_DOC_SIZE_BY_JOB,
+ variables: { jobId: jobid },
+ });
+
+ const transferedDocSizeTotal = selectedImages.reduce(
+ (acc, val) => acc + val.size,
+ 0
+ );
+
+ const shouldPreventTransfer =
+ bodyshop.jobsizelimit -
+ newJobData.data.documents_aggregate.aggregate.sum.size <
+ transferedDocSizeTotal;
+
+ if (shouldPreventTransfer) {
+ notification.open({
+ key: "cannotuploaddocuments",
+ type: "error",
+ message: t("documents.labels.reassign_limitexceeded_title"),
+ description: t("documents.labels.reassign_limitexceeded"),
+ });
+ setLoading(false);
+ return;
+ }
+
const res = await axios.post("/media/rename", {
documents: selectedImages.map((i) => {
//Need to check if the current key folder is null, or another job.
diff --git a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.component.jsx b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.component.jsx
index dc8bede19..9d017c71f 100644
--- a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.component.jsx
+++ b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.component.jsx
@@ -18,6 +18,7 @@ function JobsDocumentsComponent({
billsCallback,
totalSize,
bodyshop,
+ ignoreSizeLimit,
}) {
const [galleryImages, setgalleryImages] = useState({ images: [], other: [] });
const { t } = useTranslation();
@@ -43,6 +44,7 @@ function JobsDocumentsComponent({
extension: value.extension,
id: value.id,
type: value.type,
+ size: value.size,
tags: [{ value: value.type, title: value.type }],
});
} else {
@@ -95,6 +97,7 @@ function JobsDocumentsComponent({
key: value.key,
id: value.id,
type: value.type,
+ size: value.size,
});
}
@@ -131,6 +134,7 @@ function JobsDocumentsComponent({
totalSize={totalSize}
billId={billId}
callbackAfterUpload={billsCallback || refetch}
+ ignoreSizeLimit={ignoreSizeLimit}
/>
diff --git a/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx b/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx
index d6d782556..a36c9a856 100644
--- a/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx
+++ b/client/src/components/tech-job-clock-out-button/tech-job-clock-out-button.component.jsx
@@ -31,17 +31,10 @@ export function TechClockOffButton({
const { t } = useTranslation();
const emps = bodyshop.employees.filter(
- (e) => e.id === technician && technician.id
+ (e) => e.id === (technician && technician.id)
)[0];
- console.log(
- "emps :>> ",
- emps,
- "e",
- bodyshop && bodyshop.employees,
- "tech",
- technician
- );
+ console.log(emps && emps.rates);
const handleFinish = async (values) => {
logImEXEvent("tech_clock_out_job");
diff --git a/client/src/graphql/bills.queries.js b/client/src/graphql/bills.queries.js
index 5bb4f0099..69648c933 100644
--- a/client/src/graphql/bills.queries.js
+++ b/client/src/graphql/bills.queries.js
@@ -167,6 +167,7 @@ export const QUERY_BILL_BY_PK = gql`
key
name
type
+ size
}
}
}
diff --git a/client/src/graphql/documents.queries.js b/client/src/graphql/documents.queries.js
index 3903f1d2f..443025ce9 100644
--- a/client/src/graphql/documents.queries.js
+++ b/client/src/graphql/documents.queries.js
@@ -14,6 +14,7 @@ export const GET_DOCUMENTS_BY_JOB = gql`
name
key
type
+ size
bill {
id
invoice_number
@@ -27,6 +28,18 @@ export const GET_DOCUMENTS_BY_JOB = gql`
}
`;
+export const GET_DOC_SIZE_BY_JOB = gql`
+ query GET_DOC_SIZE_BY_JOB($jobId: uuid!) {
+ documents_aggregate(where: { jobid: { _eq: $jobId } }) {
+ aggregate {
+ sum {
+ size
+ }
+ }
+ }
+ }
+`;
+
export const INSERT_NEW_DOCUMENT = gql`
mutation INSERT_NEW_DOCUMENT($docInput: [documents_insert_input!]!) {
insert_documents(objects: $docInput) {
@@ -34,6 +47,7 @@ export const INSERT_NEW_DOCUMENT = gql`
id
name
key
+ size
}
}
}
@@ -60,6 +74,7 @@ export const QUERY_TEMPORARY_DOCS = gql`
key
type
extension
+ size
}
}
`;
@@ -75,6 +90,7 @@ export const UPDATE_DOCUMENT = gql`
name
type
key
+ size
}
}
`;
diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js
index 148938192..6dc3dc82f 100644
--- a/client/src/graphql/jobs.queries.js
+++ b/client/src/graphql/jobs.queries.js
@@ -811,6 +811,7 @@ export const QUERY_TECH_JOB_DETAILS = gql`
documents(order_by: { created_at: desc }) {
id
key
+ size
}
}
}
diff --git a/client/src/pages/temporary-docs/temporary-docs.component.jsx b/client/src/pages/temporary-docs/temporary-docs.component.jsx
index d54898375..bec1287d9 100644
--- a/client/src/pages/temporary-docs/temporary-docs.component.jsx
+++ b/client/src/pages/temporary-docs/temporary-docs.component.jsx
@@ -17,6 +17,7 @@ export default function TemporaryDocsComponent() {
jobId={null}
billId={null}
refetch={refetch}
+ ignoreSizeLimit
/>
);
}
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index 6830d07e2..ad3484ac4 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -658,11 +658,13 @@
"confirmdelete": "Are you sure you want to delete these documents. This CANNOT be undone.",
"doctype": "Document Type",
"newjobid": "Assign to Job",
+ "reassign_limitexceeded": "Reassigning all selected documents will exceed the job storage limit for your shop. ",
+ "reassign_limitexceeded_title": "Unable to reassign document(s)",
"storageexceeded": "You've exceeded your storage limit for this job. Please remove documents, or increase your storage plan.",
"storageexceeded_title": "Storage Limit Exceeded",
"upload": "Upload",
- "upload_limitexceeded": "Uploading all selected files will exceed the job storage limit for your shop. ",
- "upload_limitexceeded_title": "Unable to upload file(s)",
+ "upload_limitexceeded": "Uploading all selected documents will exceed the job storage limit for your shop. ",
+ "upload_limitexceeded_title": "Unable to upload document(s)",
"usage": "of job storage used. ({{used}} / {{total}})"
},
"successes": {
@@ -1703,6 +1705,7 @@
"vendor": "Vendor"
},
"templates": {
+ "credits_not_received_date": "Credits not Received by Date",
"estimator_detail": "Jobs by Estimator (Detail)",
"estimator_summary": "Jobs by Estimator (Summary)",
"hours_sold_detail_closed": "Hours Sold Detail - Closed",
@@ -1719,6 +1722,7 @@
"purchases_by_vendor_detailed_date_range": "Purchases By Vendor - Detailed",
"purchases_by_vendor_summary_date_range": "Purchases by Vendor - Summary",
"schedule": "Appointment Schedule",
+ "supplement_ratio_source": "Supplement Ratio by Source",
"timetickets": "Time Tickets",
"timetickets_employee": "Employee Time Tickets",
"timetickets_summary": "Time Tickets Summary"
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index e12ada3d4..4d77ac2f3 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -658,6 +658,8 @@
"confirmdelete": "",
"doctype": "",
"newjobid": "",
+ "reassign_limitexceeded": "",
+ "reassign_limitexceeded_title": "",
"storageexceeded": "",
"storageexceeded_title": "",
"upload": "Subir",
@@ -1703,6 +1705,7 @@
"vendor": ""
},
"templates": {
+ "credits_not_received_date": "",
"estimator_detail": "",
"estimator_summary": "",
"hours_sold_detail_closed": "",
@@ -1719,6 +1722,7 @@
"purchases_by_vendor_detailed_date_range": "",
"purchases_by_vendor_summary_date_range": "",
"schedule": "",
+ "supplement_ratio_source": "",
"timetickets": "",
"timetickets_employee": "",
"timetickets_summary": ""
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 9e0e9f0c5..c308bc2f7 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -658,6 +658,8 @@
"confirmdelete": "",
"doctype": "",
"newjobid": "",
+ "reassign_limitexceeded": "",
+ "reassign_limitexceeded_title": "",
"storageexceeded": "",
"storageexceeded_title": "",
"upload": "Télécharger",
@@ -1703,6 +1705,7 @@
"vendor": ""
},
"templates": {
+ "credits_not_received_date": "",
"estimator_detail": "",
"estimator_summary": "",
"hours_sold_detail_closed": "",
@@ -1719,6 +1722,7 @@
"purchases_by_vendor_detailed_date_range": "",
"purchases_by_vendor_summary_date_range": "",
"schedule": "",
+ "supplement_ratio_source": "",
"timetickets": "",
"timetickets_employee": "",
"timetickets_summary": ""
diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js
index 6ba044339..1a6b49802 100644
--- a/client/src/utils/TemplateConstants.js
+++ b/client/src/utils/TemplateConstants.js
@@ -511,6 +511,22 @@ export const TemplateList = (type, context) => {
//idtype: "vendor",
disabled: false,
},
+ supplement_ratio_source: {
+ title: i18n.t("reportcenter.templates.supplement_ratio_source"),
+ description: "",
+ subject: i18n.t("reportcenter.templates.supplement_ratio_source"),
+ key: "supplement_ratio_source",
+ //idtype: "vendor",
+ disabled: false,
+ },
+ credits_not_received_date: {
+ title: i18n.t("reportcenter.templates.credits_not_received_date"),
+ description: "",
+ subject: i18n.t("reportcenter.templates.credits_not_received_date"),
+ key: "credits_not_received_date",
+ //idtype: "vendor",
+ disabled: false,
+ },
}
: {}),
...(!type || type === "courtesycarcontract"
diff --git a/server.js b/server.js
index 5cda0bf8b..5d4c4e4c1 100644
--- a/server.js
+++ b/server.js
@@ -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");
diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js
index e586cf0d6..4112bd830 100644
--- a/server/graphql-client/queries.js
+++ b/server/graphql-client/queries.js
@@ -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
+ }
+ }
+}
+`;
diff --git a/server/job/job-costing.js b/server/job/job-costing.js
new file mode 100644
index 000000000..5d7fba619
--- /dev/null
+++ b/server/job/job-costing.js
@@ -0,0 +1,238 @@
+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;
diff --git a/server/job/job.js b/server/job/job.js
index b41c1dab0..975342045 100644
--- a/server/job/job.js
+++ b/server/job/job.js
@@ -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;