diff --git a/client/src/components/email-overlay/email-overlay.component.jsx b/client/src/components/email-overlay/email-overlay.component.jsx index a5b439da0..3037322e8 100644 --- a/client/src/components/email-overlay/email-overlay.component.jsx +++ b/client/src/components/email-overlay/email-overlay.component.jsx @@ -41,7 +41,7 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b const emailsToMenu = { items: [ ...bodyshop.employees - .filter((e) => e.user_email) + .filter((e) => e.user_email && e.active === true) .map((e, idx) => ({ key: idx, label: `${e.first_name} ${e.last_name}`, @@ -59,7 +59,7 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b const menuCC = { items: [ ...bodyshop.employees - .filter((e) => e.user_email) + .filter((e) => e.user_email && e.active === true) .map((e, idx) => ({ key: idx, label: `${e.first_name} ${e.last_name}`, diff --git a/client/src/components/job-detail-lines/job-lines.component.jsx b/client/src/components/job-detail-lines/job-lines.component.jsx index d5df5480e..16a3d6c87 100644 --- a/client/src/components/job-detail-lines/job-lines.component.jsx +++ b/client/src/components/job-detail-lines/job-lines.component.jsx @@ -33,7 +33,7 @@ import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-re import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import _ from "lodash"; import { FaTasks } from "react-icons/fa"; -import { selectBodyshop } from "../../redux/user/user.selectors"; +import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors"; import dayjs from "../../utils/day"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; @@ -49,6 +49,7 @@ import JobLinesPartPriceChange from "./job-lines-part-price-change.component"; import JobLinesExpanderSimple from "./jobs-lines-expander-simple.component"; import { logImEXEvent } from "../../firebase/firebase.utils"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component.jsx"; const UPDATE_JOB_LINES_LOCATION_BULK = gql` mutation UPDATE_JOB_LINES_LOCATION_BULK($ids: [uuid!]!, $location: String!) { @@ -66,7 +67,8 @@ const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, jobRO: selectJobReadOnly, technician: selectTechnician, - isPartsEntry: selectIsPartsEntry + isPartsEntry: selectIsPartsEntry, + authLevel: selectAuthLevel }); const mapDispatchToProps = (dispatch) => ({ @@ -94,7 +96,8 @@ export function JobLinesComponent({ setTaskUpsertContext, billsQuery, handlePartsOrderOnRowClick, - isPartsEntry + isPartsEntry, + authLevel }) { const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK); const [bulkUpdateLocations] = useMutation(UPDATE_JOB_LINES_LOCATION_BULK); @@ -386,18 +389,20 @@ export function JobLinesComponent({ key: "actions", render: (text, record) => ( - {(record.manual_line || jobIsPrivate) && !technician && ( - - {!isPartsEntry && ( + {!isPartsEntry && HasRbacAccess({ bodyshop, authLevel, action: "jobs:manual-line" }) && ( + ); diff --git a/client/src/components/jobs-admin-dates/jobs-admin-dates.component.jsx b/client/src/components/jobs-admin-dates/jobs-admin-dates.component.jsx index d6cd0f5ef..6f2be05e6 100644 --- a/client/src/components/jobs-admin-dates/jobs-admin-dates.component.jsx +++ b/client/src/components/jobs-admin-dates/jobs-admin-dates.component.jsx @@ -157,7 +157,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) { - diff --git a/client/src/components/jobs-admin-owner-reassociate/jobs-admin-owner-reassociate.component.jsx b/client/src/components/jobs-admin-owner-reassociate/jobs-admin-owner-reassociate.component.jsx index 3487ad672..0b4cde1e0 100644 --- a/client/src/components/jobs-admin-owner-reassociate/jobs-admin-owner-reassociate.component.jsx +++ b/client/src/components/jobs-admin-owner-reassociate/jobs-admin-owner-reassociate.component.jsx @@ -54,7 +54,7 @@ export default function JobAdminOwnerReassociate({ job }) {
{t("jobs.labels.associationwarning")}
- diff --git a/client/src/components/jobs-admin-vehicle-reassociate/jobs-admin-vehicle-reassociate.component.jsx b/client/src/components/jobs-admin-vehicle-reassociate/jobs-admin-vehicle-reassociate.component.jsx index 79ebe9aa0..565bd403c 100644 --- a/client/src/components/jobs-admin-vehicle-reassociate/jobs-admin-vehicle-reassociate.component.jsx +++ b/client/src/components/jobs-admin-vehicle-reassociate/jobs-admin-vehicle-reassociate.component.jsx @@ -54,7 +54,7 @@ export default function JobAdminOwnerReassociate({ job }) {
{t("jobs.labels.associationwarning")}
- diff --git a/client/src/components/jobs-close-lines/jobs-close-lines.component.jsx b/client/src/components/jobs-close-lines/jobs-close-lines.component.jsx index e737173e6..6b52cbeee 100644 --- a/client/src/components/jobs-close-lines/jobs-close-lines.component.jsx +++ b/client/src/components/jobs-close-lines/jobs-close-lines.component.jsx @@ -138,7 +138,7 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) { showSearch={{ optionFilterProp: "children", filterOption: (input, option) => - option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 + option?.value?.toLowerCase().indexOf(input?.toLowerCase()) >= 0 }} disabled={jobRO} options={bodyshop.md_responsibility_centers.profits.map((p) => ({ @@ -166,7 +166,7 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) { showSearch={{ optionFilterProp: "children", filterOption: (input, option) => - option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 + option?.value?.toLowerCase().indexOf(input?.toLowerCase()) >= 0 }} disabled={jobRO} options={bodyshop.md_responsibility_centers.profits.map((p) => ({ diff --git a/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx b/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx index 050fb2ec0..9c06ce4e2 100644 --- a/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx +++ b/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx @@ -70,6 +70,12 @@ export function PartsOrderListTableComponent({ const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER); const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : []; + + const enrichedPartsOrders = parts_orders.map((order) => ({ + ...order, + invoice_number: order.bill?.invoice_number + })); + const { refetch } = billsQuery; const recordActions = (record, showView = false) => ( @@ -222,7 +228,12 @@ export function PartsOrderListTableComponent({ dataIndex: "order_number", key: "order_number", sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number), - sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order + sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order, + render: (text, record) => ( + + {record.order_number} {record.invoice_number && `(${record.invoice_number})`} + + ) }, { title: t("parts_orders.fields.order_date"), @@ -272,10 +283,10 @@ export function PartsOrderListTableComponent({ setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); }; - const filteredPartsOrders = parts_orders + const filteredPartsOrders = enrichedPartsOrders ? searchText === "" - ? parts_orders - : parts_orders.filter( + ? enrichedPartsOrders + : enrichedPartsOrders.filter( (b) => (b.order_number || "").toString().toLowerCase().includes(searchText.toLowerCase()) || (b.vendor.name || "").toLowerCase().includes(searchText.toLowerCase()) diff --git a/client/src/components/production-list-columns/production-list-columns.comment.component.jsx b/client/src/components/production-list-columns/production-list-columns.comment.component.jsx index b2e730842..10d084cb7 100644 --- a/client/src/components/production-list-columns/production-list-columns.comment.component.jsx +++ b/client/src/components/production-list-columns/production-list-columns.comment.component.jsx @@ -1,7 +1,7 @@ import Icon from "@ant-design/icons"; import { useMutation } from "@apollo/client/react"; import { Button, Input, Popover, Tooltip } from "antd"; -import { useState } from "react"; +import { useState, useRef } from "react"; import { useTranslation } from "react-i18next"; import { FaRegStickyNote } from "react-icons/fa"; import { UPDATE_JOB } from "../../graphql/jobs.queries"; @@ -9,10 +9,10 @@ import { logImEXEvent } from "../../firebase/firebase.utils"; export default function ProductionListColumnComment({ record, usePortal = false }) { const { t } = useTranslation(); - const [note, setNote] = useState(record.comment || ""); - const [open, setOpen] = useState(false); + const textAreaRef = useRef(null); + const rafIdRef = useRef(null); const [updateAlert] = useMutation(UPDATE_JOB); @@ -38,23 +38,35 @@ export default function ProductionListColumnComment({ record, usePortal = false }; const handleOpenChange = (flag) => { + if (rafIdRef.current) { + cancelAnimationFrame(rafIdRef.current); + rafIdRef.current = null; + } setOpen(flag); - if (flag) setNote(record.comment || ""); + if (flag) { + setNote(record.comment || ""); + rafIdRef.current = requestAnimationFrame(() => { + rafIdRef.current = null; + if (textAreaRef.current?.focus) { + try { + textAreaRef.current.focus({ preventScroll: true }); + } catch { + textAreaRef.current.focus(); + } + } + }); + } }; const content = ( -
e.stopPropagation()} - onPointerDown={(e) => e.stopPropagation()} - > +
e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}> @@ -67,13 +79,13 @@ export default function ProductionListColumnComment({ record, usePortal = false ); return ( - trigger.parentElement || document.body } : {})} >
{ + if (rafIdRef.current) { + cancelAnimationFrame(rafIdRef.current); + rafIdRef.current = null; + } setOpen(flag); - if (flag) setNote(record.production_vars?.note || ""); + if (flag) { + setNote(record.production_vars?.note || ""); + rafIdRef.current = requestAnimationFrame(() => { + rafIdRef.current = null; + if (textAreaRef.current?.focus) { + try { + textAreaRef.current.focus({ preventScroll: true }); + } catch { + textAreaRef.current.focus(); + } + } + }); + } }, [record] ); const content = ( -
e.stopPropagation()} - onPointerDown={(e) => e.stopPropagation()} - > +
e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}> @@ -96,13 +110,13 @@ function ProductionListColumnProductionNote({ record, setNoteUpsertContext, useP ); return ( - trigger.parentElement || document.body } : {})} >
, + + + , ({ }); export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) { - const breakpoints = Grid.useBreakpoint(); - const selectedBreakpoint = Object.entries(breakpoints) - .filter(([, isOn]) => !!isOn) - .slice(-1)[0]; + const screens = Grid.useBreakpoint(); const bpoints = { xs: "100%", @@ -36,10 +33,16 @@ export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) { md: "100%", lg: "100%", xl: "90%", - xxl: "85%" + xxl: "90%" }; - const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%"; + let drawerPercentage = "100%"; + if (screens.xxl) drawerPercentage = bpoints.xxl; + else if (screens.xl) drawerPercentage = bpoints.xl; + else if (screens.lg) drawerPercentage = bpoints.lg; + else if (screens.md) drawerPercentage = bpoints.md; + else if (screens.sm) drawerPercentage = bpoints.sm; + else if (screens.xs) drawerPercentage = bpoints.xs; const location = useLocation(); const history = useNavigate(); diff --git a/client/src/graphql/bills.queries.js b/client/src/graphql/bills.queries.js index f7cad4209..36f3b2f73 100644 --- a/client/src/graphql/bills.queries.js +++ b/client/src/graphql/bills.queries.js @@ -91,6 +91,10 @@ export const QUERY_PARTS_BILLS_BY_JOBID = gql` order_number comments user_email + bill { + id + invoice_number + } } parts_dispatch(where: { jobid: { _eq: $jobid } }) { id diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index 21d9522b1..35a3a1f26 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -1375,6 +1375,9 @@ export const QUERY_JOB_FOR_DUPE = gql` agt_ph2x area_of_damage cat_no + cieca_pfl + cieca_pfo + cieca_pft cieca_stl cieca_ttl clm_addr1 @@ -1452,6 +1455,7 @@ export const QUERY_JOB_FOR_DUPE = gql` labor_rate_desc labor_rate_id local_tax_rate + materials other_amount_payable owner_owing ownerid diff --git a/client/src/pages/jobs-create/jobs-create.component.jsx b/client/src/pages/jobs-create/jobs-create.component.jsx index d760aa0cc..7d48d035a 100644 --- a/client/src/pages/jobs-create/jobs-create.component.jsx +++ b/client/src/pages/jobs-create/jobs-create.component.jsx @@ -10,14 +10,12 @@ import JobsCreateOwnerInfoContainer from "../../components/jobs-create-owner-inf import JobsCreateVehicleInfoContainer from "../../components/jobs-create-vehicle-info/jobs-create-vehicle-info.container"; import JobCreateContext from "../../pages/jobs-create/jobs-create.context"; -export default function JobsCreateComponent({ form }) { +export default function JobsCreateComponent({ form, isSubmitting }) { const [pageIndex, setPageIndex] = useState(0); - const [errorMessage, setErrorMessage] = useState(null); - const [state] = useContext(JobCreateContext); - const { t } = useTranslation(); + const steps = [ { title: t("jobs.labels.create.vehicleinfo"), @@ -42,11 +40,9 @@ export default function JobsCreateComponent({ form }) { const next = () => { setPageIndex(pageIndex + 1); - console.log("Next"); }; const prev = () => { setPageIndex(pageIndex - 1); - console.log("Previous"); }; const ProgressButtons = ({ top }) => { @@ -79,17 +75,19 @@ export default function JobsCreateComponent({ form }) { {pageIndex === steps.length - 1 && ( )} @@ -146,13 +144,11 @@ export default function JobsCreateComponent({ form }) { ) : (
- {errorMessage ? (
) : null} - {steps.map((item, idx) => (
{ notification.error({ title: t("jobs.errors.creating", { error: error }) }); setState({ ...state, error: error }); + setIsSubmitting(false); }); }; const handleFinish = (values) => { + setIsSubmitting(true); let job = Object.assign( {}, values, @@ -297,7 +301,7 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, curr }) }} > - + diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 02e952d61..e216b9cd5 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -519,6 +519,7 @@ "list-active": "Jobs -> List Active", "list-all": "Jobs -> List All", "list-ready": "Jobs -> List Ready", + "manual-line": "Jobs -> Manual Line", "partsqueue": "Jobs -> Parts Queue", "void": "Jobs -> Void" }, @@ -1295,6 +1296,7 @@ "delete": "Delete", "deleteall": "Delete All", "deselectall": "Deselect All", + "done": "Done", "download": "Download", "edit": "Edit", "gotoadmin": "Go to Admin Panel", @@ -3372,8 +3374,10 @@ "void_ros": "Void ROs", "work_in_progress_committed_labour": "Work in Progress - Committed Labor", "work_in_progress_jobs": "Work in Progress - Jobs", - "work_in_progress_labour": "Work in Progress - Labor", - "work_in_progress_payables": "Work in Progress - Payables" + "work_in_progress_labour": "Work in Progress - Labor (Detail)", + "work_in_progress_labour_summary": "Work in Progress - Labor (Summary)", + "work_in_progress_payables": "Work in Progress - Payables (Detail)", + "work_in_progress_payables_summary": "Work in Progress - Payables (Summary)" } }, "schedule": { diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index f10585f6f..50b336d37 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -519,6 +519,7 @@ "list-active": "", "list-all": "", "list-ready": "", + "manual-line": "", "partsqueue": "", "void": "" }, @@ -1295,6 +1296,7 @@ "delete": "Borrar", "deleteall": "", "deselectall": "", + "done": "", "download": "", "edit": "Editar", "gotoadmin": "", @@ -3373,7 +3375,9 @@ "work_in_progress_committed_labour": "", "work_in_progress_jobs": "", "work_in_progress_labour": "", - "work_in_progress_payables": "" + "work_in_progress_labour_summary": "", + "work_in_progress_payables": "", + "work_in_progress_payables_summary": "" } }, "schedule": { diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 4f7edc58d..727d689f8 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -519,6 +519,7 @@ "list-active": "", "list-all": "", "list-ready": "", + "manual-line": "", "partsqueue": "", "void": "" }, @@ -1295,6 +1296,7 @@ "delete": "Effacer", "deleteall": "", "deselectall": "", + "done": "", "download": "", "edit": "modifier", "gotoadmin": "", @@ -3373,7 +3375,9 @@ "work_in_progress_committed_labour": "", "work_in_progress_jobs": "", "work_in_progress_labour": "", - "work_in_progress_payables": "" + "work_in_progress_labour_summary": "", + "work_in_progress_payables": "", + "work_in_progress_payables_summary": "" } }, "schedule": { diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index 83a6eca4a..4e27d4d6b 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -1717,6 +1717,20 @@ export const TemplateList = (type, context) => { group: "jobs", featureNameRestricted: "timetickets" }, + work_in_progress_labour_summary: { + title: i18n.t("reportcenter.templates.work_in_progress_labour_summary"), + description: "", + subject: i18n.t("reportcenter.templates.work_in_progress_labour_summary"), + key: "work_in_progress_labour_summary", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_open") + }, + group: "jobs", + featureNameRestricted: "timetickets" + }, work_in_progress_committed_labour: { title: i18n.t("reportcenter.templates.work_in_progress_committed_labour"), description: "", @@ -1746,6 +1760,20 @@ export const TemplateList = (type, context) => { group: "jobs", featureNameRestricted: "bills" }, + work_in_progress_payables_summary: { + title: i18n.t("reportcenter.templates.work_in_progress_payables_summary"), + description: "", + subject: i18n.t("reportcenter.templates.work_in_progress_payables_summary"), + key: "work_in_progress_payables_summary", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_open") + }, + group: "jobs", + featureNameRestricted: "bills" + }, lag_time: { title: i18n.t("reportcenter.templates.lag_time"), description: "", diff --git a/server/accounting/qbo/qbo-payables.js b/server/accounting/qbo/qbo-payables.js index 85c989b21..23f538a48 100644 --- a/server/accounting/qbo/qbo-payables.js +++ b/server/accounting/qbo/qbo-payables.js @@ -130,12 +130,13 @@ exports.default = async (req, res) => { async function QueryVendorRecord(oauthClient, qbo_realmId, req, bill) { try { + const url = urlBuilder( + qbo_realmId, + "query", + `select * From vendor where DisplayName = '${StandardizeName(bill.vendor.name)}'` + ); const result = await oauthClient.makeApiCall({ - url: urlBuilder( - qbo_realmId, - "query", - `select * From vendor where DisplayName = '${StandardizeName(bill.vendor.name)}'` - ), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -150,6 +151,11 @@ async function QueryVendorRecord(oauthClient, qbo_realmId, req, bill) { bodyshopid: bill.job.shopid, email: req.user.email }); + logger.log("qbo-payables-query", "DEBUG", req.user.email, null, { + method: "QueryVendorRecord", + call: url, + result: result.json + }); return result.json?.QueryResponse?.Vendor?.[0]; } catch (error) { @@ -167,8 +173,9 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) { DisplayName: StandardizeName(bill.vendor.name) }; try { + const url = urlBuilder(qbo_realmId, "vendor"); const result = await oauthClient.makeApiCall({ - url: urlBuilder(qbo_realmId, "vendor"), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -184,6 +191,12 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) { bodyshopid: bill.job.shopid, email: req.user.email }); + logger.log("qbo-payments-insert", "DEBUG", req.user.email, null, { + method: "InsertVendorRecord", + call: url, + Vendor: Vendor, + result: result.json + }); if (result.status >= 400) { throw new Error(JSON.stringify(result.json.Fault)); @@ -274,11 +287,12 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) VendorRef: { value: vendor.Id }, - ...(vendor.TermRef && !bill.is_credit_memo && { - SalesTermRef: { - value: vendor.TermRef.value - } - }), + ...(vendor.TermRef && + !bill.is_credit_memo && { + SalesTermRef: { + value: vendor.TermRef.value + } + }), TxnDate: moment(bill.date) //.tz(bill.job.bodyshop.timezone) .format("YYYY-MM-DD"), @@ -318,8 +332,9 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) [logKey]: logValue }); try { + const url = urlBuilder(qbo_realmId, bill.is_credit_memo ? "vendorcredit" : "bill"); const result = await oauthClient.makeApiCall({ - url: urlBuilder(qbo_realmId, bill.is_credit_memo ? "vendorcredit" : "bill"), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -335,6 +350,12 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) bodyshopid: bill.job.shopid, email: req.user.email }); + logger.log("qbo-payables-insert", "DEBUG", req.user.email, null, { + method: "InsertBill", + call: url, + postingObj: bill.is_credit_memo ? VendorCredit : billQbo, + result: result.json + }); if (result.status >= 400) { throw new Error(JSON.stringify(result.json.Fault)); diff --git a/server/accounting/qbo/qbo-payments.js b/server/accounting/qbo/qbo-payments.js index 47cdc9c97..dc17908b2 100644 --- a/server/accounting/qbo/qbo-payments.js +++ b/server/accounting/qbo/qbo-payments.js @@ -82,14 +82,7 @@ exports.default = async (req, res) => { if (isThreeTier || (!isThreeTier && twoTierPref === "name")) { //Insert the name/owner and account for whether the source should be the ins co in 3 tier.. - ownerCustomerTier = await QueryOwner( - oauthClient, - qbo_realmId, - req, - payment.job, - isThreeTier, - insCoCustomerTier - ); + ownerCustomerTier = await QueryOwner(oauthClient, qbo_realmId, req, payment.job, insCoCustomerTier); //Query for the owner itself. if (!ownerCustomerTier) { ownerCustomerTier = await InsertOwner( @@ -229,8 +222,9 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef) paymentQbo }); try { + const url = urlBuilder(qbo_realmId, "payment"); const result = await oauthClient.makeApiCall({ - url: urlBuilder(qbo_realmId, "payment"), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -246,6 +240,12 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef) bodyshopid: payment.job.shopid, email: req.user.email }); + logger.log("qbo-payments-insert", "DEBUG", req.user.email, null, { + method: "InsertPayment", + call: url, + paymentQbo: paymentQbo, + result: result.json + }); if (result.status >= 400) { throw new Error(JSON.stringify(result.json.Fault)); @@ -428,8 +428,9 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe paymentQbo }); try { + const url = urlBuilder(qbo_realmId, "creditmemo"); const result = await oauthClient.makeApiCall({ - url: urlBuilder(qbo_realmId, "creditmemo"), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -445,6 +446,12 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe bodyshopid: req.user.bodyshopid, email: req.user.email }); + logger.log("qbo-metadata-query", "DEBUG", req.user.email, null, { + method: "InsertCreditMemo", + call: url, + paymentQbo: paymentQbo, + result: result.json + }); if (result.status >= 400) { throw new Error(JSON.stringify(result.json.Fault)); diff --git a/server/accounting/qbo/qbo-receivables.js b/server/accounting/qbo/qbo-receivables.js index ef64fcdd8..71c935a5f 100644 --- a/server/accounting/qbo/qbo-receivables.js +++ b/server/accounting/qbo/qbo-receivables.js @@ -213,12 +213,13 @@ exports.default = async (req, res) => { async function QueryInsuranceCo(oauthClient, qbo_realmId, req, job) { try { + const url = urlBuilder( + qbo_realmId, + "query", + `select * From Customer where DisplayName = '${StandardizeName(job.ins_co_nm.trim())}' and Active = true` + ); const result = await oauthClient.makeApiCall({ - url: urlBuilder( - qbo_realmId, - "query", - `select * From Customer where DisplayName = '${StandardizeName(job.ins_co_nm.trim())}' and Active = true` - ), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -233,6 +234,11 @@ async function QueryInsuranceCo(oauthClient, qbo_realmId, req, job) { jobid: job.id, email: req.user.email }); + logger.log("qbo-receivables-query", "DEBUG", req.user.email, job.id, { + method: "QueryInsuranceCo", + call: url, + result: result.json + }); return result.json?.QueryResponse?.Customer?.[0]; } catch (error) { @@ -266,8 +272,9 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) { } }; try { + const url = urlBuilder(qbo_realmId, "customer"); const result = await oauthClient.makeApiCall({ - url: urlBuilder(qbo_realmId, "customer"), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -283,6 +290,12 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) { jobid: job.id, email: req.user.email }); + logger.log("qbo-receivables-insert", "DEBUG", req.user.email, job.id, { + method: "InsertInsuranceCo", + call: url, + customerObj: Customer, + result: result.json + }); return result.json?.Customer; } catch (error) { @@ -298,12 +311,13 @@ exports.InsertInsuranceCo = InsertInsuranceCo; async function QueryOwner(oauthClient, qbo_realmId, req, job, parentTierRef) { const ownerName = generateOwnerTier(job, true, null); + const url = urlBuilder( + qbo_realmId, + "query", + `select * From Customer where DisplayName = '${StandardizeName(ownerName)}' and Active = true` + ); const result = await oauthClient.makeApiCall({ - url: urlBuilder( - qbo_realmId, - "query", - `select * From Customer where DisplayName = '${StandardizeName(ownerName)}' and Active = true` - ), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -318,6 +332,11 @@ async function QueryOwner(oauthClient, qbo_realmId, req, job, parentTierRef) { jobid: job.id, email: req.user.email }); + logger.log("qbo-receivables-query", "DEBUG", req.user.email, job.id, { + method: "QueryOwner", + call: url, + result: result.json + }); return result.json?.QueryResponse?.Customer?.find((x) => x.ParentRef?.value === parentTierRef?.Id); } @@ -347,8 +366,9 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare : {}) }; try { + const url = urlBuilder(qbo_realmId, "customer"); const result = await oauthClient.makeApiCall({ - url: urlBuilder(qbo_realmId, "customer"), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -364,6 +384,12 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare jobid: job.id, email: req.user.email }); + logger.log("qbo-receivables-insert", "DEBUG", req.user.email, job.id, { + method: "InsertOwner", + call: url, + customerObj: Customer, + result: result.json + }); return result.json?.Customer; } catch (error) { @@ -378,12 +404,13 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare exports.InsertOwner = InsertOwner; async function QueryJob(oauthClient, qbo_realmId, req, job, parentTierRef) { + const url = urlBuilder( + qbo_realmId, + "query", + `select * From Customer where DisplayName = '${job.ro_number}' and Active = true` + ); const result = await oauthClient.makeApiCall({ - url: urlBuilder( - qbo_realmId, - "query", - `select * From Customer where DisplayName = '${job.ro_number}' and Active = true` - ), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -398,6 +425,11 @@ async function QueryJob(oauthClient, qbo_realmId, req, job, parentTierRef) { jobid: job.id, email: req.user.email }); + logger.log("qbo-receivables-query", "DEBUG", req.user.email, job.id, { + method: "QueryJob", + call: url, + result: result.json + }); const customers = result.json?.QueryResponse?.Customer; return customers && (parentTierRef ? customers.find((x) => x.ParentRef.value === parentTierRef.Id) : customers[0]); @@ -423,8 +455,9 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) { } }; try { + const url = urlBuilder(qbo_realmId, "customer"); const result = await oauthClient.makeApiCall({ - url: urlBuilder(qbo_realmId, "customer"), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -440,6 +473,12 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) { jobid: job.id, email: req.user.email }); + logger.log("qbo-receivables-insert", "DEBUG", req.user.email, job.id, { + method: "InsertJob", + call: url, + customerObj: Customer, + result: result.json + }); if (result.status >= 400) { throw new Error(JSON.stringify(result.json.Fault)); diff --git a/server/cdk/cdk-get-makes.js b/server/cdk/cdk-get-makes.js index 319ecd131..9067f869e 100644 --- a/server/cdk/cdk-get-makes.js +++ b/server/cdk/cdk-get-makes.js @@ -66,7 +66,12 @@ exports.default = async function ReloadCdkMakes(req, res) { } catch (error) { logger.log("cdk-replace-makes-models-error", "ERROR", req.user.email, null, { cdk_dealerid, - error + error: { + message: error?.message, + stack: error?.stack, + name: error?.name, + code: error?.code + } }); res.status(500).json(error); } @@ -105,7 +110,12 @@ async function GetCdkMakes(req, cdk_dealerid) { } catch (error) { logger.log("cdk-replace-makes-models-error", "ERROR", req.user.email, null, { cdk_dealerid, - error + error: { + message: error?.message, + stack: error?.stack, + name: error?.name, + code: error?.code + } }); throw new Error(error); @@ -141,7 +151,12 @@ async function GetFortellisMakes(req, cdk_dealerid) { } catch (error) { logger.log("fortellis-replace-makes-models-error", "ERROR", req.user.email, null, { cdk_dealerid, - error + error: { + message: error?.message, + stack: error?.stack, + name: error?.name, + code: error?.code + } }); throw new Error(error); diff --git a/server/job/job-totals.js b/server/job/job-totals.js index 35cc4e6c1..1a578c724 100644 --- a/server/job/job-totals.js +++ b/server/job/job-totals.js @@ -315,7 +315,12 @@ function CalculateRatesTotals(ratesList) { if (item.mod_lbr_ty) { //Check to see if it has 0 hours and a price instead. //Extend for when there are hours and a price. - if (item.lbr_op === "OP14" && item.act_price > 0 && (!item.part_type || item.mod_lb_hrs === 0) && !IsAdditionalCost(item)) { + if ( + item.lbr_op === "OP14" && + item.act_price > 0 && + (!item.part_type || item.mod_lb_hrs === 0) && + !IsAdditionalCost(item) + ) { //Scenario where SGI may pay out hours using a part price. if (!ret[item.mod_lbr_ty.toLowerCase()].total) { ret[item.mod_lbr_ty.toLowerCase()].total = Dinero(); @@ -339,38 +344,30 @@ function CalculateRatesTotals(ratesList) { let subtotal = Dinero({ amount: 0 }); let rates_subtotal = Dinero({ amount: 0 }); - for (const property in ret) { + for (const [property, values] of Object.entries(ret)) { //Skip calculating mapa and mash if we got the amounts. - if (!((property === "mapa" && hasMapaLine) || (property === "mash" && hasMashLine))) { - if (!ret[property].total) { - ret[property].total = Dinero(); - } - let threshold; - //Check if there is a max for this type. - if (ratesList.materials && ratesList.materials[property]) { - // - if (ratesList.materials[property].cal_maxdlr && ratesList.materials[property].cal_maxdlr > 0) { - //It has an upper threshhold. - threshold = Dinero({ - amount: Math.round(ratesList.materials[property].cal_maxdlr * 100) - }); - } - } + const shouldSkipCalculation = (property === "mapa" && hasMapaLine) || (property === "mash" && hasMashLine); + + if (!shouldSkipCalculation) { + values.total ??= Dinero(); + + //Check if there is a max for this type and apply it. + const maxDollar = + ratesList.materials?.[property]?.cal_maxdlr || ratesList.materials?.[property.toUpperCase()]?.cal_maxdlr; + const threshold = maxDollar > 0 ? Dinero({ amount: Math.round(maxDollar * 100) }) : null; const total = Dinero({ - amount: Math.round((ret[property].rate || 0) * 100) - }).multiply(ret[property].hours); + amount: Math.round((values.rate || 0) * 100) + }).multiply(values.hours); - if (threshold && total.greaterThanOrEqual(threshold)) { - ret[property].total = ret[property].total.add(threshold); - } else { - ret[property].total = ret[property].total.add(total); - } + values.total = values.total.add(threshold && total.greaterThanOrEqual(threshold) ? threshold : total); } - subtotal = subtotal.add(ret[property].total); + subtotal = subtotal.add(values.total); - if (property !== "mapa" && property !== "mash") rates_subtotal = rates_subtotal.add(ret[property].total); + if (property !== "mapa" && property !== "mash") { + rates_subtotal = rates_subtotal.add(values.total); + } } ret.subtotal = subtotal;