Compare commits

..

22 Commits

Author SHA1 Message Date
Allan Carr
7bc105ea46 IO-3368 QB Bill Accumulator
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-10-02 21:26:41 -07:00
Allan Carr
ac4fcf1694 IO-3368 QB Bill Accumulator
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-10-02 21:22:35 -07:00
Patrick Fic
46be1fa889 Merged in feature/IO-3356-pbs-ro-posting (pull request #2577)
Feature/IO-3356 pbs ro posting

Approved-by: Dave Richer
2025-10-02 20:27:48 +00:00
Patrick Fic
40a8036472 Adjust logging statements. 2025-10-02 13:26:12 -07:00
Patrick Fic
22970ac149 Remove additional RO posting. 2025-10-02 13:20:15 -07:00
Dave Richer
63eeab888f Merged in feature/IO-3388-parts-queue-fix (pull request #2603)
feature/IO-3388 - Parts Queue Fix
2025-10-02 16:56:37 +00:00
Dave
367ced88d8 feature/IO-3388 - Parts Queue Fix 2025-10-02 12:55:50 -04:00
Allan Carr
7f67b01c87 Merged in feature/IO-3387-Mitchell-OP14-Import-USA (pull request #2599)
IO-3384 Mitchell OP14 Import USA

Approved-by: Dave Richer
2025-10-01 23:33:56 +00:00
Patrick Fic
93e137c84e Final changes for pbs. 2025-10-01 13:59:11 -07:00
Allan Carr
36d4a02be0 Merged in feature/IO-3330-CARFAX-Adjustments (pull request #2598)
IO-3330 CARFAX Adjustment

Approved-by: Dave Richer
2025-10-01 18:23:01 +00:00
Allan Carr
31367f278e IO-3330 CARFAX Adjustment
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-09-29 10:08:11 -07:00
Patrick Fic
9ff3311579 Add vehicle query. 2025-09-26 08:19:57 -07:00
Patrick Fic
05385fca6d Reoslve DMS allocation scroll. 2025-09-18 14:26:23 -07:00
Patrick Fic
1355d79fa4 Comments and cleanup. 2025-09-18 14:23:51 -07:00
Patrick Fic
8805538706 Merge branch 'master-AIO' into feature/IO-3356-pbs-ro-posting 2025-09-18 14:06:24 -07:00
Patrick Fic
9e35b0f123 Add and remove logging. 2025-09-18 14:05:14 -07:00
Patrick Fic
fd229d5d09 Debug level changes, vehicle upsert correction, and logic updates. 2025-09-12 13:14:38 -07:00
Patrick Fic
1bb66a5378 Merge branch 'master-AIO' into feature/IO-3356-pbs-ro-posting 2025-09-11 14:40:08 -07:00
Patrick Fic
01fc0cbc08 IO-3356 PBS posting initial working commit. 2025-09-05 10:45:59 -07:00
Patrick Fic
9ab3d8c81a Merge branch 'master-AIO' into feature/IO-XXXX-pbs-ro-posting 2025-09-02 15:24:15 -07:00
Patrick Fic
83c0696a3e Reformat translations. 2025-09-02 15:24:01 -07:00
Patrick Fic
5552c73721 WIP PBS RO 2025-08-22 08:21:29 -07:00
32 changed files with 493 additions and 183 deletions

View File

@@ -1,9 +1,5 @@
root = true
# NOTE: Keep only EditorConfig properties that Prettier actually respects.
# Style choices (quotes, bracket spacing, print width, etc.) live in `.prettierrc.js`.
# Removing pseudo/unsupported keys (quote_type, bracketSpacing, max_line_length) that
# previously caused editor vs CLI confusion.
[*]
charset = utf-8
indent_style = space
@@ -11,11 +7,14 @@ indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = false
quote_type = single
max_line_length = 100
bracketSpacing = false
[*.md]
trim_trailing_whitespace = false
[*\.yml]
[*.yml]
indent_size = 2
[*.json]

View File

@@ -1,11 +0,0 @@
# Folders & files to skip during formatting
node_modules
build
dist
dev-dist
coverage
logs
*.log
# Generated / compiled assets
client/dev-dist
client/dist

10
.vscode/settings.json vendored
View File

@@ -39,13 +39,5 @@
"timetickets",
"touchtime"
],
"eslint.workingDirectories": ["./", "./client"],
// Formatting alignment: ensure VS Code uses project Prettier config
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// Disable eslint's fixAll on save if it causes style churn; turn back on once stylistic rules removed or using eslint-config-prettier
"prettier.useEditorConfig": true
"eslint.workingDirectories": ["./", "./client"]
}

View File

@@ -5305,6 +5305,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>ro_posting</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>sendmaterialscosting</name>
<definition_loaded>false</definition_loaded>

View File

@@ -92,6 +92,7 @@ export function DmsAllocationsSummary({ socket, bodyshop, jobId, title }) {
rowKey="center"
dataSource={allocationsSummary}
locale={{ emptyText: t("dms.labels.refreshallocations") }}
scroll={{ x: true }}
summary={() => {
const totals =
allocationsSummary &&

View File

@@ -90,7 +90,7 @@ export function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
text: s || t("dashboard.errors.status"),
value: [s]
};
})) ||
@@ -103,7 +103,7 @@ export function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
<div>
<CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}>
<PartsStatusPie joblines_status={joblines_status} />
<Table key="id" columns={columns} dataSource={filteredJobLines ? filteredJobLines : []} />
<Table key="id" columns={columns} dataSource={filteredJobLines || []} />
</CardTemplate>
</div>
);

View File

@@ -318,7 +318,7 @@ export function JobLinesComponent({
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
text: s || t("dashboard.errors.status"),
value: [s]
};
})) ||

View File

@@ -1,8 +1,9 @@
import { useMemo } from "react";
import { Col, Row, Tag, Tooltip } from "antd";
import { Tag, Tooltip } from "antd";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -11,65 +12,67 @@ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export const DEFAULT_COL_LAYOUT = { xs: 24, sm: 24, md: 8, lg: 4, xl: 4, xxl: 4 };
export default connect(mapStateToProps, mapDispatchToProps)(JobPartsQueueCount);
export function JobPartsQueueCount({ bodyshop, parts, defaultColLayout = DEFAULT_COL_LAYOUT }) {
export function JobPartsQueueCount({ bodyshop, parts }) {
const { t } = useTranslation();
const partsStatus = useMemo(() => {
if (!parts) return null;
const statusKeys = ["default_bo", "default_ordered", "default_received", "default_returned"];
return parts.reduce(
(acc, val) => {
if (val.part_type === "PAS" || val.part_type === "PASL") return acc;
acc.total = acc.total + val.count;
acc[val.status] = acc[val.status] + val.count;
return acc;
},
{
total: 0,
null: 0,
[bodyshop.md_order_statuses.default_bo]: 0,
[bodyshop.md_order_statuses.default_ordered]: 0,
[bodyshop.md_order_statuses.default_received]: 0,
[bodyshop.md_order_statuses.default_returned]: 0
...Object.fromEntries(statusKeys.map((key) => [bodyshop.md_order_statuses[key], 0]))
}
);
}, [bodyshop, parts]);
if (!parts) return null;
return (
<Row>
<Col {...defaultColLayout}>
<Tooltip title="Total">
<Tag>{partsStatus.total}</Tag>
</Tooltip>
</Col>
<Col {...defaultColLayout}>
<Tooltip title="No Status">
<Tag color="gold">{partsStatus["null"]}</Tag>
</Tooltip>
</Col>
<Col {...defaultColLayout}>
<Tooltip title={bodyshop.md_order_statuses.default_ordered}>
<Tag color="blue">{partsStatus[bodyshop.md_order_statuses.default_ordered]}</Tag>
</Tooltip>
</Col>
<Col {...defaultColLayout}>
<Tooltip title={bodyshop.md_order_statuses.default_received}>
<Tag color="green">{partsStatus[bodyshop.md_order_statuses.default_received]}</Tag>
</Tooltip>
</Col>
<Col {...defaultColLayout}>
<Tooltip title={bodyshop.md_order_statuses.default_returned}>
<Tag color="orange">{partsStatus[bodyshop.md_order_statuses.default_returned]}</Tag>
</Tooltip>
</Col>
<Col {...defaultColLayout}>
<Tooltip title={bodyshop.md_order_statuses.default_bo}>
<Tag color="red">{partsStatus[bodyshop.md_order_statuses.default_bo]}</Tag>
</Tooltip>
</Col>
</Row>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(40px, 1fr))",
gap: "8px",
width: "100%",
justifyItems: "start"
}}
>
<Tooltip title="Total">
<Tag style={{ minWidth: "40px", textAlign: "center" }}>{partsStatus.total}</Tag>
</Tooltip>
<Tooltip title={t("dashboard.errors.status_normal")}>
<Tag color="gold" style={{ minWidth: "40px", textAlign: "center" }}>
{partsStatus["null"]}
</Tag>
</Tooltip>
<Tooltip title={bodyshop.md_order_statuses.default_bo}>
<Tag color="red" style={{ minWidth: "40px", textAlign: "center" }}>
{partsStatus[bodyshop.md_order_statuses.default_bo]}
</Tag>
</Tooltip>
<Tooltip title={bodyshop.md_order_statuses.default_ordered}>
<Tag color="blue" style={{ minWidth: "40px", textAlign: "center" }}>
{partsStatus[bodyshop.md_order_statuses.default_ordered]}
</Tag>
</Tooltip>
<Tooltip title={bodyshop.md_order_statuses.default_received}>
<Tag color="green" style={{ minWidth: "40px", textAlign: "center" }}>
{partsStatus[bodyshop.md_order_statuses.default_received]}
</Tag>
</Tooltip>
<Tooltip title={bodyshop.md_order_statuses.default_returned}>
<Tag color="orange" style={{ minWidth: "40px", textAlign: "center" }}>
{partsStatus[bodyshop.md_order_statuses.default_returned]}
</Tag>
</Tooltip>
</div>
);
}

View File

@@ -166,7 +166,7 @@ export function JobsList({ bodyshop }) {
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
text: s || t("dashboard.errors.status"),
value: [s]
};
})

View File

@@ -165,7 +165,7 @@ export function JobsReadyList({ bodyshop }) {
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
text: s || t("dashboard.errors.status"),
value: [s]
};
})

View File

@@ -145,7 +145,7 @@ export function PartsQueueJobLinesComponent({ loading, jobLines }) {
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
text: s || t("dashboard.errors.status"),
value: [s]
};
})) ||

View File

@@ -171,7 +171,7 @@ export function PartsQueueListComponent({ bodyshop }) {
filters:
bodyshop.md_ro_statuses.active_statuses.map((s) => {
return {
text: s || "No Status*",
text: s || t("dashboard.errors.status"),
value: [s]
};
}) || [],

View File

@@ -34,8 +34,9 @@ const getEmployeeName = (employeeId, employees) => {
return employee ? `${employee.first_name} ${employee.last_name}` : "";
};
const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatments }) => {
const productionListColumnsData = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatments }) => {
const { Enhanced_Payroll } = treatments;
return [
{
title: i18n.t("jobs.actions.viewdetail"),
@@ -313,7 +314,7 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
activeStatuses
?.map((s) => {
return {
text: s || "No Status*",
text: s || i18n.t("dashboard.errors.status"),
value: [s]
};
})
@@ -584,4 +585,4 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
}
];
};
export default r;
export default productionListColumnsData;

View File

@@ -425,7 +425,15 @@ export function ShopInfoGeneral({ form, bodyshop }) {
]
: [])
]
: [])
: []),
<Form.Item
key="accumulatePayableLines"
name={["accountingconfig", "accumulatePayableLines"]}
label={t("bodyshop.fields.accumulatePayableLines")}
valuePropName="checked"
>
<Switch />
</Form.Item>
]}
</LayoutFormRow>
<FeatureWrapper featureName="scoreboard" noauth={() => null}>

View File

@@ -138,6 +138,15 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Switch />
</Form.Item>
)}
{bodyshop.pbs_serialnumber && (
<Form.Item
label={t("bodyshop.fields.dms.ro_posting")}
valuePropName="checked"
name={["pbs_configuration", "ro_posting"]}
>
<Switch />
</Form.Item>
)}
{bodyshop.pbs_serialnumber && (
<Form.Item
label={t("bodyshop.fields.dms.appostingaccount")}

View File

@@ -103,7 +103,7 @@ export function SimplifiedPartsJobsListComponent({
return record.status || t("general.labels.na");
},
filteredValue: filter?.status || null,
filters: bodyshop.md_ro_statuses?.parts_statuses.map((s) => {
filters: bodyshop?.md_ro_statuses?.parts_statuses?.map((s) => {
return { text: s, value: [s] };
}),
onFilter: (value, record) => value.includes(record.status)

View File

@@ -111,7 +111,7 @@ export function TechLookupJobsList({ bodyshop }) {
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
text: s || t("dashboard.errors.status"),
value: [s]
};
})) ||

View File

@@ -4,7 +4,7 @@ import { getAuth, updatePassword, updateProfile } from "@firebase/auth";
import { getFirestore } from "@firebase/firestore";
import { getMessaging, getToken, onMessage } from "@firebase/messaging";
import { store } from "../redux/store";
//import * as amplitude from '@amplitude/analytics-browser';
import * as amplitude from '@amplitude/analytics-browser';
import posthog from 'posthog-js'
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
@@ -91,14 +91,14 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
// dbevent: false,
// env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
// });
// console.log(
// "%c[Analytics]",
// "background-color: green ;font-weight:bold;",
// eventName,
// eventParams
// );
console.log(
"%c[Analytics]",
"background-color: green ;font-weight:bold;",
eventName,
eventParams
);
logEvent(analytics, eventName, eventParams);
//amplitude.track(eventName, eventParams);
amplitude.track(eventName, eventParams);
posthog.capture(eventName, eventParams);
} finally {

View File

@@ -14,7 +14,7 @@ import { persistor, store } from "./redux/store";
import reportWebVitals from "./reportWebVitals";
import "./translations/i18n";
import "./utils/CleanAxios";
//import * as amplitude from "@amplitude/analytics-browser";
import * as amplitude from "@amplitude/analytics-browser";
import { PostHogProvider } from "posthog-js/react";
import posthog from "posthog-js";
@@ -26,23 +26,23 @@ registerSW({ immediate: true });
// Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN";
// amplitude.init(import.meta.env.VITE_APP_AMP_KEY, {
// defaultTracking: true,
// serverUrl: import.meta.env.VITE_APP_AMP_URL
// // {
// // attribution: {
// // excludeReferrers: true,
// // initialEmptyValue: true,
// // resetSessionOnNewCampaign: true,
// // },
// // fileDownloads: true,
// // formInteractions: true,
// // pageViews: {
// // trackHistoryChanges: 'all'
// // },
// // sessions: true
// // }
// });
amplitude.init(import.meta.env.VITE_APP_AMP_KEY, {
defaultTracking: true,
serverUrl: import.meta.env.VITE_APP_AMP_URL
// {
// attribution: {
// excludeReferrers: true,
// initialEmptyValue: true,
// resetSessionOnNewCampaign: true,
// },
// fileDownloads: true,
// formInteractions: true,
// pageViews: {
// trackHistoryChanges: 'all'
// },
// sessions: true
// }
});
posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
autocapture: false,

View File

@@ -48,7 +48,7 @@ export const socket = SocketIO(
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, insertAuditTrail }) {
const { t } = useTranslation();
const [logLevel, setLogLevel] = useState("DEBUG");
const [logLevel, setLogLevel] = useState(determineDmsType(bodyshop) === "pbs" ? "INFO" : "DEBUG");
const history = useNavigate();
const [logs, setLogs] = useState([]);
const search = queryString.parse(useLocation().search);

View File

@@ -97,7 +97,7 @@ export function TechAssignedProdJobs({ setTimeTicketTaskContext, technician, bod
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
text: s || t("dashboard.errors.status"),
value: [s]
};
})) ||

View File

@@ -49,7 +49,7 @@ import {
validatePasswordResetSuccess
} from "./user.actions";
import UserActionTypes from "./user.types";
//import * as amplitude from '@amplitude/analytics-browser';
import * as amplitude from '@amplitude/analytics-browser';
import posthog from 'posthog-js';
const fpPromise = FingerprintJS.load();
@@ -92,7 +92,7 @@ export function* isUserAuthenticated() {
}
LogRocket.identify(user.email);
//amplitude.setUserId(user.email);
amplitude.setUserId(user.email);
posthog.identify(user.email);
const eulaQuery = yield client.query({
@@ -139,7 +139,7 @@ export function* signOutStart() {
imexshopid: state.user.bodyshop.imexshopid,
type: "messaging"
});
//amplitude.reset();
amplitude.reset();
} catch {
console.log("No FCM token. Skipping unsubscribe.");
}
@@ -365,7 +365,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
}
try {
//amplitude.setGroup('Shop', payload.shopname);
amplitude.setGroup('Shop', payload.shopname);
window.$crisp.push(["set", "user:company", [payload.shopname]]);
if (authRecord[0] && authRecord[0].user.validemail) {
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);

View File

@@ -281,6 +281,7 @@
},
"fields": {
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
"accumulatePayableLines": "Accumulate Payable Lines",
"address1": "Address 1",
"address2": "Address 2",
"appt_alt_transport": "Appointment Alternative Transportation Options",
@@ -321,6 +322,7 @@
"itc_local": "Local Tax is ITC?",
"itc_state": "State Tax is ITC?",
"mappingname": "DMS Mapping Name",
"ro_posting": "Create $0 RO?",
"sendmaterialscosting": "Materials Cost as % of Sale",
"srcco": "Source Company #/Dealer #"
},
@@ -996,6 +998,7 @@
"insco": "No Ins. Co.*",
"refreshrequired": "You must refresh the dashboard data to see this component.",
"status": "No Status*",
"status_normal": "No Status",
"updatinglayout": "Error saving updated layout {{message}}"
},
"labels": {

View File

@@ -281,6 +281,7 @@
},
"fields": {
"ReceivableCustomField": "",
"accumulatePayableLines": "",
"address1": "",
"address2": "",
"appt_alt_transport": "",
@@ -321,6 +322,7 @@
"itc_local": "",
"itc_state": "",
"mappingname": "",
"ro_posting": "",
"sendmaterialscosting": "",
"srcco": ""
},
@@ -996,6 +998,7 @@
"insco": "",
"refreshrequired": "",
"status": "",
"status_normal": "",
"updatinglayout": ""
},
"labels": {

View File

@@ -281,6 +281,7 @@
},
"fields": {
"ReceivableCustomField": "",
"accumulatePayableLines": "",
"address1": "",
"address2": "",
"appt_alt_transport": "",
@@ -321,6 +322,7 @@
"itc_local": "",
"itc_state": "",
"mappingname": "",
"ro_posting": "",
"sendmaterialscosting": "",
"srcco": ""
},
@@ -996,6 +998,7 @@
"insco": "",
"refreshrequired": "",
"status": "",
"status_normal": "",
"updatinglayout": ""
},
"labels": {

View File

@@ -8,7 +8,7 @@
value_from_env: DATAPUMP_AUTH
- name: CARFAX Data Pump
webhook: '{{HASURA_API_URL}}/data/carfax'
schedule: 0 7 * * 6
schedule: 0 7 * * 0
include_in_metadata: true
payload: {}
headers:

View File

@@ -10,8 +10,7 @@
"setup": "rm -rf node_modules && npm i && cd client && rm -rf node_modules && npm i",
"setup:win": "rimraf node_modules && npm i && cd client && rimraf node_modules && npm i",
"start": "node server.js",
"makeitpretty": "npm run format",
"format": "prettier --config .prettierrc.js --ignore-path .prettierignore --write .",
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\"",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test:unit": "vitest run",

View File

@@ -6,10 +6,6 @@ const PBS_CREDENTIALS = {
};
exports.PBS_CREDENTIALS = PBS_CREDENTIALS;
// const cdkDomain =
// process.env.NODE_ENV === "production"
// ? "https://3pa.dmotorworks.com"
// : "https://uat-3pa.dmotorworks.com";
const pbsDomain = `https://partnerhub.pbsdealers.com/json/reply`;
exports.PBS_ENDPOINTS = {
@@ -18,5 +14,9 @@ exports.PBS_ENDPOINTS = {
VehicleGet: `${pbsDomain}/VehicleGet`,
AccountingPostingChange: `${pbsDomain}/AccountingPostingChange`,
ContactChange: `${pbsDomain}/ContactChange`,
VehicleChange: `${pbsDomain}/VehicleChange`
VehicleChange: `${pbsDomain}/VehicleChange`,
RepairOrderChange: `${pbsDomain}/RepairOrderChange`, //TODO: Verify that this is correct. Docs have /reply/ in path.
RepairOrderGet: `${pbsDomain}/RepairOrderGet`,
RepairOrderContactVehicleGet: `${pbsDomain}/RepairOrderContactVehicleGet`,
RepairOrderContactVehicleChange: `${pbsDomain}/RepairOrderContactVehicleChange`,
};

View File

@@ -19,12 +19,11 @@ axios.interceptors.request.use((x) => {
...x.headers[x.method],
...x.headers
};
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
//console.log(printable);
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
//logRequestToFile(printable);
CdkBase.createJsonEvent(socket, "SILLY", `Raw Request: ${printable}`, x.data);
CdkBase.createJsonEvent(socket, "DEBUG", `Raw Request: ${printable}`, x.data);
return x;
});
@@ -32,23 +31,39 @@ axios.interceptors.request.use((x) => {
axios.interceptors.response.use((x) => {
const socket = x.config.socket;
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
//console.log(printable);
CdkBase.createJsonEvent(socket, "SILLY", `Raw Response: ${printable}`, x.data);
const printable = `${new Date()} | Response: ${x.status} ${x.statusText} |${JSON.stringify(x.data)}`;
//logRequestToFile(printable);
CdkBase.createJsonEvent(socket, "DEBUG", `Raw Response: ${printable}`, x.data);
return x;
});
const fs = require('fs');
const path = require("path");
function logRequestToFile(printable) {
try {
const logDir = path.join(process.cwd(), "logs");
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const logFile = path.join(logDir, "pbs-http.log");
fs.appendFileSync(logFile, `${printable}\n`);
} catch (err) {
console.error("Unexpected error in logRequestToFile:", err);
}
}
exports.default = async function (socket, { txEnvelope, jobid }) {
socket.logEvents = [];
socket.recordid = jobid;
socket.txEnvelope = txEnvelope;
try {
CdkBase.createLogEvent(socket, "DEBUG", `Received Job export request for id ${jobid}`);
CdkBase.createLogEvent(socket, "INFO", `Received Job export request for id ${jobid}`);
const JobData = await QueryJobData(socket, jobid);
socket.JobData = JobData;
CdkBase.createLogEvent(socket, "DEBUG", `Querying the DMS for the Vehicle Record.`);
CdkBase.createLogEvent(socket, "INFO", `Querying the DMS for the Vehicle Record.`);
//Query for the Vehicle record to get the associated customer.
socket.DmsVeh = await QueryVehicleFromDms(socket);
//Todo: Need to validate the lines and methods below.
@@ -69,42 +84,52 @@ exports.default = async function (socket, { txEnvelope, jobid }) {
exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selectedCustomerId) {
try {
if (socket.JobData.bodyshop.pbs_configuration.disablecontactvehicle === false) {
CdkBase.createLogEvent(socket, "DEBUG", `User selected customer ${selectedCustomerId || "NEW"}`);
socket.selectedCustomerId = selectedCustomerId;
if (socket.JobData.bodyshop.pbs_configuration.disablecontactvehicle !== true) {
CdkBase.createLogEvent(socket, "INFO", `User selected customer ${selectedCustomerId || "NEW"}`);
//Upsert the contact information as per Wafaa's Email.
CdkBase.createLogEvent(
socket,
"DEBUG",
`Upserting contact information to DMS for ${
socket.JobData.ownr_fn || ""
"INFO",
`Upserting contact information to DMS for ${socket.JobData.ownr_fn || ""
} ${socket.JobData.ownr_ln || ""} ${socket.JobData.ownr_co_nm || ""}`
);
const ownerRef = await UpsertContactData(socket, selectedCustomerId);
CdkBase.createLogEvent(socket, "DEBUG", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
await UpsertVehicleData(socket, ownerRef.ReferenceId);
socket.ownerRef = ownerRef;
CdkBase.createLogEvent(socket, "INFO", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
const vehicleRef = await UpsertVehicleData(socket, ownerRef.ReferenceId);
socket.vehicleRef = vehicleRef;
} else {
CdkBase.createLogEvent(
socket,
"DEBUG",
`Contact and Vehicle updates disabled. Skipping to accounting data insert.`
"INFO",
`Contact and Vehicle updates disabled. Querying data and skipping to accounting data insert.`
);
//Must query for records to insert $0 RO.
if (!socket.ownerRef) {
const ownerRef = (await QueryCustomerBycodeFromDms(socket, selectedCustomerId))?.[0];
socket.ownerRef = ownerRef;
}
const vehicleRef = await GetVehicleData(socket, socket.ownerRef?.ReferenceId || socket.selectedCustomerId);
socket.vehicleRef = vehicleRef;
}
CdkBase.createLogEvent(socket, "DEBUG", `Inserting account data.`);
CdkBase.createLogEvent(socket, "DEBUG", `Inserting accounting posting data..`);
CdkBase.createLogEvent(socket, "INFO", `Inserting accounting posting data..`);
const insertResponse = await InsertAccountPostingData(socket);
if (insertResponse.WasSuccessful) {
CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported.`);
await MarkJobExported(socket, socket.JobData.id);
if (socket.JobData.bodyshop.pbs_configuration.ro_posting) {
await CreateRepairOrderInPBS(socket, socket.ownerRef, socket.vehicleRef)
}
CdkBase.createLogEvent(socket, "INFO", `Marking job as exported.`);
await MarkJobExported(socket, socket.JobData.id);
socket.emit("export-success", socket.JobData.id);
} else {
CdkBase.createLogEvent(socket, "ERROR", `Export was not successful.`);
}
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkSelectedCustomer. ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsSelectedCustomer. ${error}`);
await InsertFailedExportLog(socket, error);
}
};
@@ -112,22 +137,22 @@ exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selecte
// Was Successful
async function CheckForErrors(socket, response) {
if (response.WasSuccessful === undefined || response.WasSuccessful === true) {
CdkBase.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
CdkBase.createLogEvent(socket, "INFO", `Successful response from DMS. ${response.Message || ""}`);
} else {
CdkBase.createLogEvent(socket, "ERROR", `Error received from DMS: ${response.Message}`);
CdkBase.createLogEvent(socket, "SILLY", `Error received from DMS: ${JSON.stringify(response)}`);
CdkBase.createLogEvent(socket, "DEBUG", `Error received from DMS: ${JSON.stringify(response)}`);
}
}
exports.CheckForErrors = CheckForErrors;
async function QueryJobData(socket, jobid) {
CdkBase.createLogEvent(socket, "DEBUG", `Querying job data for id ${jobid}`);
CdkBase.createLogEvent(socket, "INFO", `Querying job data for id ${jobid}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.QUERY_JOBS_FOR_PBS_EXPORT, { id: jobid });
CdkBase.createLogEvent(socket, "SILLY", `Job data query result ${JSON.stringify(result, null, 2)}`);
CdkBase.createLogEvent(socket, "DEBUG", `Job data query result ${JSON.stringify(result, null, 2)}`);
return result.jobs_by_pk;
}
@@ -247,15 +272,15 @@ async function UpsertContactData(socket, selectedCustomerId) {
Code: socket.JobData.owner.accountingid,
...(socket.JobData.ownr_co_nm
? {
//LastName: socket.JobData.ownr_ln,
FirstName: socket.JobData.ownr_co_nm,
IsBusiness: true
}
//LastName: socket.JobData.ownr_ln,
FirstName: socket.JobData.ownr_co_nm,
IsBusiness: true
}
: {
LastName: socket.JobData.ownr_ln,
FirstName: socket.JobData.ownr_fn,
IsBusiness: false
}),
LastName: socket.JobData.ownr_ln,
FirstName: socket.JobData.ownr_fn,
IsBusiness: false
}),
//Salutation: "String",
//MiddleName: "String",
@@ -332,7 +357,7 @@ async function UpsertVehicleData(socket, ownerRef) {
//FleetNumber: "String",
//Status: "String",
OwnerRef: ownerRef, // "00000000000000000000000000000000",
ModelNumber: socket.JobData.vehicle && socket.JobData.vehicle.v_makecode,
// ModelNumber: socket.JobData.vehicle && socket.JobData.vehicle.v_makecode,
Make: socket.JobData.v_make_desc,
Model: socket.JobData.v_model_desc,
Trim: socket.JobData.vehicle && socket.JobData.vehicle.v_trimcode,
@@ -340,7 +365,7 @@ async function UpsertVehicleData(socket, ownerRef) {
Year: socket.JobData.v_model_yr,
Odometer: socket.JobData.kmout,
ExteriorColor: {
Code: socket.JobData.v_color,
// Code: socket.JobData.v_color,
Description: socket.JobData.v_color
}
// InteriorColor: { Code: "String", Description: "String" },
@@ -470,6 +495,57 @@ async function UpsertVehicleData(socket, ownerRef) {
}
}
async function GetVehicleData(socket, ownerRef) {
try {
const { data: { Vehicles } } = await axios.post(
PBS_ENDPOINTS.VehicleGet,
{
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
// "VehicleId": "00000000000000000000000000000000",
// "Year": "String",
// "YearFrom": "String",
// "YearTo": "String",
// "Make": "String",
// "Model": "String",
// "Trim": "String",
// "ModelNumber": "String",
// "StockNumber": "String",
VIN: socket.JobData.v_vin,
// "LicenseNumber": "String",
// "Lot": "String",
// "Status": "String",
// "StatusList": ["String"],
// "OwnerRef": "00000000000000000000000000000000",
// "ModifiedSince": "0001-01-01T00:00:00.0000000Z",
// "ModifiedUntil": "0001-01-01T00:00:00.0000000Z",
// "LastSaleSince": "0001-01-01T00:00:00.0000000Z",
// "VehicleIDList": ["00000000000000000000000000000000"],
// "IncludeInactive": false,
// "IncludeBuildVehicles": false,
// "IncludeBlankLot": false,
// "ShortVIN": "String",
// "ResultLimit": 0,
// "LotAccessDivisions": [0],
// "OdometerTo": 0,
// "OdometerFrom": 0
}
,
{ auth: PBS_CREDENTIALS, socket }
);
CheckForErrors(socket, Vehicles);
if (Vehicles.length === 1) {
return Vehicles[0];
} else {
CdkBase.createLogEvent(socket, "ERROR", `Error in Getting Vehicle Data - ${Vehicles.length} vehicle(s) found`);
}
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertVehicleData - ${error}`);
throw new Error(error);
}
}
async function InsertAccountPostingData(socket) {
try {
const allocations = await CalculateAllocations(socket, socket.JobData.id);
@@ -572,7 +648,7 @@ async function InsertAccountPostingData(socket) {
}
async function MarkJobExported(socket, jobid) {
CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported for id ${jobid}`);
CdkBase.createLogEvent(socket, "INFO", `Marking job as exported for id ${jobid}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
@@ -618,3 +694,158 @@ async function InsertFailedExportLog(socket, error) {
CdkBase.createLogEvent(socket, "ERROR", `Error in InsertFailedExportLog - ${error} - ${JSON.stringify(error2)}`);
}
}
async function CreateRepairOrderInPBS(socket) {
try {
const { RepairOrders } = await RepairOrderGet(socket);
if (RepairOrders.length === 0) {
const InsertedRepairOrder = await RepairOrderChange(socket)
socket.InsertedRepairOrder = InsertedRepairOrder;
CdkBase.createLogEvent(socket, "INFO", `No repair orders found for vehicle. Inserting record.`);
} else if (RepairOrders.length > 0) {
//Find out if it's a matching RO.
//This logic is used because the integration will simply add another line to an open RO if it exists.
const matchingRo = RepairOrders.find(ro => ro.Memo?.toLowerCase()?.includes(socket.JobData.ro_number.toLowerCase()))
if (!matchingRo) {
CdkBase.createLogEvent(socket, "INFO", `ROs found for vehicle, but none match. Inserting record.`);
const InsertedRepairOrder = await RepairOrderChange(socket)
socket.InsertedRepairOrder = InsertedRepairOrder;
} else {
CdkBase.createLogEvent(socket, "WARN", `Repair order appears to already exist in PBS. ${matchingRo.RepairOrderNumber}`);
}
}
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error in CreateRepairOrderInPBS - ${error} - ${JSON.stringify(error)}`);
}
}
async function RepairOrderGet(socket) {
try {
const { data: RepairOrderGet } = await axios.post(
PBS_ENDPOINTS.RepairOrderGet,
{
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
//"RepairOrderId": "374728766",
//"RepairOrderNumber": "4" || socket.JobData.ro_number,
//"RawRepairOrderNumber": socket.JobData.ro_number,
// "Tag": "String",
//"ContactRef": socket.contactRef,
// "ContactRefList": ["00000000000000000000000000000000"],
"VehicleRef": socket.vehicleRef?.ReferenceId || socket.vehicleRef?.VehicleId,
// "VehicleRefList": ["00000000000000000000000000000000"],
// "Status": "String",
// "CashieredSince": "0001-01-01T00:00:00.0000000Z",
// "CashieredUntil": "0001-01-01T00:00:00.0000000Z",
// "OpenDateSince": "0001-01-01T00:00:00.0000000Z",
// "OpenDateUntil": "0001-01-01T00:00:00.0000000Z",
//"ModifiedSince": "2025-01-01T00:00:00.0000000Z",
// "ModifiedUntil": "0001-01-01T00:00:00.0000000Z",
// "Shop": "String"
},
{ auth: PBS_CREDENTIALS, socket }
);
CheckForErrors(socket, RepairOrderGet);
return RepairOrderGet;
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error in RepairOrderChange - ${error}`);
throw new Error(error);
}
}
async function RepairOrderChange(socket) {
try {
const { data: RepairOrderChangeResponse } = await axios.post(
PBS_ENDPOINTS.RepairOrderChange,
{ //Additional details at https://partnerhub.pbsdealers.com/json/metadata?op=RepairOrderChange
"RepairOrderInfo": {
//"Id": "string/00000000-0000-0000-0000-000000000000",
//"RepairOrderId": "00000000000000000000000000000000",
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
"RepairOrderNumber": "00000000000000000000000000000000", //This helps force a new RO.
"RawRepairOrderNumber": "00000000000000000000000000000000",
// "RepairOrderNumber": socket.JobData.ro_number, //These 2 values are ignored as confirmed by PBS.
// "RawRepairOrderNumber": socket.JobData.ro_number,
"DateOpened": moment(),
// "DateOpenedUTC": "0001-01-01T00:00:00.0000000Z",
// "DateCashiered": "0001-01-01T00:00:00.0000000Z",
// "DateCashieredUTC": "0001-01-01T00:00:00.0000000Z",
"DatePromised": socket.JobData.scheduled_completion,
// "DatePromisedUTC": "0001-01-01T00:00:00.0000000Z",
"DateVehicleCompleted": socket.JobData.actual_completion,
// "DateCustomerNotified": "0001-01-01T00:00:00.0000000Z",
// "CSR": "String",
// "CSRRef": "00000000000000000000000000000000",
// "BookingUser": "String",
// "BookingUserRef": "00000000000000000000000000000000",
"ContactRef": socket.ownerRef?.ReferenceId || socket.ownerRef?.ContactId,
"VehicleRef": socket.vehicleRef?.ReferenceId || socket.vehicleRef?.VehicleId,
"MileageIn": socket.JobData.km_in,
"Tag": "BODYSHOP",
//"Status": "CLOSED", //Values here do not impact the status. Confirmed by PBS support.
Requests: [
{
// "RepairOrderRequestRef": "b1842ecad62c4279bbc2fef4f6bf6cde",
// "RepairOrderRequestId": 1,
// "CSR": "PBS",
// "CSRRef": "1ce12ac692564e94bda955d529ee911a",
// "Skill": "GEN",
"RequestCode": "MISC",
"RequestDescription": `VEHICLE REPAIRED AT BODYSHOP. PLEASE REFERENCE IMEX SHOP MANAGEMENT SYSTEM. ${socket.txEnvelope.story}`,
"Status": "Completed",
// "TechRef": "00000000000000000000000000000000",
"AllowedHours": 0,
"EstimateLabour": 0,
"EstimateParts": 0,
"ComeBack": false,
"AddedOperation": true,
"PartLines": [],
"PartRequestLines": [],
"LabourLines": [],
"SubletLines": [],
"TimePunches": [],
"Summary": {
"Labour": 0,
"Parts": 0,
"OilGas": 0,
"SubletTow": 0,
"Misc": 0,
"Environment": 0,
"ShopSupplies": 0,
"Freight": 0,
"WarrantyDeductible": 0,
"Discount": 0,
"SubTotal": 0,
"Tax1": 0,
"Tax2": 0,
"InvoiceTotal": 0,
"CustomerDeductible": 0,
"GrandTotal": 0,
"LabourDiscount": 0,
"PartDiscount": 0,
"ServiceFeeTotal": 0,
"OEMDiscount": 0
},
"LineType": "RequestLine",
},
],
"Memo": socket.txEnvelope.story,
},
"IsAsynchronous": false,
// "UserRequest": "String",
// "UserRef": "00000000000000000000000000000000"
}
,
{ auth: PBS_CREDENTIALS, socket }
);
CheckForErrors(socket, RepairOrderChangeResponse);
return RepairOrderChangeResponse;
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error in RepairOrderChange - ${error}`);
throw new Error(error);
}
}

View File

@@ -205,21 +205,49 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) {
async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) {
const { accounts, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req, bill.job.shopid);
let lines;
if (bodyshop.accountingconfig.accumulatePayableLines === true) {
lines = Object.values(
bill.billlines.reduce((acc, il) => {
const { cost_center, actual_cost, quantity = 1 } = il;
const lines = bill.billlines.map((il) =>
generateBillLine(
il,
accounts,
bill.job.class,
bodyshop.md_responsibility_centers.sales_tax_codes,
classes,
taxCodes,
bodyshop.md_responsibility_centers.costs,
bodyshop.accountingconfig,
bodyshop.region_config
)
);
if (!acc[cost_center]) {
acc[cost_center] = { ...il, actual_cost: 0, quantity: 1 };
}
acc[cost_center].actual_cost += Math.round(actual_cost * quantity * 100);
return acc;
}, {})
).map((il) => {
il.actual_cost /= 100;
return generateBillLine(
il,
accounts,
bill.job.class,
bodyshop.md_responsibility_centers.sales_tax_codes,
classes,
taxCodes,
bodyshop.md_responsibility_centers.costs,
bodyshop.accountingconfig,
bodyshop.region_config
);
});
} else {
lines = bill.billlines.map((il) =>
generateBillLine(
il,
accounts,
bill.job.class,
bodyshop.md_responsibility_centers.sales_tax_codes,
classes,
taxCodes,
bodyshop.md_responsibility_centers.costs,
bodyshop.accountingconfig,
bodyshop.region_config
)
);
}
//QB USA with GST
//This was required for the No. 1 Collision Group.
if (
@@ -241,7 +269,7 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
Amount: Dinero({
amount: Math.round(
bill.billlines.reduce((acc, val) => {
return acc + (val.applicable_taxes?.federal ? (val.actual_cost * val.quantity ?? 0) : 0);
return acc + (val.applicable_taxes?.federal ? val.actual_cost * val.quantity || 0 : 0);
}, 0) * 100
)
})

View File

@@ -46,6 +46,28 @@ exports.default = async (req, res) => {
};
const generateBill = (bill, bodyshop) => {
let lines;
if (bodyshop.accountingconfig.accumulatePayableLines === true) {
lines = Object.values(
bill.billlines.reduce((acc, il) => {
const { cost_center, actual_cost, quantity = 1 } = il;
if (!acc[cost_center]) {
acc[cost_center] = { ...il, actual_cost: 0, quantity: 1 };
}
acc[cost_center].actual_cost += Math.round(actual_cost * quantity * 100);
return acc;
}, {})
).map((il) => {
il.actual_cost /= 100;
return generateBillLine(il, bodyshop.md_responsibility_centers, bill.job.class);
});
} else {
lines = bill.billlines.map((il) => generateBillLine(il, bodyshop.md_responsibility_centers, bill.job.class));
}
const billQbxmlObj = {
QBXML: {
QBXMLMsgsRq: {
@@ -67,9 +89,7 @@ const generateBill = (bill, bodyshop) => {
}),
RefNumber: bill.invoice_number,
Memo: `RO ${bill.job.ro_number || ""}`,
ExpenseLineAdd: bill.billlines.map((il) =>
generateBillLine(il, bodyshop.md_responsibility_centers, bill.job.class)
)
ExpenseLineAdd: lines
}
}
}

View File

@@ -420,6 +420,8 @@ query QUERY_JOBS_FOR_PBS_EXPORT($id: uuid!) {
v_make_desc
v_color
ca_customer_gst
scheduled_completion
actual_completion
vehicle {
v_trimcode
v_makecode
@@ -2159,18 +2161,16 @@ exports.UPDATE_OLD_TRANSITION = `mutation UPDATE_OLD_TRANSITION($jobid: uuid!, $
exports.INSERT_NEW_TRANSITION = (
includeOldTransition
) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${
includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
}) {
) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
}) {
insert_transitions_one(object: $newTransition) {
id
}
${
includeOldTransition
? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
${includeOldTransition
? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
affected_rows
}`
: ""
: ""
}
}`;