diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel
index a77e7e73f..b94eb957c 100644
--- a/bodyshop_translations.babel
+++ b/bodyshop_translations.babel
@@ -25204,6 +25204,27 @@
+
+ cost_sublet
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
costs
false
@@ -27119,6 +27140,27 @@
+
+ sale_sublet
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
sales
false
@@ -35691,6 +35733,27 @@
+
+ exported_payroll
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
diff --git a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx
index 000f56a3a..a720968b9 100644
--- a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx
+++ b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx
@@ -97,7 +97,9 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
render: (text, record) =>
record.cccontracts.length === 1 ? (
- {record.cccontracts[0].job.ro_number}
+ {`${record.cccontracts[0].job.ro_number} - ${
+ record.cccontracts[0].job.ownr_fn || ""
+ } ${record.cccontracts[0].job.ownr_ln || ""} ${record.cccontracts[0].job.ownr_co_nm || ""}`}
) : null,
},
diff --git a/client/src/components/form-date-picker/form-date-picker.component.jsx b/client/src/components/form-date-picker/form-date-picker.component.jsx
index 3a9fe2553..e9ace8082 100644
--- a/client/src/components/form-date-picker/form-date-picker.component.jsx
+++ b/client/src/components/form-date-picker/form-date-picker.component.jsx
@@ -23,20 +23,21 @@ export function FormDatePicker({
onChange,
onBlur,
onlyFuture,
+ isDateOnly = true,
...restProps
}) {
const ref = useRef();
const handleChange = (newDate) => {
if (value !== newDate && onChange) {
- onChange(newDate);
+ onChange(isDateOnly ? newDate && newDate.format("YYYY-MM-DD") : newDate);
}
};
const handleKeyDown = (e) => {
if (e.key.toLowerCase() === "t") {
if (onChange) {
- onChange(moment());
+ onChange(isDateOnly ? moment().format("YYYY-MM-DD") : moment());
// if (ref.current && ref.current.blur) ref.current.blur();
}
} else if (e.key.toLowerCase() === "enter") {
@@ -64,7 +65,8 @@ export function FormDatePicker({
});
}
- if (_a.isValid() && onChange) onChange(_a);
+ if (_a.isValid() && onChange)
+ onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a);
};
return (
diff --git a/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx b/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx
index 34131d479..79b01c529 100644
--- a/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx
+++ b/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx
@@ -26,19 +26,20 @@ const DateTimePicker = (
value={value}
onBlur={onBlur}
onChange={onChange}
+ isDateOnly={false}
/>
moment().isAfter(d),
- })}
- onChange={onChange}
- showSecond={false}
- minuteStep={15}
- onBlur={onBlur}
- format="hh:mm a"
+ value={value ? moment(value) : null}
+ {...(onlyFuture && {
+ disabledDate: (d) => moment().isAfter(d),
+ })}
+ onChange={onChange}
+ showSecond={false}
+ minuteStep={15}
+ onBlur={onBlur}
+ format="hh:mm a"
+ {...restProps}
/>
);
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 fe73a76ac..c92d3848c 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
@@ -16,6 +16,10 @@ export default function JobCostingStatistics({ summaryData }) {
value={Dinero(summaryData.totalPartsSales).toFormat()}
title={t("jobs.labels.sale_parts")}
/>
+
+
({
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
+});
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(JobTotalsTableTotals);
+
+export function JobTotalsTableTotals({ bodyshop, job }) {
const { t } = useTranslation();
const data = useMemo(() => {
@@ -21,6 +36,14 @@ export default function JobTotalsTableTotals({ job }) {
key: t("jobs.labels.state_tax_amt"),
total: job.job_totals.totals.state_tax,
},
+ ...(bodyshop.region_config === "CA_BC"
+ ? [
+ {
+ key: t("jobs.fields.ca_bc_pvrt"),
+ total: job.job_totals.additional.pvrt,
+ },
+ ]
+ : []),
{
key: t("jobs.labels.federal_tax_amt"),
total: job.job_totals.totals.federal_tax,
@@ -58,7 +81,7 @@ export default function JobTotalsTableTotals({ job }) {
bold: true,
},
];
- }, [job.job_totals, t]);
+ }, [job.job_totals, t, bodyshop.region_config]);
const columns = [
{
diff --git a/client/src/components/production-list-columns/production-list-columns.data.js b/client/src/components/production-list-columns/production-list-columns.data.js
index 240d8074b..cda6fd57a 100644
--- a/client/src/components/production-list-columns/production-list-columns.data.js
+++ b/client/src/components/production-list-columns/production-list-columns.data.js
@@ -96,7 +96,7 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
sortOrder:
state.sortedInfo.columnKey === "actual_in" && state.sortedInfo.order,
render: (text, record) => (
-
+
),
},
{
@@ -114,6 +114,7 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
record={record}
field="scheduled_completion"
pastIndicator
+ time
/>
),
},
@@ -165,6 +166,7 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
record={record}
field="scheduled_delivery"
pastIndicator
+ time
/>
),
},
diff --git a/client/src/components/production-list-columns/production-list-columns.date.component.jsx b/client/src/components/production-list-columns/production-list-columns.date.component.jsx
index 94ca00c88..5927c99e2 100644
--- a/client/src/components/production-list-columns/production-list-columns.date.component.jsx
+++ b/client/src/components/production-list-columns/production-list-columns.date.component.jsx
@@ -20,9 +20,9 @@ export default function ProductionListDate({
const handleChange = (date) => {
logImEXEvent("product_toggle_date", { field });
- if (date.isSame(record[field] && moment(record[field]))) {
- return;
- }
+ // if (date.isSame(record[field] && moment(record[field]))) {
+ // return;
+ // }
//e.stopPropagation();
updateAlert({
@@ -67,12 +67,14 @@ export default function ProductionListDate({
value={(record[field] && moment(record[field])) || null}
onChange={handleChange}
format="MM/DD/YYYY"
+ isDateOnly={!time}
/>
{time && (
e.stopPropagation()}
value={(record[field] && moment(record[field])) || null}
onChange={handleChange}
+ minuteStep={15}
format="hh:mm a"
/>
)}
diff --git a/client/src/components/production-list-detail/production-list-detail.component.jsx b/client/src/components/production-list-detail/production-list-detail.component.jsx
index 1ea6c49d0..9494d5fb2 100644
--- a/client/src/components/production-list-detail/production-list-detail.component.jsx
+++ b/client/src/components/production-list-detail/production-list-detail.component.jsx
@@ -1,5 +1,5 @@
import { useQuery } from "@apollo/client";
-import { Descriptions, Drawer, Space } from "antd";
+import { Descriptions, Drawer, Space, PageHeader, Button } from "antd";
import queryString from "query-string";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -16,8 +16,25 @@ import JobEmployeeAssignments from "../job-employee-assignments/job-employee-ass
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import ProductionRemoveButton from "../production-remove-button/production-remove-button.component";
import JobAtChange from "../job-at-change/job-at-change.component";
+import { PrinterFilled } from "@ant-design/icons";
-export default function ProductionListDetail({ jobs }) {
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { setModalContext } from "../../redux/modals/modals.actions";
+
+const mapStateToProps = createStructuredSelector({
+ //currentUser: selectCurrentUser
+});
+const mapDispatchToProps = (dispatch) => ({
+ setPrintCenterContext: (context) =>
+ dispatch(setModalContext({ context: context, modal: "printCenter" })),
+});
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(ProductionListDetail);
+
+export function ProductionListDetail({ jobs, setPrintCenterContext }) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const { selected } = search;
@@ -39,11 +56,29 @@ export default function ProductionListDetail({ jobs }) {
return (
- {t("production.labels.jobdetail")}
- {theJob.ro_number}
-
-
+
+ {" "}
+
+
+ }
+ />
}
placement="right"
width={"33%"}
diff --git a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx
index 9fc42ea67..6c0673ca3 100644
--- a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx
+++ b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx
@@ -101,7 +101,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
@@ -116,7 +116,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
@@ -140,7 +140,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
@@ -152,7 +152,7 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
/>
-
+
-
+
-
+
diff --git a/client/src/components/time-ticket-list/time-ticket-list.component.jsx b/client/src/components/time-ticket-list/time-ticket-list.component.jsx
index 46e9462c3..40af8ca48 100644
--- a/client/src/components/time-ticket-list/time-ticket-list.component.jsx
+++ b/client/src/components/time-ticket-list/time-ticket-list.component.jsx
@@ -76,6 +76,21 @@ export function TimeTicketList({
state.sortedInfo.columnKey === "employee" && state.sortedInfo.order,
render: (text, record) =>
`${record.employee.first_name} ${record.employee.last_name}`,
+ filters:
+ timetickets
+ .map((l) => l.employeeid)
+ .filter(onlyUnique)
+ .map((s) => {
+ return {
+ text: (() => {
+ const emp = bodyshop.employees.find((e) => e.id === s);
+
+ return `${emp.first_name} ${emp.last_name}`;
+ })(), //
+ value: [s],
+ };
+ }) || [],
+ onFilter: (value, record) => value.includes(record.employeeid),
},
{
title: t("timetickets.fields.cost_center"),
diff --git a/client/src/components/time-ticket-modal/time-ticket-modal.component.jsx b/client/src/components/time-ticket-modal/time-ticket-modal.component.jsx
index cc12ce503..83087d374 100644
--- a/client/src/components/time-ticket-modal/time-ticket-modal.component.jsx
+++ b/client/src/components/time-ticket-modal/time-ticket-modal.component.jsx
@@ -159,8 +159,10 @@ export function TimeTicketModalComponent({
name="flat_rate"
label={t("timetickets.fields.flat_rate")}
valuePropName="checked"
+ noStyle
+ style={{ display: "none" }}
>
-
+
@@ -212,6 +214,7 @@ export function TimeTicketModalComponent({
<>
{
+ setLoading(true);
+
+ await GenerateDocument(
+ {
+ name: PayrollTemplate.key,
+ variables: {
+ start: start
+ ? start
+ : moment().startOf("week").subtract(7, "days").format("YYYY-MM-DD"),
+ end: end ? end : moment().endOf("week").format("YYYY-MM-DD"),
+ },
+ },
+ {},
+ "x"
+ );
+
+ setLoading(false);
+ };
+
+ return (
+
+ );
+}
diff --git a/client/src/graphql/courtesy-car.queries.js b/client/src/graphql/courtesy-car.queries.js
index ddf4e41e7..efb24b2c1 100644
--- a/client/src/graphql/courtesy-car.queries.js
+++ b/client/src/graphql/courtesy-car.queries.js
@@ -89,6 +89,9 @@ export const QUERY_ALL_CC = gql`
job {
id
ro_number
+ ownr_fn
+ ownr_ln
+ ownr_co_nm
}
}
}
diff --git a/client/src/pages/time-tickets/time-tickets.container.jsx b/client/src/pages/time-tickets/time-tickets.container.jsx
index c5bbbccbd..222be5117 100644
--- a/client/src/pages/time-tickets/time-tickets.container.jsx
+++ b/client/src/pages/time-tickets/time-tickets.container.jsx
@@ -1,5 +1,5 @@
import { useQuery } from "@apollo/client";
-import { Col, Row } from "antd";
+import { Col, Row, Space } from "antd";
import moment from "moment";
import queryString from "query-string";
import React, { useEffect } from "react";
@@ -11,6 +11,7 @@ import AlertComponent from "../../components/alert/alert.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import TimeTicketsDatesSelector from "../../components/ticket-tickets-dates-selector/time-tickets-dates-selector.component";
import TimeTicketList from "../../components/time-ticket-list/time-ticket-list.component";
+import TimeTicketsPayrollTable from "../../components/time-tickets-payroll-table/time-tickets-payroll-table.component";
import TimeTicketsSummaryEmployees from "../../components/time-tickets-summary-employees/time-tickets-summary-employees.component";
import { QUERY_TIME_TICKETS_IN_RANGE } from "../../graphql/timetickets.queries";
import {
@@ -68,7 +69,12 @@ export function TimeTicketsContainer({
}
+ extra={
+
+
+
+
+ }
/>
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index 4377750a0..d8f5825dc 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -21,7 +21,7 @@
"actions": {
"block": "Block Day",
"calculate": "Calculate SMART Dates",
- "cancel": "Cancel",
+ "cancel": "Cancel Appointment",
"intake": "Intake",
"new": "New Appointment",
"preview": "Preview",
@@ -1490,6 +1490,7 @@
"cost_Additional": "Cost - Additional",
"cost_labor": "Cost - Labor",
"cost_parts": "Cost - Parts",
+ "cost_sublet": "Cost - Sublet",
"costs": "Costs",
"create": {
"jobinfo": "Job Info",
@@ -1589,7 +1590,8 @@
"rosaletotal": "RO Parts Total",
"sale_additional": "Sales - Additional",
"sale_labor": "Sales - Labor",
- "sale_parts": "Sales - Parts & Sublet",
+ "sale_parts": "Sales - Parts",
+ "sale_sublet": "Sales - Sublet",
"sales": "Sales",
"savebeforeconversion": "You have unsaved changes on the job. Please save them before converting it. ",
"scheduledinchange": "The scheduled in is based off the latest appointment. To change this date, please schedule or reschedule the job. ",
@@ -2117,7 +2119,8 @@
"title": "Print Center"
},
"payments": {
- "ca_bc_etf_table": "ICBC ETF Table"
+ "ca_bc_etf_table": "ICBC ETF Table",
+ "exported_payroll": "Payroll Table"
},
"subjects": {
"jobs": {
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index 28bddd12a..7b695ceef 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -1490,6 +1490,7 @@
"cost_Additional": "",
"cost_labor": "",
"cost_parts": "",
+ "cost_sublet": "",
"costs": "",
"create": {
"jobinfo": "",
@@ -1590,6 +1591,7 @@
"sale_additional": "",
"sale_labor": "",
"sale_parts": "",
+ "sale_sublet": "",
"sales": "",
"savebeforeconversion": "",
"scheduledinchange": "",
@@ -2117,7 +2119,8 @@
"title": ""
},
"payments": {
- "ca_bc_etf_table": ""
+ "ca_bc_etf_table": "",
+ "exported_payroll": ""
},
"subjects": {
"jobs": {
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 6fc35a8a7..7bfc53f31 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -1490,6 +1490,7 @@
"cost_Additional": "",
"cost_labor": "",
"cost_parts": "",
+ "cost_sublet": "",
"costs": "",
"create": {
"jobinfo": "",
@@ -1590,6 +1591,7 @@
"sale_additional": "",
"sale_labor": "",
"sale_parts": "",
+ "sale_sublet": "",
"sales": "",
"savebeforeconversion": "",
"scheduledinchange": "",
@@ -2117,7 +2119,8 @@
"title": ""
},
"payments": {
- "ca_bc_etf_table": ""
+ "ca_bc_etf_table": "",
+ "exported_payroll": ""
},
"subjects": {
"jobs": {
diff --git a/client/src/utils/RenderTemplate.js b/client/src/utils/RenderTemplate.js
index f1559b1e2..9511416f8 100644
--- a/client/src/utils/RenderTemplate.js
+++ b/client/src/utils/RenderTemplate.js
@@ -17,7 +17,8 @@ const Templates = TemplateList();
export default async function RenderTemplate(
templateObject,
bodyshop,
- renderAsHtml = false
+ renderAsHtml = false,
+ renderAsExcel = false
) {
//Query assets that match the template name. Must be in format <>.query
let { contextData, useShopSpecificTemplate } = await fetchContextData(
@@ -30,6 +31,7 @@ export default async function RenderTemplate(
? `/${bodyshop.imexshopid}/${templateObject.name}`
: `/${templateObject.name}`,
...(renderAsHtml ? {} : { recipe: "chrome-pdf" }),
+ ...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}),
},
data: {
...contextData,
@@ -182,6 +184,9 @@ export const GenerateDocument = async (
template,
})
);
+ } else if (sendType === "x") {
+ console.log("excel");
+ await RenderTemplate(template, bodyshop, false, true);
} else {
await RenderTemplate(template, bodyshop);
}
diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js
index 64aebaa7c..b478fd969 100644
--- a/client/src/utils/TemplateConstants.js
+++ b/client/src/utils/TemplateConstants.js
@@ -1635,6 +1635,13 @@ export const TemplateList = (type, context) => {
key: "ca_bc_etf_table",
disabled: false,
},
+ exported_payroll: {
+ title: i18n.t("printcenter.payments.exported_payroll"),
+ description: "Est Detail",
+ subject: i18n.t("printcenter.payments.exported_payroll"),
+ key: "exported_payroll",
+ disabled: false,
+ },
production_by_technician_one: {
title: i18n.t(
"reportcenter.templates.production_by_technician_one"
diff --git a/server/job/job-costing.js b/server/job/job-costing.js
index 205e39537..5ccdeba0d 100644
--- a/server/job/job-costing.js
+++ b/server/job/job-costing.js
@@ -63,10 +63,12 @@ async function JobCostingMulti(req, res) {
totalLaborSales: Dinero({ amount: 0 }),
totalPartsSales: Dinero({ amount: 0 }),
totalAdditionalSales: Dinero({ amount: 0 }),
+ totalSubletSales: Dinero({ amount: 0 }),
totalSales: Dinero({ amount: 0 }),
totalLaborCost: Dinero({ amount: 0 }),
totalPartsCost: Dinero({ amount: 0 }),
totalAdditionalCost: Dinero({ amount: 0 }),
+ totalSubletCost: Dinero({ amount: 0 }),
totalCost: Dinero({ amount: 0 }),
gpdollars: Dinero({ amount: 0 }),
gppercent: null,
@@ -74,12 +76,15 @@ async function JobCostingMulti(req, res) {
totalLaborGp: Dinero({ amount: 0 }),
totalPartsGp: Dinero({ amount: 0 }),
totalAdditionalGp: Dinero({ amount: 0 }),
+ totalSubletGp: Dinero({ amount: 0 }),
totalLaborGppercent: null,
totalLaborGppercentFormatted: null,
totalPartsGppercent: null,
totalPartsGppercentFormatted: null,
totalAdditionalGppercent: null,
totalAdditionalGppercentFormatted: null,
+ totalSubletGppercent: null,
+ totalSubletGppercentFormatted: null,
},
};
@@ -110,6 +115,9 @@ async function JobCostingMulti(req, res) {
sale_additional_dinero: multiSummary.costCenterData[
CostCenterIndex
].sale_additional_dinero.add(c.sale_additional_dinero),
+ sale_sublet_dinero: multiSummary.costCenterData[
+ CostCenterIndex
+ ].sale_sublet_dinero.add(c.sale_sublet_dinero),
cost_labor_dinero: multiSummary.costCenterData[
CostCenterIndex
].cost_labor_dinero.add(c.cost_labor_dinero),
@@ -119,6 +127,9 @@ async function JobCostingMulti(req, res) {
cost_additional_dinero: multiSummary.costCenterData[
CostCenterIndex
].cost_additional_dinero.add(c.cost_additional_dinero),
+ cost_sublet_dinero: multiSummary.costCenterData[
+ CostCenterIndex
+ ].cost_sublet_dinero.add(c.cost_sublet_dinero),
gpdollars_dinero: multiSummary.costCenterData[
CostCenterIndex
].gpdollars_dinero.add(c.gpdollars_dinero),
@@ -144,6 +155,10 @@ async function JobCostingMulti(req, res) {
multiSummary.summaryData.totalAdditionalSales.add(
costingData.summaryData.totalAdditionalSales
);
+ multiSummary.summaryData.totalSubletSales =
+ multiSummary.summaryData.totalSubletSales.add(
+ costingData.summaryData.totalSubletSales
+ );
multiSummary.summaryData.totalSales =
multiSummary.summaryData.totalSales.add(
costingData.summaryData.totalSales
@@ -164,6 +179,10 @@ async function JobCostingMulti(req, res) {
multiSummary.summaryData.totalAdditionalCost.add(
costingData.summaryData.totalAdditionalCost
);
+ multiSummary.summaryData.totalSubletCost =
+ multiSummary.summaryData.totalSubletCost.add(
+ costingData.summaryData.totalSubletCost
+ );
multiSummary.summaryData.totalCost =
multiSummary.summaryData.totalCost.add(
costingData.summaryData.totalCost
@@ -185,6 +204,10 @@ async function JobCostingMulti(req, res) {
multiSummary.summaryData.totalAdditionalGp.add(
costingData.summaryData.totalAdditionalGp
);
+ multiSummary.summaryData.totalSubletGp =
+ multiSummary.summaryData.totalSubletGp.add(
+ costingData.summaryData.totalSubletGp
+ );
//Take the summary data & add it to total summary data.
});
@@ -219,6 +242,16 @@ async function JobCostingMulti(req, res) {
multiSummary.summaryData.totalAdditionalGppercentFormatted =
formatGpPercent(multiSummary.summaryData.totalAdditionalGppercent);
+ multiSummary.summaryData.totalSubletGppercent = (
+ (multiSummary.summaryData.totalSubletGp.getAmount() /
+ multiSummary.summaryData.totalSubletSales.getAmount()) *
+ 100
+ ).toFixed(2);
+
+ multiSummary.summaryData.totalSubletGppercentFormatted = formatGpPercent(
+ multiSummary.summaryData.totalSubletGppercent
+ );
+
multiSummary.summaryData.gppercent = (
(multiSummary.summaryData.gpdollars.getAmount() /
multiSummary.summaryData.totalSales.getAmount()) *
@@ -236,11 +269,13 @@ async function JobCostingMulti(req, res) {
sale_parts: c.sale_parts_dinero && c.sale_parts_dinero.toFormat(),
sale_additional:
c.sale_additional_dinero && c.sale_additional_dinero.toFormat(),
+ sale_sublet: c.sale_sublet_dinero && c.sale_sublet_dinero.toFormat(),
sales: c.sales_dinero.toFormat(),
cost_parts: c.cost_parts_dinero && c.cost_parts_dinero.toFormat(),
cost_labor: c.cost_labor_dinero && c.cost_labor_dinero.toFormat(),
cost_additional:
c.cost_additional_dinero && c.cost_additional_dinero.toFormat(),
+ cost_sublet: c.cost_sublet_dinero && c.cost_sublet_dinero.toFormat(),
costs: c.costs_dinero.toFormat(),
gpdollars: c.gpdollars_dinero.toFormat(),
gppercent: formatGpPercent(
@@ -269,9 +304,6 @@ async function JobCostingMulti(req, res) {
}
function GenerateCostingData(job) {
- if (job.id === "b97353ef-24c8-4b3f-a6c1-2190391c823e") {
- console.log("here");
- }
const defaultProfits =
job.bodyshop.md_responsibility_centers.defaults.profits;
const allCenters = _.union(
@@ -320,7 +352,12 @@ function GenerateCostingData(job) {
}
}
- if (val.part_type && val.part_type !== "PAE") {
+ if (
+ val.part_type &&
+ val.part_type !== "PAE" &&
+ val.part_type !== "PAS" &&
+ val.part_type !== "PASL"
+ ) {
const partsProfitCenter =
val.profitcenter_part || defaultProfits[val.part_type] || "?";
@@ -352,6 +389,42 @@ function GenerateCostingData(job) {
acc.parts[partsProfitCenter] =
acc.parts[partsProfitCenter].add(partsAmount);
}
+ if (
+ val.part_type &&
+ val.part_type !== "PAE" &&
+ (val.part_type === "PAS" || val.part_type === "PASL")
+ ) {
+ const partsProfitCenter =
+ val.profitcenter_part || defaultProfits[val.part_type] || "?";
+
+ if (partsProfitCenter === "?")
+ console.log("Unknown type", val.line_desc, val.part_type);
+
+ if (!partsProfitCenter)
+ console.log(
+ "Unknown cost/profit center mapping for sublet.",
+ val.line_desc,
+ val.part_type
+ );
+ const partsAmount = Dinero({
+ amount: Math.round((val.act_price || 0) * 100),
+ })
+ .multiply(val.part_qty || 1)
+ .add(
+ val.prt_dsmk_m && val.prt_dsmk_m !== 0
+ ? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
+ : Dinero({
+ amount: Math.round(val.act_price * 100),
+ })
+ .multiply(val.part_qty || 0)
+ .percentage(Math.abs(val.prt_dsmk_p || 0))
+ .multiply(val.prt_dsmk_p > 0 ? 1 : -1)
+ );
+ if (!acc.sublet[partsProfitCenter])
+ acc.sublet[partsProfitCenter] = Dinero();
+ acc.sublet[partsProfitCenter] =
+ acc.sublet[partsProfitCenter].add(partsAmount);
+ }
//To deal with additional costs.
if (!val.part_type && !val.mod_lbr_ty) {
@@ -389,7 +462,7 @@ function GenerateCostingData(job) {
return acc;
},
- { parts: {}, labor: {}, additional: {} }
+ { parts: {}, labor: {}, additional: {}, sublet: {} }
);
if (!hasMapaLine) {
@@ -444,6 +517,12 @@ function GenerateCostingData(job) {
.multiply(bill_val.is_credit_memo ? -1 : 1)
);
} else {
+ const isSubletCostCenter =
+ line_val.cost_center ===
+ job.bodyshop.md_responsibility_centers.defaults.costs.PAS ||
+ line_val.cost_center ===
+ job.bodyshop.md_responsibility_centers.defaults.costs.PASL;
+
const isAdditionalCostCenter =
// line_val.cost_center ===
// job.bodyshop.md_responsibility_centers.defaults.costs.PAS ||
@@ -468,6 +547,19 @@ function GenerateCostingData(job) {
.multiply(line_val.quantity)
.multiply(bill_val.is_credit_memo ? -1 : 1)
);
+ } else if (isSubletCostCenter) {
+ if (!bill_acc.subletCosts[line_val.cost_center])
+ bill_acc.subletCosts[line_val.cost_center] = Dinero();
+
+ bill_acc.subletCosts[line_val.cost_center] = bill_acc.subletCosts[
+ 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)
+ );
} else {
if (!bill_acc[line_val.cost_center])
bill_acc[line_val.cost_center] = Dinero();
@@ -486,7 +578,7 @@ function GenerateCostingData(job) {
});
return bill_acc;
},
- { additionalCosts: {} }
+ { additionalCosts: {}, subletCosts: {} }
);
//If the hourly rates for job costing are set, add them in.
@@ -586,14 +678,17 @@ function GenerateCostingData(job) {
totalLaborSales: Dinero({ amount: 0 }),
totalPartsSales: Dinero({ amount: 0 }),
totalAdditionalSales: Dinero({ amount: 0 }),
+ totalSubletSales: Dinero({ amount: 0 }),
totalSales: Dinero({ amount: 0 }),
totalLaborCost: Dinero({ amount: 0 }),
totalPartsCost: Dinero({ amount: 0 }),
totalAdditionalCost: Dinero({ amount: 0 }),
+ totalSubletCost: Dinero({ amount: 0 }),
totalCost: Dinero({ amount: 0 }),
totalLaborGp: Dinero({ amount: 0 }),
totalPartsGp: Dinero({ amount: 0 }),
totalAdditionalGp: Dinero({ amount: 0 }),
+ totalSubletGp: Dinero({ amount: 0 }),
gpdollars: Dinero({ amount: 0 }),
totalLaborGppercent: null,
totalLaborGppercentFormatted: null,
@@ -601,6 +696,8 @@ function GenerateCostingData(job) {
totalPartsGppercentFormatted: null,
totalAdditionalGppercent: null,
totalAdditionalGppercentFormatted: null,
+ totalSubletGppercent: null,
+ totalSubletGppercentFormatted: null,
gppercent: null,
gppercentFormatted: null,
};
@@ -613,14 +710,24 @@ function GenerateCostingData(job) {
jobLineTotalsByProfitCenter.parts[ccVal] || Dinero({ amount: 0 });
const sale_additional =
jobLineTotalsByProfitCenter.additional[ccVal] || Dinero({ amount: 0 });
+ const sale_sublet =
+ jobLineTotalsByProfitCenter.sublet[ccVal] || Dinero({ amount: 0 });
const cost_labor = ticketTotalsByCostCenter[ccVal] || Dinero({ amount: 0 });
const cost_parts = billTotalsByCostCenters[ccVal] || Dinero({ amount: 0 });
const cost_additional =
billTotalsByCostCenters.additionalCosts[ccVal] || Dinero({ amount: 0 });
+ const cost_sublet =
+ billTotalsByCostCenters.subletCosts[ccVal] || Dinero({ amount: 0 });
- const costs = cost_labor.add(cost_parts).add(cost_additional);
- const totalSales = sale_labor.add(sale_parts).add(sale_additional);
+ const costs = cost_labor
+ .add(cost_parts)
+ .add(cost_additional)
+ .add(cost_sublet);
+ const totalSales = sale_labor
+ .add(sale_parts)
+ .add(sale_additional)
+ .add(sale_sublet);
const gpdollars = totalSales.subtract(costs);
const gppercent = (
(gpdollars.getAmount() / totalSales.getAmount()) *
@@ -632,11 +739,14 @@ function GenerateCostingData(job) {
summaryData.totalPartsSales = summaryData.totalPartsSales.add(sale_parts);
summaryData.totalAdditionalSales =
summaryData.totalAdditionalSales.add(sale_additional);
+ summaryData.totalSubletSales =
+ summaryData.totalSubletSales.add(sale_sublet);
summaryData.totalSales = summaryData.totalSales.add(totalSales);
summaryData.totalLaborCost = summaryData.totalLaborCost.add(cost_labor);
summaryData.totalPartsCost = summaryData.totalPartsCost.add(cost_parts);
summaryData.totalAdditionalCost =
summaryData.totalAdditionalCost.add(cost_additional);
+ summaryData.totalSubletCost = summaryData.totalSubletCost.add(cost_sublet);
summaryData.totalCost = summaryData.totalCost.add(costs);
return {
@@ -648,6 +758,8 @@ function GenerateCostingData(job) {
sale_parts_dinero: sale_parts,
sale_additional: sale_additional && sale_additional.toFormat(),
sale_additional_dinero: sale_additional,
+ sale_sublet: sale_sublet && sale_sublet.toFormat(),
+ sale_sublet_dinero: sale_sublet,
sales: totalSales.toFormat(),
sales_dinero: totalSales,
cost_parts: cost_parts && cost_parts.toFormat(),
@@ -656,6 +768,8 @@ function GenerateCostingData(job) {
cost_labor_dinero: cost_labor,
cost_additional: cost_additional && cost_additional.toFormat(),
cost_additional_dinero: cost_additional,
+ cost_sublet: cost_sublet && cost_sublet.toFormat(),
+ cost_sublet_dinero: cost_sublet,
costs: costs.toFormat(),
costs_dinero: costs,
gpdollars_dinero: gpdollars,
@@ -678,8 +792,10 @@ function GenerateCostingData(job) {
sale_labor_dinero: Dinero(),
sale_parts: Dinero().toFormat(),
sale_parts_dinero: Dinero(),
- sale_additional: Dinero(),
+ sale_additional: Dinero(),
sale_additional_dinero: Dinero(),
+ sale_sublet: Dinero(),
+ sale_sublet_dinero: Dinero(),
sales: Dinero().toFormat(),
sales_dinero: Dinero(),
cost_parts: Dinero().toFormat(),
@@ -688,6 +804,8 @@ function GenerateCostingData(job) {
cost_labor_dinero: Adjustment,
cost_additional: Dinero(),
cost_additional_dinero: Dinero(),
+ cost_sublet: Dinero(),
+ cost_sublet_dinero: Dinero(),
costs: Adjustment.toFormat(),
costs_dinero: Adjustment,
gpdollars_dinero: Dinero(),
@@ -732,6 +850,17 @@ function GenerateCostingData(job) {
summaryData.totalAdditionalGppercentFormatted = formatGpPercent(
summaryData.totalAdditionalGppercent
);
+ summaryData.totalSubletGp = summaryData.totalSubletSales.subtract(
+ summaryData.totalSubletCost
+ );
+ summaryData.totalSubletGppercent = (
+ (summaryData.totalSubletGp.getAmount() /
+ summaryData.totalSubletSales.getAmount()) *
+ 100
+ ).toFixed(2);
+ summaryData.totalSubletGppercentFormatted = formatGpPercent(
+ summaryData.totalSubletGppercent
+ );
summaryData.gpdollars = summaryData.totalSales.subtract(
summaryData.totalCost
diff --git a/server/job/job-totals.js b/server/job/job-totals.js
index 57fe94c87..31dc995a6 100644
--- a/server/job/job-totals.js
+++ b/server/job/job-totals.js
@@ -442,8 +442,8 @@ function CalculateAdditional(job) {
ret.total = ret.additionalCosts
.add(ret.adjustments) //IO-813 Adjustment takes care of GST & PST at labor rate.
.add(ret.towing)
- .add(ret.storage)
- .add(ret.pvrt);
+ .add(ret.storage);
+ //.add(ret.pvrt);
return ret;
}
@@ -453,6 +453,7 @@ function CalculateTaxesTotals(job, otherTotals) {
.add(otherTotals.parts.sublets.subtotal)
.add(otherTotals.rates.subtotal) //No longer using just rates subtotal to include mapa/mash.
.add(otherTotals.additional.total);
+
// .add(Dinero({ amount: (job.towing_payable || 0) * 100 }))
// .add(Dinero({ amount: (job.storage_payable || 0) * 100 }));
@@ -522,7 +523,13 @@ function CalculateTaxesTotals(job, otherTotals) {
let ret = {
subtotal: subtotal,
- federal_tax: subtotal.percentage((job.federal_tax_rate || 0) * 100),
+ federal_tax: subtotal
+ .percentage((job.federal_tax_rate || 0) * 100)
+ .add(
+ otherTotals.additional.pvrt.percentage(
+ (job.federal_tax_rate || 0) * 100
+ )
+ ),
statePartsTax,
state_tax: statePartsTax
.add(
@@ -540,12 +547,14 @@ function CalculateTaxesTotals(job, otherTotals) {
otherTotals.additional.storage.percentage((job.tax_str_rt || 0) * 100)
)
.add(additionalItemsTax),
+ // .add(otherTotals.additional.pvrt),
local_tax: subtotal.percentage((job.local_tax_rate || 0) * 100),
};
ret.total_repairs = ret.subtotal
.add(ret.federal_tax)
.add(ret.state_tax)
- .add(ret.local_tax);
+ .add(ret.local_tax)
+ .add(otherTotals.additional.pvrt);
ret.custPayable = {
deductible: Dinero({ amount: Math.round((job.ded_amt || 0) * 100) }) || 0,
diff --git a/server/tasks/tasks.js b/server/tasks/tasks.js
index 9eca856d3..827bd9795 100644
--- a/server/tasks/tasks.js
+++ b/server/tasks/tasks.js
@@ -9,30 +9,84 @@ const axios = require("axios");
const client = require("../graphql-client/graphql-client").client;
const emailer = require("../email/sendemail");
const logger = require("../utils/logger");
-
+const moment = require("moment-timezone");
exports.taskHandler = async (req, res) => {
try {
- const { bodyshopid, query, variables, text, to, subject } = req.body;
+ const { bodyshopid, query, variables, text, to, subject, timezone } =
+ req.body;
//Run the query
+
+ //Check the variables to see if they are an object.
+ Object.keys(variables).forEach((key) => {
+ if (typeof variables[key] === "object") {
+ if (variables[key].function) {
+ variables[key] = functionMapper(variables[key].function, timezone);
+ }
+ }
+ });
+
const response = await client.request(query, variables);
//Massage the data
//Send the email
const rootElement = response[Object.keys(response)[0]]; //This element shoudl always be an array.
let converter = require("json-2-csv");
- converter.json2csv(rootElement, (err, csv) => {
- if (err) {
- res.status(500).json(err);
- }
+ converter.json2csv(
+ rootElement,
+ (err, csv) => {
+ if (err) {
+ res.status(500).json(err);
+ }
- emailer.sendTaskEmail({
- to,
- subject,
- text,
- attachments: [{ filename: "query.csv", content: csv }],
- });
- res.status(200).send(csv);
- });
+ emailer.sendTaskEmail({
+ to,
+ subject,
+ text,
+ attachments: [{ filename: "query.csv", content: csv }],
+ });
+ res.status(200).send(csv);
+ },
+ { emptyFieldValue: "" }
+ );
} catch (error) {
- res.status(500).json({ error });
+ res.status(500).json({ error: error.message, stack: error.stackTrace });
}
};
+
+const isoformat = "YYYY-MM-DD";
+function functionMapper(f, timezone) {
+ switch (f) {
+ case "date.today":
+ return moment().tz(timezone).format(isoformat);
+ case "date.now":
+ return moment().tz(timezone);
+ case "date.yesterday":
+ return moment().tz(timezone).subtract(1, "day").format(isoformat);
+ case "date.3daysago":
+ return moment().tz(timezone).subtract(3, "days").format(isoformat);
+ case "date.7daysago":
+ return moment().tz(timezone).subtract(7, "days").format(isoformat);
+ case "date.tomorrow":
+ return moment().tz(timezone).add(1, "day").format(isoformat);
+ case "date.3daysfromnow":
+ return moment().tz(timezone).add(3, "days").format(isoformat);
+ case "date.7daysfromnow":
+ return moment().tz(timezone).add(7, "days").format(isoformat);
+ case "date.yesterdaytz":
+ return moment().tz(timezone).subtract(1, "day");
+ case "date.3daysagotz":
+ return moment().tz(timezone).subtract(3, "days");
+ case "date.7daysagotz":
+ return moment().tz(timezone).subtract(7, "days");
+ case "date.tomorrowtz":
+ return moment().tz(timezone).add(1, "day");
+ case "date.3daysfromnowtz":
+ return moment().tz(timezone).add(3, "days");
+ case "date.7daysfromnowtz":
+ return moment().tz(timezone).add(7, "days");
+
+ case "date.now":
+ return moment().tz(timezone);
+ default:
+ return f;
+ }
+}