diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index f292acf4f..38857c3ad 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -25469,6 +25469,27 @@ + + invoicedatefuture + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + kmoutnotgreaterthankmin false @@ -38154,6 +38175,27 @@ + + production_by_technician_one + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + purchases_by_cost_center_detail false diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss index ccaf3b89f..e7243044d 100644 --- a/client/src/App/App.styles.scss +++ b/client/src/App/App.styles.scss @@ -91,13 +91,13 @@ color: blue; } -.production-completion-1 { - animation: production-completion-1-blinker 5s linear infinite; +.production-completion-soon { + color: rgba(255, 140, 0, 0.8); + font-weight: bold; } -@keyframes production-completion-1-blinker { - 50% { - background: rgba(207, 12, 12, 0.555); - } +.production-completion-past { + color: rgba(255, 0, 0, 0.8); + font-weight: bold; } .react-resizable { diff --git a/client/src/components/breadcrumbs/breadcrumbs.component.jsx b/client/src/components/breadcrumbs/breadcrumbs.component.jsx index ed3331da1..ee0d51bf6 100644 --- a/client/src/components/breadcrumbs/breadcrumbs.component.jsx +++ b/client/src/components/breadcrumbs/breadcrumbs.component.jsx @@ -5,21 +5,25 @@ import { connect } from "react-redux"; import { Link } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import { selectBreadcrumbs } from "../../redux/application/application.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import GlobalSearch from "../global-search/global-search.component"; import "./breadcrumbs.styles.scss"; const mapStateToProps = createStructuredSelector({ breadcrumbs: selectBreadcrumbs, + bodyshop: selectBodyshop, }); -export function BreadCrumbs({ breadcrumbs }) { +export function BreadCrumbs({ breadcrumbs, bodyshop }) { return ( - + {" "} + {(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) || + ""} {breadcrumbs.map((item) => diff --git a/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx b/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx index a08464db1..049fa58dc 100644 --- a/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx +++ b/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx @@ -171,7 +171,12 @@ export function JobChecklistForm({ }); } }; - + console.log(job,{ + removeFromProduction: true, + actual_completion: + job && job.actual_completion && moment(job.actual_completion), + actual_delivery: job && job.actual_delivery && moment(job.actual_delivery), + }); return ( fi.value) diff --git a/client/src/components/jobs-documents-gallery/jobs-document-gallery.download.component.jsx b/client/src/components/jobs-documents-gallery/jobs-document-gallery.download.component.jsx index 957fe3344..7d322c61d 100644 --- a/client/src/components/jobs-documents-gallery/jobs-document-gallery.download.component.jsx +++ b/client/src/components/jobs-documents-gallery/jobs-document-gallery.download.component.jsx @@ -36,7 +36,7 @@ export function JobsDocumentsDownloadButton({ ); const imagesToDownload = [ ...galleryImages.images.filter((image) => image.isSelected), - // ...galleryImages.other.filter((image) => image.isSelected), + ...galleryImages.other.filter((image) => image.isSelected), ]; function downloadProgress(progressEvent) { @@ -123,6 +123,7 @@ export function JobsDocumentsDownloadButton({ a.click(); } }; + return ( <> e.id === card.employee_csr); // } + const pastDueAlert = + !!card.scheduled_completion && + ((moment().isSameOrAfter(moment(card.scheduled_completion), "day") && + "production-completion-past") || + (moment() + .add(1, "day") + .isSame(moment(card.scheduled_completion), "day") && + "production-completion-soon")); + return ( - + {card.scheduled_completion} 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 00f279a66..d8d547665 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 @@ -109,7 +109,7 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => { state.sortedInfo.columnKey === "scheduled_completion" && state.sortedInfo.order, render: (text, record) => ( - + ), }, { @@ -156,7 +156,7 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => { state.sortedInfo.columnKey === "scheduled_delivery" && state.sortedInfo.order, render: (text, record) => ( - + ), }, { 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 51999c288..1b201032e 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 @@ -8,10 +8,12 @@ import { DateFormatter } from "../../utils/DateFormatter"; import { useTranslation } from "react-i18next"; -const OneCalendarDay = 60 * 60 * 24 * 1000; -const Now = new Date(); - -export default function ProductionListDate({ record, field, time }) { +export default function ProductionListDate({ + record, + field, + time, + pastIndicator, +}) { const [updateAlert] = useMutation(UPDATE_JOB); const [visible, setVisible] = useState(false); const { t } = useTranslation(); @@ -34,6 +36,15 @@ export default function ProductionListDate({ record, field, time }) { }); }; + let className = ""; + if (pastIndicator) { + className = + !!record[field] && + ((moment().isSameOrAfter(moment(record[field]), "day") && + "production-completion-past") || + (moment().add(1, "day").isSame(moment(record[field]), "day") && + "production-completion-soon")); + } return ( - - {record[field]} - + {record[field]} diff --git a/client/src/components/production-list-table/production-list-print.component.jsx b/client/src/components/production-list-table/production-list-print.component.jsx index bd26d2313..aad4287aa 100644 --- a/client/src/components/production-list-table/production-list-print.component.jsx +++ b/client/src/components/production-list-table/production-list-print.component.jsx @@ -4,9 +4,24 @@ import { TemplateList } from "../../utils/TemplateConstants"; import { useTranslation } from "react-i18next"; import { GenerateDocument } from "../../utils/RenderTemplate"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; const ProdTemplates = TemplateList("production"); +const ProductionByTech = TemplateList("special").production_by_technician_one; -export default function ProductionListPrint() { +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(ProductionListPrint); + +export function ProductionListPrint({ bodyshop }) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); return ( @@ -33,6 +48,29 @@ export default function ProductionListPrint() { {ProdTemplates[key].title} ))} + + {bodyshop.employees.map((e) => ( + { + setLoading(true); + await GenerateDocument( + { + name: ProductionByTech.key, + variables: { id: e.id }, + }, + {}, + "p" + ); + setLoading(false); + }} + > + {e.first_name} {e.last_name} + + ))} + } > diff --git a/client/src/components/tech-job-clock-in-form/tech-job-clock-in-form.container.jsx b/client/src/components/tech-job-clock-in-form/tech-job-clock-in-form.container.jsx index 499f40af1..eeff6d2de 100644 --- a/client/src/components/tech-job-clock-in-form/tech-job-clock-in-form.container.jsx +++ b/client/src/components/tech-job-clock-in-form/tech-job-clock-in-form.container.jsx @@ -1,5 +1,5 @@ import { useMutation } from "@apollo/client"; -import { Button, Card, Form, notification } from "antd"; +import { Button, Card, Form, notification, Space } from "antd"; import axios from "axios"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -9,6 +9,7 @@ import { INSERT_NEW_TIME_TICKET } from "../../graphql/timetickets.queries"; import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; import TechClockInComponent from "./tech-job-clock-in-form.component"; +import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component"; const mapStateToProps = createStructuredSelector({ technician: selectTechnician, @@ -71,9 +72,16 @@ export function TechClockInContainer({ technician, bodyshop }) { form.submit()} loading={loading}> - {t("timetickets.actions.clockin")} - + + + form.submit()} + loading={loading} + > + {t("timetickets.actions.clockin")} + + } > diff --git a/client/src/components/tech-job-print-tickets/tech-job-print-tickets.component.jsx b/client/src/components/tech-job-print-tickets/tech-job-print-tickets.component.jsx new file mode 100644 index 000000000..e09691a65 --- /dev/null +++ b/client/src/components/tech-job-print-tickets/tech-job-print-tickets.component.jsx @@ -0,0 +1,125 @@ +import { Button, Card, DatePicker, Form, Popover, Space } from "antd"; +import moment from "moment"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { logImEXEvent } from "../../firebase/firebase.utils"; +import { selectTechnician } from "../../redux/tech/tech.selectors"; +import DatePIckerRanges from "../../utils/DatePickerRanges"; +import { GenerateDocument } from "../../utils/RenderTemplate"; +import { TemplateList } from "../../utils/TemplateConstants"; + +const mapStateToProps = createStructuredSelector({ + technician: selectTechnician, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(TechJobPrintTickets); + +export function TechJobPrintTickets({ technician, event }) { + const { t } = useTranslation(); + + const [loading, setLoading] = useState(false); + const [form] = Form.useForm(); + const [visibility, setVisibility] = useState(false); + + useEffect(() => { + if (visibility && event) { + form.setFieldsValue(event); + } + }, [visibility, form, event]); + + const handleFinish = async (values) => { + logImEXEvent("schedule_manual_event"); + + setLoading(true); + const start = values.dates[0]; + const end = values.dates[1]; + + try { + await GenerateDocument( + { + name: TemplateList().timetickets_employee.key, + variables: { + ...(start + ? { start: moment(start).startOf("day").format("YYYY-MM-DD") } + : {}), + ...(end + ? { end: moment(end).endOf("day").format("YYYY-MM-DD") } + : {}), + ...(start ? { starttz: moment(start).startOf("day") } : {}), + ...(end ? { endtz: moment(end).endOf("day") } : {}), + + id: technician.id, + }, + }, + { + to: technician.email, + subject: TemplateList().timetickets_employee.subject, + }, + "p" + ); + } catch (error) { + console.log(error); + } finally { + setLoading(false); + setVisibility(false); + form.resetFields(); + } + }; + + const overlay = ( + + + + + + + + + form.submit()}> + {t("general.actions.print")} + + { + setVisibility(false); + form.resetFields(); + }} + > + {t("general.actions.cancel")} + + + + + + ); + + const handleClick = (e) => { + setVisibility(true); + }; + + return ( + + + {t("general.actions.print")} + + + ); +} diff --git a/client/src/graphql/bodyshop.queries.js b/client/src/graphql/bodyshop.queries.js index 878f3a3e9..23905296a 100644 --- a/client/src/graphql/bodyshop.queries.js +++ b/client/src/graphql/bodyshop.queries.js @@ -262,6 +262,9 @@ export const QUERY_DELIVER_CHECKLIST = gql` jobs_by_pk(id: $jobId) { id ro_number + actual_completion + actual_delivery + } } `; diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index 999d56224..8f927d4ff 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -1878,6 +1878,7 @@ export const QUERY_JOB_CLOSE_DETAILS = gql` scheduled_delivery actual_delivery scheduled_in + date_invoiced actual_in kmin kmout diff --git a/client/src/pages/jobs-close/jobs-close.component.jsx b/client/src/pages/jobs-close/jobs-close.component.jsx index 71d961c99..697211ee6 100644 --- a/client/src/pages/jobs-close/jobs-close.component.jsx +++ b/client/src/pages/jobs-close/jobs-close.component.jsx @@ -56,7 +56,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) { jobId: job.id, job: { status: bodyshop.md_ro_statuses.default_invoiced || "", - date_invoiced: new Date(), + date_invoiced: values.date_invoiced, actual_in: values.actual_in, actual_completion: values.actual_completion, actual_delivery: values.actual_delivery, @@ -119,6 +119,9 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) { actual_delivery: job.actual_delivery ? moment(job.actual_delivery) : job.scheduled_delivery && moment(job.scheduled_delivery), + date_invoiced: job.date_invoiced + ? moment(job.date_invoiced) + : moment(), kmin: job.kmin, kmout: job.kmout, dms_allocation: job.dms_allocation, @@ -219,6 +222,32 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) { > + ({ + validator(_, value) { + if (!bodyshop.cdk_dealerid) return Promise.resolve(); + if (!value || moment(value).isSameOrAfter(moment(), "day")) { + return Promise.resolve(); + } + + return Promise.reject( + new Error(t("jobs.labels.dms.invoicedatefuture")) + ); + }, + }), + ]} + > + + {(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && ( diff --git a/client/src/redux/user/user.sagas.js b/client/src/redux/user/user.sagas.js index 46d07443f..2daf7a907 100644 --- a/client/src/redux/user/user.sagas.js +++ b/client/src/redux/user/user.sagas.js @@ -270,7 +270,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) { factory.client(payload.imexshopid); const authRecord = payload.associations.filter( - (a) => a.useremail === userEmail + (a) => a.useremail.toLowerCase() === userEmail.toLowerCase() ); yield put(setAuthlevel(authRecord[0] ? authRecord[0].authlevel : 0)); diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 8e08327d7..2ec4ddfb9 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1507,6 +1507,7 @@ "diskscan": "Scan Disk for Estimates", "dms": { "defaultstory": "Bodyshop RO {{ro_number}}. Damage to $t(jobs.fields.area_of_damage_impact.{{area_of_damage}}).", + "invoicedatefuture": "Invoice date must be today or in the future for CDK posting.", "kmoutnotgreaterthankmin": "Mileage out must be greater than mileage in.", "logs": "Logs", "notallocated": "Not Allocated", @@ -2267,6 +2268,7 @@ "production_by_ro": "Production by RO", "production_by_target_date": "Production by Target Date", "production_by_technician": "Production by Technician", + "production_by_technician_one": "Production filtered by Technician", "purchases_by_cost_center_detail": "Purchases by Cost Center (Detail)", "purchases_by_cost_center_summary": "Purchases by Cost Center (Summary)", "purchases_by_date_range_detail": "Purchases by Date - Detail", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index e89b26bd5..59983bd51 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -1507,6 +1507,7 @@ "diskscan": "", "dms": { "defaultstory": "", + "invoicedatefuture": "", "kmoutnotgreaterthankmin": "", "logs": "", "notallocated": "", @@ -2267,6 +2268,7 @@ "production_by_ro": "", "production_by_target_date": "", "production_by_technician": "", + "production_by_technician_one": "", "purchases_by_cost_center_detail": "", "purchases_by_cost_center_summary": "", "purchases_by_date_range_detail": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 0f1c0c51c..f20e02e42 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -1507,6 +1507,7 @@ "diskscan": "", "dms": { "defaultstory": "", + "invoicedatefuture": "", "kmoutnotgreaterthankmin": "", "logs": "", "notallocated": "", @@ -2267,6 +2268,7 @@ "production_by_ro": "", "production_by_target_date": "", "production_by_technician": "", + "production_by_technician_one": "", "purchases_by_cost_center_detail": "", "purchases_by_cost_center_summary": "", "purchases_by_date_range_detail": "", diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index e486000ce..1c7fe6558 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -1590,14 +1590,6 @@ export const TemplateList = (type, context) => { //idtype: "vendor", disabled: false, }, - production_by_technician: { - title: i18n.t("reportcenter.templates.production_by_technician"), - description: "", - subject: i18n.t("reportcenter.templates.production_by_technician"), - key: "production_by_technician", - //idtype: "vendor", - disabled: false, - }, production_by_category: { title: i18n.t("reportcenter.templates.production_by_category"), description: "", @@ -1606,6 +1598,14 @@ export const TemplateList = (type, context) => { //idtype: "vendor", disabled: false, }, + production_by_technician: { + title: i18n.t("reportcenter.templates.production_by_technician"), + description: "", + subject: i18n.t("reportcenter.templates.production_by_technician"), + key: "production_by_technician", + //idtype: "vendor", + disabled: false, + }, } : {}), ...(!type || type === "special" @@ -1617,6 +1617,18 @@ export const TemplateList = (type, context) => { key: "ca_bc_etf_table", disabled: false, }, + production_by_technician_one: { + title: i18n.t( + "reportcenter.templates.production_by_technician_one" + ), + description: "", + subject: i18n.t( + "reportcenter.templates.production_by_technician_one" + ), + key: "production_by_technician_one", + //idtype: "vendor", + disabled: false, + }, } : {}), }; diff --git a/server/accounting/pbs/pbs-job-export.js b/server/accounting/pbs/pbs-job-export.js index 2d38bb46c..8ba6f8fc1 100644 --- a/server/accounting/pbs/pbs-job-export.js +++ b/server/accounting/pbs/pbs-job-export.js @@ -30,7 +30,7 @@ axios.interceptors.request.use((x) => { } | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`; console.log(printable); - CdkBase.createLogEvent(socket, "TRACE", `Raw Request: ${printable}`); + CdkBase.createJsonEvent(socket, "TRACE", `Raw Request: ${printable}`, x.data); return x; }); @@ -42,7 +42,12 @@ axios.interceptors.response.use((x) => { x.data )}`; console.log(printable); - CdkBase.createLogEvent(socket, "TRACE", `Raw Response: ${printable}`); + CdkBase.createJsonEvent( + socket, + "TRACE", + `Raw Response: ${printable}`, + x.data + ); return x; }); diff --git a/server/accounting/qb-receivables-lines.js b/server/accounting/qb-receivables-lines.js index c2a271f48..107b081f7 100644 --- a/server/accounting/qb-receivables-lines.js +++ b/server/accounting/qb-receivables-lines.js @@ -38,7 +38,9 @@ exports.default = function ({ if ( (jobline.prt_dsmk_p && jobline.prt_dsmk_p !== 0) || - ((jobline.db_ref === "900511" || jobline.db_ref === "900510") && + ((jobline.db_ref === "900511" || + jobline.db_ref === "900510" || + jobline.db_ref === "900500") && jobline.prt_dsmk_m && jobline.prt_dsmk_m !== 0) ) { diff --git a/server/cdk/cdk-calculate-allocations.js b/server/cdk/cdk-calculate-allocations.js index e87508b97..bbafa4646 100644 --- a/server/cdk/cdk-calculate-allocations.js +++ b/server/cdk/cdk-calculate-allocations.js @@ -72,7 +72,9 @@ exports.default = async function (socket, jobid) { if ( (val.prt_dsmk_p && val.prt_dsmk_p !== 0) || - ((val.db_ref === "900511" || val.db_ref === "900510") && + ((val.db_ref === "900511" || + val.db_ref === "900510" || + val.db_ref === "900500") && val.prt_dsmk_m && val.prt_dsmk_m !== 0) ) { diff --git a/server/data/arms.js b/server/data/arms.js index 7124c07ad..2d23ab8d3 100644 --- a/server/data/arms.js +++ b/server/data/arms.js @@ -2,6 +2,7 @@ const path = require("path"); const queries = require("../graphql-client/queries"); const Dinero = require("dinero.js"); const moment = require("moment"); +const fs = require("fs"); const _ = require("lodash"); const logger = require("../utils/logger"); @@ -28,7 +29,6 @@ exports.default = async (req, res) => { logger.log("arms-start", "DEBUG", "api", null, null); const { bodyshops } = await client.request(queries.GET_ENTEGRAL_SHOPS); - const allxmlsToUpload = []; const allErrors = []; try { for (const bodyshop of bodyshops) { @@ -40,10 +40,12 @@ exports.default = async (req, res) => { const { jobs } = await client.request(queries.ENTEGRAL_EXPORT, { bodyshopid: bodyshop.id, }); - const ret = jobs.map((job) => { + const jobsToPush = []; + + jobs.forEach((job) => { const transId = uuid(); // Can this actually be the job id? - return { + let obj = { RqUID: transId, DocumentInfo: { BMSVer: "4.0.0", @@ -55,16 +57,18 @@ exports.default = async (req, res) => { TransmitDateTime: moment().format(momentFormat), // Omitted from ARMS docs }, EventInfo: { - AssignmentEvent: { - CreateDateTime: - job.asgn_date && moment(job.asgn_date).format(momentFormat), - }, - EstimateEvent: { - UploadDateTime: moment().format(momentFormat), - }, + // AssignmentEvent: { + // CreateDateTime: + // job.asgn_date && moment(job.asgn_date).format(momentFormat), + // }, + // EstimateEvent: { + // UploadDateTime: moment().format(momentFormat), + // }, RepairEvent: { - CreatedDateTime: - job.date_open && moment(job.date_open).format(momentFormat), + CreatedDateTime: (job.date_open + ? moment(job.date_open) + : moment() + ).format(momentFormat), ArrivalDateTime: job.actual_in && moment(job.actual_in).format(momentFormat), ArrivalOdometerReading: job.kmin, @@ -92,35 +96,35 @@ exports.default = async (req, res) => { IDQualifierCode: "US", IDNum: 44, // ** Not sure where to get this entegral ID from? }, - Communications: [ - { - CommQualifier: "WA", - Address: { - Address1: job.ins_addr1, - Address2: job.ins_addr2, - City: job.ins_city, - StateProvince: job.ins_st, - PostalCode: job.ins_zip, - CountryCode: job.ins_ctry, - }, - }, - { - CommQualifier: "WP", - CommPhone: job.ins_ph1, - }, - { - CommQualifier: "WF", - CommPhone: job.ins_ph2, - }, - ], - }, - ContactInfo: { - ContactJobTitle: "Adjuster", - ContactName: { - FirstName: job.est_ct_fn, - LastName: job.est_ct_ln, - }, + // Communications: [ + // { + // CommQualifier: "WA", + // Address: { + // Address1: job.ins_addr1, + // Address2: job.ins_addr2, + // City: job.ins_city, + // StateProvince: job.ins_st, + // PostalCode: job.ins_zip, + // CountryCode: job.ins_ctry, + // }, + // }, + // { + // CommQualifier: "WP", + // CommPhone: job.ins_ph1, + // }, + // { + // CommQualifier: "WF", + // CommPhone: job.ins_ph2, + // }, + // ], }, + // ContactInfo: { + // ContactJobTitle: "Adjuster", + // ContactName: { + // FirstName: job.est_ct_fn, + // LastName: job.est_ct_ln, + // }, + // }, }, }, // InsuranceAgent: { @@ -141,16 +145,16 @@ exports.default = async (req, res) => { // }, // }, // }, - Insured: { - Party: { - PersonInfo: { - PersonName: { - FirstName: job.insd_fn, - LastName: job.insd_ln, - }, - }, - }, - }, + // Insured: { + // Party: { + // PersonInfo: { + // PersonName: { + // FirstName: job.insd_fn, + // LastName: job.insd_ln, + // }, + // }, + // }, + // }, Owner: { Party: { PersonInfo: { @@ -158,68 +162,70 @@ exports.default = async (req, res) => { FirstName: job.ownr_fn, LastName: job.ownr_ln, }, - Communications: [ - { - CommQualifier: "HA", - Address: { - Address1: job.ownr_addr1, + // Communications: [ + // { + // CommQualifier: "HA", + // Address: { + // Address1: job.ownr_addr1, - City: job.ownr_city, - StateProvince: job.ownr_st, - PostalCode: job.ownr_zip, - CountryCode: job.ownr_ctry, - }, - }, - { - CommQualifier: "HP", - CommPhone: job.ownr_ph1, - }, - { - CommQualifier: "WP", - CommPhone: job.ownr_ph2, - }, - { - CommQualifier: "CP", - CommPhone: job.ownr_ph1, - }, - { - CommQualifier: "EM", - CommEmail: job.ownr_ea, - }, - ], - }, - }, - }, - Claimant: { - Party: { - PersonInfo: { - PersonName: { - FirstName: job.clm_ct_fn, - LastName: job.clm_ct_ln, - }, - }, - }, - OwnerInd: true, - }, - Estimator: { - Party: { - PersonInfo: { - PersonName: { - FirstName: job.est_ct_fn, - LastName: job.est_ct_ln, - }, - // IDInfo: { - // IDQualifierCode: "US", - // IDNum: 2941, - // }, + // City: job.ownr_city, + // StateProvince: job.ownr_st, + // PostalCode: job.ownr_zip, + // CountryCode: job.ownr_ctry, + // }, + // }, + // { + // CommQualifier: "HP", + // CommPhone: job.ownr_ph1, + // }, + // { + // CommQualifier: "WP", + // CommPhone: job.ownr_ph2, + // }, + // { + // CommQualifier: "CP", + // CommPhone: job.ownr_ph1, + // }, + // { + // CommQualifier: "EM", + // CommEmail: job.ownr_ea, + // }, + // ], }, }, }, + // Claimant: { + // Party: { + // PersonInfo: { + // PersonName: { + // FirstName: job.clm_ct_fn, + // LastName: job.clm_ct_ln, + // }, + // }, + // }, + // OwnerInd: true, + // }, + // Estimator: { + // Party: { + // PersonInfo: { + // PersonName: { + // FirstName: job.est_ct_fn, + // LastName: job.est_ct_ln, + // }, + // // IDInfo: { + // // IDQualifierCode: "US", + // // IDNum: 2941, + // // }, + // }, + // }, + // }, RepairFacility: { - //This section not in documentation. Party: { OrgInfo: { - CompanyName: bodyshop.shopname, + CompanyName: + process.env.NODE_ENV === "production" + ? bodyshop.shopname + : "IMEX Test Shop", IDInfo: { IDQualifierCode: "US", IDNum: bodyshop.entegral_id, @@ -233,7 +239,7 @@ exports.default = async (req, res) => { // VendorCode: "C", // EstimateDocumentID: "1223HJ76", }, - //RepairOrderType: "DRP", + RepairOrderType: "DirectRepairProgram", //Need to get from Entegral //ReferralSourceType: "Yellow Pages", VehicleInfo: { VINInfo: { @@ -241,9 +247,9 @@ exports.default = async (req, res) => { VINNum: job.v_vin, }, }, - License: { - LicensePlateNum: job.plate_no, - }, + // License: { + // LicensePlateNum: job.plate_no, + // }, VehicleDesc: { //ProductionDate: "2009-10", ModelYear: @@ -255,23 +261,23 @@ exports.default = async (req, res) => { MakeDesc: job.v_make_desc, ModelName: job.v_model_desc, }, - Paint: { - Exterior: { - Color: { - ColorName: job.v_color, - // OEMColorCode: "1M3", - }, - }, - }, + // Paint: { + // Exterior: { + // Color: { + // ColorName: job.v_color, + // // OEMColorCode: "1M3", + // }, + // }, + // }, // Body: { // BodyStyle: "2 Door Convertible", // Trim: { // TrimCode: "1B3", // }, // }, - Condition: { - DrivableInd: job.driveable ? "Y" : "N", - }, + // Condition: { + // DrivableInd: job.driveable ? "Y" : "N", + // }, }, ClaimInfo: { ClaimNum: job.clm_no, @@ -296,7 +302,7 @@ exports.default = async (req, res) => { }, }, ProfileInfo: { - //ProfileName: "Shop Standard Rates", + ProfileName: "ImEX", RateInfo: [ { RateType: "PA", @@ -306,7 +312,7 @@ exports.default = async (req, res) => { TaxableInd: true, TaxTierInfo: { TierNum: 1, - Percentage: 0, //TODO Find the best place to take the tax rates for parts. + Percentage: job.parts_tax_rates.PAN.prt_tax_rt * 100, //TODO Find the best place to take the tax rates for parts. }, }, }, @@ -318,7 +324,7 @@ exports.default = async (req, res) => { TaxableInd: true, TaxTierInfo: { TierNum: 1, - Percentage: 0, //TODO Find the best place to take the tax rates for labor. + Percentage: job.parts_tax_rates.PAN.prt_tax_rt * 100, //TODO Find the best place to take the tax rates for labor. }, }, }, @@ -495,56 +501,56 @@ exports.default = async (req, res) => { TotalType: "PAA", TotalTypeDesc: "Aftermarket Parts", TotalAmt: Dinero( - job.job_totals.parts.parts.list.paa && - job.job_totals.parts.parts.list.paa.total + job.job_totals.parts.parts.list.PAA && + job.job_totals.parts.parts.list.PAA.total ).toFormat("0.00"), }, { TotalType: "PAC", TotalTypeDesc: "Re-Chromed Parts", TotalAmt: Dinero( - job.job_totals.parts.parts.list.pac && - job.job_totals.parts.parts.list.pac.total + job.job_totals.parts.parts.list.PAC && + job.job_totals.parts.parts.list.PAC.total ).toFormat("0.00"), }, { TotalType: "PAG", TotalTypeDesc: "Glass Parts", TotalAmt: Dinero( - job.job_totals.parts.parts.list.pag && - job.job_totals.parts.parts.list.pag.total + job.job_totals.parts.parts.list.PAG && + job.job_totals.parts.parts.list.PAG.total ).toFormat("0.00"), }, { TotalType: "PAL", TotalTypeDesc: "LKQ/Used Parts", TotalAmt: Dinero( - job.job_totals.parts.parts.list.pal && - job.job_totals.parts.parts.list.pal.total + job.job_totals.parts.parts.list.PAL && + job.job_totals.parts.parts.list.PAL.total ).toFormat("0.00"), }, { TotalType: "PAM", TotalTypeDesc: "Remanufactured Parts", TotalAmt: Dinero( - job.job_totals.parts.parts.list.pam && - job.job_totals.parts.parts.list.pam.total + job.job_totals.parts.parts.list.PAM && + job.job_totals.parts.parts.list.PAM.total ).toFormat("0.00"), }, { TotalType: "PAN", TotalTypeDesc: "New Parts", TotalAmt: Dinero( - job.job_totals.parts.parts.list.pan && - job.job_totals.parts.parts.list.pan.total + job.job_totals.parts.parts.list.PAN && + job.job_totals.parts.parts.list.PAN.total ).toFormat("0.00"), }, { TotalType: "PAR", TotalTypeDesc: "Recored Parts", TotalAmt: Dinero( - job.job_totals.parts.parts.list.par && - job.job_totals.parts.parts.list.par.total + job.job_totals.parts.parts.list.PAR && + job.job_totals.parts.parts.list.PAR.total ).toFormat("0.00"), }, ], @@ -616,6 +622,14 @@ exports.default = async (req, res) => { "0.00" ), }, + { + TotalType: "TOT", + TotalSubType: "SM", + TotalTypeDesc: "Supplement Total", + TotalAmt: job.cieca_ttl + ? job.cieca_ttl.data.supp_amt + : Dinero().toFormat("0.00"), + }, { TotalType: "TOT", TotalSubType: "F7", @@ -632,12 +646,14 @@ exports.default = async (req, res) => { "0.00" ), }, - // { - // TotalType: "TOT", - // TotalSubType: "SM", - // TotalTypeDesc: "Supplement Total", - // TotalAmt: 0, - // }, + { + TotalType: "TOT", + TotalSubType: "D8", + TotalTypeDesc: "Bottom Line Discount", + TotalAmt: Dinero( + job.job_totals.additional.adjustments + ).toFormat("0.00"), + }, { TotalType: "TOT", TotalSubType: "D2", @@ -658,15 +674,13 @@ exports.default = async (req, res) => { TotalType: "TOT", TotalSubType: "AA", TotalTypeDesc: "Appearance Allowance", - TotalAmt: 0, + TotalAmt: Dinero().toFormat("0.00"), }, { TotalType: "TOT", - TotalSubType: "D8", - TotalTypeDesc: "Bottom Line Discount", - TotalAmt: Dinero( - job.job_totals.additional.adjustments - ).toFormat("0.00"), + TotalSubType: "DEPOSIT", + TotalTypeDesc: "Deposit", + TotalAmt: Dinero().toFormat("0.00"), }, { TotalType: "TOT", @@ -676,12 +690,6 @@ exports.default = async (req, res) => { .subtract(Dinero(job.job_totals.totals.custPayable.total)) .toFormat("0.00"), }, - // { - // TotalType: "TOT", - // TotalSubType: "DEPOSIT", - // TotalTypeDesc: "Deposit", - // TotalAmt: 0, - // }, { TotalType: "TOT", TotalSubType: "CUST", @@ -691,7 +699,7 @@ exports.default = async (req, res) => { ).toFormat("0.00"), }, ], - RepairTotalsType: 1, + // RepairTotalsType: 1, }, // RepairLabor: { // LaborAllocations: { @@ -741,7 +749,7 @@ exports.default = async (req, res) => { // }, ProductionStatus: { ProductionStage: { - ProductionStageCode: GetProductionStageCode(job), + ProductionStageCode: GetProductionStageCode(job, bodyshop), ProductionStageDateTime: moment().format(momentFormat), // ProductionStageStatusComment: // "Going to be painted this afternoon", @@ -777,6 +785,9 @@ exports.default = async (req, res) => { // }, // }, }; + + deleteNullKeys(obj); + jobsToPush.push(obj); }); if (erroredJobs.length > 0) { @@ -784,13 +795,12 @@ exports.default = async (req, res) => { count: erroredJobs.length, jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number)), }); + allErrors = [...allErrors, ...erroredJobs]; } logger.log("arms-end-shop-extract", "DEBUG", "api", bodyshop.id, { shopname: bodyshop.shopname, }); - const abc = ret[1]; - deleteNullKeys(abc); try { const entegralSoapClient = await soap.createClientAsync( @@ -798,7 +808,7 @@ exports.default = async (req, res) => { { ignoredNamespaces: true, wsdl_options: { - useEmptyTag: true, + // useEmptyTag: true, }, wsdl_headers: { Authorization: `Basic ${new Buffer.from( @@ -816,14 +826,20 @@ exports.default = async (req, res) => { ); const entegralResponse = - await entegralSoapClient.RepairOrderFolderAddRqAsync(abc); + await entegralSoapClient.RepairOrderFolderAddRqAsync( + jobsToPush, + function (err, result, rawResponse, soapHeader, rawRequest) { + fs.writeFileSync(`./logs/arms-request.xml`, rawRequest); + fs.writeFileSync(`./logs/arms-response.xml`, rawResponse); + + res.json(err || result); + } + ); const [result, rawResponse, , rawRequest] = entegralResponse; - console.log("🚀 ~ file: arms.js ~ line 806 ~ result", result); - res.json({ result, obj: abc }); } catch (error) { + fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml); console.log(error); - res.json(error); } } catch (error) { //Error at the shop level. @@ -846,15 +862,18 @@ exports.default = async (req, res) => { } } - // res.sendStatus(200); + res.sendStatus(200); } catch (error) { res.status(200).json(error); } }; function GetSupplementNumber(joblines) { - return 0; - return _.max(joblines.map((jl) => jl.line_ind)); + const max = _.max( + joblines.map((jl) => parseInt((jl.line_ind || "0").replace(/[^\d.-]/g, ""))) + ); + + return max || 0; } function GetDocumentstatus(job, bodyshop) { @@ -873,7 +892,9 @@ function GetDocumentstatus(job, bodyshop) { function GetRepairStatusCode(job) { return "25"; } -function GetProductionStageCode(job) { +function GetProductionStageCode(job, bodyshop) { + if (bodyshop.md_ro_statuses.post_production_statuses.includes(job.status)) + return "8D"; return "33"; } diff --git a/server/data/autohouse.js b/server/data/autohouse.js index 60e250965..d4def25b8 100644 --- a/server/data/autohouse.js +++ b/server/data/autohouse.js @@ -37,6 +37,7 @@ exports.default = async (req, res) => { //Query for the List of Bodyshop Clients. logger.log("autohouse-start", "DEBUG", "api", null, null); const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS); + 639; const allxmlsToUpload = []; const allErrors = []; @@ -47,16 +48,19 @@ exports.default = async (req, res) => { }); const erroredJobs = []; try { - const { jobs } = await client.request(queries.AUTOHOUSE_QUERY, { - bodyshopid: bodyshop.id, - start: moment().subtract(3, "days").startOf("day"), - }); + const { jobs, bodyshops_by_pk } = await client.request( + queries.AUTOHOUSE_QUERY, + { + bodyshopid: bodyshop.id, + start: moment().subtract(3, "days").startOf("day"), + } + ); const autoHouseObject = { AutoHouseExport: { RepairOrder: jobs.map((j) => CreateRepairOrderTag( - { ...j, bodyshop }, + { ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) { erroredJobs.push({ job: job, error: error.toString() }); } @@ -113,6 +117,14 @@ exports.default = async (req, res) => { }); } } + + for (const xmlObj of allxmlsToUpload) { + fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml); + } + + res.json(allxmlsToUpload); + return; + let sftp = new Client(); sftp.on("error", (errors) => logger.log("autohouse-sftp-error", "ERROR", "api", null, { @@ -395,21 +407,23 @@ const CreateRepairOrderTag = (job, errorCallback) => { // AmountDue: null, // }, RevisedTotals: { - BodyHours: job.job_totals.rates.lab.hours, + BodyHours: job.job_totals.rates.lab.hours.toFixed(2), BodyRepairHours: job.joblines .filter((line) => repairOpCodes.includes(line.lbr_op)) - .reduce((acc, val) => acc + val.mod_lb_hrs, 0), + .reduce((acc, val) => acc + val.mod_lb_hrs, 0) + .toFixed(2), BodyReplaceHours: job.joblines .filter((line) => replaceOpCodes.includes(line.lbr_op)) - .reduce((acc, val) => acc + val.mod_lb_hrs, 0), - RefinishHours: job.job_totals.rates.lar.hours, - MechanicalHours: job.job_totals.rates.lam.hours, - StructuralHours: job.job_totals.rates.las.hours, + .reduce((acc, val) => acc + val.mod_lb_hrs, 0) + .toFixed(2), + RefinishHours: job.job_totals.rates.lar.hours.toFixed(2), + MechanicalHours: job.job_totals.rates.lam.hours.toFixed(2), + StructuralHours: job.job_totals.rates.las.hours.toFixed(2), - ElectricalHours: job.job_totals.rates.lae.hours, - FrameHours: job.job_totals.rates.laf.hours, - GlassHours: job.job_totals.rates.lag.hours, - DetailHours: job.job_totals.rates.lad.hours, + ElectricalHours: job.job_totals.rates.lae.hours.toFixed(2), + FrameHours: job.job_totals.rates.laf.hours.toFixed(2), + GlassHours: job.job_totals.rates.lag.hours.toFixed(2), + DetailHours: job.job_totals.rates.lad.hours.toFixed(2), LaborMiscHours: 0, PartsTotal: Dinero(job.job_totals.parts.parts.total).toFormat( @@ -500,11 +514,11 @@ const CreateRepairOrderTag = (job, errorCallback) => { PMTotal: Dinero(job.job_totals.rates.mapa.total).toFormat( AHDineroFormat ), - PMTotalCost: 0, + PMTotalCost: repairCosts.PMTotalCost.toFormat(AHDineroFormat), BMTotal: Dinero(job.job_totals.rates.mash.total).toFormat( AHDineroFormat ), - BMTotalCost: 0, + BMTotalCost: repairCosts.BMTotalCost.toFormat(AHDineroFormat), MiscTotal: 0, MiscTotalCost: 0, TowingTotal: Dinero(job.job_totals.additional.towing).toFormat( @@ -721,7 +735,7 @@ const StatusMapping = (status, md_ro_statuses) => { else if (status === default_delivered) return "DEL"; else if (status === default_invoiced || status === default_exported) return "CLO"; - else if (status === default_void) return "CLO"; + else if (status === default_void) return "VOID"; else if (md_ro_statuses.production_statuses.includes(status)) return "IPR"; else return "UNDEFINED"; @@ -731,12 +745,7 @@ const StatusMapping = (status, md_ro_statuses) => { const GenerateDetailLines = (line, statuses) => { const ret = { BackOrdered: line.status === statuses.default_bo ? "1" : "0", - Cost: - (line.billlines[0] && - (line.billlines[0].actual_cost * line.billlines[0].quantity).toFixed( - 2 - )) || - 0, + Cost: (line.billlines[0] && line.billlines[0].actual_cost.toFixed(2)) || 0, //Critical: null, Description: line.line_desc || "", DiscountMarkup: line.prt_dsmk_m || 0, @@ -744,27 +753,39 @@ const GenerateDetailLines = (line, statuses) => { IOUPart: 0, LineNumber: line.line_no || 0, MarkUp: null, - OrderedOn: null, + OrderedOn: + (line.parts_order_lines[0] && + moment(line.parts_order_lines[0].parts_order.order_date).format( + AhDateFormat + )) || + "", OriginalCost: null, OriginalInvoiceNumber: null, - PriceEach: (line.billlines[0] && line.billlines[0].retail_price) || 0, + PriceEach: line.act_price || 0, PartNumber: _.escape(line.oem_partno), ProfitPercent: null, PurchaseOrderNumber: null, Qty: line.part_qty || 0, Status: line.status || "", - SupplementNumber: line.line_ind || "", + SupplementNumber: line.line_ind ? line.line_ind.replace(/[^\d.-]/g, "") : 0, Type: line.part_type || "", Vendor: (line.billlines[0] && line.billlines[0].bill.vendor.name) || "", VendorPaid: null, - VendorPrice: (line.billlines[0] && line.billlines[0].actual_price) || 0, + VendorPrice: + (line.billlines[0] && + (line.billlines[0].actual_price * line.billlines[0].quantity).toFixed( + 2 + )) || + 0, Deleted: null, ExpectedOn: null, - ReceivedOn: null, + ReceivedOn: + line.billlines[0] && + moment(line.billlines[0].bill.date).format(AhDateFormat), OrderedBy: null, ShipVia: null, VendorContact: null, - EstimateAmount: line.act_price * line.part_qty || 0, //Rebecca + EstimateAmount: (line.act_price * line.part_qty).toFixed(2) || 0, //Rebecca }; return ret; }; @@ -780,7 +801,7 @@ const generateNullDetailLine = () => { IOUPart: 0, LineNumber: 0, MarkUp: null, - OrderedOn: null, + OrderedOn: "", OriginalCost: null, OriginalInvoiceNumber: null, PriceEach: 0, diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 3ed050f80..9be777295 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -543,6 +543,23 @@ exports.QUERY_EMPLOYEE_PIN = `query QUERY_EMPLOYEE_PIN($shopId: uuid!, $employee }`; exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshopid: uuid!) { + bodyshops_by_pk(id: $bodyshopid){ + + id + shopname + address1 + city + state + zip_post + country + phone + md_ro_statuses + md_order_statuses + autohouseid + md_responsibility_centers + jc_hourly_rates + + } jobs(where: {_and: [{converted: {_eq: true}}, {updated_at: {_gt: $start}}, {shopid: {_eq: $bodyshopid}}]}) { id ro_number @@ -608,21 +625,7 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop job_totals driveable parts_tax_rates - bodyshop { - id - shopname - address1 - city - state - zip_post - country - phone - md_ro_statuses - md_order_statuses - autohouseid - md_responsibility_centers - jc_hourly_rates - } + joblines(where: {removed: {_eq: false}}) { id line_no @@ -642,7 +645,13 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop lbr_op profitcenter_part profitcenter_labor - billlines(order_by: {bill: {date: desc_nulls_last}}) { + parts_order_lines(order_by: {parts_order: {order_date: desc_nulls_last}} limit: 1){ + parts_order{ + id + order_date + } + } + billlines(order_by: {bill: {date: desc_nulls_last}} limit: 1) { actual_cost actual_price quantity @@ -651,6 +660,7 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop name } invoice_number + date } } } @@ -695,8 +705,6 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop } } } - - `; exports.ENTEGRAL_EXPORT = ` @@ -783,6 +791,8 @@ query ENTEGRAL_EXPORT($bodyshopid: uuid!) { rate_matd job_totals ded_amt + cieca_ttl + adjustment_bottom_line } } `; @@ -1030,6 +1040,7 @@ exports.QUERY_JOB_COSTING_DETAILS = ` query QUERY_JOB_COSTING_DETAILS($id: uuid! status ca_bc_pvrt ca_customer_gst + dms_allocation joblines(where: { removed: { _eq: false } }) { id db_ref @@ -1073,11 +1084,14 @@ exports.QUERY_JOB_COSTING_DETAILS = ` query QUERY_JOB_COSTING_DETAILS($id: uuid! actualhrs productivehrs flat_rate + ciecacode } bodyshop{ id md_responsibility_centers jc_hourly_rates + cdk_dealerid + pbs_serialnumber } } }`; @@ -1133,6 +1147,7 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT status ca_bc_pvrt ca_customer_gst + dms_allocation joblines(where: {removed: {_eq: false}}) { id db_ref @@ -1176,11 +1191,14 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT actualhrs productivehrs flat_rate + ciecacode } bodyshop { id md_responsibility_centers jc_hourly_rates + cdk_dealerid + pbs_serialnumber } } } diff --git a/server/job/job-costing.js b/server/job/job-costing.js index 7756810f6..e5c4c5883 100644 --- a/server/job/job-costing.js +++ b/server/job/job-costing.js @@ -261,7 +261,7 @@ function GenerateCostingData(job) { val.profitcenter_labor || defaultProfits[val.mod_lbr_ty] || "?"; if (laborProfitCenter === "?") - console.log("Unknown type", val.mod_lbr_ty); + console.log("Unknown type", val.line_desc, val.mod_lbr_ty); const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`; const laborAmount = Dinero({ @@ -285,11 +285,12 @@ function GenerateCostingData(job) { val.profitcenter_part || defaultProfits[val.part_type] || "?"; if (partsProfitCenter === "?") - console.log("Unknown type", val.part_type); + console.log("Unknown type", val.line_desc, val.part_type); if (!partsProfitCenter) console.log( "Unknown cost/profit center mapping for parts.", + val.line_desc, val.part_type ); const partsAmount = Dinero({ @@ -298,13 +299,13 @@ function GenerateCostingData(job) { .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) + ? 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.parts[partsProfitCenter]) acc.parts[partsProfitCenter] = Dinero(); @@ -322,7 +323,7 @@ function GenerateCostingData(job) { "?"; if (partsProfitCenter === "?") { - console.log("Unknown type", val.part_type); + console.log("Unknown type", val.line_desc, val.part_type); } else { const partsAmount = Dinero({ amount: Math.round((val.act_price || 0) * 100), @@ -330,13 +331,13 @@ function GenerateCostingData(job) { .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) + ? 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.parts[partsProfitCenter]) @@ -372,21 +373,43 @@ function GenerateCostingData(job) { ); } + //Is it a DMS Setup? + const selectedDmsAllocationConfig = + (job.bodyshop.md_responsibility_centers.dms_defaults && + job.bodyshop.md_responsibility_centers.dms_defaults.find( + (d) => d.name === job.dms_allocation + )) || + job.bodyshop.md_responsibility_centers.defaults; + const billTotalsByCostCenters = job.bills.reduce((bill_acc, bill_val) => { //At the bill level. bill_val.billlines.map((line_val) => { //At the bill line level. - //console.log("JobCostingPartsTable -> line_val", line_val); - if (!bill_acc[line_val.cost_center]) - bill_acc[line_val.cost_center] = Dinero(); + if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid) { + if (!bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]]) + bill_acc[selectedDmsAllocationConfig.costs[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) - ); + bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] = + bill_acc[selectedDmsAllocationConfig.costs[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(); + + 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; }); @@ -443,20 +466,42 @@ function GenerateCostingData(job) { const ticketTotalsByCostCenter = 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.flat_rate - ? ticket_val.productivehrs || ticket_val.actualhrs || 0 - : ticket_val.actualhrs || ticket_val.productivehrs || 0 - ) //Should base this on the employee. - ); + if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid) { + if ( + !ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] + ) + ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] = + Dinero(); + + ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] = + ticket_acc[ + selectedDmsAllocationConfig.costs[ticket_val.ciecacode] + ].add( + Dinero({ + amount: Math.round((ticket_val.rate || 0) * 100), + }).multiply( + ticket_val.flat_rate + ? ticket_val.productivehrs || ticket_val.actualhrs || 0 + : ticket_val.actualhrs || ticket_val.productivehrs || 0 + ) //Should base this on the employee. + ); + } else { + 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.flat_rate + ? ticket_val.productivehrs || ticket_val.actualhrs || 0 + : ticket_val.actualhrs || ticket_val.productivehrs || 0 + ) //Should base this on the employee. + ); + } return ticket_acc; }, diff --git a/server/job/job-totals.js b/server/job/job-totals.js index 7262a332a..57fe94c87 100644 --- a/server/job/job-totals.js +++ b/server/job/job-totals.js @@ -271,7 +271,8 @@ function CalculatePartsTotals(jobLines) { .multiply(value.part_qty || 0) .add( (value.db_ref === "900511" || - value.db_ref === "900510") && + value.db_ref === "900510" || + value.db_ref === "900500") && value.prt_dsmk_m && value.prt_dsmk_m !== 0 ? Dinero({ amount: Math.round(value.prt_dsmk_m * 100) }) @@ -298,7 +299,9 @@ function CalculatePartsTotals(jobLines) { parts: { ...acc.parts, prt_dsmk_total: acc.parts.prt_dsmk_total.add( - (value.db_ref === "900511" || value.db_ref === "900510") && + (value.db_ref === "900511" || + value.db_ref === "900510" || + value.db_ref === "900500") && value.prt_dsmk_m && value.prt_dsmk_m !== 0 ? Dinero({ amount: Math.round(value.prt_dsmk_m * 100) }) @@ -344,7 +347,9 @@ function CalculatePartsTotals(jobLines) { }).multiply(value.part_qty || 0) ) .add( - (value.db_ref === "900511" || value.db_ref === "900510") && + (value.db_ref === "900511" || + value.db_ref === "900510" || + value.db_ref === "900500") && value.prt_dsmk_m && value.prt_dsmk_m !== 0 ? Dinero({ amount: Math.round(value.prt_dsmk_m * 100) }) diff --git a/server/web-sockets/web-socket.js b/server/web-sockets/web-socket.js index 315cf1c10..e4f3febf9 100644 --- a/server/web-sockets/web-socket.js +++ b/server/web-sockets/web-socket.js @@ -17,7 +17,10 @@ const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default; const { isArray } = require("lodash"); const logger = require("../utils/logger"); -const {default: PbsExportJob, PbsSelectedCustomer} = require("../accounting/pbs/pbs-job-export"); +const { + default: PbsExportJob, + PbsSelectedCustomer, +} = require("../accounting/pbs/pbs-job-export"); io.use(function (socket, next) { try { @@ -159,6 +162,37 @@ function createLogEvent(socket, level, message) { } } +function createJsonEvent(socket, level, message, json) { + if (LogLevelHierarchy(socket.log_level) >= LogLevelHierarchy(level)) { + console.log( + `[WS LOG EVENT] ${level} - ${new Date()} - ${socket.user.email} - ${ + socket.id + } - ${message}` + ); + socket.emit("log-event", { + timestamp: new Date(), + level, + message, + }); + + logger.log("ws-log-event-json", level, socket.user.email, socket.recordid, { + wsmessage: message, + json + }); + + if (socket.logEvents && isArray(socket.logEvents)) { + socket.logEvents.push({ + timestamp: new Date(), + level, + message, + }); + } + // if (level === "ERROR") { + // throw new Error(message); + // } + } +} + function createXmlEvent(socket, xml, message, isError = false) { if (LogLevelHierarchy(socket.log_level) >= LogLevelHierarchy("TRACE")) { socket.emit("log-event", { @@ -210,3 +244,4 @@ function LogLevelHierarchy(level) { exports.createLogEvent = createLogEvent; exports.createXmlEvent = createXmlEvent; +exports.createJsonEvent = createJsonEvent;