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; + } +}