Compare commits

..

87 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
0fee89623c Prevent no reply loop 2022-09-24 11:04:08 -07: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
9249222fab Add missing query field. 2022-09-23 14:10:16 -07:00
Patrick Fic
4b6eea41c2 Updated recipieint for bounced emails since testing confirmed. 2022-09-23 14:08:45 -07: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
ac270a608f Updates to bounced email tracker. 2022-09-23 13:59:58 -07: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
39efb52497 Update SNS handling 2022-09-23 13:48:45 -07: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
7aeac03b2c update formatting of bounced emails. 2022-09-23 13:35:33 -07:00
Patrick Fic
c2dd122370 Merge branch 'release/2022-09-23' of https://bitbucket.org/snaptsoft/bodyshop into release/2022-09-23 2022-09-22 14:46:12 -07:00
Patrick Fic
7fc8cbcca4 IO-2061 Handled Bounced Emails. 2022-09-22 14:46:08 -07:00
Patrick Fic
ceafab55fa IO-2054 QBO Split AR 2022-09-22 11:21:08 -07:00
Patrick Fic
cccd025a24 Resolve PPG data pump issue. 2022-09-20 15:30:32 -07:00
Patrick Fic
78a04067c5 IO-2053 Confirm before marking as exported. 2022-09-19 19:21:44 -07:00
Patrick Fic
2db2c8edbf IO-2049 Document delete&move on server side 2022-09-19 14:48:06 -07:00
Patrick Fic
38efe03889 IO-233 Logging & Credentials 2022-09-19 11:00:04 -07:00
Patrick Fic
2042ded99b IO-233 Update ARMS trigger & error logging. 2022-09-19 10:53:48 -07: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
Patrick Fic
3d9a24de4f IO-782 Parts Label Multi. 2022-09-13 15:43:05 -07:00
Patrick Fic
9792773cf0 IO-2043 Add LMS bill upload notification. 2022-09-13 15:26:55 -07:00
Patrick Fic
7c4ba416f7 IO-2028 LMS Deletion. 2022-09-13 15:13:43 -07:00
Patrick Fic
9dbb4b586f IO-233 ARMS event based updates. 2022-09-13 14:00:54 -07:00
Patrick Fic
169fdf6ae8 Merged in release/2022-09-09 (pull request #576)
Release/2022 09 09
2022-09-09 20:13:00 +00:00
Patrick Fic
5f3c1fc95e IO-2039 LMS support for non media files. 2022-09-08 10:03:24 -07:00
Patrick Fic
ba9ea17805 Add QBO error for missing insurance company name. 2022-09-06 09:41:02 -07:00
Patrick Fic
d7c68441e8 Merged in releaase/2022-09-02 (pull request #574)
Releaase/2022 09 02
2022-09-02 15:03:09 +00:00
Patrick Fic
c0973b6098 Remove unnecessary console log. 2022-09-01 16:48:29 -07:00
Patrick Fic
e10196bb62 IO-1325 Populate 3rd party payer modal correctly. 2022-09-01 16:39:05 -07:00
Patrick Fic
637a33e670 IO-1981 Disable order type for OEC orders. 2022-08-31 14:48:47 -07:00
Patrick Fic
8bcc903f2b IO-1325 Automatically populate 3rd party modal. 2022-08-31 13:37:01 -07:00
Patrick Fic
5ec5be0852 IO-1981 Add Sublet Order. 2022-08-31 13:04:36 -07:00
Patrick Fic
cb0c4d55df IO-1950 Parts Order Discounts 2022-08-31 10:32:41 -07:00
Patrick Fic
dea4d50821 IO-1949 Add additional buttons to email overlay. 2022-08-30 16:16:07 -07:00
Patrick Fic
df1adc34a2 IO-2033 Mark payment as exported. 2022-08-30 14:11:34 -07:00
Patrick Fic
e44e2bd7dd Merged in release/2022-08-26 (pull request #570)
IO-2032 Add psr by make template.

Approved-by: Patrick Fic
2022-08-29 23:50:53 +00:00
Patrick Fic
b557100fc6 Merged in release/2022-08-26 (pull request #568)
IO-1984 Resolve error on email audit tab.

Approved-by: Patrick Fic
2022-08-29 15:13:57 +00:00
Patrick Fic
d9a8831eb3 Merged in release/2022-08-26 (pull request #566)
Minor release fixes.

Approved-by: Patrick Fic
2022-08-26 21:06:43 +00:00
Patrick Fic
137370812d Merged in hotfix/2022-08-23 (pull request #563)
Query only active QBO tax codes.
2022-08-23 17:01:03 +00:00
92 changed files with 47850 additions and 1393 deletions

View File

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

3
.gitignore vendored
View File

@@ -113,3 +113,6 @@ firebase/.env
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml
logs/oAuthClient-log.log
.node-persist/**

View File

@@ -1,4 +1,4 @@
<babeledit_project be_version="2.7.1" version="1.2">
<babeledit_project version="1.2" be_version="2.7.1">
<!--
BabelEdit project file
@@ -1233,6 +1233,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>
<concept_node>
<name>subject</name>
<definition_loaded>false</definition_loaded>
@@ -3057,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>
@@ -6879,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>
@@ -13447,6 +13510,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>deleting</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>deleting_cloudinary</name>
<definition_loaded>false</definition_loaded>
@@ -13641,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>
@@ -15416,6 +15521,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>send</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>senderrortosupport</name>
<definition_loaded>false</definition_loaded>
@@ -16754,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>
@@ -22225,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>
@@ -22414,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>
@@ -24515,6 +24704,53 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>qb_multiple_payers</name>
<children>
<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>name</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>
<concept_node>
<name>queued_for_parts</name>
<definition_loaded>false</definition_loaded>
@@ -25974,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>
@@ -26079,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>
@@ -27194,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>
@@ -27721,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>
@@ -27952,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>
@@ -28183,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>
@@ -34329,6 +34691,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>custompercent</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>discount</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>email</name>
<definition_loaded>false</definition_loaded>
@@ -34476,6 +34880,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>order_type</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>orderhistory</name>
<definition_loaded>false</definition_loaded>
@@ -34497,6 +34922,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>parts_order</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_orders</name>
<definition_loaded>false</definition_loaded>
@@ -34602,6 +35048,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>sublet_order</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>
@@ -35068,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>
@@ -35220,6 +35708,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>markexported</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>payment</name>
<definition_loaded>false</definition_loaded>
@@ -37012,6 +37521,48 @@
</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>
<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_single</name>
<definition_loaded>false</definition_loaded>
@@ -37390,6 +37941,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>sublet_order</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>supplement_request</name>
<definition_loaded>false</definition_loaded>
@@ -38006,6 +38578,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>sublet_order</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>
</children>
@@ -40177,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>
@@ -40240,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>
@@ -40303,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>
@@ -40366,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>
@@ -40681,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>
@@ -42530,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>
@@ -46624,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,19 +1,20 @@
import { Card, Input, Space, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -184,6 +185,13 @@ export function AccountingPayablesTableComponent({
<Card
extra={
<Space wrap>
<PaymentMarkSelectedExported
paymentIds={selectedPayments}
disabled={transInProgress || selectedPayments.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedPayments}
refetch={refetch}
/>
<PaymentsExportAllButton
paymentIds={selectedPayments}
disabled={transInProgress || selectedPayments.length === 0}

View File

@@ -1,9 +1,8 @@
import React, { useState } from "react";
import { Table } from "antd";
import { alphaSort } from "../../utils/sorters";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
export default function EmailAuditTrailListComponent({ loading, data }) {
const [state, setState] = useState({
@@ -11,7 +10,7 @@ export default function EmailAuditTrailListComponent({ loading, data }) {
filteredInfo: {},
});
const { t } = useTranslation();
const columns = [
const columns = [
{
title: t("audit.fields.created"),
dataIndex: " created",

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

@@ -23,6 +23,7 @@ export function DocumentsLocalUploadComponent({
vendorid,
invoice_number,
callbackAfterUpload,
allowAllTypes,
}) {
const [fileList, setFileList] = useState([]);
@@ -52,7 +53,9 @@ export function DocumentsLocalUploadComponent({
},
})
}
accept="audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx"
{...(!allowAllTypes && {
accept: "audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx",
})}
>
{children || (
<>

View File

@@ -2,6 +2,8 @@ import cleanAxios from "../../utils/CleanAxios";
import { store } from "../../redux/store";
import { addMediaForJob } from "../../redux/media/media.actions";
import normalizeUrl from "normalize-url";
import { notification } from "antd";
import i18n from "i18next";
export const handleUpload = async ({ ev, context }) => {
const { onError, onSuccess, onProgress, file } = ev;
@@ -45,6 +47,11 @@ export const handleUpload = async ({ ev, context }) => {
}
} else {
onSuccess && onSuccess(file);
notification.open({
type: "success",
key: "docuploadsuccess",
message: i18n.t("documents.successes.insert"),
});
store.dispatch(
addMediaForJob({
jobid,
@@ -53,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

@@ -1,4 +1,4 @@
import { Divider, Form, Modal, notification } from "antd";
import { Button, Divider, Form, Modal, notification, Space } from "antd";
import axios from "axios";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -175,13 +175,17 @@ 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,
disabled:
selectedMedia &&
(selectedMedia
@@ -191,21 +195,56 @@ export function EmailOverlayContainer({
selectedMedia.filter((s) => s.isSelected).length > 10),
}}
>
<Form layout="vertical" form={form} onFinish={handleFinish}>
{loading && (
<div>
<LoadingSkeleton />
<Divider>{t("emails.labels.preview")}</Divider>
<LoadingSpinner message={t("emails.labels.generatingemail")} />
</div>
)}
{!loading && (
<EmailOverlayComponent
form={form}
selectedMediaState={[selectedMedia, setSelectedMedia]}
/>
)}
</Form>
<div>
<div
style={{
marginTop: "1rem",
display: "flex",
justifyContent: "flex-end",
}}
>
<Space style={{ alignSelf: "flex-end" }} align="right">
<Button
onClick={() => {
toggleEmailOverlayVisible();
}}
>
{t("general.actions.cancel")}
</Button>
<Button
loading={sending}
onClick={() => form.submit()}
disabled={
selectedMedia &&
(selectedMedia
.filter((s) => s.isSelected)
.reduce((acc, val) => (acc = acc + val.size), 0) >=
10485760 - new Blob([form.getFieldValue("html")]).size ||
selectedMedia.filter((s) => s.isSelected).length > 10)
}
type="primary"
>
{t("general.actions.send")}
</Button>
</Space>
</div>
<Form layout="vertical" form={form} onFinish={handleFinish}>
{loading && (
<div>
<LoadingSkeleton />
<Divider>{t("emails.labels.preview")}</Divider>
<LoadingSpinner message={t("emails.labels.generatingemail")} />
</div>
)}
{!loading && (
<EmailOverlayComponent
form={form}
selectedMediaState={[selectedMedia, setSelectedMedia]}
/>
)}
</Form>
</div>
</Modal>
);
}

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

@@ -19,7 +19,7 @@ const mapDispatchToProps = (dispatch) => ({
});
export default connect(mapStateToProps, mapDispatchToProps)(Jobd3RdPartyModal);
export function Jobd3RdPartyModal({ bodyshop, jobId }) {
export function Jobd3RdPartyModal({ bodyshop, jobId, job }) {
const [isModalVisible, setIsModalVisible] = useState(false);
const { t } = useTranslation();
const [form] = Form.useForm();
@@ -33,6 +33,11 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
);
const showModal = () => {
form.setFieldsValue({
ded_amt: job.ded_amt,
depreciation: job.depreciation_taxes,
custgst: job.ca_customer_gst,
});
setIsModalVisible(true);
};
@@ -42,6 +47,7 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
};
const handleCancel = () => {
form.resetFields();
setIsModalVisible(false);
};
const handleFinish = (values) => {

View File

@@ -85,6 +85,11 @@ export function JobAuditTrail({ currentUser, jobId }) {
dataIndex: "subject",
key: "subject",
},
{
title: t("audit.fields.status"),
dataIndex: "status",
key: "status",
},
...(currentUser?.email.includes("@imex.")
? [
{

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,14 +1,11 @@
import { useApolloClient, useMutation } from "@apollo/client";
import { useApolloClient } from "@apollo/client";
import { Button, Form, notification, Popover, Space } from "antd";
import axios from "axios";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
GET_DOC_SIZE_BY_JOB,
UPDATE_DOCUMENT,
} from "../../graphql/documents.queries";
import { GET_DOC_SIZE_BY_JOB } from "../../graphql/documents.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import JobSearchSelect from "../job-search-select/job-search-select.component";
@@ -23,7 +20,11 @@ export default connect(
mapDispatchToProps
)(JobsDocumentsGalleryReassign);
export function JobsDocumentsGalleryReassign({ bodyshop, galleryImages }) {
export function JobsDocumentsGalleryReassign({
bodyshop,
galleryImages,
callback,
}) {
const { t } = useTranslation();
const [form] = Form.useForm();
@@ -36,34 +37,33 @@ export function JobsDocumentsGalleryReassign({ bodyshop, galleryImages }) {
const client = useApolloClient();
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [updateDocument] = useMutation(UPDATE_DOCUMENT);
const updateImage = async (i, jobid) => {
//Move the cloudinary image
// const updateImage = async (i, jobid) => {
// //Move the cloudinary image
//Update it in the database.
const result = await updateDocument({
variables: {
id: i.id,
document: {
key: i.public_id,
jobid: jobid,
},
},
});
// //Update it in the database.
// const result = await updateDocument({
// variables: {
// id: i.id,
// document: {
// key: i.public_id,
// jobid: jobid,
// },
// },
// });
if (!!result.errors) {
notification["error"]({
message: t("documents.errors.updating", {
message: JSON.stringify(result.errors),
}),
});
} else {
notification["success"]({
message: t("documents.successes.updated"),
});
}
};
// if (!!result.errors) {
// notification["error"]({
// message: t("documents.errors.updating", {
// message: JSON.stringify(result.errors),
// }),
// });
// } else {
// notification["success"]({
// message: t("documents.successes.updated"),
// });
// }
// };
const handleFinish = async ({ jobid }) => {
setLoading(true);
@@ -96,6 +96,7 @@ export function JobsDocumentsGalleryReassign({ bodyshop, galleryImages }) {
}
const res = await axios.post("/media/rename", {
tojobid: jobid,
documents: selectedImages.map((i) => {
//Need to check if the current key folder is null, or another job.
const currentKeys = i.key.split("/");
@@ -110,24 +111,21 @@ export function JobsDocumentsGalleryReassign({ bodyshop, galleryImages }) {
};
}),
});
//Add in confirmation & errors.
if (callback) callback();
res.data
.filter((d) => d.error)
.forEach((d) => {
notification["error"]({ message: t("documents.errors.updating") });
console.error("Error updating job document", d);
if (res.errors) {
notification["error"]({
message: t("documents.errors.updating", {
message: JSON.stringify(res.errors),
}),
});
const proms = [];
res.data
.filter((d) => !d.error)
.forEach((d) => {
proms.push(updateImage(d, jobid));
}
if (!res.mutationResult?.errors) {
notification["success"]({
message: t("documents.successes.updated"),
});
await Promise.all(proms);
}
setVisible(false);
setLoading(false);
};

View File

@@ -125,7 +125,10 @@ function JobsDocumentsComponent({
deletionCallback={billsCallback || refetch}
/>
{!billId && (
<JobsDocumentsGalleryReassign galleryImages={galleryImages} />
<JobsDocumentsGalleryReassign
galleryImages={galleryImages}
callback={refetch}
/>
)}
</Space>
</Col>

View File

@@ -1,11 +1,9 @@
import { QuestionCircleOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, notification, Popconfirm } from "antd";
import axios from "axios";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { DELETE_DOCUMENTS } from "../../graphql/documents.queries";
//Context: currentUserEmail, bodyshop, jobid, invoiceid
export default function JobsDocumentsDeleteButton({
@@ -13,7 +11,7 @@ export default function JobsDocumentsDeleteButton({
deletionCallback,
}) {
const { t } = useTranslation();
const [deleteDocument] = useMutation(DELETE_DOCUMENTS);
const imagesToDelete = [
...galleryImages.images.filter((image) => image.isSelected),
...galleryImages.other.filter((image) => image.isSelected),
@@ -27,31 +25,10 @@ export default function JobsDocumentsDeleteButton({
ids: imagesToDelete,
});
const successfulDeletes = [];
res.data.forEach((resType) => {
Object.keys(resType.deleted).forEach((key) => {
if (resType.deleted[key] !== "deleted") {
notification["error"]({
message: t("documents.errors.deleting_cloudinary", {
message: JSON.stringify(resType.deleted[key]),
}),
});
} else {
successfulDeletes.push(key.replace(/\.[^/.]+$/, ""));
}
});
});
const delres = await deleteDocument({
variables: {
ids: imagesToDelete
.filter((i) => successfulDeletes.includes(i.key))
.map((i) => i.id),
},
});
if (delres.errors) {
if (res.data.error) {
notification["error"]({
message: t("documents.errors.deleting", {
message: JSON.stringify(delres.errors),
error: JSON.stringify(res.data.error.response.errors),
}),
});
} else {

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";
@@ -14,6 +14,7 @@ import { selectAllMedia } from "../../redux/media/media.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { CreateExplorerLinkForJob } from "../../utils/localmedia";
import DocumentsLocalUploadComponent from "../documents-local-upload/documents-local-upload.component";
import JobsDocumentsLocalDeleteButton from "./jobs-documents-local-gallery.delete.component";
import JobsLocalGalleryDownloadButton from "./jobs-documents-local-gallery.download";
import JobsDocumentsLocalGalleryReassign from "./jobs-documents-local-gallery.reassign.component";
import JobsDocumentsLocalGallerySelectAllComponent from "./jobs-documents-local-gallery.selectall.component";
@@ -57,7 +58,7 @@ export function JobsDocumentsLocalGallery({
}
}
}, [job, invoice_number, getJobMedia, getBillMedia]);
let optimized;
const jobMedia =
allMedia && allMedia[job.id]
? allMedia[job.id].reduce(
@@ -67,15 +68,23 @@ 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);
acc.other.push({
...val,
tags: [{ value: val.filename, title: val.filename }],
});
}
return acc;
},
{ images: [], other: [] }
)
: { images: [], other: [] };
return (
<div>
<Space wrap>
@@ -98,12 +107,14 @@ export function JobsDocumentsLocalGallery({
<JobsDocumentsLocalGalleryReassign jobid={job.id} />
<JobsDocumentsLocalGallerySelectAllComponent jobid={job.id} />
<JobsLocalGalleryDownloadButton job={job} />
<JobsDocumentsLocalDeleteButton jobid={job.id} />
</Space>
<Card>
<DocumentsLocalUploadComponent
job={job}
invoice_number={invoice_number}
vendorid={vendorid}
allowAllTypes
/>
</Card>
<Card title={t("jobs.labels.documents-images")}>
@@ -113,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

@@ -0,0 +1,81 @@
import { QuestionCircleOutlined } from "@ant-design/icons";
import { Button, notification, Popconfirm } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import cleanAxios from "../../utils/CleanAxios";
//Context: currentUserEmail, bodyshop, jobid, invoiceid
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { getJobMedia } from "../../redux/media/media.actions";
import { selectAllMedia } from "../../redux/media/media.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
allMedia: selectAllMedia,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
getJobMedia: (id) => dispatch(getJobMedia(id)),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsDocumentsLocalDeleteButton);
export function JobsDocumentsLocalDeleteButton({
bodyshop,
getJobMedia,
allMedia,
jobid,
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const handleDelete = async () => {
logImEXEvent("job_documents_delete");
setLoading(true);
const delres = await cleanAxios.post(
`${bodyshop.localmediaserverhttp}/jobs/delete`,
{
jobid: jobid,
files: ((allMedia && allMedia[jobid]) || [])
.filter((i) => i.isSelected)
.map((i) => i.filename),
},
{ headers: { ims_token: bodyshop.localmediatoken } }
);
if (delres.errors) {
notification["error"]({
message: t("documents.errors.deleting", {
message: JSON.stringify(delres.errors),
}),
});
} else {
notification.open({
key: "docdeletedsuccesfully",
type: "success",
message: t("documents.successes.delete"),
});
}
getJobMedia(jobid);
setLoading(false);
};
return (
<Popconfirm
icon={<QuestionCircleOutlined style={{ color: "red" }} />}
onConfirm={handleDelete}
title={t("documents.labels.confirmdelete")}
okText={t("general.actions.delete")}
okButtonProps={{ type: "danger" }}
cancelText={t("general.actions.cancel")}
>
<Button loading={loading}>{t("documents.actions.delete")}</Button>
</Popconfirm>
);
}

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

@@ -0,0 +1,97 @@
import { DownOutlined } from "@ant-design/icons";
import { Dropdown, InputNumber, Menu, Space } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
export default function PartsOrderModalPriceChange({ form, field }) {
const { t } = useTranslation();
const menu = (
<Menu
onClick={({ key }) => {
if (key === "custom") return;
const values = form.getFieldsValue();
const { parts_order_lines } = values;
form.setFieldsValue({
parts_order_lines: {
data: parts_order_lines.data.map((p, idx) => {
if (idx !== field.name) return p;
return {
...p,
act_price: (p.act_price || 0) * ((100 - key) / 100),
};
}),
},
});
}}
items={[
{
key: "5",
label: t("parts_orders.labels.discount", { percent: "5%" }),
},
{
key: "10",
label: t("parts_orders.labels.discount", { percent: "10%" }),
},
{
key: "15",
label: t("parts_orders.labels.discount", { percent: "15%" }),
},
{
key: "20",
label: t("parts_orders.labels.discount", { percent: "20%" }),
},
{
key: "25",
label: t("parts_orders.labels.discount", { percent: "25%" }),
},
{
key: "custom",
label: (
<InputNumber
onClick={(e) => e.stopPropagation()}
addonAfter="%"
onKeyUp={(e) => {
if (e.key === "Enter") {
const values = form.getFieldsValue();
const { parts_order_lines } = values;
form.setFieldsValue({
parts_order_lines: {
data: parts_order_lines.data.map((p, idx) => {
if (idx !== field.name) return p;
console.log(
p,
e.target.value,
(p.act_price || 0) *
((100 - (e.target.value || 0)) / 100)
);
return {
...p,
act_price:
(p.act_price || 0) *
((100 - (e.target.value || 0)) / 100),
};
}),
},
});
e.target.value = 0;
}
}}
min={0}
max={100}
/>
),
},
]}
/>
);
return (
<Dropdown overlay={menu} trigger="click">
<Space>
%
<DownOutlined />
</Space>
</Dropdown>
);
}

View File

@@ -23,6 +23,7 @@ import CurrencyInput from "../form-items-formatted/currency-form-item.component"
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -133,6 +134,21 @@ export function PartsOrderModalComponent({
<Checkbox />
</Form.Item>
)}
<Form.Item
name="order_type"
initialValue="parts_order"
label={t("parts_orders.labels.order_type")}
>
<Radio.Group disabled={sendType === "oec"}>
<Radio value={"parts_order"}>
{t("parts_orders.labels.parts_order")}
</Radio>
<Radio value={"sublet"}>
{t("parts_orders.labels.sublet_order")}
</Radio>
</Radio.Group>
</Form.Item>
</LayoutFormRow>
<Divider orientation="left">
{t("parts_orders.labels.inthisorder")}
@@ -246,7 +262,14 @@ export function PartsOrderModalComponent({
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<CurrencyInput />
<CurrencyInput
addonBefore={
<PartsOrderModalPriceChange
form={form}
field={field}
/>
}
/>
</Form.Item>
{isReturn && (
<Form.Item

View File

@@ -94,6 +94,7 @@ export function PartsOrderModalContainer({
const [updateJob] = useMutation(UPDATE_JOB);
const handleFinish = async ({
order_type,
removefrompartsqueue,
is_quote,
...values
@@ -102,47 +103,46 @@ export function PartsOrderModalContainer({
setSaving(true);
let insertResult;
insertResult = await insertPartOrder({
variables: {
po: [
{
...values,
order_date: moment().format("YYYY-MM-DD"),
orderedby: currentUser.email,
jobid: jobId,
user_email: currentUser.email,
return: isReturn,
status: is_quote
? bodyshop.md_order_statuses.default_quote || "Quote"
: bodyshop.md_order_statuses.default_ordered || "Ordered*",
},
],
},
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
insertResult = await insertPartOrder({
variables: {
po: [
{
...values,
order_date: moment().format("YYYY-MM-DD"),
orderedby: currentUser.email,
jobid: jobId,
user_email: currentUser.email,
return: isReturn,
status: is_quote
? bodyshop.md_order_statuses.default_quote || "Quote"
: bodyshop.md_order_statuses.default_ordered || "Ordered*",
},
],
},
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
});
if (!!insertResult.errors) {
notification["error"]({
message: t("parts_orders.errors.creating"),
description: JSON.stringify(insertResult.errors),
});
if (!!insertResult.errors) {
notification["error"]({
message: t("parts_orders.errors.creating"),
description: JSON.stringify(insertResult.errors),
});
return;
}
notification["success"]({
message: values.isReturn
? t("parts_orders.successes.return_created")
: t("parts_orders.successes.created"),
});
insertAuditTrail({
jobid: jobId,
operation: isReturn
? AuditTrailMapping.jobspartsreturn(
insertResult.data.insert_parts_orders.returning[0].order_number
)
: AuditTrailMapping.jobspartsorder(
insertResult.data.insert_parts_orders.returning[0].order_number
),
});
return;
}
notification["success"]({
message: values.isReturn
? t("parts_orders.successes.return_created")
: t("parts_orders.successes.created"),
});
insertAuditTrail({
jobid: jobId,
operation: isReturn
? AuditTrailMapping.jobspartsreturn(
insertResult.data.insert_parts_orders.returning[0].order_number
)
: AuditTrailMapping.jobspartsorder(
insertResult.data.insert_parts_orders.returning[0].order_number
),
});
const jobLinesResult = await updateJobLines({
variables: {
@@ -228,7 +228,9 @@ export function PartsOrderModalContainer({
{
name: isReturn
? Templates.parts_return_slip.key
: Templates.parts_order.key,
: order_type === "parts_order"
? Templates.parts_order.key
: Templates.sublet_order.key,
variables: {
id: insertResult.data.insert_parts_orders.returning[0].id,
},
@@ -238,7 +240,9 @@ export function PartsOrderModalContainer({
replyTo: bodyshop.email,
subject: isReturn
? Templates.parts_return_slip.subject
: Templates.parts_order.subject,
: order_type === "parts_order"
? Templates.parts_order.subject
: Templates.sublet_order.subject,
},
"e",
jobId
@@ -248,7 +252,9 @@ export function PartsOrderModalContainer({
{
name: isReturn
? Templates.parts_return_slip.key
: Templates.parts_order.key,
: order_type === "parts_order"
? Templates.parts_order.key
: Templates.sublet_order.key,
variables: {
id: insertResult.data.insert_parts_orders.returning[0].id,
},

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,5 +1,5 @@
import { gql, useMutation } from "@apollo/client";
import { Button, notification } from "antd";
import { Button, notification, Popconfirm } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -34,6 +34,7 @@ export function BillMarkSelectedExported({
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [visible, setVisible] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [updateBill] = useMutation(gql`
mutation UPDATE_BILL($billIds: [uuid!]!) {
@@ -84,11 +85,24 @@ export function BillMarkSelectedExported({
completedCallback && completedCallback([]);
setLoading(false);
refetch && refetch();
setVisible(false);
};
return (
<Button loading={loading} disabled={disabled} onClick={handleUpdate}>
{t("bills.labels.markexported")}
</Button>
<Popconfirm
visible={visible}
title={t("general.labels.areyousure")}
onCancel={() => setVisible(false)}
onConfirm={handleUpdate}
disabled={disabled}
>
<Button
loading={loading}
disabled={disabled}
onClick={() => setVisible(true)}
>
{t("bills.labels.markexported")}
</Button>
</Popconfirm>
);
}

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

@@ -0,0 +1,111 @@
import { gql, useMutation } from "@apollo/client";
import { Button, notification, Popconfirm } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(PaymentMarkSelectedExported);
export function PaymentMarkSelectedExported({
bodyshop,
currentUser,
paymentIds,
disabled,
loadingCallback,
completedCallback,
refetch,
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [visible, setVisible] = useState(false);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [updatePayments] = useMutation(gql`
mutation UPDATE_PAYMENTS($paymentIds: [uuid!]!, $exportedat: timestamptz!) {
update_payments(
where: { id: { _in: $paymentIds } }
_set: { exportedat: $exportedat }
) {
returning {
id
exportedat
}
}
}
`);
const handleUpdate = async () => {
setLoading(true);
loadingCallback(true);
const result = await updatePayments({
variables: { paymentIds: paymentIds, exportedat: new Date() },
update(cache) {},
});
await insertExportLog({
variables: {
logs: paymentIds.map((id) => {
return {
bodyshopid: bodyshop.id,
paymentid: id,
successful: true,
message: JSON.stringify([t("general.labels.markedexported")]),
useremail: currentUser.email,
};
}),
},
});
if (!result.errors) {
notification["success"]({
message: t("payments.successes.markexported"),
});
} else {
notification["error"]({
message: t("bills.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
loadingCallback(false);
completedCallback && completedCallback([]);
setLoading(false);
refetch && refetch();
setVisible(false);
};
return (
<Popconfirm
visible={visible}
title={t("general.labels.areyousure")}
onCancel={() => setVisible(false)}
onConfirm={handleUpdate}
disabled={disabled}
>
<Button
loading={loading}
disabled={disabled}
onClick={() => setVisible(true)}
>
{t("bills.labels.markexported")}
</Button>
</Popconfirm>
);
}

View File

@@ -1,4 +1,4 @@
import { Button, Card, Form, InputNumber, Popover } from "antd";
import { Button, Card, Form, InputNumber, Popover, Radio } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -24,7 +24,8 @@ export function PrintCenterJobsLabels({ bodyshop, jobId }) {
const { t } = useTranslation();
const [form] = Form.useForm();
const handleOk = () => {
const handleOk = (e) => {
e.stopPropagation();
form.submit();
setIsModalVisible(false);
};
@@ -33,12 +34,12 @@ export function PrintCenterJobsLabels({ bodyshop, jobId }) {
setIsModalVisible(false);
setLoading(false);
};
const handleFinish = async (values) => {
const handleFinish = async ({ template, ...values }) => {
const { sendtype, ...restVals } = values;
setLoading(true);
await GenerateDocument(
{
name: TemplateList("job_special").folder_label_multiple.key,
name: TemplateList("job_special")[template].key,
variables: { id: jobId },
context: restVals,
},
@@ -48,6 +49,7 @@ export function PrintCenterJobsLabels({ bodyshop, jobId }) {
);
setLoading(false);
setIsModalVisible(false);
form.resetFields();
};
const content = (
@@ -58,13 +60,28 @@ export function PrintCenterJobsLabels({ bodyshop, jobId }) {
layout="vertical"
form={form}
>
<Form.Item required name="template">
<Radio.Group>
<Radio.Button value="parts_label_multiple">
{t("printcenter.jobs.parts_label_multiple")}
</Radio.Button>
<Radio.Button value="folder_label_multiple">
{t("printcenter.jobs.folder_label_multiple")}
</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item
required
label={t("printcenter.jobs.labels.position")}
name="position"
>
<InputNumber min={1} precision={0} />
</Form.Item>
<Form.Item label={t("printcenter.jobs.labels.count")} name="count">
<Form.Item
required
label={t("printcenter.jobs.labels.count")}
name="count"
>
<InputNumber min={1} precision={0} max={99} />
</Form.Item>
<Button type="primary" loading={loading} onClick={handleOk}>

View File

@@ -20,7 +20,7 @@ const mapDispatchToProps = (dispatch) => ({});
export function PrintCenterJobsComponent({ printCenterModal, bodyshop }) {
const [search, setSearch] = useState("");
const { id: jobId } = printCenterModal.context;
const { id: jobId, job } = printCenterModal.context;
const tempList = TemplateList("job", {});
const { t } = useTranslation();
const JobsReportsList = Object.keys(tempList)
@@ -54,7 +54,7 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop }) {
extra={
<Space wrap>
<PrintCenterJobsLabels jobId={jobId} />
<Jobd3RdPartyModal jobId={jobId} />
<Jobd3RdPartyModal jobId={jobId} job={job} />
<Input.Search
onChange={(e) => setSearch(e.target.value)}
value={search}

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,6 +4663,26 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<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

@@ -53,7 +53,6 @@ export const requestForToken = () => {
})
.then((currentToken) => {
if (currentToken) {
console.log("current token for client: ", currentToken);
window.sessionStorage.setItem("fcmtoken", currentToken);
// Perform any other necessary action with the token
} else {

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

@@ -26,6 +26,7 @@ export const QUERY_AUDIT_TRAIL = gql`
subject
to
useremail
status
}
}
`;

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
@@ -1890,6 +1895,7 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
actual_in
kmin
kmout
qb_multiple_payers
joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) {
id
removed
@@ -2114,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

@@ -1,35 +1,43 @@
import { DeleteFilled } from "@ant-design/icons";
import { useApolloClient, useMutation } from "@apollo/client";
import {
Button,
Form,
notification,
Popconfirm,
Space,
Alert,
Button,
Col,
Divider,
PageHeader,
InputNumber,
Form,
Input,
InputNumber,
notification,
PageHeader,
Popconfirm,
Row,
Select,
Space,
Statistic,
Switch,
Typography,
} from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
//import { useHistory } from "react-router-dom";
import { useTreatments } from "@splitsoftware/splitio-react";
import moment from "moment";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import DateTimePicker from "../../components/form-date-time-picker/form-date-time-picker.component";
import FormsFieldChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../../components/form-items-formatted/currency-form-item.component";
import JobsScoreboardAdd from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component";
import JobsCloseAutoAllocate from "../../components/jobs-close-auto-allocate/jobs-close-auto-allocate.component";
import JobsCloseLines from "../../components/jobs-close-lines/jobs-close-lines.component";
import LayoutFormRow from "../../components/layout-form-row/layout-form-row.component";
import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import LayoutFormRow from "../../components/layout-form-row/layout-form-row.component";
import DateTimePicker from "../../components/form-date-time-picker/form-date-time-picker.component";
import moment from "moment";
import { Link } from "react-router-dom";
import Dinero from "dinero.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
@@ -42,6 +50,11 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
// const history = useHistory();
const [closeJob] = useMutation(UPDATE_JOB);
const [loading, setLoading] = useState(false);
const { Qb_Multi_Ar } = useTreatments(
["Qb_Multi_Ar"],
{},
bodyshop && bodyshop.imexshopid
);
const handleFinish = async ({ removefromproduction, ...values }) => {
setLoading(true);
@@ -65,6 +78,9 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
kmout: values.kmout,
dms_allocation: values.dms_allocation,
...(removefromproduction ? { inproduction: false } : {}),
...(values.qb_multiple_payers
? { qb_multiple_payers: values.qb_multiple_payers }
: {}),
},
},
refetchQueries: ["QUERY_JOB_CLOSE_DETAILS"],
@@ -127,6 +143,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
kmin: job.kmin,
kmout: job.kmout,
dms_allocation: job.dms_allocation,
qb_multiple_payers: job.qb_multiple_payers,
}}
scrollToFirstError
>
@@ -312,6 +329,165 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
</Form.Item>
)}
</LayoutFormRow>
<Divider>{t("jobs.labels.multipayers")}</Divider>
{Qb_Multi_Ar.treatment === "on" && (
<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,
},
]}
>
<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>
<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>
</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",
}}
value={discrep.toFormat()}
/>
</Space>
);
}}
</Form.Item>
</Col>
</Row>
)}
<Divider />
<JobsCloseLines job={job} />
</Form>

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

@@ -86,6 +86,7 @@
"contents": "Contents",
"created": "Time",
"operation": "Operation",
"status": "Status",
"subject": "Subject",
"to": "To",
"useremail": "User",
@@ -194,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",
@@ -435,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",
@@ -835,6 +838,7 @@
},
"errors": {
"deletes3": "Error deleting document from storage. ",
"deleting": "Error deleting documents {{error}}",
"deleting_cloudinary": "Error deleting document from storage. {{message}}",
"getpresignurl": "Error obtaining presigned URL for document. {{message}}",
"insert": "Unable to upload file. {{message}}",
@@ -846,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.",
@@ -964,6 +969,7 @@
"save": "Save",
"saveandnew": "Save and New",
"selectall": "Select All",
"send": "Send",
"senderrortosupport": "Send Error to Support",
"submit": "Submit",
"tryagain": "Try Again",
@@ -1033,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. "
},
@@ -1349,6 +1356,7 @@
"depreciation_taxes": "Depreciation/Taxes",
"dms": {
"address": "Customer Address",
"amount": "Amount",
"center": "Center",
"cost": "Cost",
"cost_dms_acctnumber": "Cost DMS Acct #",
@@ -1358,6 +1366,7 @@
"id": "DMS ID",
"inservicedate": "In Service Date",
"journal": "Journal #",
"lines": "Posting Lines",
"name1": "Customer Name",
"payer": {
"amount": "Amount",
@@ -1466,6 +1475,10 @@
"production_vars": {
"note": "Production Note"
},
"qb_multiple_payers": {
"amount": "Amount",
"name": "Name"
},
"queued_for_parts": "Queued for Parts",
"rate_ats": "ATS Rate",
"rate_la1": "LA1",
@@ -1539,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.",
@@ -1605,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.",
@@ -1631,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",
@@ -1642,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",
@@ -1653,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>.",
@@ -2032,6 +2051,8 @@
"labels": {
"allpartsto": "All Parts Location",
"confirmdelete": "Are you sure you want to delete this item? It cannot be recovered. Job line statuses will not be updated and may require manual review. ",
"custompercent": "Custom %",
"discount": "Discount {{percent}}",
"email": "Send by Email",
"inthisorder": "Parts in this Order",
"is_quote": "Parts Quote?",
@@ -2039,12 +2060,15 @@
"newpartsorder": "New Parts Order",
"notyetordered": "This part has not yet been ordered.",
"oec": "Order via OEC",
"order_type": "Order Type",
"orderhistory": "Order History",
"parts_order": "Parts Order",
"parts_orders": "Parts Orders",
"print": "Show Printed Form",
"receive": "Receive Parts Order",
"removefrompartsqueue": "Remove from Parts Queue?",
"returnpartsorder": "Return Parts Order"
"returnpartsorder": "Return Parts Order",
"sublet_order": "Sublet Order"
},
"successes": {
"created": "Parts order created successfully. ",
@@ -2076,6 +2100,7 @@
"customer": "Customer",
"edit": "Edit Payment",
"electronicpayment": "Use Electronic Payment Processing?",
"external": "External",
"findermodal": "ICBC Payment Finder",
"insurance": "Insurance",
"new": "New Payment",
@@ -2085,6 +2110,7 @@
},
"successes": {
"exported": "Payment(s) exported successfully.",
"markexported": "Payment(s) marked exported.",
"payment": "Payment created successfully. ",
"stripe": "Credit card transaction charged successfully."
}
@@ -2174,7 +2200,7 @@
"filing_coversheet_portrait": "Filing Coversheet (Portrait)",
"final_invoice": "Final Invoice",
"fippa_authorization": "FIPPA Authorization",
"folder_label_multiple": "Folder Label Multiple",
"folder_label_multiple": "Folder Label - Multi",
"glass_express_checklist": "Glass Express Checklist",
"guarantee": "Repair Guarantee",
"individual_job_note": "Job Note RO # {{ro_number}}",
@@ -2195,6 +2221,8 @@
"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",
"parts_order": "Parts Order Confirmation",
@@ -2213,6 +2241,7 @@
"sgi_certificate_of_repairs": "SGI - Certificate of Repairs",
"sgi_windshield_auth": "SGI - Windshield Authorization",
"stolen_recovery_checklist": "Stolen Recovery Checklist",
"sublet_order": "Sublet Order",
"supplement_request": "Supplement Request",
"thank_you_ro": "Thank You Letter",
"thirdpartypayer": "Third Party Payer",
@@ -2251,7 +2280,8 @@
},
"subjects": {
"jobs": {
"parts_order": "Parts Order PO: {{ro_number}} - {{name}}"
"parts_order": "Parts Order PO: {{ro_number}} - {{name}}",
"sublet_order": "Sublet Order PO: {{ro_number}} - {{name}}"
}
},
"vendors": {
@@ -2388,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",
@@ -2412,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",
@@ -2529,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",
@@ -2769,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

@@ -86,6 +86,7 @@
"contents": "",
"created": "",
"operation": "",
"status": "",
"subject": "",
"to": "",
"useremail": "",
@@ -194,6 +195,7 @@
"entered_total": "",
"enteringcreditmemo": "",
"federal_tax": "",
"generatepartslabel": "",
"iouexists": "",
"local_tax": "",
"markexported": "",
@@ -435,6 +437,7 @@
"ar": "",
"ats": "",
"federal_tax": "",
"federal_tax_itc": "",
"gst_override": "",
"la1": "",
"la2": "",
@@ -835,6 +838,7 @@
},
"errors": {
"deletes3": "Error al eliminar el documento del almacenamiento.",
"deleting": "",
"deleting_cloudinary": "",
"getpresignurl": "Error al obtener la URL prescrita para el documento. {{message}}",
"insert": "Incapaz de cargar el archivo. {{message}}",
@@ -846,6 +850,7 @@
"doctype": "",
"newjobid": "",
"openinexplorer": "",
"optimizedimage": "",
"reassign_limitexceeded": "",
"reassign_limitexceeded_title": "",
"storageexceeded": "",
@@ -964,6 +969,7 @@
"save": "Salvar",
"saveandnew": "",
"selectall": "",
"send": "",
"senderrortosupport": "",
"submit": "",
"tryagain": "",
@@ -1033,6 +1039,7 @@
"sendby": "",
"signin": "",
"sms": "",
"status": "",
"sub_status": {
"expired": ""
},
@@ -1349,6 +1356,7 @@
"depreciation_taxes": "Depreciación / Impuestos",
"dms": {
"address": "",
"amount": "",
"center": "",
"cost": "",
"cost_dms_acctnumber": "",
@@ -1358,6 +1366,7 @@
"id": "",
"inservicedate": "",
"journal": "",
"lines": "",
"name1": "",
"payer": {
"amount": "",
@@ -1466,6 +1475,10 @@
"production_vars": {
"note": ""
},
"qb_multiple_payers": {
"amount": "",
"name": ""
},
"queued_for_parts": "",
"rate_ats": "",
"rate_la1": "Tarifa LA1",
@@ -1539,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": "",
@@ -1605,6 +1620,7 @@
"difference": "",
"diskscan": "",
"dms": {
"apexported": "",
"damageto": "",
"defaultstory": "",
"invoicedatefuture": "",
@@ -1631,6 +1647,7 @@
"importnote": "",
"inproduction": "",
"intakechecklist": "",
"iou": "",
"job": "",
"jobcosting": "",
"jobtotals": "",
@@ -1642,6 +1659,7 @@
"mapa": "",
"markforreexport": "",
"mash": "",
"multipayers": "",
"net_repairs": "",
"notes": "Notas",
"othertotal": "",
@@ -1653,6 +1671,7 @@
"partsfilter": "",
"partssubletstotal": "",
"partstotal": "",
"pimraryamountpayable": "",
"plitooltips": {
"billtotal": "",
"calculatedcreditsnotreceived": "",
@@ -2032,6 +2051,8 @@
"labels": {
"allpartsto": "",
"confirmdelete": "",
"custompercent": "",
"discount": "",
"email": "Enviar por correo electrónico",
"inthisorder": "Partes en este pedido",
"is_quote": "",
@@ -2039,12 +2060,15 @@
"newpartsorder": "",
"notyetordered": "",
"oec": "",
"order_type": "",
"orderhistory": "Historial de pedidos",
"parts_order": "",
"parts_orders": "",
"print": "Mostrar formulario impreso",
"receive": "",
"removefrompartsqueue": "",
"returnpartsorder": ""
"returnpartsorder": "",
"sublet_order": ""
},
"successes": {
"created": "Pedido de piezas creado con éxito.",
@@ -2076,6 +2100,7 @@
"customer": "",
"edit": "",
"electronicpayment": "",
"external": "",
"findermodal": "",
"insurance": "",
"new": "",
@@ -2085,6 +2110,7 @@
},
"successes": {
"exported": "",
"markexported": "",
"payment": "",
"stripe": ""
}
@@ -2195,6 +2221,8 @@
"mpi_eglass_auth": "",
"mpi_final_acct_sheet": "",
"paint_grid": "",
"parts_invoice_label_single": "",
"parts_label_multiple": "",
"parts_label_single": "",
"parts_list": "",
"parts_order": "",
@@ -2213,6 +2241,7 @@
"sgi_certificate_of_repairs": "",
"sgi_windshield_auth": "",
"stolen_recovery_checklist": "",
"sublet_order": "",
"supplement_request": "",
"thank_you_ro": "",
"thirdpartypayer": "",
@@ -2251,7 +2280,8 @@
},
"subjects": {
"jobs": {
"parts_order": ""
"parts_order": "",
"sublet_order": ""
}
},
"vendors": {
@@ -2388,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": "",
@@ -2412,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": "",
@@ -2529,7 +2564,8 @@
"creating": "",
"deleting": "",
"noemployeeforuser": "",
"noemployeeforuser_sub": ""
"noemployeeforuser_sub": "",
"shiftalreadyclockedon": ""
},
"fields": {
"actualhrs": "",
@@ -2769,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

@@ -86,6 +86,7 @@
"contents": "",
"created": "",
"operation": "",
"status": "",
"subject": "",
"to": "",
"useremail": "",
@@ -194,6 +195,7 @@
"entered_total": "",
"enteringcreditmemo": "",
"federal_tax": "",
"generatepartslabel": "",
"iouexists": "",
"local_tax": "",
"markexported": "",
@@ -435,6 +437,7 @@
"ar": "",
"ats": "",
"federal_tax": "",
"federal_tax_itc": "",
"gst_override": "",
"la1": "",
"la2": "",
@@ -835,6 +838,7 @@
},
"errors": {
"deletes3": "Erreur lors de la suppression du document du stockage.",
"deleting": "",
"deleting_cloudinary": "",
"getpresignurl": "Erreur lors de l'obtention de l'URL présignée pour le document. {{message}}",
"insert": "Incapable de télécharger le fichier. {{message}}",
@@ -846,6 +850,7 @@
"doctype": "",
"newjobid": "",
"openinexplorer": "",
"optimizedimage": "",
"reassign_limitexceeded": "",
"reassign_limitexceeded_title": "",
"storageexceeded": "",
@@ -964,6 +969,7 @@
"save": "sauvegarder",
"saveandnew": "",
"selectall": "",
"send": "",
"senderrortosupport": "",
"submit": "",
"tryagain": "",
@@ -1033,6 +1039,7 @@
"sendby": "",
"signin": "",
"sms": "",
"status": "",
"sub_status": {
"expired": ""
},
@@ -1349,6 +1356,7 @@
"depreciation_taxes": "Amortissement / taxes",
"dms": {
"address": "",
"amount": "",
"center": "",
"cost": "",
"cost_dms_acctnumber": "",
@@ -1358,6 +1366,7 @@
"id": "",
"inservicedate": "",
"journal": "",
"lines": "",
"name1": "",
"payer": {
"amount": "",
@@ -1466,6 +1475,10 @@
"production_vars": {
"note": ""
},
"qb_multiple_payers": {
"amount": "",
"name": ""
},
"queued_for_parts": "",
"rate_ats": "",
"rate_la1": "Taux LA1",
@@ -1539,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": "",
@@ -1605,6 +1620,7 @@
"difference": "",
"diskscan": "",
"dms": {
"apexported": "",
"damageto": "",
"defaultstory": "",
"invoicedatefuture": "",
@@ -1631,6 +1647,7 @@
"importnote": "",
"inproduction": "",
"intakechecklist": "",
"iou": "",
"job": "",
"jobcosting": "",
"jobtotals": "",
@@ -1642,6 +1659,7 @@
"mapa": "",
"markforreexport": "",
"mash": "",
"multipayers": "",
"net_repairs": "",
"notes": "Remarques",
"othertotal": "",
@@ -1653,6 +1671,7 @@
"partsfilter": "",
"partssubletstotal": "",
"partstotal": "",
"pimraryamountpayable": "",
"plitooltips": {
"billtotal": "",
"calculatedcreditsnotreceived": "",
@@ -2032,6 +2051,8 @@
"labels": {
"allpartsto": "",
"confirmdelete": "",
"custompercent": "",
"discount": "",
"email": "Envoyé par email",
"inthisorder": "Pièces dans cette commande",
"is_quote": "",
@@ -2039,12 +2060,15 @@
"newpartsorder": "",
"notyetordered": "",
"oec": "",
"order_type": "",
"orderhistory": "Historique des commandes",
"parts_order": "",
"parts_orders": "",
"print": "Afficher le formulaire imprimé",
"receive": "",
"removefrompartsqueue": "",
"returnpartsorder": ""
"returnpartsorder": "",
"sublet_order": ""
},
"successes": {
"created": "Commande de pièces créée avec succès.",
@@ -2076,6 +2100,7 @@
"customer": "",
"edit": "",
"electronicpayment": "",
"external": "",
"findermodal": "",
"insurance": "",
"new": "",
@@ -2085,6 +2110,7 @@
},
"successes": {
"exported": "",
"markexported": "",
"payment": "",
"stripe": ""
}
@@ -2195,6 +2221,8 @@
"mpi_eglass_auth": "",
"mpi_final_acct_sheet": "",
"paint_grid": "",
"parts_invoice_label_single": "",
"parts_label_multiple": "",
"parts_label_single": "",
"parts_list": "",
"parts_order": "",
@@ -2213,6 +2241,7 @@
"sgi_certificate_of_repairs": "",
"sgi_windshield_auth": "",
"stolen_recovery_checklist": "",
"sublet_order": "",
"supplement_request": "",
"thank_you_ro": "",
"thirdpartypayer": "",
@@ -2251,7 +2280,8 @@
},
"subjects": {
"jobs": {
"parts_order": ""
"parts_order": "",
"sublet_order": ""
}
},
"vendors": {
@@ -2388,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": "",
@@ -2412,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": "",
@@ -2529,7 +2564,8 @@
"creating": "",
"deleting": "",
"noemployeeforuser": "",
"noemployeeforuser_sub": ""
"noemployeeforuser_sub": "",
"shiftalreadyclockedon": ""
},
"fields": {
"actualhrs": "",
@@ -2769,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

@@ -504,6 +504,19 @@ export const TemplateList = (type, context) => {
key: "folder_label_multiple",
disabled: false,
},
parts_label_multiple: {
title: i18n.t("printcenter.jobs.parts_label_multiple"),
description: "Parts Label Multiple",
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",
@@ -551,6 +564,20 @@ export const TemplateList = (type, context) => {
}),
disabled: false,
},
sublet_order: {
title: i18n.t("printcenter.jobs.sublet_order"),
description: "Parts Order",
key: "sublet_order",
subject: i18n.t("printcenter.subjects.jobs.sublet_order", {
ro_number: context && context.job && context.job.ro_number,
name: (
(context && context.job && context.job.ownr_ln) ||
(context && context.job && context.job.ownr_co_nm) ||
""
).trim(),
}),
disabled: false,
},
parts_return_slip: {
title: i18n.t("printcenter.jobs.parts_return_slip"),
subject: i18n.t("printcenter.jobs.parts_return_slip"),
@@ -766,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"
@@ -1559,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

@@ -1,3 +1,3 @@
Must set the environment variables using:
firebase functions:config:set auth.graphql_endpoint="https://db.development.bodyshop.app/v1/graphql" auth.hasura_secret_admin_key="Dev-BodyShopApp!"
firebase functions:config:set auth.graphql_endpoint="https://db.dev.bodyshop.app/v1/graphql" auth.hasura_secret_admin_key="Dev-BodyShopApp!"

View File

@@ -0,0 +1,24 @@
Issue when migrating events resolved by running SQL below. User needed updating when running in prod.
CREATE TABLE IF NOT EXISTS hdb_catalog.hdb_source_catalog_version
(
version text COLLATE pg_catalog."default" NOT NULL,
upgraded_on timestamp with time zone NOT NULL
)
TABLESPACE pg_default;
ALTER TABLE hdb_catalog.hdb_source_catalog_version
OWNER to postgres;
-- Index: hdb_source_catalog_version_one_row
-- DROP INDEX hdb_catalog.hdb_source_catalog_version_one_row;
CREATE UNIQUE INDEX hdb_source_catalog_version_one_row
ON hdb_catalog.hdb_source_catalog_version USING btree
((version IS NOT NULL) ASC NULLS LAST)
TABLESPACE pg_default;
INSERT INTO hdb_catalog.hdb_source_catalog_version (version, upgraded_on) VALUES ('2', NOW());
https://devscope.io/code/hasura/graphql-engine/issues/8694

View File

@@ -1,5 +1,5 @@
version: 2
endpoint: https://db.development.bodyshop.app
endpoint: https://db.dev.bodyshop.app
admin_secret: Dev-BodyShopApp!
metadata_directory: metadata
actions:

View File

@@ -1,30 +1,30 @@
- function:
schema: public
name: search_bills
- function:
schema: public
- function:
name: search_cccontracts
- function:
schema: public
- function:
name: search_dms_vehicles
- function:
schema: public
- function:
name: search_exportlog
- function:
schema: public
- function:
name: search_inventory
- function:
schema: public
- function:
name: search_jobs
- function:
schema: public
- function:
name: search_owners
- function:
schema: public
- function:
name: search_payments
- function:
schema: public
- function:
name: search_phonebook
- function:
schema: public
- function:
name: search_vehicles
schema: public

File diff suppressed because it is too large Load Diff

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"."jobs" add column "qb_multiple_payers" jsonb
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."jobs" add column "qb_multiple_payers" jsonb
null;

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"."email_audit_trail" add column "sesmessageid" text
-- null;

View File

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

View File

@@ -0,0 +1 @@
DROP INDEX IF EXISTS "public"."email_audit_trail_sesmessageid";

View File

@@ -0,0 +1,2 @@
CREATE INDEX "email_audit_trail_sesmessageid" on
"public"."email_audit_trail" using btree ("sesmessageid");

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"."email_audit_trail" add column "status" text
-- null;

View File

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

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"."email_audit_trail" add column "status_context" jsonb
-- null default jsonb_build_array();

View File

@@ -0,0 +1,2 @@
alter table "public"."email_audit_trail" add column "status_context" jsonb
null default jsonb_build_array();

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;

File diff suppressed because one or more lines are too long

View File

@@ -41,6 +41,7 @@
"moment-timezone": "^0.5.34",
"multer": "^1.4.5-lts.1",
"node-mailjet": "^5.1.0",
"node-persist": "^3.1.0",
"node-quickbooks": "^2.0.39",
"nodemailer": "^6.7.7",
"phone": "^3.1.23",

View File

@@ -64,6 +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);
//Test route to ensure Express is responding.
app.get("/test", async function (req, res) {
@@ -210,7 +211,7 @@ app.post("/qbo/payments", fb.validateFirebaseIdToken, qbo.payments);
var data = require("./server/data/data");
app.post("/data/ah", data.autohouse);
app.post("/data/arms", data.arms);
app.post("/record-handler/arms", data.arms);
var taskHandler = require("./server/tasks/tasks");
app.post("/taskHandler", taskHandler.taskHandler);

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,
@@ -648,6 +644,56 @@ exports.default = function ({
});
}
//Check if there are multiple payers. If there are, add a deduction line and make sure we create new invoices.
if (
jobs_by_pk.qb_multiple_payers &&
jobs_by_pk.qb_multiple_payers.length > 0
) {
jobs_by_pk.qb_multiple_payers.forEach((payer) => {
if (qbo) {
InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail",
Amount: Dinero({ amount: (payer.amount || 0) * 100 * -1 }).toFormat(
DineroQbFormat
),
SalesItemLineDetail: {
...(jobs_by_pk.class
? { ClassRef: { value: classes[jobs_by_pk.class] } }
: {}),
ItemRef: {
value:
items[responsibilityCenters.qb_multiple_payers?.accountitem],
},
Qty: 1,
TaxCodeRef: {
value:
taxCodes[
findTaxCode(
{
local: false,
federal: false,
state: false,
},
bodyshop.md_responsibility_centers.sales_tax_codes
)
],
},
},
});
} else {
InvoiceLineAdd.push({
ItemRef: {
FullName: responsibilityCenters.qb_multiple_payers?.accountitem,
},
Desc: `${payer.name} Liability`,
Amount: Dinero({ amount: (payer.amount || 0) * 100 * -1 }).toFormat(
DineroQbFormat
),
});
}
});
}
return InvoiceLineAdd;
};
@@ -667,3 +713,65 @@ const findTaxCode = ({ local, state, federal }, taxcode) => {
}
};
exports.findTaxCode = findTaxCode;
exports.createMultiQbPayerLines = function ({
bodyshop,
jobs_by_pk,
qbo = false,
items,
taxCodes,
classes,
payer,
}) {
const InvoiceLineAdd = [];
const responsibilityCenters = bodyshop.md_responsibility_centers;
const invoiceLineHash = {}; //The hash of cost and profit centers based on the center name.
if (qbo) {
//Going to always assume that we need to apply GST and PST for labor.
const taxAccountCode = findTaxCode(
{
local: false,
federal: false,
state: false,
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
const QboTaxId = taxCodes[taxAccountCode];
InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail",
Amount: Dinero({
amount: Math.round((payer.amount || 0) * 100),
}).toFormat(DineroQbFormat),
SalesItemLineDetail: {
...(jobs_by_pk.class
? { ClassRef: { value: classes[jobs_by_pk.class] } }
: {}),
ItemRef: {
value: items[responsibilityCenters.qb_multiple_payers?.accountitem],
},
TaxCodeRef: {
value: QboTaxId,
},
Qty: 1,
},
});
} else {
InvoiceLineAdd.push({
ItemRef: {
FullName: responsibilityCenters.qb_multiple_payers?.accountitem,
},
Desc: `${payer.name} Liability`,
Quantity: 1,
Amount: Dinero({
amount: Math.round((payer.amount || 0) * 100),
}).toFormat(DineroQbFormat),
SalesTaxCodeRef: {
FullName: "E",
},
});
}
return InvoiceLineAdd;
};

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

@@ -21,6 +21,7 @@ const moment = require("moment-timezone");
const GraphQLClient = require("graphql-request").GraphQLClient;
const { generateOwnerTier } = require("../qbxml/qbxml-utils");
const { createMultiQbPayerLines } = require("../qb-receivables-lines");
exports.default = async (req, res) => {
const oauthClient = new OAuthClient({
@@ -99,7 +100,9 @@ exports.default = async (req, res) => {
oauthClient,
qbo_realmId,
req,
job
job,
isThreeTier,
insCoCustomerTier
);
//Query for the owner itself.
if (!ownerCustomerTier) {
@@ -115,7 +118,17 @@ exports.default = async (req, res) => {
}
//Query for the Job or Create it.
jobTier = await QueryJob(oauthClient, qbo_realmId, req, job);
jobTier = await QueryJob(
oauthClient,
qbo_realmId,
req,
job,
isThreeTier
? ownerCustomerTier
: twoTierPref === "source"
? insCoCustomerTier
: ownerCustomerTier
);
// Need to validate that the job tier is associated to the right individual?
@@ -140,6 +153,65 @@ exports.default = async (req, res) => {
jobTier
);
if (job.qb_multiple_payers && job.qb_multiple_payers.length > 0) {
for (const [index, payer] of job.qb_multiple_payers.entries()) {
//do the thing.
//Create the source level.
let insCoCustomerTier, ownerCustomerTier, jobTier;
//Insert the insurance company tier.
//Query for top level customer, the insurance company name.
insCoCustomerTier = await QueryInsuranceCo(
oauthClient,
qbo_realmId,
req,
{ ...job, ins_co_nm: payer.name }
);
if (!insCoCustomerTier) {
//Creating the Insurance Customer.
insCoCustomerTier = await InsertInsuranceCo(
oauthClient,
qbo_realmId,
req,
{ ...job, ins_co_nm: payer.name },
bodyshop
);
}
//Query for the Job or Create it.
jobTier = await QueryJob(
oauthClient,
qbo_realmId,
req,
job,
insCoCustomerTier
);
// Need to validate that the job tier is associated to the right individual?
if (!jobTier) {
jobTier = await InsertJob(
oauthClient,
qbo_realmId,
req,
job,
insCoCustomerTier
);
}
//Create the RO level
await InsertInvoiceMultiPayerInvoice(
oauthClient,
qbo_realmId,
req,
job,
bodyshop,
jobTier,
payer,
`-${index + 1}`
);
}
}
// //No error. Mark the job exported & insert export log.
if (elgen) {
const result = await client
@@ -212,7 +284,7 @@ async function QueryInsuranceCo(oauthClient, qbo_realmId, req, job) {
"query",
`select * From Customer where DisplayName = '${StandardizeName(
job.ins_co_nm.trim()
)}'`
)}' and Active = true`
),
method: "POST",
headers: {
@@ -238,6 +310,12 @@ exports.QueryInsuranceCo = QueryInsuranceCo;
async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) {
const insCo = bodyshop.md_ins_cos.find((i) => i.name === job.ins_co_nm);
if (!insCo) {
throw new Error(
`Insurance Company '${job.ins_co_nm}' not found in shop configuration. Please make sure it exists or change the insurance company name on the job to one that exists.`
);
return;
}
const Customer = {
DisplayName: job.ins_co_nm.trim(),
BillWithParent: true,
@@ -270,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(
@@ -278,7 +363,7 @@ async function QueryOwner(oauthClient, qbo_realmId, req, job) {
"query",
`select * From Customer where DisplayName = '${StandardizeName(
ownerName
)}'`
)}' and Active = true`
),
method: "POST",
headers: {
@@ -290,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;
@@ -342,12 +429,12 @@ async function InsertOwner(
}
}
exports.InsertOwner = InsertOwner;
async function QueryJob(oauthClient, qbo_realmId, req, job) {
async function QueryJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
const result = await oauthClient.makeApiCall({
url: urlBuilder(
qbo_realmId,
"query",
`select * From Customer where DisplayName = '${job.ro_number}'`
`select * From Customer where DisplayName = '${job.ro_number}' and Active = true`
),
method: "POST",
headers: {
@@ -359,9 +446,14 @@ async function QueryJob(oauthClient, qbo_realmId, req, job) {
result.json &&
result.json.QueryResponse &&
result.json.QueryResponse.Customer &&
result.json.QueryResponse.Customer[0]
(parentTierRef
? result.json.QueryResponse.Customer.find(
(x) => x.ParentRef.value === parentTierRef.Id
)
: result.json.QueryResponse.Customer[0])
);
}
exports.QueryJob = QueryJob;
async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
const Customer = {
@@ -596,3 +688,137 @@ async function InsertInvoice(
throw error;
}
}
async function InsertInvoiceMultiPayerInvoice(
oauthClient,
qbo_realmId,
req,
job,
bodyshop,
parentTierRef,
payer,
suffix
) {
const { items, taxCodes, classes } = await QueryMetaData(
oauthClient,
qbo_realmId,
req
);
const InvoiceLineAdd = createMultiQbPayerLines({
bodyshop,
jobs_by_pk: job,
qbo: true,
items,
taxCodes,
classes,
payer,
suffix,
});
const invoiceObj = {
Line: InvoiceLineAdd,
TxnDate: moment(job.date_invoiced)
.tz(bodyshop.timezone)
.format("YYYY-MM-DD"),
DocNumber: job.ro_number + suffix,
...(job.class ? { ClassRef: { value: classes[job.class] } } : {}),
CustomerMemo: {
value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${
job.po_number ? `PO No: ${job.po_number}` : ``
} Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
job.v_model_desc || ""
} ${job.v_vin || ""} ${job.plate_no || ""} `.trim(),
},
CustomerRef: {
value: parentTierRef.Id,
},
...(bodyshop.accountingconfig.qbo_departmentid &&
bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && {
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid },
}),
CustomField: [
...(bodyshop.accountingconfig.ReceivableCustomField1
? [
{
DefinitionId: "1",
StringValue:
job[bodyshop.accountingconfig.ReceivableCustomField1],
Type: "StringType",
},
]
: []),
...(bodyshop.accountingconfig.ReceivableCustomField2
? [
{
DefinitionId: "2",
StringValue:
job[bodyshop.accountingconfig.ReceivableCustomField2],
Type: "StringType",
},
]
: []),
...(bodyshop.accountingconfig.ReceivableCustomField3
? [
{
DefinitionId: "3",
StringValue:
job[bodyshop.accountingconfig.ReceivableCustomField3],
Type: "StringType",
},
]
: []),
],
...(bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
bodyshop.accountingconfig.qbo_usa &&
bodyshop.region_config.includes("CA_") && {
TxnTaxDetail: {
TxnTaxCodeRef: {
value:
taxCodes[
bodyshop.md_responsibility_centers.taxes.state.accountitem
],
},
},
}),
...(bodyshop.accountingconfig.printlater
? { PrintStatus: "NeedToPrint" }
: {}),
...(bodyshop.accountingconfig.emaillater && job.ownr_ea
? { EmailStatus: "NeedToSend" }
: {}),
BillAddr: {
Line3: `${job.ownr_city || ""}, ${job.ownr_st || ""} ${
job.ownr_zip || ""
}`.trim(),
Line2: job.ownr_addr1 || "",
Line1: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
job.ownr_co_nm || ""
}`,
},
};
logger.log("qbo-receivable-objectlog", "DEBUG", req.user.email, job.id, {
invoiceObj,
});
try {
const result = await oauthClient.makeApiCall({
url: urlBuilder(qbo_realmId, "invoice"),
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(invoiceObj),
});
setNewRefreshToken(req.user.email, result);
return result && result.json && result.json.Invoice;
} catch (error) {
logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, {
error,
method: "InsertOwner",
});
throw error;
}
}

File diff suppressed because it is too large Load Diff

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

@@ -148,6 +148,7 @@ exports.sendEmail = async (req, res) => {
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
messageId: info.messageId,
});
res.json({
success: true, //response: info
@@ -190,6 +191,8 @@ async function logEmail(req, email) {
useremail: req.user.email,
contents: req.body.html,
jobid: req.body.jobid,
sesmessageid: email.messageId,
status: "Sent",
},
});
} catch (error) {
@@ -202,3 +205,62 @@ async function logEmail(req, email) {
});
}
}
exports.emailBounce = async function (req, res, next) {
try {
const body = JSON.parse(req.body);
if (body.Type === "SubscriptionConfirmation") {
logger.log("SNS-message", "DEBUG", "api", null, {
body: req.body,
});
}
if (body.notificationType === "Bounce") {
// const message = JSON.parse(body.Message);
let replyTo, subject, messageId;
body.mail.headers.forEach((header) => {
if (header.name === "Reply-To") {
replyTo = header.value;
} else if (header.name === "Subject") {
subject = header.value;
} else if (header.name === "Message-ID") {
messageId = header.value;
}
});
if (replyTo === "noreply@imex.online") {
res.sendStatus(200);
return;
}
//If it's bounced, log it as bounced in audit log. Send an email to the user.
const result = await client.request(queries.UPDATE_EMAIL_AUDIT, {
sesid: messageId,
status: "Bounced",
context: body.bounce?.bouncedRecipients,
});
transporter.sendMail(
{
from: `ImEX Online <noreply@imex.online>`,
to: replyTo,
bcc: "patrick@snapt.ca",
subject: `ImEX Online Bounced Email - RE: ${subject}`,
text: `ImEX Online has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error.
${body.bounce?.bouncedRecipients.map(
(r) =>
`Recipient: ${r.emailAddress} | Status: ${r.action} | Code: ${r.diagnosticCode}
`
)}
`,
},
(err, info) => {
console.log("***", err || info);
}
);
}
} catch (error) {
logger.log("sns-error", "ERROR", "api", null, {
error: JSON.stringify(error),
});
}
res.sendStatus(200);
};

View File

@@ -191,6 +191,7 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
storage_payable
adjustment_bottom_line
state_tax_rate
qb_multiple_payers
owner {
accountingid
}
@@ -331,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
@@ -622,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
@@ -1503,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) {
@@ -1611,3 +1631,65 @@ exports.INSERT_EMAIL_AUDIT = `mutation INSERT_EMAIL_AUDIT($email: email_audit_tr
}
}
`;
exports.DELETE_MEDIA_DOCUMENTS = `
mutation DELETE_DOCUMENTS($ids: [uuid!]!) {
delete_documents(where: { id: { _in: $ids } }) {
returning {
id
}
}
}
`;
exports.UPDATE_EMAIL_AUDIT = `
mutation ($sesid: String!, $status: String, $context: jsonb) {
update_email_audit_trail(where: {sesmessageid: {_eq: $sesid}}, _set: {status: $status, status_context: $context}) {
returning {
contents
}
}
}`;
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

@@ -1,6 +1,8 @@
const path = require("path");
const _ = require("lodash");
const logger = require("../utils/logger");
const client = require("../graphql-client/graphql-client").client;
const queries = require("../graphql-client/queries");
require("dotenv").config({
path: path.resolve(
@@ -69,11 +71,38 @@ exports.deleteFiles = async (req, res) => {
);
}
res.send(returns);
// Delete it on apollo.
const successfulDeletes = [];
returns.forEach((resType) => {
Object.keys(resType.deleted).forEach((key) => {
if (
resType.deleted[key] === "deleted" ||
resType.deleted[key] === "not_found"
) {
successfulDeletes.push(key.replace(/\.[^/.]+$/, ""));
}
});
});
try {
const result = await client.request(queries.DELETE_MEDIA_DOCUMENTS, {
ids: ids
.filter((i) => successfulDeletes.includes(i.key))
.map((i) => i.id),
});
res.send({ returns, result });
} catch (error) {
logger.log("media-delete-error", "ERROR", req.user.email, null, [
{ ids, error: error.message || JSON.stringify(error) },
]);
res.json({ error });
}
};
exports.renameKeys = async (req, res) => {
const { documents } = req.body;
const { documents, tojobid } = req.body;
logger.log("media-bulk-rename", "DEBUG", req.user.email, null, documents);
const proms = [];
@@ -98,8 +127,37 @@ exports.renameKeys = async (req, res) => {
let result;
result = await Promise.all(proms);
const errors = [];
result
.filter((d) => d.error)
.forEach((d) => {
errors.push(d);
});
res.send(result);
let mutations = "";
result
.filter((d) => !d.error)
.forEach((d, idx) => {
//Create mutation text
mutations =
mutations +
`
update_doc${idx}:update_documents_by_pk(pk_columns: { id: "${d.id}" }, _set: {key: "${d.public_id}", jobid: "${tojobid}"}){
id
}
`;
});
if (mutations !== "") {
const mutationResult = await client.request(`mutation {
${mutations}
}`);
res.json({ errors, mutationResult });
} else {
res.json({ errors: "No images were succesfully moved on remote server. " });
}
};
//Also needs to be updated in upload utility and mobile app.

View File

@@ -24,9 +24,8 @@ exports.mixdataUpload = async (req, res) => {
});
try {
req.files.forEach(async (element) => {
for (const element of req.files) {
const b = Buffer.from(element.buffer);
console.log(b.toString());
const inboundRequest = await xml2js.parseStringPromise(b.toString(), {
explicitArray: false,
@@ -59,23 +58,24 @@ exports.mixdataUpload = async (req, res) => {
ScaleType,
jobHash
);
const foundJobs = MixDataArray.filter((m) => m.jobid);
const MixDataQuery = `
mutation UPSERT_MIXDATA{
${MixDataArray.map((md, idx) =>
GenerateGqlForMixData(md, idx)
).join(" ")}
${foundJobs
.map((md, idx) => GenerateGqlForMixData(md, idx))
.join(" ")}
}
`;
const resp = await client.request(MixDataQuery);
if (foundJobs.length > 1) {
const resp = await client.request(MixDataQuery);
}
//Process the list of ROs and return an object to generate the queries.
}
});
}
res.sendStatus(200);
} catch (error) {
res.status(500).JSON(error);
res.status(500).json(error);
logger.log("job-mixdata-upload-error", "ERROR", null, null, {
error: error.message,
...error,
@@ -98,7 +98,7 @@ function DetermineScaleType(inboundRequest) {
function GetListOfRos(inboundRequest, ScaleType) {
if (ScaleType.company === "PPG" && ScaleType.version === "1.3.0") {
return inboundRequest.PPG.DataExportInterface.ROData.RepairOrders.RO.map(
return inboundRequest.PPG.MixDataInterface.ROData.RepairOrders.RO.map(
(r) => r.RONumber
);
}
@@ -106,11 +106,11 @@ function GetListOfRos(inboundRequest, ScaleType) {
function GenerateMixDataArray(inboundRequest, ScaleType, jobHash) {
if (ScaleType.company === "PPG" && ScaleType.version === "1.3.0") {
return inboundRequest.PPG.DataExportInterface.ROData.RepairOrders.RO.map(
return inboundRequest.PPG.MixDataInterface.ROData.RepairOrders.RO.map(
(r) => {
return {
jobid: jobHash[r.RONumber].jobid,
id: jobHash[r.RONumber].mixdataid,
jobid: jobHash[r.RONumber]?.jobid,
id: jobHash[r.RONumber]?.mixdataid,
mixdata: r,
totalliquidcost: r.TotalLiquidCost,
totalsundrycost: r.TotalSundryCost,

View File

@@ -5,7 +5,7 @@ const logger = new graylog2.graylog({
});
function log(message, type, user, record, object) {
if (type !== "ioevent" && type !== "DEBUG")
if (type !== "ioevent")
console.log(message, {
type,
env: process.env.NODE_ENV || "development",

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.`);

View File

@@ -2715,6 +2715,11 @@ node-mailjet@^5.1.0:
json-bigint "^1.0.0"
url-join "^4.0.0"
node-persist@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/node-persist/-/node-persist-3.1.0.tgz#9d4b03950bba70d37d13d3d3551840e25fd17e09"
integrity sha512-/j+fd/u71wNgKf3V2bx4tnDm+3GvLnlCuvf2MXbJ3wern+67IAb6zN9Leu1tCWPlPNZ+v1hLSibVukkPK2HqJw==
node-quickbooks@^2.0.39:
version "2.0.39"
resolved "https://registry.yarnpkg.com/node-quickbooks/-/node-quickbooks-2.0.39.tgz#a2534d24063e8a0cea5bb80c66b0727c1c942081"