Compare commits

...

53 Commits

Author SHA1 Message Date
Patrick Fic
d0bd2aeaba Minor bug fixes. 2022-11-03 10:01:22 -07:00
Patrick Fic
b3ed9106f0 Add notification of complete export. 2022-11-03 08:25:44 -07:00
Patrick Fic
6354ccee87 AP changes based on validation testing. 2022-11-01 14:26:13 -07:00
Patrick Fic
ede1cdb89b WIP PBS AP. 2022-10-31 15:59:00 -07:00
Patrick Fic
cd96de3a96 Merge branch 'release/2022-10-31' into feature/pbs-ap
* release/2022-10-31:
  Improved LR activiation.
  Improved LR tracking.
2022-10-31 11:34:56 -07:00
Patrick Fic
790fd4611f Improved LR activiation. 2022-10-31 11:30:45 -07:00
Patrick Fic
c72fdb81af Improved LR tracking. 2022-10-31 11:27:33 -07:00
Patrick Fic
8d5202f46d WIP PBS AP. 2022-10-31 10:42:42 -07:00
Patrick Fic
7d81898a45 Merged in release/2022-10-28 (pull request #607)
Release/2022 10 28
2022-10-31 14:18:40 +00:00
Patrick Fic
107265c2fc IO-223 Remove LA1 from ARMS. 2022-10-27 11:11:29 -07:00
Patrick Fic
d6d75cacd8 Merged in release/2022-10-27 (pull request #605)
Resolve PBS query error.
2022-10-27 15:16:27 +00:00
Patrick Fic
bfefc99434 Resolve PBS query error. 2022-10-27 08:15:41 -07:00
Patrick Fic
c26cc9e908 Resolve PBS query error. 2022-10-27 08:13:57 -07:00
Patrick Fic
21d05553ff Add ARMS logging. 2022-10-26 09:26:39 -07:00
Patrick Fic
fdcd97d195 Merged in release/2022-10-28 (pull request #603)
release/2022-10-28

Approved-by: Patrick Fic
2022-10-24 21:02:19 +00:00
Patrick Fic
15bf1937d3 Remove logging statement. 2022-10-24 14:01:37 -07:00
Patrick Fic
41b32a25d7 IO-2086 LMS Optimized imges bugfix. 2022-10-24 13:49:57 -07:00
Patrick Fic
dc0d8526a3 IO-2086 Improved local media image display. 2022-10-24 12:50:52 -07:00
Patrick Fic
84fcca239a Merged in release/2022-10-21 (pull request #600)
Release/2022 10 21
2022-10-19 22:51:50 +00:00
Patrick Fic
7039eff354 Minimum bill quantity set to 1. 2022-10-19 15:20:06 -07:00
Patrick Fic
05bfd217be IO-2070 Credit memos no longer update line status. 2022-10-18 10:39:58 -07:00
Patrick Fic
c83ff03ae8 IO-2038 Resolve duplicate shift clock login. 2022-10-18 09:51:55 -07:00
Patrick Fic
b363b2f7e5 Resolve CI issue. 2022-10-18 09:22:21 -07:00
Patrick Fic
d14a3f77f1 IO-2082 Closing $0 RO. 2022-10-18 09:19:58 -07:00
Patrick Fic
04f4ce97ed IO-2059 Add invoice history to job line expander. 2022-10-18 09:00:21 -07:00
Patrick Fic
1d39e574cf IO-2072 Update template for parts label. 2022-10-17 17:30:30 -07:00
Patrick Fic
ac6903edcb IO-2072 add parts label to bill posting screen. 2022-10-17 16:58:03 -07:00
Patrick Fic
0129868bb0 IO-2058 Add RO/Name to resent parts order. 2022-10-17 16:30:45 -07:00
Patrick Fic
1012d4acda IO-2066 Update item used for adjustments. 2022-10-17 16:21:44 -07:00
Patrick Fic
2b5e8f9a81 IO-2013 Add IOU flag to job header in certain areas. 2022-10-17 16:07:16 -07:00
Patrick Fic
dbc83aab00 Merged in release/2022-10-14 (pull request #596)
Release/2022 10 14
2022-10-14 18:49:16 +00:00
Patrick Fic
ebcf27feea IO-2074 updated labels for reports. 2022-10-14 08:46:51 -07:00
Patrick Fic
c391b7d90b IO-2067 IO-2074 Add new reports. 2022-10-13 13:21:53 -07:00
Patrick Fic
41b505748c Merged in release/2022-10-03 (pull request #593)
IO-2054 Resolve issue for multi AR.
2022-10-03 18:01:21 +00:00
Patrick Fic
54feb0b541 IO-2054 Resolve issue for multi AR. 2022-10-03 08:00:29 -10:00
Patrick Fic
2b36bcc56b Merged in release/2022-09-30 (pull request #592)
Release/2022 09 30
2022-09-28 23:26:19 +00:00
Patrick Fic
d699e49369 IO-223 Use NA for owner company name. 2022-09-28 12:44:09 -07:00
Patrick Fic
1105431909 IO-2054 Improved spacing for QB Multi Payer 2022-09-28 12:07:27 -07:00
Patrick Fic
1c222db5a3 IO-2051 Mark appointment as arrived from checklist. 2022-09-28 11:55:46 -07:00
Patrick Fic
f6b72ab428 IO-2065 Resolve parts status filtering. 2022-09-28 11:47:48 -07:00
Patrick Fic
045a3660a3 IO-2054 Updated labels for multi AR. 2022-09-28 11:30:11 -07:00
Patrick Fic
45fca7206b IO-2054 Bug fix for payments. 2022-09-27 17:07:42 -07:00
Patrick Fic
e8fc29ea61 Autohouse Updates. 2022-09-27 15:07:26 -07:00
Patrick Fic
918bc402f6 IO-2054 Add Payments Export 2022-09-27 15:07:15 -07:00
Patrick Fic
c154e7be2e IO-223 ARMS bug fixes. 2022-09-27 08:41:45 -07:00
Patrick Fic
1f8edf764d IO-2054 Update responsibility center page. 2022-09-26 14:40:10 -07:00
Patrick Fic
acd3f545b3 IO-2054 RO Splitting for QBO 2022-09-26 11:33:58 -07:00
Patrick Fic
220e863482 Merged in release/2022-09-23 (pull request #586)
Prevent no reply loop
2022-09-24 18:05:24 +00:00
Patrick Fic
79c7ef327c Merged in release/2022-09-23 (pull request #585)
Updated recipieint for bounced emails since testing confirmed.
2022-09-23 21:10:48 +00:00
Patrick Fic
f7adbd9a20 Merged in release/2022-09-23 (pull request #584)
Updates to bounced email tracker.
2022-09-23 21:03:19 +00:00
Patrick Fic
32110b13c2 Merged in release/2022-09-23 (pull request #583)
Update SNS handling
2022-09-23 20:49:06 +00:00
Patrick Fic
b4fd674535 Merged in release/2022-09-23 (pull request #582)
Release/2022 09 23
2022-09-23 20:36:42 +00:00
Patrick Fic
d75a6328e8 Merged in release/2022-09-16 (pull request #578)
Release/2022 09 16
2022-09-19 15:50:43 +00:00
52 changed files with 2206 additions and 348 deletions

View File

@@ -16,6 +16,6 @@
"rules": {
"no-console": "off"
},
"settings": {},
"plugins": ["cypress"]
"settings": {}
//"plugins": ["cypress"]
}

View File

@@ -3078,6 +3078,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>generatepartslabel</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>iouexists</name>
<definition_loaded>false</definition_loaded>
@@ -6900,6 +6921,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>federal_tax_itc</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>gst_override</name>
<definition_loaded>false</definition_loaded>
@@ -13683,6 +13725,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>optimizedimage</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>reassign_limitexceeded</name>
<definition_loaded>false</definition_loaded>
@@ -16817,6 +16880,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>status</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>
<folder_node>
<name>sub_status</name>
<children>
@@ -22288,6 +22372,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>amount</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>center</name>
<definition_loaded>false</definition_loaded>
@@ -22477,6 +22582,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>lines</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>name1</name>
<definition_loaded>false</definition_loaded>
@@ -26084,6 +26210,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>additionalpayeroverallocation</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>additionaltotal</name>
<definition_loaded>false</definition_loaded>
@@ -26189,6 +26336,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>alreadyaddedtoscoreboard</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>alreadyclosed</name>
<definition_loaded>false</definition_loaded>
@@ -27304,6 +27472,27 @@
<folder_node>
<name>dms</name>
<children>
<concept_node>
<name>apexported</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>damageto</name>
<definition_loaded>false</definition_loaded>
@@ -27831,6 +28020,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>iou</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>job</name>
<definition_loaded>false</definition_loaded>
@@ -28062,6 +28272,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>multipayers</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>net_repairs</name>
<definition_loaded>false</definition_loaded>
@@ -28293,6 +28524,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>pimraryamountpayable</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>
<folder_node>
<name>plitooltips</name>
<children>
@@ -35283,6 +35535,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>external</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>findermodal</name>
<definition_loaded>false</definition_loaded>
@@ -37248,6 +37521,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>parts_invoice_label_single</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>parts_label_multiple</name>
<definition_loaded>false</definition_loaded>
@@ -40476,6 +40770,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>hours_sold_detail_closed_status</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>hours_sold_detail_open</name>
<definition_loaded>false</definition_loaded>
@@ -40539,6 +40854,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>hours_sold_detail_open_status</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>hours_sold_summary_closed</name>
<definition_loaded>false</definition_loaded>
@@ -40602,6 +40938,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>hours_sold_summary_closed_status</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>hours_sold_summary_open</name>
<definition_loaded>false</definition_loaded>
@@ -40665,6 +41022,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>hours_sold_summary_open_status</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>job_costing_ro_csr</name>
<definition_loaded>false</definition_loaded>
@@ -40980,6 +41358,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>parts_received_not_scheduled</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>payments_by_date</name>
<definition_loaded>false</definition_loaded>
@@ -42829,6 +43228,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>shiftalreadyclockedon</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>
</children>
</folder_node>
<folder_node>
@@ -46923,6 +47343,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>dmsid</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>due_date</name>
<definition_loaded>false</definition_loaded>

View File

@@ -1,4 +1,4 @@
import { useTreatments } from "@splitsoftware/splitio-react";
import { useClient } from "@splitsoftware/splitio-react";
import { Button, Result } from "antd";
import LogRocket from "logrocket";
import React, { lazy, Suspense, useEffect } from "react";
@@ -51,11 +51,7 @@ export function App({
online,
setOnline,
}) {
const { LogRocket_Tracking } = useTreatments(
["LogRocket_Tracking"],
{},
bodyshop && bodyshop.imexshopid
);
const client = useClient();
useEffect(() => {
if (!navigator.onLine) {
@@ -78,15 +74,15 @@ export function App({
setOnline(true);
});
useEffect(() => {
if (currentUser.authorized) {
if (
process.env.NODE_ENV === "production" &&
LogRocket_Tracking.treatment === "on"
) {
if (currentUser.authorized && bodyshop) {
client.setAttribute("imexshopid", bodyshop.imexshopid);
if (client.getTreatment("LogRocket_Tracking") === "on") {
console.log("LR Start");
LogRocket.init("gvfvfw/bodyshopapp");
}
}
}, [currentUser.authorized, LogRocket_Tracking.treatment]);
}, [bodyshop, client, currentUser.authorized]);
if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")} />;

View File

@@ -1,5 +1,5 @@
import { useApolloClient, useMutation } from "@apollo/client";
import { Button, Form, Modal, notification, Space } from "antd";
import { Button, Checkbox, Form, Modal, notification, Space } from "antd";
import _ from "lodash";
import React, { useEffect, useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
@@ -26,6 +26,9 @@ import BillFormContainer from "../bill-form/bill-form.container";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import { handleUpload } from "../documents-upload/documents-upload.utility";
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
import useLocalStorage from "../../utils/useLocalStorage";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal,
@@ -38,6 +41,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(insertAuditTrail({ jobid, operation })),
});
const Templates = TemplateList("job_special");
function BillEnterModalContainer({
billEnterModal,
toggleModalVisible,
@@ -54,7 +59,10 @@ function BillEnterModalContainer({
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
const [loading, setLoading] = useState(false);
const client = useApolloClient();
const [generateLabel, setGenerateLabel] = useLocalStorage(
"enter_bill_generate_label",
false
);
const formValues = useMemo(() => {
return {
...billEnterModal.context.bill,
@@ -221,23 +229,26 @@ function BillEnterModalContainer({
});
}
}
//If it's not a credit memo, update the statuses.
await Promise.all(
remainingValues.billlines
.filter((il) => il.joblineid !== "noline")
.map((li) => {
return updateJobLines({
variables: {
lineId: li.joblineid,
line: {
location: li.location || location,
status:
bodyshop.md_order_statuses.default_received || "Received*",
if (!values.is_credit_memo) {
await Promise.all(
remainingValues.billlines
.filter((il) => il.joblineid !== "noline")
.map((li) => {
return updateJobLines({
variables: {
lineId: li.joblineid,
line: {
location: li.location || location,
status:
bodyshop.md_order_statuses.default_received || "Received*",
},
},
},
});
})
);
});
})
);
}
/////////////////////////
if (upload && upload.length > 0) {
@@ -275,6 +286,20 @@ function BillEnterModalContainer({
notification["success"]({
message: t("bills.successes.created"),
});
if (generateLabel) {
GenerateDocument(
{
name: Templates.parts_invoice_label_single.key,
variables: {
id: billId,
},
},
{},
"p"
);
}
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
insertAuditTrail({
@@ -330,6 +355,12 @@ function BillEnterModalContainer({
}}
footer={
<Space>
<Checkbox
checked={generateLabel}
onChange={(e) => setGenerateLabel(e.target.checked)}
>
{t("bills.labels.generatepartslabel")}
</Checkbox>
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
<Button loading={loading} onClick={() => form.submit()}>
{t("general.actions.save")}

View File

@@ -171,7 +171,7 @@ export function BillEnterModalLinesComponent({
};
},
formInput: (record, index) => (
<InputNumber precision={0} min={0} disabled={disabled} />
<InputNumber precision={0} min={1} disabled={disabled} />
),
},
{

View File

@@ -0,0 +1,150 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Form, Input, Table } from "antd";
import Dinero from "dinero.js";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(DmsAllocationsSummaryAp);
export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
const { t } = useTranslation();
const [allocationsSummary, setAllocationsSummary] = useState([]);
useEffect(() => {
socket.on("ap-export-success", (billid) => {
setAllocationsSummary((allocationsSummary) =>
allocationsSummary.map((a) => {
if (a.billid !== billid) return a;
return { ...a, status: "Successful" };
})
);
});
socket.on("ap-export-failure", ({ billid, error }) => {
allocationsSummary.map((a) => {
if (a.billid !== billid) return a;
return { ...a, status: error };
});
});
if (socket.disconnected) socket.connect();
return () => {
socket.removeListener("ap-export-success");
socket.removeListener("ap-export-failure");
//socket.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (socket.connected) {
socket.emit("pbs-calculate-allocations-ap", billids, (ack) => {
setAllocationsSummary(ack);
socket.allocationsSummary = ack;
});
}
}, [socket, socket.connected, billids]);
console.log(allocationsSummary);
const columns = [
{
title: t("general.labels.status"),
dataIndex: "status",
key: "status",
},
{
title: t("jobs.fields.ro_number"),
dataIndex: ["Posting", "Reference"],
key: "reference",
},
{
title: t("jobs.fields.dms.lines"),
dataIndex: "Lines",
key: "Lines",
render: (text, record) => (
<table style={{ tableLayout: "auto", width: "100%" }}>
<tr>
<th>{t("bills.fields.invoice_number")}</th>
<th>{t("bodyshop.fields.dms.dms_acctnumber")}</th>
<th>{t("jobs.fields.dms.amount")}</th>
</tr>
{record.Posting.Lines.map((l, idx) => (
<tr key={idx}>
<td>{l.InvoiceNumber}</td>
<td>{l.Account}</td>
<td>{l.Amount}</td>
</tr>
))}
</table>
),
},
];
const handleFinish = async (values) => {
socket.emit(`pbs-export-ap`, {
billids,
txEnvelope: values,
});
};
return (
<Card
title={title}
extra={
<Button
onClick={() => {
socket.emit("pbs-calculate-allocations-ap", billids, (ack) =>
setAllocationsSummary(ack)
);
}}
>
<SyncOutlined />
</Button>
}
>
<Table
pagination={{ position: "top", defaultPageSize: 50 }}
columns={columns}
rowKey={(record) => `${record.InvoiceNumber}${record.Account}`}
dataSource={allocationsSummary}
locale={{ emptyText: t("dms.labels.refreshallocations") }}
/>
<Form layout="vertical" onFinish={handleFinish}>
<Form.Item
name="journal"
label={t("jobs.fields.dms.journal")}
initialValue={
bodyshop.cdk_configuration &&
bodyshop.cdk_configuration.default_journal
}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Button disabled={!socket.allocationsSummary} htmlType="submit">
{t("jobs.actions.dms.post")}
</Button>
</Form>
</Card>
);
}

View File

@@ -60,6 +60,11 @@ export const handleUpload = async ({ ev, context }) => {
...d,
selected: false,
src: normalizeUrl(`${bodyshop.localmediaserverhttp}/${d.src}`),
...(d.optimized && {
optimized: normalizeUrl(
`${bodyshop.localmediaserverhttp}/${d.optimized}`
),
}),
thumbnail: normalizeUrl(
`${bodyshop.localmediaserverhttp}/${d.thumbnail}`
),

View File

@@ -175,11 +175,13 @@ export function EmailOverlayContainer({
<Modal
destroyOnClose={true}
visible={modalVisible}
maskClosable={false}
width={"80%"}
onOk={() => form.submit()}
onCancel={() => {
toggleEmailOverlayVisible();
}}
//closeIcon={() => null}
okText={t("general.actions.send")}
okButtonProps={{
loading: sending,

View File

@@ -89,6 +89,11 @@ function Header({
{},
bodyshop && bodyshop.imexshopid
);
const { DmsAp } = useTreatments(
["DmsAp"],
{},
bodyshop && bodyshop.imexshopid
);
const { t } = useTranslation();
@@ -264,10 +269,11 @@ function Header({
{t("menus.header.accounting-receivables")}
</Link>
</Menu.Item>
{!(
{(!(
(bodyshop && bodyshop.cdk_dealerid) ||
(bodyshop && bodyshop.pbs_serialnumber)
) && (
) ||
DmsAp.treatment === "on") && (
<Menu.Item key="payables">
<Link to="/manage/accounting/payables">
{t("menus.header.accounting-payables")}

View File

@@ -7,7 +7,10 @@ import { connect } from "react-redux";
import { useHistory, useLocation, useParams } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../../../firebase/firebase.utils";
import { MARK_LATEST_APPOINTMENT_AS_ARRIVED } from "../../../../graphql/appointments.queries";
import {
MARK_APPOINTMENT_ARRIVED,
MARK_LATEST_APPOINTMENT_ARRIVED,
} from "../../../../graphql/appointments.queries";
import { UPDATE_JOB } from "../../../../graphql/jobs.queries";
import {
selectBodyshop,
@@ -41,7 +44,8 @@ export function JobChecklistForm({
const { t } = useTranslation();
const [intakeJob] = useMutation(UPDATE_JOB);
const [loading, setLoading] = useState(false);
const [markAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_AS_ARRIVED);
const [markAptArrived] = useMutation(MARK_APPOINTMENT_ARRIVED);
const [markLatestAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_ARRIVED);
const [updateOwner] = useMutation(UPDATE_OWNER);
const { jobId } = useParams();
@@ -125,6 +129,18 @@ export function JobChecklistForm({
variables: { appointmentId: search.appointmentId },
});
if (!!appUpdate.errors) {
notification["error"]({
message: t("checklist.errors.complete", {
error: JSON.stringify(result.errors),
}),
});
}
} else if (type === "intake" && !search.appointmentId) {
const appUpdate = await markLatestAptArrived({
variables: { jobId: jobId },
});
if (!!appUpdate.errors) {
notification["error"]({
message: t("checklist.errors.complete", {
@@ -133,6 +149,7 @@ export function JobChecklistForm({
});
}
}
if (type === "intake" && job.owner && job.owner.id) {
//Updae Owner Allow to Text
const updateOwnerResult = await updateOwner({
@@ -175,12 +192,7 @@ 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 (
<Card
title={t("checklist.labels.checklist")}

View File

@@ -1,11 +1,12 @@
import { useQuery } from "@apollo/client";
import { Row, Col, Timeline, Typography, Space, Divider, Skeleton } from "antd";
import { Col, Divider, Row, Skeleton, Space, Timeline, Typography } from "antd";
import React from "react";
import { GET_JOB_LINE_ORDERS } from "../../graphql/jobs.queries";
import { useTranslation } from "react-i18next";
import AlertComponent from "../alert/alert.component";
import { DateFormatter } from "../../utils/DateFormatter";
import { Link } from "react-router-dom";
import { GET_JOB_LINE_ORDERS } from "../../graphql/jobs.queries";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component";
export default function JobLinesExpander({ jobline, jobid }) {
const { t } = useTranslation();
@@ -48,7 +49,46 @@ export default function JobLinesExpander({ jobline, jobid }) {
)}
</Timeline>
</Col>
<Col md={24} lg={12}></Col>
<Col md={24} lg={12}>
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
<Timeline>
{data.billlines.length > 0 ? (
data.billlines.map((line) => (
<Timeline.Item key={line.id}>
<Row wrap>
<Col span={4}>
<Link
to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}
>
{line.bill.invoice_number}
</Link>
</Col>
<Col span={4}>
<span>
{`${t("billlines.fields.actual_price")}: `}
<CurrencyFormatter>{line.actual_price}</CurrencyFormatter>
</span>
</Col>
<Col span={4}>
<span>
{`${t("billlines.fields.actual_cost")}: `}
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
</span>
</Col>
<Col span={4}>
<DateFormatter>{line.bill.date}</DateFormatter>
</Col>
<Col span={4}> {line.bill.vendor.name}</Col>
</Row>
</Timeline.Item>
))
) : (
<Timeline.Item>
{t("parts_orders.labels.notyetordered")}
</Timeline.Item>
)}
</Timeline>
</Col>
</Row>
);
}

View File

@@ -1,4 +1,5 @@
import { useMutation, useLazyQuery } from "@apollo/client";
import { CheckCircleOutlined } from "@ant-design/icons";
import {
Button,
Card,
@@ -141,6 +142,12 @@ export default function ScoreboardAddButton({
</Form>
)}
</div>
{entryData && entryData.scoreboard && entryData.scoreboard[0] && (
<Space>
<CheckCircleOutlined style={{ color: "green" }} />
<span>{t("jobs.labels.alreadyaddedtoscoreboard")}</span>
</Space>
)}
</Card>
);

View File

@@ -2,8 +2,9 @@ import {
ExclamationCircleFilled,
PauseCircleOutlined,
WarningFilled,
BranchesOutlined,
} from "@ant-design/icons";
import { Card, Col, Row, Space, Tag } from "antd";
import { Card, Col, Row, Space, Tag, Tooltip } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -78,6 +79,13 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
{job.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{job.iouparent && (
<Link to={`/manage/jobs/${job.iouparent}`}>
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
</Link>
)}
{job.production_vars && job.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}

View File

@@ -1,5 +1,5 @@
import { SyncOutlined, FileExcelFilled } from "@ant-design/icons";
import { Button, Card, Space } from "antd";
import { Alert, Button, Card, Space } from "antd";
import React, { useEffect } from "react";
import Gallery from "react-grid-gallery";
import { useTranslation } from "react-i18next";
@@ -58,7 +58,7 @@ export function JobsDocumentsLocalGallery({
}
}
}, [job, invoice_number, getJobMedia, getBillMedia]);
let optimized;
const jobMedia =
allMedia && allMedia[job.id]
? allMedia[job.id].reduce(
@@ -68,7 +68,11 @@ export function JobsDocumentsLocalGallery({
val.type.mime &&
val.type.mime.startsWith("image")
) {
acc.images.push(val);
acc.images.push({
...val,
...(val.optimized && { src: val.optimized, fullsize: val.src }),
});
if (val.optimized) optimized = true;
} else {
acc.other.push({
...val,
@@ -80,6 +84,7 @@ export function JobsDocumentsLocalGallery({
{ images: [], other: [] }
)
: { images: [], other: [] };
return (
<div>
<Space wrap>
@@ -119,9 +124,22 @@ export function JobsDocumentsLocalGallery({
onSelectImage={(index, image) => {
toggleMediaSelected({ jobid: job.id, filename: image.filename });
}}
{...(optimized && {
customControls: [
<Alert
style={{ margin: "4px" }}
message={t("documents.labels.optimizedimage")}
type="success"
/>,
],
})}
onClickImage={(props) => {
const media = allMedia[job.id].find(
(m) => m.optimized === props.target.src
);
window.open(
props.target.src,
media ? media.src : props.target.src,
"_blank",
"toolbar=0,location=0,menubar=0"
);

View File

@@ -2,9 +2,10 @@ import {
SyncOutlined,
ExclamationCircleFilled,
PauseCircleOutlined,
BranchesOutlined,
} from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Grid, Input, Space, Table } from "antd";
import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -128,6 +129,11 @@ export function JobsList({ bodyshop }) {
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</Space>
</Link>
),

View File

@@ -2,9 +2,10 @@ import {
ExclamationCircleFilled,
PauseCircleOutlined,
SyncOutlined,
BranchesOutlined,
} from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Grid, Input, Space, Table } from "antd";
import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd";
import queryString from "query-string";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -140,6 +141,11 @@ export function JobsReadyList({ bodyshop }) {
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</Space>
</Link>
),

View File

@@ -72,7 +72,8 @@ export function PartsOrderListTableComponent({
? bpoints[selectedBreakpoint[0]]
: "100%";
const responsibilityCenters = bodyshop.md_responsibility_centers;
const Templates = TemplateList("partsorder");
const Templates = TemplateList("partsorder", { job });
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},

View File

@@ -14,6 +14,7 @@ import {
import { logImEXEvent } from "../../firebase/firebase.utils";
import _ from "lodash";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { Link } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -176,6 +177,13 @@ export function PayableExportAll({
setLoading(false);
};
if (bodyshop.pbs_serialnumber)
return (
<Link to={{ state: { billids }, pathname: `/manage/dmsap` }}>
<Button>{t("jobs.actions.export")}</Button>
</Link>
);
return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
{t("jobs.actions.exportselected")}

View File

@@ -13,6 +13,7 @@ import {
} from "../../redux/user/user.selectors";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { Link } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -179,6 +180,13 @@ export function PayableExportButton({
setLoading(false);
};
if (bodyshop.pbs_serialnumber)
return (
<Link to={{ state: { billids: [billId] }, pathname: `/manage/dmsap` }}>
<Button>{t("jobs.actions.export")}</Button>
</Link>
);
return (
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
{t("jobs.actions.export")}

View File

@@ -1,3 +1,4 @@
import { useTreatments } from "@splitsoftware/splitio-react";
import { CardElement } from "@stripe/react-stripe-js";
import { Checkbox, Form, Input, Radio, Select } from "antd";
import React from "react";
@@ -23,6 +24,11 @@ export function PaymentFormComponent({
disabled,
}) {
const [stripeState, setStripeState] = stripeStateArr;
const { Qb_Multi_Ar } = useTreatments(
["Qb_Multi_Ar"],
{},
bodyshop && bodyshop.imexshopid
);
const { t } = useTranslation();
const handleStripeChange = (e) => {
@@ -106,9 +112,21 @@ export function PaymentFormComponent({
<Select.Option value={t("payments.labels.customer")}>
{t("payments.labels.customer")}
</Select.Option>
<Select.Option value={t("payments.labels.insurance")}>
{t("payments.labels.insurance")}
</Select.Option>
{Qb_Multi_Ar.treatment === "on" ? (
<>
<Select.OptGroup label={t("payments.labels.external")}>
{bodyshop.md_ins_cos.map((i, idx) => (
<Select.Option key={idx} value={i.name}>
{i.name}
</Select.Option>
))}
</Select.OptGroup>
</>
) : (
<Select.Option value={t("payments.labels.insurance")}>
{t("payments.labels.insurance")}
</Select.Option>
)}
</Select>
</Form.Item>

View File

@@ -3,8 +3,9 @@ import {
EyeFilled,
DownloadOutlined,
PauseCircleOutlined,
BranchesOutlined,
} from "@ant-design/icons";
import { Card, Col, Row, Space } from "antd";
import { Card, Col, Row, Space, Tooltip } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
@@ -63,6 +64,11 @@ export default function ProductionBoardCard(
{card.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{card.iouparent && (
<Tooltip title={t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
<span style={{ fontWeight: "bolder" }}>
<Link
to={

View File

@@ -1,5 +1,5 @@
import { PauseCircleOutlined } from "@ant-design/icons";
import { Space } from "antd";
import { PauseCircleOutlined, BranchesOutlined } from "@ant-design/icons";
import { Space, Tooltip } from "antd";
import i18n from "i18next";
import moment from "moment";
import { Link } from "react-router-dom";
@@ -61,6 +61,11 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
{record.iouparent && (
<Tooltip title={i18n.t("jobs.labels.iou")}>
<BranchesOutlined style={{ color: "orangered" }} />
</Tooltip>
)}
</Space>
</Link>
),

View File

@@ -16,6 +16,7 @@ import DataLabel from "../data-label/data-label.component";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { useTreatments } from "@splitsoftware/splitio-react";
const SelectorDiv = styled.div`
.ant-form-item .ant-select {
@@ -37,7 +38,16 @@ export default connect(
export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
const { t } = useTranslation();
const { Qb_Multi_Ar } = useTreatments(
["Qb_Multi_Ar"],
{},
bodyshop && bodyshop.imexshopid
);
const { DmsAp } = useTreatments(
["DmsAp"],
{},
bodyshop && bodyshop.imexshopid
);
const [costOptions, setCostOptions] = useState(
[
...((form.getFieldValue(["md_responsibility_centers", "costs"]) &&
@@ -4153,6 +4163,121 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<InputNumber precision={2} />
</Form.Item>
</LayoutFormRow>
{DmsAp.treatment === "on" && (
<LayoutFormRow>
<Form.Item
label={t("bodyshop.fields.responsibilitycenters.federal_tax_itc")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_responsibility_centers", "taxes", "federal_itc", "name"]}
>
<Input />
</Form.Item>
{/* <Form.Item
label={t("bodyshop.fields.responsibilitycenter_accountnumber")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={[
"md_responsibility_centers",
"taxes",
"federal_itc",
"accountnumber",
]}
>
<Input />
</Form.Item> */}
{/* <Form.Item
label={t("bodyshop.fields.responsibilitycenter_accountname")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={[
"md_responsibility_centers",
"taxes",
"federal_itc",
"accountname",
]}
>
<Input />
</Form.Item> */}
<Form.Item
label={t("bodyshop.fields.responsibilitycenter_accountdesc")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={[
"md_responsibility_centers",
"taxes",
"federal_itc",
"accountdesc",
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.responsibilitycenter_accountitem")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={[
"md_responsibility_centers",
"taxes",
"federal_itc",
"accountitem",
]}
>
<Input />
</Form.Item>
{(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && (
<Form.Item
label={t("bodyshop.fields.dms.dms_acctnumber")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={[
"md_responsibility_centers",
"taxes",
"federal_itc",
"dms_acctnumber",
]}
>
<Input />
</Form.Item>
)}
<Form.Item
label={t("bodyshop.fields.responsibilitycenter_rate")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_responsibility_centers", "taxes", "federal_itc", "rate"]}
>
<InputNumber precision={2} />
</Form.Item>
</LayoutFormRow>
)}
<LayoutFormRow>
<Form.Item
label={t("bodyshop.fields.responsibilitycenters.state_tax")}
@@ -4411,68 +4536,71 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Input />
</Form.Item> */}
</LayoutFormRow>
{/* <LayoutFormRow>
<Form.Item
label={t("bodyshop.fields.responsibilitycenters.ap")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_responsibility_centers", "ap", "name"]}
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.responsibilitycenter_accountnumber")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_responsibility_centers", "ap", "accountnumber"]}
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.responsibilitycenter_accountname")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_responsibility_centers", "ap", "accountname"]}
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.responsibilitycenter_accountdesc")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_responsibility_centers", "ap", "accountdesc"]}
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.responsibilitycenter_accountitem")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_responsibility_centers", "ap", "accountitem"]}
>
<Input />
</Form.Item>
</LayoutFormRow> */}
{DmsAp.treatment === "on" && (
<LayoutFormRow header={t("bodyshop.fields.responsibilitycenters.ap")}>
{/* <Form.Item
label={t("bodyshop.fields.responsibilitycenters.ap")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_responsibility_centers", "ap", "name"]}
>
<Input />
</Form.Item> */}
{/* <Form.Item
label={t("bodyshop.fields.responsibilitycenter_accountnumber")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_responsibility_centers", "ap", "accountnumber"]}
>
<Input />
</Form.Item> */}
<Form.Item
label={t("bodyshop.fields.responsibilitycenter_accountname")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_responsibility_centers", "ap", "accountname"]}
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.responsibilitycenter_accountdesc")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_responsibility_centers", "ap", "accountdesc"]}
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.dms.dms_acctnumber")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_responsibility_centers", "ap", "dms_acctnumber"]}
>
<Input />
</Form.Item>
</LayoutFormRow>
)}
<LayoutFormRow header={<div>Refund</div>}>
{/* <Form.Item
label={t("bodyshop.fields.responsibilitycenters.refund")}
@@ -4535,24 +4663,26 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Input />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={<div>Multiple Payers Item</div>}>
<Form.Item
label={t("bodyshop.fields.responsibilitycenter_accountitem")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={[
"md_responsibility_centers",
"qb_multiple_payers",
"accountitem",
]}
>
<Input />
</Form.Item>
</LayoutFormRow>
{Qb_Multi_Ar.treatment === "on" && (
<LayoutFormRow header={<div>Multiple Payers Item</div>}>
<Form.Item
label={t("bodyshop.fields.responsibilitycenter_accountitem")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={[
"md_responsibility_centers",
"qb_multiple_payers",
"accountitem",
]}
>
<Input />
</Form.Item>
</LayoutFormRow>
)}
<Typography.Title level={4}>
{t("bodyshop.labels.responsibilitycenters.sales_tax_codes")}
</Typography.Title>

View File

@@ -27,6 +27,7 @@ export function TimeTicektShiftContainer({
technician,
currentUser,
isTechConsole,
checkIfAlreadyClocked,
}) {
const [form] = Form.useForm();
const [insertTimeTicket] = useMutation(INSERT_NEW_TIME_TICKET);
@@ -43,36 +44,47 @@ export function TimeTicektShiftContainer({
const handleFinish = async (values) => {
setLoading(true);
const theTime = moment((await axios.post("/utils/time")).data);
const result = await insertTimeTicket({
variables: {
timeTicketInput: [
{
bodyshopid: bodyshop.id,
employeeid: isTechConsole ? technician.id : employeeId,
cost_center: "timetickets.labels.shift",
clockon: theTime,
date: theTime,
memo: values.memo,
},
],
},
awaitRefetchQueries: true,
refetchQueries: ["QUERY_ACTIVE_SHIFT_TIME_TICKETS"],
});
const alreadyClocked = await checkIfAlreadyClocked();
if (!!result.errors) {
if (alreadyClocked) {
//Show the error.
notification["error"]({
message: t("timetickets.errors.clockingin", {
message: JSON.stringify(result.errors),
}),
message: t("timetickets.errors.shiftalreadyclockedon"),
});
} else {
notification["success"]({
message: t("timetickets.successes.clockedin"),
const theTime = moment((await axios.post("/utils/time")).data);
const result = await insertTimeTicket({
variables: {
timeTicketInput: [
{
bodyshopid: bodyshop.id,
employeeid: isTechConsole ? technician.id : employeeId,
cost_center: "timetickets.labels.shift",
clockon: theTime,
date: theTime,
memo: values.memo,
},
],
},
awaitRefetchQueries: true,
refetchQueries: ["QUERY_ACTIVE_SHIFT_TIME_TICKETS"],
});
if (!!result.errors) {
notification["error"]({
message: t("timetickets.errors.clockingin", {
message: JSON.stringify(result.errors),
}),
});
} else {
notification["success"]({
message: t("timetickets.successes.clockedin"),
});
}
}
setLoading(false);
};

View File

@@ -64,6 +64,12 @@ export function TimeTicketShiftContainer({
</div>
);
const checkIfAlreadyClocked = async () => {
const { data } = await refetch();
return data.timetickets.length > 0;
};
return (
<div>
{data.timetickets.length > 0 ? (
@@ -72,7 +78,10 @@ export function TimeTicketShiftContainer({
refetch={refetch}
/>
) : (
<TimeTicketShiftFormContainer isTechConsole={isTechConsole} />
<TimeTicketShiftFormContainer
isTechConsole={isTechConsole}
checkIfAlreadyClocked={checkIfAlreadyClocked}
/>
)}
</div>
);

View File

@@ -1,5 +1,6 @@
import { DeleteFilled } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react";
import {
Button,
Divider,
@@ -20,7 +21,23 @@ import PhoneFormItem, {
} from "../form-items-formatted/phone-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorsPhonebookAdd from "../vendors-phonebook-add/vendors-phonebook-add.component";
export default function VendorsFormComponent({
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(VendorsFormComponent);
export function VendorsFormComponent({
bodyshop,
form,
formLoading,
handleDelete,
@@ -29,6 +46,12 @@ export default function VendorsFormComponent({
}) {
const { t } = useTranslation();
const client = useApolloClient();
const { DmsAp } = useTreatments(
["DmsAp"],
{},
bodyshop && bodyshop.imexshopid
);
const { getFieldValue } = form;
return (
<div>
@@ -184,6 +207,12 @@ export default function VendorsFormComponent({
// </Form.Item>
}
</LayoutFormRow>
{DmsAp.treatment === "on" && (
<Form.Item label={t("vendors.fields.dmsid")} name="dmsid">
<Input />
</Form.Item>
)}
<Divider align="left">{t("vendors.labels.preferredmakes")}</Divider>
<Form.List name="favorite">
{(fields, { add, remove }) => {

View File

@@ -1,77 +1,81 @@
import { gql } from "@apollo/client";
export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
query QUERY_ALL_ACTIVE_APPOINTMENTS(
$start: timestamptz!
$end: timestamptz!
$startd: date!
$endd: date!
) {
employee_vacation(
where: { _or: [{ start: { _gte: $startd } },
{ end: { _lte: $endd } },
{_and:[{start:{_lte: $startd}},{end:{_gte:$endd}}]}] }
query QUERY_ALL_ACTIVE_APPOINTMENTS(
$start: timestamptz!
$end: timestamptz!
$startd: date!
$endd: date!
) {
id
start
end
employee {
employee_vacation(
where: {
_or: [
{ start: { _gte: $startd } }
{ end: { _lte: $endd } }
{ _and: [{ start: { _lte: $startd } }, { end: { _gte: $endd } }] }
]
}
) {
id
last_name
first_name
start
end
employee {
id
last_name
first_name
}
}
}
appointments(
where: {
canceled: { _eq: false }
end: { _lte: $end }
start: { _gte: $start }
}
) {
start
id
end
arrived
title
isintake
block
color
note
job {
alt_transport
ro_number
ownr_ln
ownr_co_nm
ownr_fn
ownr_ph1
ownr_ph2
ownr_ea
clm_total
appointments(
where: {
canceled: { _eq: false }
end: { _lte: $end }
start: { _gte: $start }
}
) {
start
id
clm_no
ins_co_nm
v_model_yr
v_make_desc
v_model_desc
labhrs: joblines_aggregate(
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
) {
aggregate {
sum {
mod_lb_hrs
end
arrived
title
isintake
block
color
note
job {
alt_transport
ro_number
ownr_ln
ownr_co_nm
ownr_fn
ownr_ph1
ownr_ph2
ownr_ea
clm_total
id
clm_no
ins_co_nm
v_model_yr
v_make_desc
v_model_desc
labhrs: joblines_aggregate(
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
) {
aggregate {
sum {
mod_lb_hrs
}
}
}
larhrs: joblines_aggregate(
where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }
) {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
larhrs: joblines_aggregate(
where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }
) {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
}
}
`;
@@ -382,8 +386,8 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
}
`;
export const MARK_LATEST_APPOINTMENT_AS_ARRIVED = gql`
mutation MARK_LATEST_APPOINTMENT_AS_ARRIVED($appointmentId: uuid!) {
export const MARK_APPOINTMENT_ARRIVED = gql`
mutation MARK_APPOINTMENT_ARRIVED($appointmentId: uuid!) {
update_appointments(
where: { id: { _eq: $appointmentId } }
_set: { arrived: true }
@@ -396,3 +400,21 @@ export const MARK_LATEST_APPOINTMENT_AS_ARRIVED = gql`
}
}
`;
export const MARK_LATEST_APPOINTMENT_ARRIVED = gql`
mutation MARK_LATEST_APPOINTMENT_ARRIVED($jobId: uuid!) {
update_appointments(
where: {
jobid: { _eq: $jobId }
canceled: { _eq: false }
isintake: { _eq: true }
}
_set: { arrived: true }
) {
affected_rows
returning {
id
arrived
}
}
}
`;

View File

@@ -6,6 +6,7 @@ export const QUERY_ALL_ACTIVE_JOBS = gql`
where: { status: { _in: $statuses } }
order_by: { created_at: desc }
) {
iouparent
ownr_fn
ownr_ln
ownr_co_nm
@@ -109,6 +110,7 @@ export const QUERY_EXACT_JOB_IN_PRODUCTION = gql`
query QUERY_EXACT_JOB_IN_PRODUCTION($id: uuid!) {
jobs(where: { id: { _eq: $id } }) {
id
iouparent
status
ro_number
comment
@@ -187,6 +189,7 @@ export const QUERY_EXACT_JOBS_IN_PRODUCTION = gql`
query QUERY_EXACT_JOBS_IN_PRODUCTION($ids: [uuid!]!) {
jobs(where: { id: { _in: $ids } }) {
id
iouparent
status
ro_number
comment
@@ -269,6 +272,7 @@ export const QUERY_JOBS_IN_PRODUCTION = gql`
comment
status
category
iouparent
ro_number
ownr_fn
ownr_ln
@@ -609,6 +613,7 @@ export const GET_JOB_BY_PK = gql`
materials
auto_add_ats
rate_ats
iouparent
owner {
id
ownr_fn
@@ -2115,6 +2120,22 @@ export const DELETE_RELATED_RO = gql`
`;
export const GET_JOB_LINE_ORDERS = gql`
query GET_JOB_LINE_ORDERS($joblineid: uuid!) {
billlines(where: { joblineid: { _eq: $joblineid } }) {
actual_cost
actual_price
billid
quantity
bill {
id
invoice_number
date
vendorid
vendor {
id
name
}
}
}
parts_order_lines(where: { job_line_id: { _eq: $joblineid } }) {
id
act_price

View File

@@ -18,6 +18,7 @@ export const QUERY_VENDOR_BY_ID = gql`
street1
active
phone
dmsid
}
}
`;

View File

@@ -0,0 +1,163 @@
import { Button, Card, Col, notification, Row, Select, Space } from "antd";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import SocketIO from "socket.io-client";
import DmsAllocationsSummaryApComponent from "../../components/dms-allocations-summary-ap/dms-allocations-summary-ap.component";
import DmsLogEvents from "../../components/dms-log-events/dms-log-events.component";
import { auth } from "../../firebase/firebase.utils";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
export const socket = SocketIO(
process.env.NODE_ENV === "production"
? process.env.REACT_APP_AXIOS_BASE_API_URL
: window.location.origin,
{
path: "/ws",
withCredentials: true,
auth: async (callback) => {
const token = auth.currentUser && (await auth.currentUser.getIdToken());
callback({ token });
},
}
);
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation();
const [logLevel, setLogLevel] = useState("DEBUG");
const history = useHistory();
const [logs, setLogs] = useState([]);
const { state } = useLocation();
const logsRef = useRef(null);
useEffect(() => {
document.title = t("titles.dms");
setSelectedHeader("dms");
setBreadcrumbs([
{
link: "/manage/accounting/payables",
label: t("titles.bc.accounting-payables"),
},
{
link: "/manage/dms",
label: t("titles.bc.dms"),
},
]);
}, [t, setBreadcrumbs, setSelectedHeader]);
useEffect(() => {
socket.on("connect", () => socket.emit("set-log-level", logLevel));
socket.on("reconnect", () => {
setLogs((logs) => {
return [
...logs,
{
timestamp: new Date(),
level: "WARNING",
message: "Reconnected to CDK Export Service",
},
];
});
});
socket.on("log-event", (payload) => {
setLogs((logs) => {
return [...logs, payload];
});
});
socket.on("ap-export-complete", (payload) => {
notification.open({
type: "success",
message: t("jobs.labels.dms.apexported"),
});
});
if (socket.disconnected) socket.connect();
return () => {
socket.removeAllListeners();
socket.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!state?.billids) {
history.push(`/manage/accounting/payables`);
}
return (
<Row gutter={[16, 16]}>
<Col md={24} lg={12}>
<DmsAllocationsSummaryApComponent
socket={socket}
billids={state?.billids}
/>
</Col>
<Col md={24} lg={12}>
<div ref={logsRef}>
<Card
title={t("jobs.labels.dms.logs")}
extra={
<Space wrap>
<Select
placeholder="Log Level"
value={logLevel}
onChange={(value) => {
setLogLevel(value);
socket.emit("set-log-level", value);
}}
>
<Select.Option key="TRACE">TRACE</Select.Option>
<Select.Option key="DEBUG">DEBUG</Select.Option>
<Select.Option key="INFO">INFO</Select.Option>
<Select.Option key="WARNING">WARNING</Select.Option>
<Select.Option key="ERROR">ERROR</Select.Option>
</Select>
<Button onClick={() => setLogs([])}>Clear Logs</Button>
<Button
onClick={() => {
setLogs([]);
socket.disconnect();
socket.connect();
}}
>
Reconnect
</Button>
</Space>
}
>
<DmsLogEvents socket={socket} logs={logs} />
</Card>
</div>
</Col>
</Row>
);
}
export const determineDmsType = (bodyshop) => {
if (bodyshop.cdk_dealerid) return "cdk";
else {
return "pbs";
}
};

View File

@@ -3,6 +3,7 @@ import { useApolloClient, useMutation } from "@apollo/client";
import {
Alert,
Button,
Col,
Divider,
Form,
Input,
@@ -10,9 +11,12 @@ import {
notification,
PageHeader,
Popconfirm,
Row,
Select,
Space,
Statistic,
Switch,
Typography,
} from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -33,7 +37,7 @@ import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.qu
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import Dinero from "dinero.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
@@ -325,75 +329,164 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
</Form.Item>
)}
</LayoutFormRow>
<Divider>{t("jobs.labels.multipayers")}</Divider>
{Qb_Multi_Ar.treatment === "on" && (
<>
<Form.List name={["qb_multiple_payers"]}>
{(fields, { add, remove }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Space>
<Form.Item
label={t("jobs.fields.qb_multiple_payers.name")}
key={`${index}name`}
name={[field.name, "name"]}
rules={[
{
required: true,
},
]}
>
<Select
style={{ minWidth: "12rem" }}
disabled={jobRO}
<Row gutter={[16, 16]}>
<Col lg={8} md={24}>
<Form.List
name={["qb_multiple_payers"]}
rules={[
({ getFieldValue }) => ({
validator(_, value) {
let totalAllocated = Dinero();
const payers = form.getFieldValue("qb_multiple_payers");
payers &&
payers.forEach((payer) => {
totalAllocated = totalAllocated.add(
Dinero({
amount: Math.round((payer?.amount || 0) * 100),
})
);
});
const discrep = job.job_totals
? Dinero(job.job_totals.totals.total_repairs).subtract(
totalAllocated
)
: Dinero();
return discrep.getAmount() >= 0
? Promise.resolve()
: Promise.reject(
new Error(
t("jobs.labels.additionalpayeroverallocation")
)
);
},
}),
]}
>
{(fields, { add, remove }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Space>
<Form.Item
label={t("jobs.fields.qb_multiple_payers.name")}
key={`${index}name`}
name={[field.name, "name"]}
rules={[
{
required: true,
},
]}
>
{bodyshop.md_ins_cos.map((s) => (
<Select.Option key={s.name} value={s.name}>
{s.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Select
style={{ minWidth: "12rem" }}
disabled={jobRO}
>
{bodyshop.md_ins_cos.map((s) => (
<Select.Option key={s.name} value={s.name}>
{s.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("jobs.fields.qb_multiple_payers.amount")}
key={`${index}amount`}
name={[field.name, "amount"]}
rules={[
{
required: true,
},
]}
>
<CurrencyInput min={0} disabled={jobRO} />
</Form.Item>
<Form.Item
label={t("jobs.fields.qb_multiple_payers.amount")}
key={`${index}amount`}
name={[field.name, "amount"]}
rules={[
{
required: true,
},
]}
>
<CurrencyInput min={0} disabled={jobRO} />
</Form.Item>
<DeleteFilled
disabled={jobRO}
onClick={() => {
remove(field.name);
}}
/>
</Space>
<DeleteFilled
disabled={jobRO}
onClick={() => {
remove(field.name);
}}
/>
</Space>
</Form.Item>
))}
<Form.Item>
<Button
disabled={jobRO}
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("jobs.actions.dms.addpayer")}
</Button>
</Form.Item>
))}
<Form.Item>
<Button
disabled={jobRO}
onClick={() => {
if (fields.length < 3) add();
</div>
);
}}
</Form.List>
</Col>
<Col lg={16} md={24}>
<Form.Item shouldUpdate>
{() => {
//Perform Calculation to determine discrepancy.
let totalAllocated = Dinero();
const payers = form.getFieldValue("qb_multiple_payers");
payers &&
payers.forEach((payer) => {
totalAllocated = totalAllocated.add(
Dinero({
amount: Math.round((payer?.amount || 0) * 100),
})
);
});
const discrep = job.job_totals
? Dinero(job.job_totals.totals.total_repairs).subtract(
totalAllocated
)
: Dinero();
return (
<Space size="large" wrap align="center">
<Statistic
title={t("jobs.labels.total_cust_payable")}
value={(job.job_totals
? Dinero(job.job_totals.totals.custPayable)
: Dinero()
).toFormat()}
/>
<Divider type="vertical" />
<Statistic
title={t("jobs.labels.total_repairs")}
value={(job.job_totals
? Dinero(job.job_totals.totals.total_repairs)
: Dinero()
).toFormat()}
/>
<Typography.Title>-</Typography.Title>
<Statistic
title={t("jobs.labels.dms.totalallocated")}
value={totalAllocated.toFormat()}
/>
<Typography.Title>=</Typography.Title>
<Statistic
title={t("jobs.labels.pimraryamountpayable")}
valueStyle={{
color: discrep.getAmount() > 0 ? "green" : "red",
}}
style={{ width: "100%" }}
>
{t("jobs.actions.dms.addpayer")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</>
value={discrep.toFormat()}
/>
</Space>
);
}}
</Form.Item>
</Col>
</Row>
)}
<Divider />
<JobsCloseLines job={job} />

View File

@@ -89,6 +89,7 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
let job = Object.assign(
{},
values,
{ date_open: new Date() },
{
vehicle:
state.vehicle.selectedid || state.vehicle.none

View File

@@ -164,6 +164,9 @@ const EmailTest = lazy(() =>
);
const Dashboard = lazy(() => import("../dashboard/dashboard.container"));
const Dms = lazy(() => import("../dms/dms.container"));
const DmsPayables = lazy(() =>
import("../dms-payables/dms-payables.container")
);
const { Content, Footer } = Layout;
@@ -391,6 +394,7 @@ export function Manage({ match, conflict, bodyshop }) {
<Route exact path={`${match.path}/emailtest`} component={EmailTest} />
<Route exact path={`${match.path}/dashboard`} component={Dashboard} />
<Route exact path={`${match.path}/dms`} component={Dms} />
<Route exact path={`${match.path}/dmsap`} component={DmsPayables} />
</Suspense>
);

View File

@@ -1,6 +1,7 @@
import { SyncOutlined } from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Input, Space, Table } from "antd";
import _ from "lodash";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -14,7 +15,6 @@ import OwnerNameDisplay from "../../components/owner-name-display/owner-name-dis
import ProductionListColumnComment from "../../components/production-list-columns/production-list-columns.comment.component";
import { QUERY_PARTS_QUEUE } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { onlyUnique } from "../../utils/arrayHelper";
import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
@@ -94,6 +94,14 @@ export function PartsQueuePageComponent({ bodyshop }) {
// searchParams.page = pagination.current;
searchParams.sortcolumn = sorter.columnKey;
searchParams.sortorder = sorter.order;
if (filters.status) {
searchParams.statusFilters = JSON.stringify(
_.flattenDeep(filters.status)
);
} else {
delete searchParams.statusFilters;
}
setFilter(filters);
history.push({ search: queryString.stringify(searchParams) });
};
@@ -136,19 +144,14 @@ export function PartsQueuePageComponent({ bodyshop }) {
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder: sortcolumn === "status" && sortorder,
filteredValue: statusFilters ? JSON.parse(statusFilters) : null,
filters:
(jobs &&
jobs
.map((j) => j.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.status),
bodyshop.md_ro_statuses.active_statuses.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
}) || [],
render: (text, record) => {
return record.status || t("general.labels.na");
},

View File

@@ -39,6 +39,11 @@ export function* getJobMedia({ payload: jobid }) {
thumbnail: normalizeUrl(
`${localmediaserverhttp}/${d.thumbnail}`
),
...(d.optimized && {
optimized: normalizeUrl(
`${localmediaserverhttp}/${d.optimized}`
),
}),
isSelected: false,
key: idx,
};
@@ -50,6 +55,7 @@ export function* getJobMedia({ payload: jobid }) {
thumbnail: normalizeUrl(
`${localmediaserverhttp}/${d.thumbnail}`
),
isSelected: false,
key: idx,
};
@@ -91,6 +97,11 @@ export function* getBillMedia({ payload: { jobid, invoice_number } }) {
thumbnail: normalizeUrl(
`${localmediaserverhttp}/${d.thumbnail}`
),
...(d.optimized && {
optimized: normalizeUrl(
`${localmediaserverhttp}/${d.optimized}`
),
}),
isSelected: false,
key: idx,
};

View File

@@ -195,6 +195,7 @@
"entered_total": "Total of Entered Lines",
"enteringcreditmemo": "You are entering a credit memo. Please ensure you are also entering positive values.",
"federal_tax": "Federal Tax",
"generatepartslabel": "Generate Parts Labels after Saving?",
"iouexists": "An IOU exists that is associated to this RO.",
"local_tax": "Local Tax",
"markexported": "Mark Exported",
@@ -436,6 +437,7 @@
"ar": "Accounts Receivable",
"ats": "ATS",
"federal_tax": "Federal Tax",
"federal_tax_itc": "Federal Tax Credit",
"gst_override": "GST Override Account #",
"la1": "LA1",
"la2": "LA2",
@@ -848,6 +850,7 @@
"doctype": "Document Type",
"newjobid": "Assign to Job",
"openinexplorer": "Open in Explorer",
"optimizedimage": "The below image is optimized. Click on the picture below to open in a new window and view it full size, or open it in explorer.",
"reassign_limitexceeded": "Reassigning all selected documents will exceed the job storage limit for your shop. ",
"reassign_limitexceeded_title": "Unable to reassign document(s)",
"storageexceeded": "You've exceeded your storage limit for this job. Please remove documents, or increase your storage plan.",
@@ -1036,6 +1039,7 @@
"sendby": "Send By",
"signin": "Sign In",
"sms": "SMS",
"status": "Status",
"sub_status": {
"expired": "The subscription for this shop has expired. Please contact technical support to reactivate the subscription. "
},
@@ -1352,6 +1356,7 @@
"depreciation_taxes": "Depreciation/Taxes",
"dms": {
"address": "Customer Address",
"amount": "Amount",
"center": "Center",
"cost": "Cost",
"cost_dms_acctnumber": "Cost DMS Acct #",
@@ -1361,6 +1366,7 @@
"id": "DMS ID",
"inservicedate": "In Service Date",
"journal": "Journal #",
"lines": "Posting Lines",
"name1": "Customer Name",
"payer": {
"amount": "Amount",
@@ -1546,11 +1552,13 @@
"actual_completion_inferred": "$t(jobs.fields.actual_completion) inferred using $t(jobs.fields.scheduled_completion).",
"actual_delivery_inferred": "$t(jobs.fields.actual_delivery) inferred using $t(jobs.fields.scheduled_delivery).",
"actual_in_inferred": "$t(jobs.fields.actual_in) inferred using $t(jobs.fields.scheduled_in).",
"additionalpayeroverallocation": "You have allocated more than the sale of the Job to additional payers.",
"additionaltotal": "Additional Total",
"adjustmentrate": "Adjustment Rate",
"adjustments": "Adjustments",
"adminwarning": "Use the functionality on this page at your own risk. You are responsible for any and all changes to your data.",
"allocations": "Allocations",
"alreadyaddedtoscoreboard": "Job has already been added to scoreboard. Saving will update the previous entry.",
"alreadyclosed": "This job has already been closed.",
"appointmentconfirmation": "Send confirmation to customer?",
"associationwarning": "Any changes to associations will require updating the data from the new parent record to the job.",
@@ -1612,6 +1620,7 @@
"difference": "Difference",
"diskscan": "Scan Disk for Estimates",
"dms": {
"apexported": "AP export completed. See logs for details.",
"damageto": "Damage to $t(jobs.fields.area_of_damage_impact.{{area_of_damage}}).",
"defaultstory": "B/S RO: {{ro_number}}. Owner: {{ownr_nm}}. Insurance Co: {{ins_co_nm}}. Claim/PO #: {{clm_po}}",
"invoicedatefuture": "Invoice date must be today or in the future for CDK posting.",
@@ -1638,6 +1647,7 @@
"importnote": "The job was initially imported.",
"inproduction": "In Production",
"intakechecklist": "Intake Checklist",
"iou": "IOU",
"job": "Job Details",
"jobcosting": "Job Costing",
"jobtotals": "Job Totals",
@@ -1649,6 +1659,7 @@
"mapa": "Paint Materials",
"markforreexport": "Mark for Re-export",
"mash": "Shop Materials",
"multipayers": "Additional Payers",
"net_repairs": "Net Repairs",
"notes": "Notes",
"othertotal": "Other Totals",
@@ -1660,6 +1671,7 @@
"partsfilter": "Parts Only",
"partssubletstotal": "Parts & Sublets Total",
"partstotal": "Parts Total (ex. Taxes)",
"pimraryamountpayable": "Total Primary Payable",
"plitooltips": {
"billtotal": "The total amount of all bill lines that have been posted against this RO (not including credits, taxes, or labor adjustments).",
"calculatedcreditsnotreceived": "The calculated credits not received is derived by subtracting the amount of credit memos entered from the <b>retail</b> total of returns created. This does not take into account whether the credit was marked as received. You can find more information <a href=\"https://help.imex.online/en/article/credits-not-received-changes-1jy9snw\" target=\"_blank\">here</a>.",
@@ -2088,6 +2100,7 @@
"customer": "Customer",
"edit": "Edit Payment",
"electronicpayment": "Use Electronic Payment Processing?",
"external": "External",
"findermodal": "ICBC Payment Finder",
"insurance": "Insurance",
"new": "New Payment",
@@ -2208,6 +2221,7 @@
"mpi_eglass_auth": "MPI - eGlass Auth",
"mpi_final_acct_sheet": "MPI - Final Accounting Sheet",
"paint_grid": "Paint Grid",
"parts_invoice_label_single": "Parts Label Single",
"parts_label_multiple": "Parts Label - Multi",
"parts_label_single": "Parts Label - Single",
"parts_list": "Parts List",
@@ -2404,15 +2418,19 @@
"hours_sold_detail_closed": "Hours Sold Detail - Closed",
"hours_sold_detail_closed_csr": "Hours Sold Detail - Closed by CSR",
"hours_sold_detail_closed_ins_co": "Hours Sold Detail - Closed by Source",
"hours_sold_detail_closed_status": "Hours Sold Detail - Closed by Status",
"hours_sold_detail_open": "Hours Sold Detail - Open",
"hours_sold_detail_open_csr": "Hours Sold Detail - Open by CSR",
"hours_sold_detail_open_ins_co": "Hours Sold Detail - Open by Source",
"hours_sold_detail_open_status": "Hours Sold Detail - Open by Status",
"hours_sold_summary_closed": "Hours Sold Summary - Closed",
"hours_sold_summary_closed_csr": "Hours Sold Summary - Closed by CSR",
"hours_sold_summary_closed_ins_co": "Hours Sold Summary - Closed by Source",
"hours_sold_summary_closed_status": "Hours Sold Summary - Closed by Status",
"hours_sold_summary_open": "Hours Sold Summary - Open",
"hours_sold_summary_open_csr": "Hours Sold Summary - Open CSR",
"hours_sold_summary_open_ins_co": "Hours Sold Summary - Open by Source",
"hours_sold_summary_open_status": "Hours Sold Summary - Open by Status",
"job_costing_ro_csr": "Job Costing by CSR",
"job_costing_ro_date_detail": "Job Costing by RO - Detail",
"job_costing_ro_date_summary": "Job Costing by RO - Summary",
@@ -2428,6 +2446,7 @@
"parts_backorder": "IOU Parts List",
"parts_not_recieved": "Parts Not Received",
"parts_not_recieved_vendor": "Parts Not Received by Vendor",
"parts_received_not_scheduled": "Parts Received for Jobs Not Scheduled",
"payments_by_date": "Payments by Date",
"payments_by_date_type": "Payments by Date and Type",
"production_by_category": "Production by Category",
@@ -2545,7 +2564,8 @@
"creating": "Error creating time ticket. {{message}}",
"deleting": "Error deleting time ticket. {{message}}",
"noemployeeforuser": "Unable to use Shift Clock",
"noemployeeforuser_sub": "An employee record has not been created for this user. Please create one before using the shift clock. "
"noemployeeforuser_sub": "An employee record has not been created for this user. Please create one before using the shift clock. ",
"shiftalreadyclockedon": "You are already clocked onto a shift. Unable to create shift entry."
},
"fields": {
"actualhrs": "Actual Hours",
@@ -2785,6 +2805,7 @@
"country": "Country",
"discount": "Discount % (as decimal)",
"display_name": "Display Name",
"dmsid": "DMS ID",
"due_date": "Payment Due Date (# of days)",
"email": "Contact Email",
"favorite": "Favorite?",

View File

@@ -195,6 +195,7 @@
"entered_total": "",
"enteringcreditmemo": "",
"federal_tax": "",
"generatepartslabel": "",
"iouexists": "",
"local_tax": "",
"markexported": "",
@@ -436,6 +437,7 @@
"ar": "",
"ats": "",
"federal_tax": "",
"federal_tax_itc": "",
"gst_override": "",
"la1": "",
"la2": "",
@@ -848,6 +850,7 @@
"doctype": "",
"newjobid": "",
"openinexplorer": "",
"optimizedimage": "",
"reassign_limitexceeded": "",
"reassign_limitexceeded_title": "",
"storageexceeded": "",
@@ -1036,6 +1039,7 @@
"sendby": "",
"signin": "",
"sms": "",
"status": "",
"sub_status": {
"expired": ""
},
@@ -1352,6 +1356,7 @@
"depreciation_taxes": "Depreciación / Impuestos",
"dms": {
"address": "",
"amount": "",
"center": "",
"cost": "",
"cost_dms_acctnumber": "",
@@ -1361,6 +1366,7 @@
"id": "",
"inservicedate": "",
"journal": "",
"lines": "",
"name1": "",
"payer": {
"amount": "",
@@ -1546,11 +1552,13 @@
"actual_completion_inferred": "",
"actual_delivery_inferred": "",
"actual_in_inferred": "",
"additionalpayeroverallocation": "",
"additionaltotal": "",
"adjustmentrate": "",
"adjustments": "",
"adminwarning": "",
"allocations": "",
"alreadyaddedtoscoreboard": "",
"alreadyclosed": "",
"appointmentconfirmation": "¿Enviar confirmación al cliente?",
"associationwarning": "",
@@ -1612,6 +1620,7 @@
"difference": "",
"diskscan": "",
"dms": {
"apexported": "",
"damageto": "",
"defaultstory": "",
"invoicedatefuture": "",
@@ -1638,6 +1647,7 @@
"importnote": "",
"inproduction": "",
"intakechecklist": "",
"iou": "",
"job": "",
"jobcosting": "",
"jobtotals": "",
@@ -1649,6 +1659,7 @@
"mapa": "",
"markforreexport": "",
"mash": "",
"multipayers": "",
"net_repairs": "",
"notes": "Notas",
"othertotal": "",
@@ -1660,6 +1671,7 @@
"partsfilter": "",
"partssubletstotal": "",
"partstotal": "",
"pimraryamountpayable": "",
"plitooltips": {
"billtotal": "",
"calculatedcreditsnotreceived": "",
@@ -2088,6 +2100,7 @@
"customer": "",
"edit": "",
"electronicpayment": "",
"external": "",
"findermodal": "",
"insurance": "",
"new": "",
@@ -2208,6 +2221,7 @@
"mpi_eglass_auth": "",
"mpi_final_acct_sheet": "",
"paint_grid": "",
"parts_invoice_label_single": "",
"parts_label_multiple": "",
"parts_label_single": "",
"parts_list": "",
@@ -2404,15 +2418,19 @@
"hours_sold_detail_closed": "",
"hours_sold_detail_closed_csr": "",
"hours_sold_detail_closed_ins_co": "",
"hours_sold_detail_closed_status": "",
"hours_sold_detail_open": "",
"hours_sold_detail_open_csr": "",
"hours_sold_detail_open_ins_co": "",
"hours_sold_detail_open_status": "",
"hours_sold_summary_closed": "",
"hours_sold_summary_closed_csr": "",
"hours_sold_summary_closed_ins_co": "",
"hours_sold_summary_closed_status": "",
"hours_sold_summary_open": "",
"hours_sold_summary_open_csr": "",
"hours_sold_summary_open_ins_co": "",
"hours_sold_summary_open_status": "",
"job_costing_ro_csr": "",
"job_costing_ro_date_detail": "",
"job_costing_ro_date_summary": "",
@@ -2428,6 +2446,7 @@
"parts_backorder": "",
"parts_not_recieved": "",
"parts_not_recieved_vendor": "",
"parts_received_not_scheduled": "",
"payments_by_date": "",
"payments_by_date_type": "",
"production_by_category": "",
@@ -2545,7 +2564,8 @@
"creating": "",
"deleting": "",
"noemployeeforuser": "",
"noemployeeforuser_sub": ""
"noemployeeforuser_sub": "",
"shiftalreadyclockedon": ""
},
"fields": {
"actualhrs": "",
@@ -2785,6 +2805,7 @@
"country": "País",
"discount": "% De descuento",
"display_name": "Nombre para mostrar",
"dmsid": "",
"due_date": "Fecha de vencimiento del pago",
"email": "Email de contacto",
"favorite": "¿Favorito?",

View File

@@ -195,6 +195,7 @@
"entered_total": "",
"enteringcreditmemo": "",
"federal_tax": "",
"generatepartslabel": "",
"iouexists": "",
"local_tax": "",
"markexported": "",
@@ -436,6 +437,7 @@
"ar": "",
"ats": "",
"federal_tax": "",
"federal_tax_itc": "",
"gst_override": "",
"la1": "",
"la2": "",
@@ -848,6 +850,7 @@
"doctype": "",
"newjobid": "",
"openinexplorer": "",
"optimizedimage": "",
"reassign_limitexceeded": "",
"reassign_limitexceeded_title": "",
"storageexceeded": "",
@@ -1036,6 +1039,7 @@
"sendby": "",
"signin": "",
"sms": "",
"status": "",
"sub_status": {
"expired": ""
},
@@ -1352,6 +1356,7 @@
"depreciation_taxes": "Amortissement / taxes",
"dms": {
"address": "",
"amount": "",
"center": "",
"cost": "",
"cost_dms_acctnumber": "",
@@ -1361,6 +1366,7 @@
"id": "",
"inservicedate": "",
"journal": "",
"lines": "",
"name1": "",
"payer": {
"amount": "",
@@ -1546,11 +1552,13 @@
"actual_completion_inferred": "",
"actual_delivery_inferred": "",
"actual_in_inferred": "",
"additionalpayeroverallocation": "",
"additionaltotal": "",
"adjustmentrate": "",
"adjustments": "",
"adminwarning": "",
"allocations": "",
"alreadyaddedtoscoreboard": "",
"alreadyclosed": "",
"appointmentconfirmation": "Envoyer une confirmation au client?",
"associationwarning": "",
@@ -1612,6 +1620,7 @@
"difference": "",
"diskscan": "",
"dms": {
"apexported": "",
"damageto": "",
"defaultstory": "",
"invoicedatefuture": "",
@@ -1638,6 +1647,7 @@
"importnote": "",
"inproduction": "",
"intakechecklist": "",
"iou": "",
"job": "",
"jobcosting": "",
"jobtotals": "",
@@ -1649,6 +1659,7 @@
"mapa": "",
"markforreexport": "",
"mash": "",
"multipayers": "",
"net_repairs": "",
"notes": "Remarques",
"othertotal": "",
@@ -1660,6 +1671,7 @@
"partsfilter": "",
"partssubletstotal": "",
"partstotal": "",
"pimraryamountpayable": "",
"plitooltips": {
"billtotal": "",
"calculatedcreditsnotreceived": "",
@@ -2088,6 +2100,7 @@
"customer": "",
"edit": "",
"electronicpayment": "",
"external": "",
"findermodal": "",
"insurance": "",
"new": "",
@@ -2208,6 +2221,7 @@
"mpi_eglass_auth": "",
"mpi_final_acct_sheet": "",
"paint_grid": "",
"parts_invoice_label_single": "",
"parts_label_multiple": "",
"parts_label_single": "",
"parts_list": "",
@@ -2404,15 +2418,19 @@
"hours_sold_detail_closed": "",
"hours_sold_detail_closed_csr": "",
"hours_sold_detail_closed_ins_co": "",
"hours_sold_detail_closed_status": "",
"hours_sold_detail_open": "",
"hours_sold_detail_open_csr": "",
"hours_sold_detail_open_ins_co": "",
"hours_sold_detail_open_status": "",
"hours_sold_summary_closed": "",
"hours_sold_summary_closed_csr": "",
"hours_sold_summary_closed_ins_co": "",
"hours_sold_summary_closed_status": "",
"hours_sold_summary_open": "",
"hours_sold_summary_open_csr": "",
"hours_sold_summary_open_ins_co": "",
"hours_sold_summary_open_status": "",
"job_costing_ro_csr": "",
"job_costing_ro_date_detail": "",
"job_costing_ro_date_summary": "",
@@ -2428,6 +2446,7 @@
"parts_backorder": "",
"parts_not_recieved": "",
"parts_not_recieved_vendor": "",
"parts_received_not_scheduled": "",
"payments_by_date": "",
"payments_by_date_type": "",
"production_by_category": "",
@@ -2545,7 +2564,8 @@
"creating": "",
"deleting": "",
"noemployeeforuser": "",
"noemployeeforuser_sub": ""
"noemployeeforuser_sub": "",
"shiftalreadyclockedon": ""
},
"fields": {
"actualhrs": "",
@@ -2785,6 +2805,7 @@
"country": "Pays",
"discount": "Remise %",
"display_name": "Afficher un nom",
"dmsid": "",
"due_date": "Date limite de paiement",
"email": "Email du contact",
"favorite": "Préféré?",

View File

@@ -510,6 +510,13 @@ export const TemplateList = (type, context) => {
key: "parts_label_multiple",
disabled: false,
},
parts_invoice_label_single: {
title: i18n.t("printcenter.jobs.parts_invoice_label_single"),
description: "Parts Label Multiple",
key: "parts_invoice_label_single",
disabled: false,
ignoreCustomMargins: true,
},
csi_invitation_action: {
title: i18n.t("printcenter.jobs.csi_invitation_action"),
description: "CSI invite",
@@ -786,6 +793,74 @@ export const TemplateList = (type, context) => {
},
group: "sales",
},
hours_sold_summary_open_status: {
title: i18n.t(
"reportcenter.templates.hours_sold_summary_open_status"
),
description: "",
subject: i18n.t(
"reportcenter.templates.hours_sold_summary_open_status"
),
key: "hours_sold_summary_open_status",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced"),
},
group: "sales",
},
hours_sold_summary_closed_status: {
title: i18n.t(
"reportcenter.templates.hours_sold_summary_closed_status"
),
description: "",
subject: i18n.t(
"reportcenter.templates.hours_sold_summary_closed_status"
),
key: "hours_sold_summary_closed_status",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced"),
},
group: "sales",
},
hours_sold_detail_open_status: {
title: i18n.t(
"reportcenter.templates.hours_sold_detail_open_status"
),
description: "",
subject: i18n.t(
"reportcenter.templates.hours_sold_detail_open_status"
),
key: "hours_sold_detail_open_status",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced"),
},
group: "sales",
},
hours_sold_detail_closed_status: {
title: i18n.t(
"reportcenter.templates.hours_sold_detail_closed_status"
),
description: "",
subject: i18n.t(
"reportcenter.templates.hours_sold_detail_closed_status"
),
key: "hours_sold_detail_closed_status",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_invoiced"),
},
group: "sales",
},
purchases_by_date_range_detail: {
title: i18n.t(
"reportcenter.templates.purchases_by_date_range_detail"
@@ -1579,6 +1654,22 @@ export const TemplateList = (type, context) => {
},
group: "jobs",
},
parts_received_not_scheduled: {
title: i18n.t(
"reportcenter.templates.parts_received_not_scheduled"
),
subject: i18n.t(
"reportcenter.templates.parts_received_not_scheduled"
),
key: "parts_received_not_scheduled",
//idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_open"),
},
group: "jobs",
},
psr_by_make: {
title: i18n.t("reportcenter.templates.psr_by_make"),
subject: i18n.t("reportcenter.templates.psr_by_make"),

View File

@@ -5396,6 +5396,7 @@
- country
- created_at
- discount
- dmsid
- due_date
- email
- favorite
@@ -5418,6 +5419,7 @@
- country
- created_at
- discount
- dmsid
- due_date
- email
- favorite
@@ -5450,6 +5452,7 @@
- country
- created_at
- discount
- dmsid
- due_date
- email
- favorite

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."vendors" add column "dmsid" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."vendors" add column "dmsid" text
null;

View File

@@ -64,11 +64,7 @@ app.use(
//Email Based Paths.
var sendEmail = require("./server/email/sendemail.js");
app.post("/sendemail", fb.validateFirebaseIdToken, sendEmail.sendEmail);
app.post(
"/emailbounce",
bodyParser.text(),
sendEmail.emailBounce
);
app.post("/emailbounce", bodyParser.text(), sendEmail.emailBounce);
//Test route to ensure Express is responding.
app.get("/test", async function (req, res) {

View File

@@ -0,0 +1,285 @@
const path = require("path");
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
});
const GraphQLClient = require("graphql-request").GraphQLClient;
const queries = require("../../graphql-client/queries");
const CdkBase = require("../../web-sockets/web-socket");
const moment = require("moment");
const Dinero = require("dinero.js");
const AxiosLib = require("axios").default;
const axios = AxiosLib.create();
const { PBS_ENDPOINTS, PBS_CREDENTIALS } = require("./pbs-constants");
const { CheckForErrors } = require("./pbs-job-export");
const uuid = require("uuid").v4;
axios.interceptors.request.use((x) => {
const socket = x.socket;
const headers = {
...x.headers.common,
...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);
CdkBase.createJsonEvent(socket, "TRACE", `Raw Request: ${printable}`, x.data);
return 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,
"TRACE",
`Raw Response: ${printable}`,
x.data
);
return x;
});
async function PbsCalculateAllocationsAp(socket, billids) {
try {
CdkBase.createLogEvent(
socket,
"DEBUG",
`Received request to calculate allocations for ${billids}`
);
const { bills, bodyshops } = await QueryBillData(socket, billids);
const bodyshop = bodyshops[0];
socket.bodyshop = bodyshop;
socket.bills = bills;
//Each bill will enter it's own top level transaction.
const transactionlist = [];
if (bills.length === 0) {
CdkBase.createLogEvent(
socket,
"ERROR",
`No bills found for export. Ensure they have not already been exported and try again.`
);
}
bills.forEach((bill) => {
//Keep the allocations at the bill level.
const transactionObject = {
SerialNumber: socket.bodyshop.pbs_serialnumber,
billid: bill.id,
Posting: {
Reference: bill.invoice_number,
JournalCode: socket.txEnvelope ? socket.txEnvelope.journal : null,
TransactionDate: moment().tz(socket.bodyshop.timezone).toISOString(), //"0001-01-01T00:00:00.0000000Z",
//Description: "Bulk AP posting.",
//AdditionalInfo: "String",
Source: "ImEX Online",
Lines: [], //socket.apAllocations,
},
};
const billHash = {
[bodyshop.md_responsibility_centers.taxes.federal_itc.name]: {
Account:
bodyshop.md_responsibility_centers.taxes.federal_itc.dms_acctnumber,
ControlNumber: bill.vendor.dmsid,
Amount: Dinero(),
// Comment: "String",
AdditionalInfo: bill.vendor.name,
InvoiceNumber: bill.invoice_number,
InvoiceDate: moment(bill.date).tz(bodyshop.timezone).toISOString(),
},
[bodyshop.md_responsibility_centers.taxes.state.name]: {
Account:
bodyshop.md_responsibility_centers.taxes.state.dms_acctnumber,
ControlNumber: bill.vendor.dmsid,
Amount: Dinero(),
// Comment: "String",
AdditionalInfo: bill.vendor.name,
InvoiceNumber: bill.invoice_number,
InvoiceDate: moment(bill.date).tz(bodyshop.timezone).toISOString(),
},
};
bill.billlines.forEach((bl) => {
let lineDinero = Dinero({
amount: Math.round((bl.actual_cost || 0) * 100),
})
.multiply(bl.quantity)
.multiply(bill.is_credit_memo ? -1 : 1);
const cc = getCostAccount(bl, bodyshop.md_responsibility_centers);
if (!billHash[cc.name]) {
billHash[cc.name] = {
Account: cc.dms_acctnumber,
ControlNumber: bill.vendor.dmsid,
Amount: Dinero(),
// Comment: "String",
AdditionalInfo: bill.vendor.name,
InvoiceNumber: bill.invoice_number,
InvoiceDate: moment(bill.date).tz(bodyshop.timezone).toISOString(),
};
}
//Add the line amount.
billHash[cc.name] = {
...billHash[cc.name],
Amount: billHash[cc.name].Amount.add(lineDinero),
};
//Does the line have taxes?
if (bl.applicable_taxes.federal) {
billHash[bodyshop.md_responsibility_centers.taxes.federal_itc.name] =
{
...billHash[
bodyshop.md_responsibility_centers.taxes.federal_itc.name
],
Amount: billHash[
bodyshop.md_responsibility_centers.taxes.federal_itc.name
].Amount.add(lineDinero.percentage(bill.federal_tax_rate || 0)),
};
}
if (bl.applicable_taxes.state) {
billHash[bodyshop.md_responsibility_centers.taxes.state.name] = {
...billHash[bodyshop.md_responsibility_centers.taxes.state.name],
Amount: billHash[
bodyshop.md_responsibility_centers.taxes.state.name
].Amount.add(lineDinero.percentage(bill.state_tax_rate || 0)),
};
}
//End tax check
});
let APAmount = Dinero();
Object.keys(billHash).map((key) => {
if (billHash[key].Amount.getAmount() > 0) {
transactionObject.Posting.Lines.push({
...billHash[key],
Amount: billHash[key].Amount.toFormat("0.00"),
});
APAmount = APAmount.add(billHash[key].Amount); //Calculate the total expense for the bill iteratively to create the corresponding credit to AP.
}
});
transactionObject.Posting.Lines.push({
Account: bodyshop.md_responsibility_centers.ap.dms_acctnumber,
ControlNumber: bill.vendor.dmsid,
Amount: APAmount.multiply(-1).toFormat("0.00"),
// Comment: "String",
AdditionalInfo: bill.vendor.name,
InvoiceNumber: bill.invoice_number,
InvoiceDate: moment(bill.date).tz(bodyshop.timezone).toISOString(),
});
transactionlist.push(transactionObject);
});
return transactionlist;
} catch (error) {
CdkBase.createLogEvent(
socket,
"ERROR",
`Error encountered in PbsCalculateAllocationsAp. ${error}`
);
}
}
exports.PbsCalculateAllocationsAp = PbsCalculateAllocationsAp;
async function QueryBillData(socket, billids) {
CdkBase.createLogEvent(
socket,
"DEBUG",
`Querying bill data for id(s) ${billids}`
);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.GET_PBS_AP_ALLOCATIONS, { billids: billids });
CdkBase.createLogEvent(
socket,
"TRACE",
`Bill data query result ${JSON.stringify(result, null, 2)}`
);
return result;
}
//@returns the account object.
function getCostAccount(billline, respcenters) {
if (!billline.cost_center) return null;
const acctName = respcenters.defaults.costs[billline.cost_center];
return respcenters.costs.find((c) => c.name === acctName);
}
exports.PbsExportAp = async function (socket, { billids, txEnvelope }) {
CdkBase.createLogEvent(socket, "DEBUG", `Exporting selected AP.`);
//apAllocations has the same shap as the lines key for the accounting posting to PBS.
socket.apAllocations = await PbsCalculateAllocationsAp(socket, billids);
socket.txEnvelope = txEnvelope;
for (const allocation of socket.apAllocations) {
const { billid, ...restAllocation } = allocation;
const { data: AccountPostingChange } = await axios.post(
PBS_ENDPOINTS.AccountingPostingChange,
restAllocation,
{ auth: PBS_CREDENTIALS, socket }
);
CheckForErrors(socket, AccountPostingChange);
if (AccountPostingChange.WasSuccessful) {
CdkBase.createLogEvent(socket, "DEBUG", `Marking bill as exported.`);
await MarkApExported(socket, [billid]);
socket.emit("ap-export-success", billid);
} else {
CdkBase.createLogEvent(socket, "ERROR", `Export was not succesful.`);
socket.emit("ap-export-failure", {
billid,
error: AccountPostingChange.Message,
});
}
}
socket.emit("ap-export-complete");
};
async function MarkApExported(socket, billids) {
CdkBase.createLogEvent(
socket,
"DEBUG",
`Marking bills as exported for id ${billids}`
);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.MARK_BILLS_EXPORTED, {
billids,
bill: {
exported: true,
exported_at: new Date(),
},
logs: socket.bills.map((bill) => ({
bodyshopid: socket.bodyshop.id,
billid: bill.id,
successful: true,
useremail: socket.user.email,
})),
});
return result;
}

View File

@@ -182,6 +182,8 @@ async function CheckForErrors(socket, response) {
}
}
exports.CheckForErrors = CheckForErrors;
async function QueryJobData(socket, jobid) {
CdkBase.createLogEvent(socket, "DEBUG", `Querying job data for id ${jobid}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});

View File

@@ -479,9 +479,7 @@ exports.default = function ({
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
const account = responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits["PAO"]
);
const QboTaxId = taxCodes[taxAccountCode];
InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail",
@@ -493,7 +491,7 @@ exports.default = function ({
? { ClassRef: { value: classes[jobs_by_pk.class] } }
: {}),
ItemRef: {
value: items[account.accountitem],
value: items[responsibilityCenters.refund.accountitem],
},
TaxCodeRef: {
value: QboTaxId,
@@ -504,9 +502,7 @@ exports.default = function ({
} else {
InvoiceLineAdd.push({
ItemRef: {
FullName: responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits["PAO"]
).accountitem,
FullName: responsibilityCenters.refund.accountitem,
},
Desc: "Adjustment",
Quantity: 1,

View File

@@ -73,11 +73,19 @@ exports.default = async (req, res) => {
for (const payment of payments) {
try {
const isThreeTier = bodyshop.accountingconfig.tiers === 3;
const twoTierPref = bodyshop.accountingconfig.twotierpref;
let isThreeTier = bodyshop.accountingconfig.tiers === 3;
let twoTierPref = bodyshop.accountingconfig.twotierpref;
//Replace this with a for-each loop to check every single Job that's included in the list.
//QB Multi AR - If it is in this scenario, overwrite whatever defaults are set since multi AR
//will always go Source => RO
if (payment.payer !== "Customer" && payment.payer !== "Insurance") {
payment.job.ins_co_nm = payment.payer;
twoTierPref = "source";
isThreeTier = false;
}
let insCoCustomerTier, ownerCustomerTier, jobTier;
if (isThreeTier || (!isThreeTier && twoTierPref === "source")) {
//Insert the insurance company tier.
@@ -106,7 +114,9 @@ exports.default = async (req, res) => {
oauthClient,
qbo_realmId,
req,
payment.job
payment.job,
isThreeTier,
insCoCustomerTier
);
//Query for the owner itself.
if (!ownerCustomerTier) {
@@ -122,7 +132,17 @@ exports.default = async (req, res) => {
}
//Query for the Job or Create it.
jobTier = await QueryJob(oauthClient, qbo_realmId, req, payment.job);
jobTier = await QueryJob(
oauthClient,
qbo_realmId,
req,
payment.job,
isThreeTier
? ownerCustomerTier
: twoTierPref === "source"
? insCoCustomerTier
: ownerCustomerTier
);
// Need to validate that the job tier is associated to the right individual?
@@ -237,7 +257,8 @@ async function InsertPayment(
qbo_realmId,
req,
payment.job.ro_number,
false
false,
parentRef
);
if (invoices && invoices.length !== 1) {
@@ -260,7 +281,7 @@ async function InsertPayment(
PaymentMethodRef: {
value: paymentMethods[payment.type],
},
...(invoices && invoices.length === 1
...(invoices && invoices.length === 1 && invoices[0]
? {
Line: [
{
@@ -305,13 +326,14 @@ async function QueryMetaData(
qbo_realmId,
req,
ro_number,
isCreditMemo
isCreditMemo,
parentTierRef
) {
const invoice = await oauthClient.makeApiCall({
url: urlBuilder(
qbo_realmId,
"query",
`select * From Invoice where DocNumber = '${ro_number}'`
`select * From Invoice where DocNumber like '${ro_number}%'`
),
method: "POST",
headers: {
@@ -407,7 +429,14 @@ async function QueryMetaData(
invoices:
invoice.json &&
invoice.json.QueryResponse &&
invoice.json.QueryResponse.Invoice,
invoice.json.QueryResponse.Invoice &&
(parentTierRef
? [
invoice.json.QueryResponse.Invoice.find(
(x) => x.CustomerRef.value === parentTierRef.Id
),
]
: [invoice.json.QueryResponse.Invoice[0]]),
};
}
async function InsertCreditMemo(
@@ -423,7 +452,8 @@ async function InsertCreditMemo(
qbo_realmId,
req,
payment.job.ro_number,
true
true,
parentRef
);
if (invoices && invoices.length !== 1) {

View File

@@ -100,7 +100,9 @@ exports.default = async (req, res) => {
oauthClient,
qbo_realmId,
req,
job
job,
isThreeTier,
insCoCustomerTier
);
//Query for the owner itself.
if (!ownerCustomerTier) {
@@ -121,7 +123,11 @@ exports.default = async (req, res) => {
qbo_realmId,
req,
job,
isThreeTier ? ownerCustomerTier : null // ownerCustomerTier || insCoCustomerTier
isThreeTier
? ownerCustomerTier
: twoTierPref === "source"
? insCoCustomerTier
: ownerCustomerTier
);
// Need to validate that the job tier is associated to the right individual?
@@ -342,7 +348,14 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) {
}
exports.InsertInsuranceCo = InsertInsuranceCo;
async function QueryOwner(oauthClient, qbo_realmId, req, job) {
async function QueryOwner(
oauthClient,
qbo_realmId,
req,
job,
isThreeTier,
parentTierRef
) {
const ownerName = generateOwnerTier(job, true, null);
const result = await oauthClient.makeApiCall({
url: urlBuilder(
@@ -362,7 +375,9 @@ async function QueryOwner(oauthClient, qbo_realmId, req, job) {
result.json &&
result.json.QueryResponse &&
result.json.QueryResponse.Customer &&
result.json.QueryResponse.Customer[0]
result.json.QueryResponse.Customer.find(
(x) => x.ParentRef?.value === parentTierRef?.Id
)
);
}
exports.QueryOwner = QueryOwner;

View File

@@ -205,8 +205,8 @@ exports.default = async (req, res) => {
Party: {
PersonInfo: {
PersonName: {
FirstName: job.ownr_fn,
LastName: job.ownr_ln,
FirstName: job.ownr_co_nm ? "N/A" : job.ownr_fn,
LastName: job.ownr_co_nm ? job.ownr_co_nm : job.ownr_ln,
},
// Communications: [
// {
@@ -336,7 +336,7 @@ exports.default = async (req, res) => {
LossDateTime:
job.loss_date &&
moment(job.loss_date)
.tz(bodyshop.timezone)
//.tz(bodyshop.timezone)
.format(momentFormat),
LossDescCode: "Collision",
PrimaryPOI: {
@@ -635,9 +635,9 @@ exports.default = async (req, res) => {
{
TotalType: "OTAC",
TotalTypeDesc: "Additional Charges",
TotalAmt: Dinero(
job.job_totals.additional.additionalCosts
).toFormat("0.00"),
TotalAmt: Dinero(job.job_totals.additional.additionalCosts)
.add(Dinero(job.job_totals.additional.pvrt))
.toFormat("0.00"),
},
],
SummaryTotalsInfo: [
@@ -853,6 +853,13 @@ exports.default = async (req, res) => {
fs.writeFileSync(`./logs/arms-request.xml`, rawRequest);
fs.writeFileSync(`./logs/arms-response.xml`, rawResponse);
logger.log("arms-job-xml-request", "DEBUG", "api", job.id, {
xml: rawRequest,
});
logger.log("arms-job-xml-response", "DEBUG", "api", job.id, {
xml: rawResponse,
});
if (err) {
sendServerEmail({
subject: `ARMS Update Failed: ${bodyshop.shopname} - ${job.ro_number}`,

View File

@@ -57,7 +57,7 @@ exports.default = async (req, res) => {
bodyshopid: bodyshop.id,
start: start
? moment(start).startOf("day")
: moment().subtract(3, "days").startOf("day"),
: moment().subtract(5, "days").startOf("day"),
...(end && { end: moment(end).startOf("day") }),
}
);
@@ -316,6 +316,10 @@ const CreateRepairOrderTag = (job, errorCallback) => {
moment(job.date_open)
.tz(job.bodyshop.timezone)
.format(AhDateFormat)) ||
(job.created_at &&
moment(job.created_at)
.tz(job.bodyshop.timezone)
.format(AhDateFormat)) ||
"",
ScheduledArrivalDate:
(job.scheduled_in &&

View File

@@ -332,6 +332,7 @@ query QUERY_JOBS_FOR_PBS_EXPORT($id: uuid!) {
ownerid
ownr_ln
ownr_fn
ownr_co_nm
ownr_addr1
ownr_addr2
ownr_ph1
@@ -623,6 +624,7 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop
}
jobs(where: {_and: [{converted: {_eq: true}}, {updated_at: {_gt: $start}}, {updated_at: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}]}) {
id
created_at
ro_number
status
est_ct_fn
@@ -1504,6 +1506,23 @@ mutation MARK_JOB_EXPORTED($jobId: uuid!, $job: jobs_set_input!, $log: exportlog
}
`;
exports.MARK_BILLS_EXPORTED = `
mutation UPDATE_BILLS($billids: [uuid!]!, $bill: bills_set_input!, $logs: [exportlog_insert_input!]!) {
update_bills(where: {id: {_in: $billids}}, _set: $bill) {
returning {
id
exported
exported_at
}
}
insert_exportlog(objects: $logs) {
returning{
id
}
}
}
`;
exports.INSERT_EXPORT_LOG = `
mutation INSERT_EXPORT_LOG($log: exportlog_insert_input!) {
insert_exportlog_one(object: $log) {
@@ -1631,3 +1650,46 @@ mutation ($sesid: String!, $status: String, $context: jsonb) {
}
}
}`;
exports.GET_PBS_AP_ALLOCATIONS = `
query GET_PBS_AP_ALLOCATIONS($billids: [uuid!]) {
bodyshops(where: {associations: {active: {_eq: true}}}) {
md_responsibility_centers
timezone
pbs_serialnumber
id
}
bills(where: {id: {_in: $billids}, exported:{_eq: false}}) {
id
date
isinhouse
invoice_number
federal_tax_rate
is_credit_memo
jobid
job {
id
ro_number
}
local_tax_rate
state_tax_rate
total
vendorid
vendor {
id
name
dmsid
}
billlines {
id
actual_cost
actual_price
applicable_taxes
cost_center
deductedfromlbr
lbr_adjustment
quantity
}
}
}
`;

View File

@@ -22,6 +22,11 @@ const {
PbsSelectedCustomer,
} = require("../accounting/pbs/pbs-job-export");
const {
PbsCalculateAllocationsAp,
PbsExportAp,
} = require("../accounting/pbs/pbs-ap-allocations");
io.use(function (socket, next) {
try {
if (socket.handshake.auth.token) {
@@ -101,7 +106,7 @@ io.on("connection", (socket) => {
});
//END CDK
//PBS
//PBS AR
socket.on("pbs-calculate-allocations", async (jobid, callback) => {
const allocations = await CdkCalculateAllocations(socket, jobid);
createLogEvent(socket, "DEBUG", `Allocations calculated.`);
@@ -125,7 +130,27 @@ io.on("connection", (socket) => {
socket.selectedCustomerId = selectedCustomerId;
PbsSelectedCustomer(socket, selectedCustomerId);
});
//End PBS
//End PBS AR
//PBS AP
socket.on("pbs-calculate-allocations-ap", async (billids, callback) => {
const allocations = await PbsCalculateAllocationsAp(socket, billids);
createLogEvent(socket, "DEBUG", `AP Allocations calculated.`);
createLogEvent(
socket,
"TRACE",
`Allocations calculated. ${JSON.stringify(allocations, null, 2)}`
);
socket.apAllocations = allocations;
callback(allocations);
});
socket.on("pbs-export-ap", ({ billids, txEnvelope }) => {
socket.txEnvelope = txEnvelope;
PbsExportAp(socket, { billids, txEnvelope });
});
//END PBS AP
socket.on("disconnect", () => {
createLogEvent(socket, "DEBUG", `User disconnected.`);