diff --git a/.circleci/config.yml b/.circleci/config.yml index 07882fa41..60f516702 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -132,6 +132,57 @@ jobs: to: "s3://rome-online-production/" - jira/notify + test-rome-hasura-migrate: + docker: + - image: cimg/node:16.15.0 + parameters: + secret: + type: string + default: $HASURA_ROME_TEST_SECRET + working_directory: ~/repo/hasura + steps: + - checkout: + path: ~/repo + - run: + name: Execute migration + command: | + npm install hasura-cli -g + echo ${HASURA_TEST_SECRET} + hasura migrate apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >> + hasura metadata apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >> + hasura metadata reload --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >> + + test-rome-app-build: + docker: + - image: cimg/node:16.15.0 + + working_directory: ~/repo/client + + steps: + - checkout: + path: ~/repo + + - restore_cache: + name: Restore Yarn Package Cache + keys: + - yarn-packages-{{ checksum "yarn.lock" }} + - run: + name: Install Dependencies + command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn + - save_cache: + name: Save Yarn Package Cache + key: yarn-packages-{{ checksum "yarn.lock" }} + paths: + - ~/.cache/yarn + + - run: yarn run build:test + + - aws-s3/sync: + from: build + to: "s3://rome-online-test/" + - jira/notify + + test-hasura-migrate: docker: - image: cimg/node:16.15.0 @@ -250,6 +301,15 @@ workflows: filters: branches: only: test + - test-rome-app-build: + filters: + branches: + only: rome/test + - test-rome-hasura-migrate: + secret: ${HASURA_ROME_TEST_SECRET} + filters: + branches: + only: rome/test #- admin-app-build: #filters: #branches: diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 3039e4986..6d11973f7 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -1009,6 +1009,27 @@ + + smspaymentreminder + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + suggesteddates false @@ -7526,6 +7547,111 @@ + + responsibilitycenter_tax_rate + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + responsibilitycenter_tax_sur + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + responsibilitycenter_tax_thres + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + responsibilitycenter_tax_tier + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + responsibilitycenter_tax_type + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + responsibilitycenters @@ -10133,6 +10259,27 @@ + + md_parts_scan + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + md_tasks_presets false @@ -17839,6 +17986,27 @@ + + reports + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + required false @@ -19556,6 +19724,27 @@ + + paymentnum + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + paymenttype false @@ -22225,6 +22414,48 @@ + + taxprofileoverride + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + taxprofileoverride_confirm + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + uninvoice false @@ -23605,6 +23836,352 @@ + + cieca_pfl + + + lbr_tax_in + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + lbr_tx_in1 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + lbr_tx_in2 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + lbr_tx_in3 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + lbr_tx_in4 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + lbr_tx_in5 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + cieca_pfo + + + stor_t_in1 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + stor_t_in2 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + stor_t_in3 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + stor_t_in4 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + stor_t_in5 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + tow_t_in1 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + tow_t_in2 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + tow_t_in3 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + tow_t_in4 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + tow_t_in5 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + claim_total false @@ -25830,6 +26407,242 @@ + + materials + + + MAPA + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + MASH + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + cal_maxdlr + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + cal_opcode + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + mat_tx_in1 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + mat_tx_in2 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + mat_tx_in3 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + mat_tx_in4 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + mat_tx_in5 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + materials + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + tax_ind + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + other_amount_payable false @@ -26274,6 +27087,111 @@ + + prt_tx_in1 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + prt_tx_in2 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + prt_tx_in3 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + prt_tx_in4 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + prt_tx_in5 + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + prt_type false @@ -28700,6 +29618,69 @@ + + cieca_pfl + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + cieca_pfo + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + cieca_pft + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + closeconfirm false @@ -30101,6 +31082,53 @@ + + materials + + + mapa + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + missingprofileinfo + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + multipayers false @@ -37693,6 +38721,27 @@ + + inserting + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -39411,6 +40460,27 @@ + + committed_timetickets_ro + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + coversheet_landscape false @@ -40151,6 +41221,27 @@ + + parts_dispatch + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + parts_invoice_label_single false @@ -40886,6 +41977,27 @@ + + worksheet_sorted_by_team + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -41229,6 +42341,27 @@ + + parts_dispatch + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + parts_order false @@ -43064,6 +44197,69 @@ + + committed_timetickets + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + committed_timetickets_employee + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + committed_timetickets_summary + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + credits_not_received_date false @@ -45122,6 +46318,27 @@ + + work_in_progress_committed_labour + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + work_in_progress_jobs false @@ -46682,6 +47899,27 @@ + + committed_at + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + cost_center false @@ -46703,6 +47941,27 @@ + + created_by + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + date false diff --git a/client/.env.test b/client/.env.test index 6768939ff..1601c4415 100644 --- a/client/.env.test +++ b/client/.env.test @@ -1,14 +1,14 @@ -REACT_APP_GRAPHQL_ENDPOINT=https://db.test.bodyshop.app/v1/graphql -REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.test.bodyshop.app/v1/graphql -REACT_APP_GA_CODE=231099835 -REACT_APP_FIREBASE_CONFIG={ "apiKey":"AIzaSyBw7_GTy7GtQyfkIRPVrWHEGKfcqeyXw0c", "authDomain":"imex-test.firebaseapp.com", "projectId":"imex-test", "storageBucket":"imex-test.appspot.com", "messagingSenderId":"991923618608", "appId":"1:991923618608:web:633437569cdad78299bef5", "measurementId":"G-TW0XLZEH18"} +REACT_APP_GRAPHQL_ENDPOINT=https://db.test.romeonline.io/v1/graphql +REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.test.romeonline.io/v1/graphql +REACT_APP_GA_CODE=231103507 +REACT_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"} REACT_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/bodyshop REACT_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/bodyshop REACT_APP_CLOUDINARY_API_KEY=473322739956866 REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250 -REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BN2GcDPjipR5MTEosO5dT4CfQ3cmrdBIsI4juoOQrRijn_5aRiHlwj1mlq0W145mOusx6xynEKl_tvYJhpCc9lo' +REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo' REACT_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g -REACT_APP_AXIOS_BASE_API_URL=https://api.romeonline.io/ -REACT_APP_REPORTS_SERVER_URL=https://reports.romeonline.io +REACT_APP_AXIOS_BASE_API_URL=https://api.test.romeonline.io/ +REACT_APP_REPORTS_SERVER_URL=https://reports.test.romeonline.io REACT_APP_IS_TEST=true REACT_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc \ No newline at end of file diff --git a/client/src/components/bill-form/bill-form.component.jsx b/client/src/components/bill-form/bill-form.component.jsx index 0fa28359a..6675990b0 100644 --- a/client/src/components/bill-form/bill-form.component.jsx +++ b/client/src/components/bill-form/bill-form.component.jsx @@ -364,13 +364,15 @@ export function BillFormComponent({ )} - - - + { + // + // + // + } - - - + { + // + // + // + } {() => { const values = form.getFieldsValue([ @@ -410,21 +414,25 @@ export function BillFormComponent({ value={totals.subtotal.toFormat()} precision={2} /> - + { + // + } - + { + // + } ), }, - { - title: t("billlines.fields.federal_tax_applicable"), - dataIndex: "applicable_taxes.federal", - editable: true, + // { + // title: t("billlines.fields.federal_tax_applicable"), + // dataIndex: "applicable_taxes.federal", + // editable: true, - formItemProps: (field) => { - return { - key: `${field.index}fedtax`, - valuePropName: "checked", - // initialValue: true, - name: [field.name, "applicable_taxes", "federal"], - }; - }, - formInput: (record, index) => , - }, + // formItemProps: (field) => { + // return { + // key: `${field.index}fedtax`, + // valuePropName: "checked", + // // initialValue: true, + // name: [field.name, "applicable_taxes", "federal"], + // }; + // }, + // formInput: (record, index) => , + // }, { title: t("billlines.fields.state_tax_applicable"), dataIndex: "applicable_taxes.state", @@ -487,20 +487,20 @@ export function BillEnterModalLinesComponent({ }, formInput: (record, index) => , }, - { - title: t("billlines.fields.local_tax_applicable"), - dataIndex: "applicable_taxes.local", - editable: true, + // { + // title: t("billlines.fields.local_tax_applicable"), + // dataIndex: "applicable_taxes.local", + // editable: true, - formItemProps: (field) => { - return { - key: `${field.index}localtax`, - valuePropName: "checked", - name: [field.name, "applicable_taxes", "local"], - }; - }, - formInput: (record, index) => , - }, + // formItemProps: (field) => { + // return { + // key: `${field.index}localtax`, + // valuePropName: "checked", + // name: [field.name, "applicable_taxes", "local"], + // }; + // }, + // formInput: (record, index) => , + // }, { title: t("general.labels.actions"), diff --git a/client/src/components/card-payment-modal/card-payment-modal.component..jsx b/client/src/components/card-payment-modal/card-payment-modal.component..jsx index 15c4ffe3a..071e2a758 100644 --- a/client/src/components/card-payment-modal/card-payment-modal.component..jsx +++ b/client/src/components/card-payment-modal/card-payment-modal.component..jsx @@ -1,12 +1,15 @@ -import { useMutation, useQuery } from "@apollo/client"; +import { DeleteFilled } from "@ant-design/icons"; +import { useLazyQuery, useMutation } from "@apollo/client"; import { Button, Card, + Col, Form, Input, - InputNumber, Row, + Space, Spin, + Statistic, notification, } from "antd"; import axios from "axios"; @@ -17,7 +20,7 @@ import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { INSERT_PAYMENT_RESPONSE, - QUERY_RO_AND_OWNER_BY_JOB_PK, + QUERY_RO_AND_OWNER_BY_JOB_PKS, } from "../../graphql/payment_response.queries"; import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries"; import { insertAuditTrail } from "../../redux/application/application.actions"; @@ -25,9 +28,8 @@ import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { selectCardPayment } from "../../redux/modals/modals.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; -import DataLabel from "../data-label/data-label.component"; +import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component"; import JobSearchSelectComponent from "../job-search-select/job-search-select.component"; -import LayoutFormRow from "../layout-form-row/layout-form-row.component"; const mapStateToProps = createStructuredSelector({ cardPaymentModal: selectCardPayment, @@ -49,18 +51,21 @@ const CardPaymentModalComponent = ({ const { context } = cardPaymentModal; const [form] = Form.useForm(); - const amount = Form.useWatch("amount", form); - const jobid = Form.useWatch("jobid", form); + const [loading, setLoading] = useState(false); const [insertPayment] = useMutation(INSERT_NEW_PAYMENT); const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE); const { t } = useTranslation(); - const { data, refetch } = useQuery(QUERY_RO_AND_OWNER_BY_JOB_PK, { - variables: { jobid: context?.jobid ?? "" }, - skip: !context?.jobid, - }); + const [, { data, refetch, queryLoading }] = useLazyQuery( + QUERY_RO_AND_OWNER_BY_JOB_PKS, + { + variables: { jobids: [context.jobid] }, + skip: true, + } + ); + console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data); //Initialize the intellipay window. const SetIntellipayCallbackFunctions = () => { console.log("*** Set IntelliPay callback functions."); @@ -76,69 +81,68 @@ const CardPaymentModalComponent = ({ window.intellipay.runOnNonApproval(async function (response) { // Mutate unsuccessful payment + + const { payments } = form.getFieldsValue(); + await insertPaymentResponse({ variables: { - paymentResponse: { - amount: response.amount, + paymentResponse: payments.map((payment) => ({ + amount: payment.amount, bodyshopid: bodyshop.id, - jobid: jobid || context.jobid, + jobid: payment.jobid, declinereason: response.declinereason, ext_paymentid: response.paymentid.toString(), successful: false, response, - }, + })), }, }); - insertAuditTrail({ - jobid: jobid || context?.jobid, - operation: AuditTrailMapping.failedpayment(), - }); + + payments.forEach((payment) => + insertAuditTrail({ + jobid: payment.jobid, + operation: AuditTrailMapping.failedpayment(), + }) + ); }); }; const handleFinish = async (values) => { try { - const paymentResult = await insertPayment({ + await insertPayment({ variables: { - paymentInput: { - amount: values.amount, + paymentInput: values.payments.map((payment) => ({ + amount: payment.amount, transactionid: (values.paymentResponse.paymentid || "").toString(), payer: t("payments.labels.customer"), type: values.paymentResponse.cardbrand, - jobid: values.jobid, + jobid: payment.jobid, date: moment(Date.now()), - }, + payment_responses: { + data: [ + { + amount: payment.amount, + bodyshopid: bodyshop.id, + + jobid: payment.jobid, + declinereason: values.paymentResponse.declinereason, + ext_paymentid: values.paymentResponse.paymentid.toString(), + successful: true, + response: values.paymentResponse, + }, + ], + }, + })), }, refetchQueries: ["GET_JOB_BY_PK"], - update(cache, { data }) { - cache.modify({ - id: cache.identify({ id: values.jobid, __typename: "jobs" }), - fields: { - payments(cachedPayments) { - return [...data.insert_payments.returning, ...cachedPayments]; - }, - }, - }); - }, - }); - - await insertPaymentResponse({ - variables: { - paymentResponse: { - amount: values.amount, - bodyshopid: bodyshop.id, - paymentid: paymentResult.data.insert_payments.returning[0].id, - jobid: values.jobid, - declinereason: values.paymentResponse.declinereason, - ext_paymentid: values.paymentResponse.paymentid.toString(), - successful: true, - response: values.paymentResponse, - }, - }, }); toggleModalVisible(); } catch (error) { console.error(error); + notification.open({ + type: "error", + message: t("payments.errors.inserting", { error: error.message }), + }); } finally { setLoading(false); } @@ -146,9 +150,16 @@ const CardPaymentModalComponent = ({ const handleIntelliPayCharge = async () => { setLoading(true); - try { - console.warn("*** Window.Intellipay", !!window.intellipay); + //Validate + try { + await form.validateFields(); + } catch (error) { + setLoading(false); + return; + } + + try { const response = await axios.post("/intellipay/lightbox_credentials", { bodyshop, refresh: !!window.intellipay, @@ -182,93 +193,175 @@ const CardPaymentModalComponent = ({ - - - { - refetch({ jobid: e }); - }} - /> - - + + {(fields, { add, remove, move }) => { + return ( + + {fields.map((field, index) => ( + + + + + + + + + + + + + + { + remove(field.name); + }} + /> + + + + ))} + + { + add(); + }} + style={{ width: "100%" }} + > + {t("general.actions.add")} + + + + ); + }} + - {/* Lighbox Input amount needs to be hidden */} - - - - {/* Lightbox payment response when it is completed */} - - + + prevValues.payments?.map((p) => p?.jobid).join() !== + curValues.payments?.map((p) => p?.jobid).join() + } + > + {() => { + console.log("Updating the owner info section."); + //If all of the job ids have been fileld in, then query and update the IP field. + const { payments } = form.getFieldsValue(); + if ( + payments?.length > 0 && + payments?.filter((p) => p?.jobid).length === payments?.length + ) { + console.log("**Calling refetch."); + refetch({ jobids: payments.map((p) => p.jobid) }); + } + console.log( + "Acc info", + data, + payments && data && data.jobs.length > 0 + ? data.jobs.map((j) => j.ro_number).join(", ") + : null + ); + return ( + <> + 0 + ? data.jobs.map((j) => j.ro_number).join(", ") + : null + } + hidden + /> + 0 + ? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea + : null + } + hidden + /> + > + ); + }} + + + prevValues.payments?.map((p) => p?.amount).join() !== + curValues.payments?.map((p) => p?.amount).join() + } + > + {() => { + const { payments } = form.getFieldsValue(); + const totalAmountToCharge = payments?.reduce((acc, val) => { + return acc + (val?.amount || 0); + }, 0); + + return ( + + + + 0)} + onClick={handleIntelliPayCharge} + > + {t("job_payments.buttons.proceedtopayment")} + + + ); + }} - - - - - - - - {t("job_payments.buttons.proceedtopayment")} - - {context?.balance && ( - - {context?.balance.toFormat()} - - )} - - + {/* Lightbox payment response when it is completed */} + + + diff --git a/client/src/components/card-payment-modal/card-payment-modal.container..jsx b/client/src/components/card-payment-modal/card-payment-modal.container..jsx index 547a0ab75..f33b7bc9e 100644 --- a/client/src/components/card-payment-modal/card-payment-modal.container..jsx +++ b/client/src/components/card-payment-modal/card-payment-modal.container..jsx @@ -43,7 +43,7 @@ function CardPaymentModalContainer({ {t("job_payments.buttons.goback")} , ]} - width="60%" + width="80%" destroyOnClose > diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index e2208d939..dcb6eff92 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -254,7 +254,7 @@ function Header({ onClick={() => { setCardPaymentContext({ actions: {}, - context: null, + context: {}, }); }} icon={} diff --git a/client/src/components/job-profile-data-warning/job-profile-data-warning.component.jsx b/client/src/components/job-profile-data-warning/job-profile-data-warning.component.jsx new file mode 100644 index 000000000..f0ba998fc --- /dev/null +++ b/client/src/components/job-profile-data-warning/job-profile-data-warning.component.jsx @@ -0,0 +1,18 @@ +import { Alert } from "antd"; +import React from "react"; +import { useTranslation } from "react-i18next"; + +export default function JobProfileDataWarning({ job }) { + const { t } = useTranslation(); + + let missingProfileInfo = + Object.keys(job.cieca_pft).length === 0 || + Object.keys(job.cieca_pfl).length === 0 || + Object.keys(job.materials).length === 0; + + if (missingProfileInfo) + return ( + + ); + return null; +} diff --git a/client/src/components/job-totals-table/job-totals.table.labor.component.jsx b/client/src/components/job-totals-table/job-totals.table.labor.component.jsx index fd79a6ce2..42cfe7c66 100644 --- a/client/src/components/job-totals-table/job-totals.table.labor.component.jsx +++ b/client/src/components/job-totals-table/job-totals.table.labor.component.jsx @@ -123,10 +123,10 @@ export default function JobTotalsTableLabor({ job }) { {t("jobs.labels.mapa")} {job.materials && - job.materials.mapa && - job.materials.mapa.cal_maxdlr !== undefined && + job.materials.MAPA && + job.materials.MAPA.cal_maxdlr !== undefined && t("jobs.labels.threshhold", { - amount: job.materials.mapa.cal_maxdlr, + amount: job.materials.MAPA.cal_maxdlr, })} @@ -147,10 +147,10 @@ export default function JobTotalsTableLabor({ job }) { {t("jobs.labels.mash")} {job.materials && - job.materials.mash && - job.materials.mash.cal_maxdlr !== undefined && + job.materials.MASH && + job.materials.MASH.cal_maxdlr !== undefined && t("jobs.labels.threshhold", { - amount: job.materials.mash.cal_maxdlr, + amount: job.materials.MASH.cal_maxdlr, })} diff --git a/client/src/components/job-totals-table/job-totals.table.totals.component.jsx b/client/src/components/job-totals-table/job-totals.table.totals.component.jsx index 16a94442b..8b0106c7a 100644 --- a/client/src/components/job-totals-table/job-totals.table.totals.component.jsx +++ b/client/src/components/job-totals-table/job-totals.table.totals.component.jsx @@ -28,26 +28,46 @@ export function JobTotalsTableTotals({ bodyshop, job }) { total: job.job_totals.totals.subtotal, bold: true, }, - { - key: t("jobs.labels.local_tax_amt"), - total: job.job_totals.totals.local_tax, - }, - { - key: t("jobs.labels.state_tax_amt"), - total: job.job_totals.totals.state_tax, - }, - ...(bodyshop.region_config === "CA_BC" + + ...(job.job_totals.totals.us_sales_tax_breakdown ? [ { - key: t("jobs.fields.ca_bc_pvrt"), - total: job.job_totals.additional.pvrt, + key: + bodyshop.md_responsibility_centers.taxes.tax_ty1?.tax_type1 || + "T1", + total: job.job_totals.totals.us_sales_tax_breakdown.ty1Tax, + }, + { + key: + bodyshop.md_responsibility_centers.taxes.tax_ty2?.tax_type2 || + "T2", + total: job.job_totals.totals.us_sales_tax_breakdown.ty2Tax, + }, + { + key: + bodyshop.md_responsibility_centers.taxes.tax_ty3?.tax_type3 || + "T3", + total: job.job_totals.totals.us_sales_tax_breakdown.ty3Tax, + }, + { + key: + bodyshop.md_responsibility_centers.taxes.tax_ty4?.tax_type4 || + "T4", + total: job.job_totals.totals.us_sales_tax_breakdown.ty4Tax, + }, + { + key: + bodyshop.md_responsibility_centers.taxes.tax_ty5?.tax_type5 || + "T5", + total: job.job_totals.totals.us_sales_tax_breakdown.ty5Tax, }, ] - : []), - { - key: t("jobs.labels.federal_tax_amt"), - total: job.job_totals.totals.federal_tax, - }, + : [ + { + key: t("jobs.labels.state_tax_amt"), + total: job.job_totals.totals.state_tax, + }, + ]), { key: t("jobs.labels.total_repairs"), total: job.job_totals.totals.total_repairs, @@ -57,10 +77,10 @@ export function JobTotalsTableTotals({ bodyshop, job }) { key: t("jobs.fields.ded_amt"), total: job.job_totals.totals.custPayable.deductible, }, - { - key: t("jobs.fields.federal_tax_payable"), - total: job.job_totals.totals.custPayable.federal_tax, - }, + // { + // key: t("jobs.fields.federal_tax_payable"), + // total: job.job_totals.totals.custPayable.federal_tax, + // }, { key: t("jobs.fields.other_amount_payable"), total: job.job_totals.totals.custPayable.other_customer_amount, @@ -81,7 +101,7 @@ export function JobTotalsTableTotals({ bodyshop, job }) { bold: true, }, ]; - }, [job.job_totals, t, bodyshop.region_config]); + }, [job.job_totals, t, bodyshop.md_responsibility_centers]); const columns = [ { diff --git a/client/src/components/jobs-available-table/jobs-available-table.container.jsx b/client/src/components/jobs-available-table/jobs-available-table.container.jsx index f2fdf600f..4de0b4ab1 100644 --- a/client/src/components/jobs-available-table/jobs-available-table.container.jsx +++ b/client/src/components/jobs-available-table/jobs-available-table.container.jsx @@ -6,7 +6,7 @@ import { useQuery, } from "@apollo/client"; import { useTreatments } from "@splitsoftware/splitio-react"; -import { Col, Row, notification } from "antd"; +import { Button, Col, Row, notification } from "antd"; import Axios from "axios"; import _ from "lodash"; import moment from "moment"; @@ -31,7 +31,6 @@ import { selectCurrentUser, } from "../../redux/user/user.selectors"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; -import confirmDialog from "../../utils/asyncConfirm"; import CriticalPartsScan from "../../utils/criticalPartsScan"; import AlertComponent from "../alert/alert.component"; import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component"; @@ -90,13 +89,14 @@ export function JobsAvailableContainer({ const modalSearchState = useState(""); //Import Scenario - const onOwnerFindModalOk = async () => { + const onOwnerFindModalOk = async (lazyData) => { logImEXEvent("job_import_new"); setOwnerModalVisible(false); setInsertLoading(true); - - const estData = replaceEmpty(estDataRaw.data.available_jobs_by_pk); + const estData = replaceEmpty( + lazyData?.available_jobs_by_pk || estDataRaw.data.available_jobs_by_pk + ); if (!(estData && estData.est_data)) { //We don't have the right data. Error! @@ -165,47 +165,46 @@ export function JobsAvailableContainer({ newJob.kmin = null; } - insertNewJob({ - variables: { - job: newJob, - }, - }) - .then((r) => { - Axios.post("/job/totalsssu", { - id: r.data.insert_jobs.returning[0].id, - }); + try { + const r = await insertNewJob({ + variables: { + job: newJob, + }, + }); + await Axios.post("/job/totalsssu", { + id: r.data.insert_jobs.returning[0].id, + }); - if (CriticalPartsScanning.treatment === "on") { - CriticalPartsScan(r.data.insert_jobs.returning[0].id); - } - notification["success"]({ - message: t("jobs.successes.created"), - onClick: () => { - history.push(`/manage/jobs/${r.data.insert_jobs.returning[0].id}`); - }, - }); - //Job has been inserted. Clean up the available jobs record. + if (CriticalPartsScanning.treatment === "on") { + CriticalPartsScan(r.data.insert_jobs.returning[0].id); + } + notification["success"]({ + message: t("jobs.successes.created"), + onClick: () => { + history.push(`/manage/jobs/${r.data.insert_jobs.returning[0].id}`); + }, + }); + //Job has been inserted. Clean up the available jobs record. - insertAuditTrail({ - jobid: r.data.insert_jobs.returning[0].id, - operation: AuditTrailMapping.jobimported(), - }); + insertAuditTrail({ + jobid: r.data.insert_jobs.returning[0].id, + operation: AuditTrailMapping.jobimported(), + }); - deleteJob({ - variables: { id: estData.id }, - }).then((r) => { - refetch(); - setInsertLoading(false); - }); - }) - .catch((r) => { - //error while inserting - notification["error"]({ - message: t("jobs.errors.creating", { error: r.message }), - }); + await deleteJob({ + variables: { id: estData.id }, + }).then((r) => { refetch(); setInsertLoading(false); }); + } catch (r) { + //error while inserting + notification["error"]({ + message: t("jobs.errors.creating", { error: r.message }), + }); + refetch(); + setInsertLoading(false); + } }; //Suplement scenario @@ -392,6 +391,25 @@ export function JobsAvailableContainer({ onCancel={onJobModalCancel} modalSearchState={modalSearchState} /> + {currentUser.email.includes("@rome.") || + currentUser.email.includes("@imex.") ? ( + { + for (const record of data.available_jobs) { + //Query the data + console.log("Start Job", record.id); + const { data } = await loadEstData({ + variables: { id: record.id }, + }); + console.log("Query has been awaited and is complete"); + await onOwnerFindModalOk(data); + } + }} + > + Add all jobs as new. + + ) : null} + { if (line.misc_amt && line.misc_amt !== 0) { - line.act_price = line.misc_amt; - line.part_type = "PAS"; + line.act_price = line.act_price + line.misc_amt; line.tax_part = !!line.misc_tax; } }); diff --git a/client/src/components/jobs-close-auto-allocate/jobs-close-auto-allocate.component.jsx b/client/src/components/jobs-close-auto-allocate/jobs-close-auto-allocate.component.jsx index b9d787433..c315cf009 100644 --- a/client/src/components/jobs-close-auto-allocate/jobs-close-auto-allocate.component.jsx +++ b/client/src/components/jobs-close-auto-allocate/jobs-close-auto-allocate.component.jsx @@ -36,6 +36,8 @@ export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) { ret.profitcenter_part = defaults.profits["MAPA"]; } else if (lineDesc.includes("ats amount")) { ret.profitcenter_part = defaults.profits["ATS"]; + } else if (jl.act_price > 0) { + ret.profitcenter_part = defaults.profits["PAO"]; } else { ret.profitcenter_part = null; } diff --git a/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx b/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx index 795e60110..8bbda03e1 100644 --- a/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx +++ b/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx @@ -192,7 +192,7 @@ export function JobsCloseExportButton({ }); } } - if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { + if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) { notification.open({ type: "success", key: "jobsuccessexport", diff --git a/client/src/components/jobs-create-jobs-info/jobs-create-jobs-info.component.jsx b/client/src/components/jobs-create-jobs-info/jobs-create-jobs-info.component.jsx index 2bf230b27..31fa53a23 100644 --- a/client/src/components/jobs-create-jobs-info/jobs-create-jobs-info.component.jsx +++ b/client/src/components/jobs-create-jobs-info/jobs-create-jobs-info.component.jsx @@ -1,4 +1,4 @@ -import { Collapse, Form, Input, InputNumber, Select, Switch } from "antd"; +import { Collapse, Form, Input, Select, Switch } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -12,6 +12,12 @@ import FormItemPhone, { } from "../form-items-formatted/phone-form-item.component"; import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component"; import JobsDetailRatesParts from "../jobs-detail-rates/jobs-detail-rates.parts.component"; + +import JobsDetailRatesLabor from "../jobs-detail-rates/jobs-detail-rates.labor.component"; +import JobsDetailRatesMaterials from "../jobs-detail-rates/jobs-detail-rates.materials.component"; +import JobsDetailRatesOther from "../jobs-detail-rates/jobs-detail-rates.other.component"; +import JobsDetailRatesTaxes from "../jobs-detail-rates/jobs-detail-rates.taxes.component"; + import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; const mapStateToProps = createStructuredSelector({ @@ -258,26 +264,28 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) { - - - - - - - - - - - + { + // + // + // + // + // + // + // + // + // + // + // + } @@ -356,6 +364,10 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) { required={selected && true} form={form} /> + + + + ); } diff --git a/client/src/components/jobs-detail-rates/jobs-detail-rates.component.jsx b/client/src/components/jobs-detail-rates/jobs-detail-rates.component.jsx index dab973146..d66b30b41 100644 --- a/client/src/components/jobs-detail-rates/jobs-detail-rates.component.jsx +++ b/client/src/components/jobs-detail-rates/jobs-detail-rates.component.jsx @@ -1,25 +1,21 @@ -import { - Divider, - Form, - Input, - InputNumber, - Select, - Space, - Switch, - Tooltip, -} from "antd"; +import { Divider, Form, Input, Select, Space, Switch, Tooltip } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; -import CABCpvrtCalculator from "../ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component"; + import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component"; import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component"; import FormRow from "../layout-form-row/layout-form-row.component"; +import JobsDetailRatesLabor from "./jobs-detail-rates.labor.component"; +import JobsDetailRatesMaterials from "./jobs-detail-rates.materials.component"; +import JobsDetailRatesOther from "./jobs-detail-rates.other.component"; import JobsDetailRatesParts from "./jobs-detail-rates.parts.component"; +import JobsDetailRatesTaxes from "./jobs-detail-rates.taxes.component"; +import JobsDetailRatesProfileOVerride from "./jobs-detail-rates.profile-override.component"; const mapStateToProps = createStructuredSelector({ jobRO: selectJobReadOnly, @@ -84,14 +80,7 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) { > - {bodyshop.region_config === "CA_BC" && ( - - - - - - - )} + - - - - - - - - - - - {bodyshop.region_config.toLowerCase().startsWith("ca") && ( - - - - )} - + + Tax Profile + + + + + + + ); } diff --git a/client/src/components/jobs-detail-rates/jobs-detail-rates.labor.component.jsx b/client/src/components/jobs-detail-rates/jobs-detail-rates.labor.component.jsx new file mode 100644 index 000000000..75e80d4a1 --- /dev/null +++ b/client/src/components/jobs-detail-rates/jobs-detail-rates.labor.component.jsx @@ -0,0 +1,427 @@ +import { Collapse, Form, Switch } from "antd"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectJobReadOnly } from "../../redux/application/application.selectors"; +import LayoutFormRow from "../layout-form-row/layout-form-row.component"; +const mapStateToProps = createStructuredSelector({ + jobRO: selectJobReadOnly, +}); + +export function JobsDetailRatesLabor({ + jobRO, + expanded, + required = true, + form, +}) { + const { t } = useTranslation(); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} +export default connect(mapStateToProps, null)(JobsDetailRatesLabor); diff --git a/client/src/components/jobs-detail-rates/jobs-detail-rates.materials.component.jsx b/client/src/components/jobs-detail-rates/jobs-detail-rates.materials.component.jsx new file mode 100644 index 000000000..d9e641531 --- /dev/null +++ b/client/src/components/jobs-detail-rates/jobs-detail-rates.materials.component.jsx @@ -0,0 +1,145 @@ +import { Collapse, Form, Input, InputNumber, Switch } from "antd"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectJobReadOnly } from "../../redux/application/application.selectors"; +import LayoutFormRow from "../layout-form-row/layout-form-row.component"; +const mapStateToProps = createStructuredSelector({ + jobRO: selectJobReadOnly, +}); + +export function JobsDetailRatesMaterials({ + jobRO, + expanded, + required = true, + form, +}) { + const { t } = useTranslation(); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} +export default connect(mapStateToProps, null)(JobsDetailRatesMaterials); diff --git a/client/src/components/jobs-detail-rates/jobs-detail-rates.other.component.jsx b/client/src/components/jobs-detail-rates/jobs-detail-rates.other.component.jsx new file mode 100644 index 000000000..9d52c75bc --- /dev/null +++ b/client/src/components/jobs-detail-rates/jobs-detail-rates.other.component.jsx @@ -0,0 +1,104 @@ +import { Collapse, Form, Switch } from "antd"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectJobReadOnly } from "../../redux/application/application.selectors"; +import LayoutFormRow from "../layout-form-row/layout-form-row.component"; +const mapStateToProps = createStructuredSelector({ + jobRO: selectJobReadOnly, +}); + +export function JobsDetailRatesOther({ + jobRO, + expanded, + required = true, + form, +}) { + const { t } = useTranslation(); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} +export default connect(mapStateToProps, null)(JobsDetailRatesOther); diff --git a/client/src/components/jobs-detail-rates/jobs-detail-rates.parts.component.jsx b/client/src/components/jobs-detail-rates/jobs-detail-rates.parts.component.jsx index fcd56740d..2cd2b8917 100644 --- a/client/src/components/jobs-detail-rates/jobs-detail-rates.parts.component.jsx +++ b/client/src/components/jobs-detail-rates/jobs-detail-rates.parts.component.jsx @@ -68,11 +68,51 @@ export function JobsDetailRatesParts({ }, ]} > - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + - + ); }} + + + + + + + + + + + + + + + ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(JobsDetailRatesProfileOVerride); + +export function JobsDetailRatesProfileOVerride({ bodyshop, form }) { + const { t } = useTranslation(); + return ( + { + form.setFieldsValue({ + cieca_pft: { + ...bodyshop.md_responsibility_centers.taxes.tax_ty1, + ...bodyshop.md_responsibility_centers.taxes.tax_ty2, + ...bodyshop.md_responsibility_centers.taxes.tax_ty3, + ...bodyshop.md_responsibility_centers.taxes.tax_ty4, + ...bodyshop.md_responsibility_centers.taxes.tax_ty5, + }, + materials: bodyshop.md_responsibility_centers.cieca_pfm, + cieca_pfl: bodyshop.md_responsibility_centers.cieca_pfl, + parts_tax_rates: bodyshop.md_responsibility_centers.parts_tax_rates, + }); + }} + title={t("jobs.actions.taxprofileoverride_confirm")} + > + {t("jobs.actions.taxprofileoverride")} + + ); +} diff --git a/client/src/components/jobs-detail-rates/jobs-detail-rates.taxes.component.jsx b/client/src/components/jobs-detail-rates/jobs-detail-rates.taxes.component.jsx new file mode 100644 index 000000000..31d5b7213 --- /dev/null +++ b/client/src/components/jobs-detail-rates/jobs-detail-rates.taxes.component.jsx @@ -0,0 +1,155 @@ +import { Collapse, Divider, Form, Input, InputNumber, Space } from "antd"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectJobReadOnly } from "../../redux/application/application.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +const mapStateToProps = createStructuredSelector({ + jobRO: selectJobReadOnly, + bodyshop: selectBodyshop, +}); + +export function JobsDetailRatesTaxes({ + jobRO, + expanded, + bodyshop, + required = true, + form, +}) { + const { t } = useTranslation(); + const formItems = []; + for (let tyCounter = 1; tyCounter <= 5; tyCounter++) { + const section = []; + + section.push( + TaxFormItems({ + typeNum: tyCounter, + rootElements: true, + bodyshop, + jobRO, + }) + ); + + for (let iterator = 1; iterator <= 5; iterator++) { + section.push( + TaxFormItems({ + typeNum: tyCounter, + typeNumIterator: iterator, + rootElements: false, + jobRO, + }) + ); + } + formItems.push(Space({ children: section, wrap: true })); + formItems.push(); + } + return ( + + + {formItems} + + + ); +} +export default connect(mapStateToProps, null)(JobsDetailRatesTaxes); + +function TaxFormItems({ + typeNum, + typeNumIterator, + rootElements, + bodyshopjobRO, + jobRO, +}) { + const { t } = useTranslation(); + + if (rootElements) + return ( + <> + + + + > + ); + + return ( + <> + + + + + + + + + + + + + > + ); +} diff --git a/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx b/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx index 15fdb32b4..3204d7311 100644 --- a/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx +++ b/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx @@ -190,7 +190,7 @@ export function JobsExportAllButton({ }); } } - if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { + if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) { notification.open({ type: "success", key: "jobsuccessexport", diff --git a/client/src/components/payable-export-all-button/payable-export-all-button.component.jsx b/client/src/components/payable-export-all-button/payable-export-all-button.component.jsx index 723fd0526..c448e4f4a 100644 --- a/client/src/components/payable-export-all-button/payable-export-all-button.component.jsx +++ b/client/src/components/payable-export-all-button/payable-export-all-button.component.jsx @@ -192,7 +192,7 @@ export function PayableExportAll({ }); } } - if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { + if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) { notification.open({ type: "success", key: "billsuccessexport", diff --git a/client/src/components/payable-export-button/payable-export-button.component.jsx b/client/src/components/payable-export-button/payable-export-button.component.jsx index 1836acb18..c2970b8d8 100644 --- a/client/src/components/payable-export-button/payable-export-button.component.jsx +++ b/client/src/components/payable-export-button/payable-export-button.component.jsx @@ -185,7 +185,7 @@ export function PayableExportButton({ }); } } - if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { + if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) { notification.open({ type: "success", key: "billsuccessexport", diff --git a/client/src/components/payment-expanded-row/payment-expanded-row.component.jsx b/client/src/components/payment-expanded-row/payment-expanded-row.component.jsx index 6266457a7..a150eee78 100644 --- a/client/src/components/payment-expanded-row/payment-expanded-row.component.jsx +++ b/client/src/components/payment-expanded-row/payment-expanded-row.component.jsx @@ -1,15 +1,24 @@ -import React, { useState } from "react"; import { useMutation, useQuery } from "@apollo/client"; +import { + Button, + Descriptions, + InputNumber, + Modal, + Space, + notification, +} from "antd"; +import axios from "axios"; +import moment from "moment"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; import { GET_REFUNDABLE_AMOUNT_BY_JOBID, INSERT_PAYMENT_RESPONSE, QUERY_PAYMENT_RESPONSE_BY_PAYMENT_ID, } from "../../graphql/payment_response.queries"; -import { Button, Descriptions, InputNumber, Modal, notification } from "antd"; -import moment from "moment"; -import axios from "axios"; import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries"; -import { useTranslation } from "react-i18next"; +import CurrencyFormatter from "../../utils/CurrencyFormatter"; +import { DateTimeFormatter } from "../../utils/DateFormatter"; const { confirm } = Modal; @@ -137,10 +146,10 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => { {payment_response?.response?.nameOnCard ?? ""} - {record.amount} + {record.amount} - {moment(record.created_at).format("YYYY-MM-DD HH:mm:ss")} + {{record.created_at}} {record.transactionid} @@ -151,17 +160,22 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => { {record.type} + + {record.paymentnum} + {payment_response && ( - + + - showConfirm(payment_response)}> - {t("job_payments.buttons.refundpayment")} - + showConfirm(payment_response)}> + {t("job_payments.buttons.refundpayment")} + + )} diff --git a/client/src/components/payment-export-button/payment-export-button.component.jsx b/client/src/components/payment-export-button/payment-export-button.component.jsx index 91f9ccc88..931254b92 100644 --- a/client/src/components/payment-export-button/payment-export-button.component.jsx +++ b/client/src/components/payment-export-button/payment-export-button.component.jsx @@ -191,7 +191,7 @@ export function PaymentExportButton({ }); } } - if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { + if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) { notification.open({ type: "success", key: "paymentsuccessexport", diff --git a/client/src/components/payments-export-all-button/payments-export-all-button.component.jsx b/client/src/components/payments-export-all-button/payments-export-all-button.component.jsx index 721e906d5..0b2e6cd7b 100644 --- a/client/src/components/payments-export-all-button/payments-export-all-button.component.jsx +++ b/client/src/components/payments-export-all-button/payments-export-all-button.component.jsx @@ -179,7 +179,7 @@ export function PaymentsExportAllButton({ }); } } - if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { + if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) { notification.open({ type: "success", key: "paymentsuccessexport", diff --git a/client/src/components/shop-info/shop-info.general.component.jsx b/client/src/components/shop-info/shop-info.general.component.jsx index e37042128..712c47b05 100644 --- a/client/src/components/shop-info/shop-info.general.component.jsx +++ b/client/src/components/shop-info/shop-info.general.component.jsx @@ -188,20 +188,22 @@ export function ShopInfoGeneral({ form, bodyshop }) { - - {() => ( - - - - )} - + { + // + // {() => ( + // + // + // + // )} + // + } - - - + { + // + // + // + } - - - + { + // + // + // + } - - - - - {/* - - */} - {/* - - */} - - - - - - - {(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && ( - - - - )} - - - - + { + // + // + // + // + // {/* + // + // */} + // {/* + // + // */} + // + // + // + // + // + // + // {(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && ( + // + // + // + // )} + // + // + // + // + } {DmsAp.treatment === "on" && ( )} - - - - - {/* - - - - - */} - - - - - - - {(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && ( - - - - )} - - - - + { + // + // + // + // + // {/* + // + // + // + // + // */} + // + // + // + // + // + // + // {(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && ( + // + // + // + // )} + // + // + // + // + } + - - - - - {/* - - - - - */} - - - - - - - {(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && ( - - - - )} - - - - + { + // + // + // + // + // {/* + // + // + // + // + // */} + // + // + // + // + // + // + // {(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && ( + // + // + // + // )} + // + // + // + // + } AR} id="AR"> {/* ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(ShopInfoResponsibilityCenters); + +export function ShopInfoResponsibilityCenters({ bodyshop, form }) { + const { t } = useTranslation(); + //Iteratively build the form items. + const formItems = []; + for (let tyCounter = 1; tyCounter <= 5; tyCounter++) { + const section = []; + + section.push( + TaxFormItems({ + typeNum: tyCounter, + rootElements: true, + bodyshop, + }) + ); + for (let iterator = 1; iterator <= 5; iterator++) { + section.push( + TaxFormItems({ + typeNum: tyCounter, + typeNumIterator: iterator, + rootElements: false, + }) + ); + } + formItems.push(Space({ children: section, wrap: true })); + formItems.push(); + } + return ( + <> + + {t("jobs.labels.cieca_pft")} + + {formItems} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {() => { + return ( + + + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {() => { + return ( + + + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {() => { + return ( + + + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {() => { + return ( + + + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {() => { + return ( + + + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {() => { + return ( + + + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {() => { + return ( + + + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {() => { + return ( + + + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {() => { + return ( + + + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {() => { + return ( + + + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {() => { + return ( + + + + ); + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + > + ); +} + +function TaxFormItems({ typeNum, typeNumIterator, rootElements, bodyshop }) { + const { t } = useTranslation(); + + if (rootElements) + return ( + <> + + + + + + + + + + + + + + + {(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && ( + + + + )} + > + ); + return ( + <> + + + + + + + + + + + + + > + ); +} diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index e59083a3a..4d656858f 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -574,7 +574,6 @@ export const GET_JOB_BY_PK = gql` est_co_nm est_ct_fn est_ct_ln - est_ph1 est_ea selling_dealer @@ -756,6 +755,7 @@ export const GET_JOB_BY_PK = gql` jobid amount payer + paymentnum created_at transactionid memo @@ -779,6 +779,10 @@ export const GET_JOB_BY_PK = gql` } } cieca_ttl + cieca_pfo + cieca_pfl + cieca_pft + materials csiinvites { id completedon diff --git a/client/src/graphql/payment_response.queries.js b/client/src/graphql/payment_response.queries.js index 8722e6cd8..a23d00dbb 100644 --- a/client/src/graphql/payment_response.queries.js +++ b/client/src/graphql/payment_response.queries.js @@ -28,16 +28,14 @@ export const QUERY_PAYMENT_RESPONSE_BY_PAYMENT_ID = gql` } `; -export const QUERY_RO_AND_OWNER_BY_JOB_PK = gql` - query QUERY_RO_AND_OWNER_BY_JOB_PK($jobid: uuid!) { - jobs_by_pk(id: $jobid) { +export const QUERY_RO_AND_OWNER_BY_JOB_PKS = gql` + query QUERY_RO_AND_OWNER_BY_JOB_PKS($jobids: [uuid!]!) { + jobs(where: { id: { _in: $jobids } }) { ro_number - owner { - ownr_fn - ownr_ln - ownr_ea - ownr_zip - } + ownr_fn + ownr_ln + ownr_ea + ownr_zip } } `; diff --git a/client/src/pages/dms/dms.container.jsx b/client/src/pages/dms/dms.container.jsx index 77c670e60..3a700a89e 100644 --- a/client/src/pages/dms/dms.container.jsx +++ b/client/src/pages/dms/dms.container.jsx @@ -43,10 +43,10 @@ const mapDispatchToProps = (dispatch) => ({ 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, - // "http://localhost:4000", // for dev testing, + // process.env.NODE_ENV === "production" + // ? process.env.REACT_APP_AXIOS_BASE_API_URL + // : window.location.origin, + "http://localhost:4000", // for dev testing, { path: "/ws", withCredentials: true, diff --git a/client/src/pages/jobs-create/jobs-create.container.jsx b/client/src/pages/jobs-create/jobs-create.container.jsx index 15af524ed..08f0b294f 100644 --- a/client/src/pages/jobs-create/jobs-create.container.jsx +++ b/client/src/pages/jobs-create/jobs-create.container.jsx @@ -169,96 +169,16 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate / 100, state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate / 100, local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate / 100, - parts_tax_rates: { - PAA: { - prt_type: "PAA", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - PAC: { - prt_type: "PAC", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - PAG: { - prt_type: "PAG", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - PAL: { - prt_type: "PAL", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - PAM: { - prt_type: "PAM", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - PAN: { - prt_type: "PAN", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - PAR: { - prt_type: "PAR", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - PAS: { - prt_type: "PAS", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - PASL: { - prt_type: "PASL", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - PAP: { - prt_type: "PAP", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, - PAO: { - prt_type: "PAO", - prt_discp: 0, - prt_mktyp: false, - prt_mkupp: 0, - prt_tax_in: true, - prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100, - }, + cieca_pft: { + ...bodyshop.md_responsibility_centers.taxes.tax_ty1, + ...bodyshop.md_responsibility_centers.taxes.tax_ty2, + ...bodyshop.md_responsibility_centers.taxes.tax_ty3, + ...bodyshop.md_responsibility_centers.taxes.tax_ty4, + ...bodyshop.md_responsibility_centers.taxes.tax_ty5, }, + materials: bodyshop.md_responsibility_centers.cieca_pfm, + cieca_pfl: bodyshop.md_responsibility_centers.cieca_pfl, + parts_tax_rates: bodyshop.md_responsibility_centers.parts_tax_rates, }} > diff --git a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx index c98e8b1a9..8eff287ff 100644 --- a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx +++ b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx @@ -53,6 +53,8 @@ import { insertAuditTrail } from "../../redux/application/application.actions"; import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container"; import UndefinedToNull from "../../utils/undefinedtonull"; import NoteUpsertModalComponent from "../../components/note-upsert-modal/note-upsert-modal.container"; +import _ from "lodash"; +import JobProfileDataWarning from "../../components/job-profile-data-warning/job-profile-data-warning.component"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -103,86 +105,121 @@ export function JobsDetailPage({ "category", "referral_source", ]), - parts_tax_rates: { - ...job.parts_tax_rates, - ...values.parts_tax_rates, - }, + // The union and spread is required to keep values coming in from the estimating system that aren't displayed. + parts_tax_rates: _.union( + Object.keys(job.parts_tax_rates), + Object.keys(values.parts_tax_rates) + ).reduce((acc, val) => { + acc[val] = { + ...job.parts_tax_rates[val], + ...values.parts_tax_rates[val], + }; + return acc; + }, {}), + materials: _.union( + Object.keys(job.materials), + Object.keys(values.materials) + ).reduce((acc, val) => { + acc[val] = { + ...job.materials[val], + ...values.materials[val], + }; + return acc; + }, {}), + cieca_pfl: _.union( + Object.keys(job.cieca_pfl), + Object.keys(values.cieca_pfl) + ).reduce((acc, val) => { + acc[val] = { + ...job.cieca_pfl[val], + ...values.cieca_pfl[val], + }; + return acc; + }, {}), + cieca_pfo: { ...job.cieca_pfo, ...values.cieca_pfo }, }, }, }); - const newTotals = await Axios.post("/job/totalsssu", { - id: job.id, - }); + try { + const newTotals = await Axios.post("/job/totalsssu", { + id: job.id, + }); - if (newTotals.status !== 200 || result.errors) { + if (newTotals.status !== 200 || result.errors) { + notification["error"]({ + message: t("jobs.errors.totalscalc"), + }); + } else { + notification["success"]({ + message: t("jobs.successes.savetitle"), + }); + const changedAuditFields = form.getFieldsValue( + [ + "scheduled_in", + "actual_in", + "scheduled_completion", + "actual_completion", + "scheduled_delivery", + "actual_delivery", + "date_invoiced", + "ins_co_nm", + "ded_amt", + "ded_status", + "date_exported", + "special_coverage_policy", + "ca_gst_registrant", + "ca_bc_pvrt", + "scheduled_in", + "rate_la1", + "rate_la2", + "rate_la3", + "rate_la4", + "rate_laa", + "rate_lab", + "rate_lad", + "rate_lae", + "rate_laf", + "rate_lag", + "rate_lam", + "rate_lar", + "rate_las", + "rate_lau", + "rate_ma2s", + "rate_ma2t", + "rate_ma3s", + "rate_mabl", + "rate_macs", + "rate_mapa", + "rate_mahw", + "rate_mash", + "rate_matd", + ], + (meta) => meta && meta.touched + ); + + Object.keys(changedAuditFields).forEach((key) => { + insertAuditTrail({ + jobid: job.id, + operation: AuditTrailMapping.jobfieldchange( + key, + changedAuditFields[key] instanceof moment + ? moment(changedAuditFields[key]).format("MM/DD/YYYY hh:mm a") + : changedAuditFields[key] + ), + }); + }); + + await refetch(); + form.setFieldsValue(transormJobToForm(job)); + form.resetFields(); + } + } catch (error) { notification["error"]({ message: t("jobs.errors.totalscalc"), }); - } else { - notification["success"]({ - message: t("jobs.successes.savetitle"), - }); - const changedAuditFields = form.getFieldsValue( - [ - "scheduled_in", - "actual_in", - "scheduled_completion", - "actual_completion", - "scheduled_delivery", - "actual_delivery", - "date_invoiced", - "ins_co_nm", - "ded_amt", - "ded_status", - "date_exported", - "special_coverage_policy", - "ca_gst_registrant", - "ca_bc_pvrt", - "scheduled_in", - "rate_la1", - "rate_la2", - "rate_la3", - "rate_la4", - "rate_laa", - "rate_lab", - "rate_lad", - "rate_lae", - "rate_laf", - "rate_lag", - "rate_lam", - "rate_lar", - "rate_las", - "rate_lau", - "rate_ma2s", - "rate_ma2t", - "rate_ma3s", - "rate_mabl", - "rate_macs", - "rate_mapa", - "rate_mahw", - "rate_mash", - "rate_matd", - ], - (meta) => meta && meta.touched - ); - - Object.keys(changedAuditFields).forEach((key) => { - insertAuditTrail({ - jobid: job.id, - operation: AuditTrailMapping.jobfieldchange( - key, - changedAuditFields[key] instanceof moment - ? moment(changedAuditFields[key]).format("MM/DD/YYYY hh:mm a") - : changedAuditFields[key] - ), - }); - }); - - await refetch(); - form.setFieldsValue(transormJobToForm(job)); - form.resetFields(); + } finally { + setLoading(false); } - setLoading(false); }; const menuExtra = ( @@ -252,6 +289,7 @@ export function JobsDetailPage({ /> + { + Object.keys(job.parts_tax_rates).forEach((parttype) => { + Object.keys(job.parts_tax_rates[parttype]).forEach((key) => { + if (key.includes("tx_in")) { + if ( + job.parts_tax_rates[parttype][key] === "Y" || + job.parts_tax_rates[parttype][key] === true + ) { + job.parts_tax_rates[parttype][key] = true; + } else { + job.parts_tax_rates[parttype][key] = false; + } + } + }); + }); + return { ...job, loss_date: job.loss_date ? moment(job.loss_date) : null, diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index b2a0bd895..ecd1e183e 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -473,6 +473,11 @@ "responsibilitycenter_accountname": "Account Name", "responsibilitycenter_accountnumber": "Account Number", "responsibilitycenter_rate": "Rate", + "responsibilitycenter_tax_rate": "Tax {{typeNum}} Tier {{typeNumIterator}} Rate", + "responsibilitycenter_tax_sur": "Tax {{typeNum}} Tier {{typeNumIterator}} Surcharge", + "responsibilitycenter_tax_thres": "Tax {{typeNum}} Tier {{typeNumIterator}} Threshold", + "responsibilitycenter_tax_tier": "Tax {{typeNum}} Tier {{typeNumIterator}}", + "responsibilitycenter_tax_type": "Tax {{typeNum}} Type", "responsibilitycenters": { "ap": "Accounts Payable", "ar": "Accounts Receivable", @@ -613,8 +618,8 @@ "jobstatuses": "Job Statuses", "laborrates": "Labor Rates", "licensing": "Licensing", - "md_tasks_presets": "Tasks Presets", "md_parts_scan": "Parts Scan Rules", + "md_tasks_presets": "Tasks Presets", "md_to_emails": "Preset To Emails", "md_to_emails_emails": "Emails", "messagingpresets": "Messaging Presets", @@ -1217,6 +1222,7 @@ "payer": "Payer", "payername": "Payer Name", "paymentid": "Payment Reference ID", + "paymentnum": "Payment Number", "paymenttype": "Payment Type", "refundamount": "Refund Amount", "transactionid": "Transaction ID" @@ -1367,6 +1373,8 @@ "sendpartspricechange": "Send Parts Price Change", "sendtodms": "Send to DMS", "sync": "Sync", + "taxprofileoverride": "Override Tax Profile with Shop Configuration", + "taxprofileoverride_confirm": "Are you sure you want to override the tax profile information? This cannot be undone without re-importing the job. ", "uninvoice": "Uninvoice", "unvoid": "Unvoid Job", "viewchecklist": "View Checklists", @@ -1438,6 +1446,26 @@ "ccf": "CC Refuel", "ccm": "CC Mileage", "cieca_id": "CIECA ID", + "cieca_pfl": { + "lbr_tax_in": "Tax Labor Indicator", + "lbr_tx_in1": "Tax 1 Indicator", + "lbr_tx_in2": "Tax 2 Indicator", + "lbr_tx_in3": "Tax 3 Indicator", + "lbr_tx_in4": "Tax 4 Indicator", + "lbr_tx_in5": "Tax 5 Indicator" + }, + "cieca_pfo": { + "stor_t_in1": "Storage Tax 1 Indicator", + "stor_t_in2": "Storage Tax 2 Indicator", + "stor_t_in3": "Storage Tax 3 Indicator", + "stor_t_in4": "Storage Tax 4 Indicator", + "stor_t_in5": "Storage Tax 5 Indicator", + "tow_t_in1": "Tow Tax 1 Indicator", + "tow_t_in2": "Tow Tax 2 Indicator", + "tow_t_in3": "Tow Tax 3 Indicator", + "tow_t_in4": "Tow Tax 4 Indicator", + "tow_t_in5": "Tow Tax 5 Indicator" + }, "claim_total": "Claim Total", "class": "Class", "clm_no": "Claim #", @@ -1551,6 +1579,19 @@ "mapa": "Paint Materials", "mash": "Shop Materials", "matd": "Tire Disposal", + "materials": { + "MAPA": "Paint Materials", + "MASH": "Shop Materials", + "cal_maxdlr": "Threshhold", + "cal_opcode": "OP Codes", + "mat_tx_in1": "Tax 1 Indicator", + "mat_tx_in2": "Tax 2 Indicator", + "mat_tx_in3": "Tax 3 Indicator", + "mat_tx_in4": "Tax 4 Indicator", + "mat_tx_in5": "Tax 5 Indicator", + "materials": "Profile - Materials", + "tax_ind": "Tax Indicator" + }, "other_amount_payable": "Other Amount Payable", "owner": "Owner", "owner_owing": "Cust. Owes", @@ -1573,6 +1614,11 @@ "prt_mkupp": "Markup %", "prt_tax_in": "Tax Indicator", "prt_tax_rt": "Part Tax Rate", + "prt_tx_in1": "Tax 1 Indicator", + "prt_tx_in2": "Tax 2 Indicator", + "prt_tx_in3": "Tax 3 Indicator", + "prt_tx_in4": "Tax 4 Indicator", + "prt_tx_in5": "Tax 5 Indicator", "prt_type": "Part Type" }, "partsstatus": "Parts Status", @@ -1700,6 +1746,9 @@ "checklistcompletedby": "Checklist completed by {{by}} at {{at}}", "checklistdocuments": "Checklist Documents", "checklists": "Checklists", + "cieca_pfl": "Profile - Labor", + "cieca_pfo": "Profile - Other", + "cieca_pft": "Profile - Taxes", "closeconfirm": "Are you sure you want to close this job? This cannot be easily undone.", "closejob": "Close Job {{ro_number}}", "closingperiod": "This Invoice Date is outside of the Closing Period.", @@ -1772,6 +1821,10 @@ "mapa": "Paint Materials", "markforreexport": "Mark for Re-export", "mash": "Shop Materials", + "materials": { + "mapa": "" + }, + "missingprofileinfo": "This job has missing tax profile info. To ensure correct totals calculations, re-import the job.", "multipayers": "Additional Payers", "net_repairs": "Net Repairs", "notes": "Notes", @@ -1798,7 +1851,7 @@ "totalreturns": "The total retail amount of returns created for this job." }, "ppc": "This line contains a part price change.", - "profileadjustments": "Profile Disc./Mkup (Already included above)", + "profileadjustments": "Profile Disc./Mkup", "prt_dsmk_total": "Line Item Adjustment", "rates": "Rates", "rates_subtotal": "All Rates Subtotal", @@ -2240,7 +2293,8 @@ }, "errors": { "exporting": "Error exporting payment(s). {{error}}", - "exporting-partner": "Error exporting to partner. Please check the partner interaction log for more errors." + "exporting-partner": "Error exporting to partner. Please check the partner interaction log for more errors.", + "inserting": "Error inserting payment. {{error}}" }, "fields": { "amount": "Amount", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index f7eb47cb8..6ca073170 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -63,6 +63,7 @@ "scheduledfor": "Cita programada para:", "severalerrorsfound": "", "smartscheduling": "", + "smspaymentreminder": "", "suggesteddates": "" }, "successes": { @@ -472,6 +473,11 @@ "responsibilitycenter_accountname": "", "responsibilitycenter_accountnumber": "", "responsibilitycenter_rate": "", + "responsibilitycenter_tax_rate": "", + "responsibilitycenter_tax_sur": "", + "responsibilitycenter_tax_thres": "", + "responsibilitycenter_tax_tier": "", + "responsibilitycenter_tax_type": "", "responsibilitycenters": { "ap": "", "ar": "", @@ -1216,6 +1222,7 @@ "payer": "", "payername": "", "paymentid": "", + "paymentnum": "", "paymenttype": "", "refundamount": "", "transactionid": "" @@ -1366,6 +1373,8 @@ "sendpartspricechange": "", "sendtodms": "", "sync": "", + "taxprofileoverride": "", + "taxprofileoverride_confirm": "", "uninvoice": "", "unvoid": "", "viewchecklist": "", @@ -1437,6 +1446,26 @@ "ccf": "", "ccm": "", "cieca_id": "CIECA ID", + "cieca_pfl": { + "lbr_tax_in": "", + "lbr_tx_in1": "", + "lbr_tx_in2": "", + "lbr_tx_in3": "", + "lbr_tx_in4": "", + "lbr_tx_in5": "" + }, + "cieca_pfo": { + "stor_t_in1": "", + "stor_t_in2": "", + "stor_t_in3": "", + "stor_t_in4": "", + "stor_t_in5": "", + "tow_t_in1": "", + "tow_t_in2": "", + "tow_t_in3": "", + "tow_t_in4": "", + "tow_t_in5": "" + }, "claim_total": "Reclamar total", "class": "", "clm_no": "Reclamación #", @@ -1550,6 +1579,19 @@ "mapa": "", "mash": "", "matd": "", + "materials": { + "MAPA": "", + "MASH": "", + "cal_maxdlr": "", + "cal_opcode": "", + "mat_tx_in1": "", + "mat_tx_in2": "", + "mat_tx_in3": "", + "mat_tx_in4": "", + "mat_tx_in5": "", + "materials": "", + "tax_ind": "" + }, "other_amount_payable": "Otra cantidad a pagar", "owner": "Propietario", "owner_owing": "Cust. Debe", @@ -1572,6 +1614,11 @@ "prt_mkupp": "", "prt_tax_in": "", "prt_tax_rt": "", + "prt_tx_in1": "", + "prt_tx_in2": "", + "prt_tx_in3": "", + "prt_tx_in4": "", + "prt_tx_in5": "", "prt_type": "" }, "partsstatus": "", @@ -1699,6 +1746,9 @@ "checklistcompletedby": "", "checklistdocuments": "", "checklists": "", + "cieca_pfl": "", + "cieca_pfo": "", + "cieca_pft": "", "closeconfirm": "", "closejob": "", "closingperiod": "", @@ -1771,6 +1821,10 @@ "mapa": "", "markforreexport": "", "mash": "", + "materials": { + "mapa": "" + }, + "missingprofileinfo": "", "multipayers": "", "net_repairs": "", "notes": "Notas", @@ -2239,7 +2293,8 @@ }, "errors": { "exporting": "", - "exporting-partner": "" + "exporting-partner": "", + "inserting": "" }, "fields": { "amount": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 95467a2eb..a65bef325 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -63,6 +63,7 @@ "scheduledfor": "Rendez-vous prévu pour:", "severalerrorsfound": "", "smartscheduling": "", + "smspaymentreminder": "", "suggesteddates": "" }, "successes": { @@ -472,6 +473,11 @@ "responsibilitycenter_accountname": "", "responsibilitycenter_accountnumber": "", "responsibilitycenter_rate": "", + "responsibilitycenter_tax_rate": "", + "responsibilitycenter_tax_sur": "", + "responsibilitycenter_tax_thres": "", + "responsibilitycenter_tax_tier": "", + "responsibilitycenter_tax_type": "", "responsibilitycenters": { "ap": "", "ar": "", @@ -1216,6 +1222,7 @@ "payer": "", "payername": "", "paymentid": "", + "paymentnum": "", "paymenttype": "", "refundamount": "", "transactionid": "" @@ -1366,6 +1373,8 @@ "sendpartspricechange": "", "sendtodms": "", "sync": "", + "taxprofileoverride": "", + "taxprofileoverride_confirm": "", "uninvoice": "", "unvoid": "", "viewchecklist": "", @@ -1437,6 +1446,26 @@ "ccf": "", "ccm": "", "cieca_id": "CIECA ID", + "cieca_pfl": { + "lbr_tax_in": "", + "lbr_tx_in1": "", + "lbr_tx_in2": "", + "lbr_tx_in3": "", + "lbr_tx_in4": "", + "lbr_tx_in5": "" + }, + "cieca_pfo": { + "stor_t_in1": "", + "stor_t_in2": "", + "stor_t_in3": "", + "stor_t_in4": "", + "stor_t_in5": "", + "tow_t_in1": "", + "tow_t_in2": "", + "tow_t_in3": "", + "tow_t_in4": "", + "tow_t_in5": "" + }, "claim_total": "Total réclamation", "class": "", "clm_no": "Prétendre #", @@ -1550,6 +1579,19 @@ "mapa": "", "mash": "", "matd": "", + "materials": { + "MAPA": "", + "MASH": "", + "cal_maxdlr": "", + "cal_opcode": "", + "mat_tx_in1": "", + "mat_tx_in2": "", + "mat_tx_in3": "", + "mat_tx_in4": "", + "mat_tx_in5": "", + "materials": "", + "tax_ind": "" + }, "other_amount_payable": "Autre montant à payer", "owner": "Propriétaire", "owner_owing": "Cust. Owes", @@ -1572,6 +1614,11 @@ "prt_mkupp": "", "prt_tax_in": "", "prt_tax_rt": "", + "prt_tx_in1": "", + "prt_tx_in2": "", + "prt_tx_in3": "", + "prt_tx_in4": "", + "prt_tx_in5": "", "prt_type": "" }, "partsstatus": "", @@ -1699,6 +1746,9 @@ "checklistcompletedby": "", "checklistdocuments": "", "checklists": "", + "cieca_pfl": "", + "cieca_pfo": "", + "cieca_pft": "", "closeconfirm": "", "closejob": "", "closingperiod": "", @@ -1771,6 +1821,10 @@ "mapa": "", "markforreexport": "", "mash": "", + "materials": { + "mapa": "" + }, + "missingprofileinfo": "", "multipayers": "", "net_repairs": "", "notes": "Remarques", @@ -2239,7 +2293,8 @@ }, "errors": { "exporting": "", - "exporting-partner": "" + "exporting-partner": "", + "inserting": "" }, "fields": { "amount": "", diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 7b3e5a4b7..9b19c1777 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -3274,6 +3274,8 @@ - cat_no - category - cieca_pfl + - cieca_pfo + - cieca_pft - cieca_stl - cieca_ttl - ciecaid @@ -3541,6 +3543,8 @@ - cat_no - category - cieca_pfl + - cieca_pfo + - cieca_pft - cieca_stl - cieca_ttl - ciecaid @@ -3819,6 +3823,8 @@ - cat_no - category - cieca_pfl + - cieca_pfo + - cieca_pft - cieca_stl - cieca_ttl - ciecaid diff --git a/hasura/migrations/1694457793769_alter_table_public_jobs_add_column_cieca_pft/down.sql b/hasura/migrations/1694457793769_alter_table_public_jobs_add_column_cieca_pft/down.sql new file mode 100644 index 000000000..2d4c20d18 --- /dev/null +++ b/hasura/migrations/1694457793769_alter_table_public_jobs_add_column_cieca_pft/down.sql @@ -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 "cieca_pft" jsonb +-- null default jsonb_build_object(); diff --git a/hasura/migrations/1694457793769_alter_table_public_jobs_add_column_cieca_pft/up.sql b/hasura/migrations/1694457793769_alter_table_public_jobs_add_column_cieca_pft/up.sql new file mode 100644 index 000000000..5baeecbb8 --- /dev/null +++ b/hasura/migrations/1694457793769_alter_table_public_jobs_add_column_cieca_pft/up.sql @@ -0,0 +1,2 @@ +alter table "public"."jobs" add column "cieca_pft" jsonb + null default jsonb_build_object(); diff --git a/hasura/migrations/1695757361064_alter_table_public_jobs_add_column_cieca_pfo/down.sql b/hasura/migrations/1695757361064_alter_table_public_jobs_add_column_cieca_pfo/down.sql new file mode 100644 index 000000000..31337afb8 --- /dev/null +++ b/hasura/migrations/1695757361064_alter_table_public_jobs_add_column_cieca_pfo/down.sql @@ -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 "cieca_pfo" jsonb +-- null default jsonb_build_object(); diff --git a/hasura/migrations/1695757361064_alter_table_public_jobs_add_column_cieca_pfo/up.sql b/hasura/migrations/1695757361064_alter_table_public_jobs_add_column_cieca_pfo/up.sql new file mode 100644 index 000000000..d21094fdd --- /dev/null +++ b/hasura/migrations/1695757361064_alter_table_public_jobs_add_column_cieca_pfo/up.sql @@ -0,0 +1,2 @@ +alter table "public"."jobs" add column "cieca_pfo" jsonb + null default jsonb_build_object(); diff --git a/job-totals-testing-util.js b/job-totals-testing-util.js index 9fd3550d4..7e6391a8a 100644 --- a/job-totals-testing-util.js +++ b/job-totals-testing-util.js @@ -19,8 +19,8 @@ require("dotenv").config({ }); async function RunTheTest() { - const bodyshopids = ["6c63a820-542c-497e-8c82-0cc38fb2bbca"]; - const bearerToken = `Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImM2MGI5ZGUwODBmZmFmYmZjMTgzMzllY2Q0NGFjNzdmN2ZhNGU4ZDMiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiUm9tZSBEZXZlbG9wbWVudCIsImh0dHBzOi8vaGFzdXJhLmlvL2p3dC9jbGFpbXMiOnsieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoiYWRtaW4iLCJ4LWhhc3VyYS1hbGxvd2VkLXJvbGVzIjpbImFkbWluIl0sIngtaGFzdXJhLXVzZXItaWQiOiJ0NlltMU5EbENET1BacjNGOWJndVdINExoU1gyIn0sImlvYWRtaW4iOnRydWUsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9yb21lLXByb2QtMSIsImF1ZCI6InJvbWUtcHJvZC0xIiwiYXV0aF90aW1lIjoxNjkyODk5ODE2LCJ1c2VyX2lkIjoidDZZbTFORGxDRE9QWnIzRjliZ3VXSDRMaFNYMiIsInN1YiI6InQ2WW0xTkRsQ0RPUFpyM0Y5Ymd1V0g0TGhTWDIiLCJpYXQiOjE2OTMyNTA1NjIsImV4cCI6MTY5MzI1NDE2MiwiZW1haWwiOiJwYXRyaWNrQHJvbWUuZGV2IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbInBhdHJpY2tAcm9tZS5kZXYiXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9fQ.POr8U2pP4XtTJEDRJ_BveRkCs92CIfDDdfU24OYe_aZh6LFPN0bQukNHXrLt3gaD30SUcg5mgmI2VUphgmwviMEGY-zizPC9o6GUKEEppZWQXfrfTyJNa1VKKH9h5zZPPFFW8UJRMi131pCc0ev26GS8Do-FJAgwHLJd6Jp2RbbqiCIeafNMhQCEoXohOk-VArNe7tPAb6-IjxqGVyNqvVyIo6niSXYvmgNjyF1WnnIw0CsnPoJlc5kVMtRdYeshJI7V117MOlUwZicF62vsm32eCunjn3qhN5XsujI7gy9us3vzwhdR1lxISZCLhLOXEYHPL373HJh7I_KN1C3NuA`; + const bodyshopids = ["52b7357c-0edd-4c95-85c3-dfdbcdfad9ac"]; + const bearerToken = `Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjlhNTE5MDc0NmU5M2JhZTI0OWIyYWE3YzJhYTRlMzA2M2UzNDFlYzciLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiUm9tZSBEZXZlbG9wbWVudCIsImh0dHBzOi8vaGFzdXJhLmlvL2p3dC9jbGFpbXMiOnsieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoidXNlciIsIngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsidXNlciJdLCJ4LWhhc3VyYS11c2VyLWlkIjoidDZZbTFORGxDRE9QWnIzRjliZ3VXSDRMaFNYMiJ9LCJpb2FkbWluIjp0cnVlLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vcm9tZS1wcm9kLTEiLCJhdWQiOiJyb21lLXByb2QtMSIsImF1dGhfdGltZSI6MTY5NTkxNDQ5NywidXNlcl9pZCI6InQ2WW0xTkRsQ0RPUFpyM0Y5Ymd1V0g0TGhTWDIiLCJzdWIiOiJ0NlltMU5EbENET1BacjNGOWJndVdINExoU1gyIiwiaWF0IjoxNjk2NjAzMTUwLCJleHAiOjE2OTY2MDY3NTAsImVtYWlsIjoicGF0cmlja0Byb21lLmRldiIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJlbWFpbCI6WyJwYXRyaWNrQHJvbWUuZGV2Il19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0.YYSEG1_Iwoqrelj0Fz5f04b78ABrueaFHVG1bBi-2c9kfkfrSiobgSs4jmYRlUHx1pRY58sFoNWvjci3cpFLwdaFSRAei5LwVFHllXlT8sMmWpxOMD4xU_fLRX9_hGM4SySlsBLAekytU5wCrtYF-BwEubYwPc7nkfi61BbaX1rBxVU3FAX123ToO7zN6VIzbTQRlrpBPBsCa3LWjhi1y-2V9vRsshOMMyezmKNMwknGvuoLwEeh9HYM4O0gDbtLYosFb5zsMRSPdrq4wjECge_psxF6QJ5p2JpKFAVyoYjK6lavM4QXZhTx05ssOj7pRz13NbYYX9of2pabhWjDSw`; const { jobs } = await client.request( gql` query GET_JOBS($bodyshopids: [uuid!]!) { @@ -42,7 +42,14 @@ async function RunTheTest() { const results = []; - for (const job of jobs) { + for (const [index, job] of jobs.entries()) { + process.stdout.cursorTo(0); + process.stdout.write( + `Processing job ${index + 1} of ${jobs.length}. Failed jobs: ${ + results.filter((r) => r.result !== "PASS").length + }` + ); + try { await axios.post( `http://localhost:4000/job/totalsssu`, @@ -61,6 +68,7 @@ async function RunTheTest() { ownr_ln ownr_co_nm ins_co_nm + comment } } `, @@ -73,19 +81,20 @@ async function RunTheTest() { id: newjob.id, owner: `${newjob.ownr_fn} ${newjob.ownr_ln} ${job.ownr_co_nm || ""}`, ins_co: newjob.ins_co_nm, + comment: newjob.comment, }; const calcTotal = newjob.job_totals.totals.total_repairs.amount; const ttlTotal = newjob.cieca_ttl.data.g_ttl_amt * 100; result.difference = (calcTotal - ttlTotal) / 100; - if (Math.abs(calcTotal - ttlTotal) > 5) { + if (Math.abs(calcTotal - ttlTotal) > 3) { //Diff is greater than 5 cents. Fail it. result.result = "***FAIL***"; } else { result.result = "PASS"; } - console.log(`${result.result} => RO ${job.ro_number}`); + // console.log(`${result.result} => RO ${job.ro_number} - ${job.id} `); results.push(result); } catch (error) { @@ -97,7 +106,7 @@ async function RunTheTest() { } } - console.table(results); + console.table(results.filter((r) => r.result !== "PASS")); const summary = results.reduce( (acc, val) => { if (val.result === "PASS") { @@ -115,3 +124,15 @@ async function RunTheTest() { } RunTheTest(); + +// mutation { +// delete_jobs(where: {shopid: {_eq: "a7ee1503-ee05-4a02-b80e-bdb11d1cc8ac"}}) { +// affected_rows +// } +// delete_owners(where: {shopid: {_eq: "a7ee1503-ee05-4a02-b80e-bdb11d1cc8ac"}}) { +// affected_rows +// } +// delete_vehicles(where: {shopid: {_eq: "a7ee1503-ee05-4a02-b80e-bdb11d1cc8ac"}}) { +// affected_rows +// } +// } diff --git a/server/accounting/qb-receivables-lines.js b/server/accounting/qb-receivables-lines.js index c27de4e84..b69e1fa7d 100644 --- a/server/accounting/qb-receivables-lines.js +++ b/server/accounting/qb-receivables-lines.js @@ -34,15 +34,10 @@ exports.default = function ({ } //Parts Lines Mappings. if (jobline.profitcenter_part) { - let DineroAmount = Dinero({ - amount: Math.round((jobline.act_price || 0) * 100), - }).multiply(jobline.part_qty || 1); - - // console.log("Have a part discount", jobline); - DineroAmount = DineroAmount.add( + const discountAmount = ((jobline.prt_dsmk_m && jobline.prt_dsmk_m !== 0) || (jobline.prt_dsmk_p && jobline.prt_dsmk_p !== 0)) && - DiscountNotAlreadyCounted(jobline, jobs_by_pk.joblines) + DiscountNotAlreadyCounted(jobline, jobs_by_pk.joblines) ? jobline.prt_dsmk_m ? Dinero({ amount: Math.round(jobline.prt_dsmk_m * 100) }) : Dinero({ @@ -51,8 +46,13 @@ exports.default = function ({ .multiply(jobline.part_qty || 0) .percentage(Math.abs(jobline.prt_dsmk_p || 0)) .multiply(jobline.prt_dsmk_p > 0 ? 1 : -1) - : Dinero() - ); + : Dinero(); + + let DineroAmount = Dinero({ + amount: Math.round((jobline.act_price || 0) * 100), + }) + .multiply(jobline.part_qty || 0) + .add(discountAmount); const account = responsibilityCenters.profits.find( (i) => jobline.profitcenter_part.toLowerCase() === i.name.toLowerCase() @@ -393,7 +393,7 @@ exports.default = function ({ //Add Towing, storage and adjustment lines. - if (jobs_by_pk.towing_payable && jobs_by_pk.towing_payable !== 0) { + if (jobs_by_pk.job_totals.additional.towing.amount > 0) { if (qbo) { //Going to always assume that we need to apply GST and PST for labor. const taxAccountCode = findTaxCode( @@ -417,9 +417,10 @@ exports.default = function ({ : taxCodes[taxAccountCode]; InvoiceLineAdd.push({ DetailType: "SalesItemLineDetail", - Amount: Dinero({ - amount: Math.round((jobs_by_pk.towing_payable || 0) * 100), - }).toFormat(DineroQbFormat), + Amount: Dinero(jobs_by_pk.job_totals.additional.towing).toFormat( + DineroQbFormat + ), + SalesItemLineDetail: { ...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } @@ -442,9 +443,9 @@ exports.default = function ({ }, Desc: "Towing", Quantity: 1, - Amount: Dinero({ - amount: Math.round((jobs_by_pk.towing_payable || 0) * 100), - }).toFormat(DineroQbFormat), + Amount: Dinero(jobs_by_pk.job_totals.additional.towing).toFormat( + DineroQbFormat + ), SalesTaxCodeRef: { FullName: bodyshop.md_responsibility_centers.taxes.itemexemptcode || "NON", @@ -452,7 +453,7 @@ exports.default = function ({ }); } } - if (jobs_by_pk.storage_payable && jobs_by_pk.storage_payable !== 0) { + if (jobs_by_pk.job_totals.additional.storage.amount > 0) { if (qbo) { //Going to always assume that we need to apply GST and PST for labor. const taxAccountCode = findTaxCode( @@ -476,9 +477,9 @@ exports.default = function ({ : taxCodes[taxAccountCode]; InvoiceLineAdd.push({ DetailType: "SalesItemLineDetail", - Amount: Dinero({ - amount: Math.round((jobs_by_pk.storage_payable || 0) * 100), - }).toFormat(DineroQbFormat), + Amount: Dinero( + jobs_by_pk.job_totals.additional.storage.amount + ).toFormat(DineroQbFormat), SalesItemLineDetail: { ...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } @@ -501,9 +502,9 @@ exports.default = function ({ }, Desc: "Storage", Quantity: 1, - Amount: Dinero({ - amount: Math.round((jobs_by_pk.storage_payable || 0) * 100), - }).toFormat(DineroQbFormat), + Amount: Dinero( + jobs_by_pk.job_totals.additional.storage.amount + ).toFormat(DineroQbFormat), SalesTaxCodeRef: { FullName: bodyshop.md_responsibility_centers.taxes.itemexemptcode || "NON", @@ -573,123 +574,117 @@ exports.default = function ({ //Add tax lines const job_totals = jobs_by_pk.job_totals; - const federal_tax = Dinero(job_totals.totals.federal_tax); - const state_tax = Dinero(job_totals.totals.state_tax); - const local_tax = Dinero(job_totals.totals.local_tax); - - if (federal_tax.getAmount() > 0) { + //Handle insurance profile adjustments + Object.keys(job_totals.parts.adjustments).forEach((key) => { if (qbo) { - // do qbo - } else { - InvoiceLineAdd.push({ - ItemRef: { - FullName: - bodyshop.md_responsibility_centers.taxes.federal.accountitem, + //Going to always assume that we need to apply GST and PST for labor. + const taxAccountCode = findTaxCode( + { + local: false, + federal: process.env.COUNTRY === "USA" ? false : true, + state: jobs_by_pk.state_tax_rate === 0 ? false : true, }, - Desc: bodyshop.md_responsibility_centers.taxes.federal.accountdesc, - Amount: federal_tax.toFormat(DineroQbFormat), - }); - } - } - - if (state_tax.getAmount() > 0) { - if (qbo) { - // do qbo - } else { - InvoiceLineAdd.push({ - ItemRef: { - FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem, - }, - Desc: bodyshop.md_responsibility_centers.taxes.state.accountdesc, - Amount: state_tax.toFormat(DineroQbFormat), - }); - } - } - - if (local_tax.getAmount() > 0) { - if (qbo) { - // do qbo - } else { - InvoiceLineAdd.push({ - ItemRef: { - FullName: bodyshop.md_responsibility_centers.taxes.local.accountitem, - }, - Desc: bodyshop.md_responsibility_centers.taxes.local.accountdesc, - Amount: local_tax.toFormat(DineroQbFormat), - }); - } - } - - //Region Specific - const { ca_bc_pvrt } = jobs_by_pk; - if (ca_bc_pvrt) { - if (qbo) { + bodyshop.md_responsibility_centers.sales_tax_codes + ); + const account = responsibilityCenters.profits.find( + (c) => c.name === responsibilityCenters.defaults.profits[key] + ); + const QboTaxId = + process.env.COUNTRY === "USA" + ? CheckQBOUSATaxID({ + // jobline: jobline, + job: jobs_by_pk, + type: "storage", + }) + : taxCodes[taxAccountCode]; InvoiceLineAdd.push({ DetailType: "SalesItemLineDetail", - Amount: Dinero({ amount: (ca_bc_pvrt || 0) * 100 }).toFormat( + Amount: Dinero(job_totals.parts.adjustments[key]).toFormat( DineroQbFormat ), + Description: `${account.accountdesc} - Adjustment`, SalesItemLineDetail: { ...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } : {}), ItemRef: { - value: items["PVRT"], + value: items[account.accountitem], + }, + TaxCodeRef: { + value: QboTaxId, }, Qty: 1, - TaxCodeRef: { - value: - taxCodes[ - findTaxCode( - { - local: false, - federal: process.env.COUNTRY === "USA" ? false : true, - state: false, - }, - bodyshop.md_responsibility_centers.sales_tax_codes - ) - ], - }, }, }); } else { InvoiceLineAdd.push({ ItemRef: { - FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem, + FullName: responsibilityCenters.profits.find( + (c) => c.name === responsibilityCenters.defaults.profits[key] + ).accountitem, }, - Desc: "PVRT", - Amount: Dinero({ amount: (ca_bc_pvrt || 0) * 100 }).toFormat( + Desc: "Storage", + Quantity: 1, + Amount: Dinero(job_totals.parts.adjustments[key]).toFormat( DineroQbFormat ), + SalesTaxCodeRef: { + FullName: + bodyshop.md_responsibility_centers.taxes.itemexemptcode || "NON", + }, }); } - } + }); - //QB USA with GST - //This was required for the No. 1 Collision Group. - // if ( - // bodyshop.accountingconfig && - // bodyshop.accountingconfig.qbo && - // bodyshop.accountingconfig.qbo_usa && - // bodyshop.region_config.includes("CA_") - // ) { - // InvoiceLineAdd.push({ - // DetailType: "SalesItemLineDetail", - // Amount: Dinero(jobs_by_pk.job_totals.totals.federal_tax).toFormat( - // DineroQbFormat - // ), - // SalesItemLineDetail: { - // ...(jobs_by_pk.class - // ? { ClassRef: { value: classes[jobs_by_pk.class] } } - // : {}), - // ItemRef: { - // value: - // items[bodyshop.md_responsibility_centers.taxes.federal.accountitem], - // }, - // Qty: 1, - // }, - // }); - // } + const federal_tax = Dinero(job_totals.totals.federal_tax); + const QboTaxId = + process.env.COUNTRY === "USA" + ? CheckQBOUSATaxID({ + // jobline: jobline, + type: "adjustment", + job: jobs_by_pk, + }) + : taxCodes[taxAccountCode]; + for (let tyCounter = 1; tyCounter <= 5; tyCounter++) { + const taxAmount = Dinero( + job_totals.totals.us_sales_tax_breakdown[`ty${tyCounter}Tax`] + ); + console.log(`Tax ${tyCounter}`, taxAmount.toFormat()); + if (taxAmount.getAmount() > 0) { + if (qbo) { + InvoiceLineAdd.push({ + DetailType: "SalesItemLineDetail", + Amount: taxAmount.toFormat(DineroQbFormat), + SalesItemLineDetail: { + ...(jobs_by_pk.class + ? { ClassRef: { value: classes[jobs_by_pk.class] } } + : {}), + ItemRef: { + value: + items[ + responsibilityCenters.taxes[`tax_ty${tyCounter}`].accountitem + ], + }, + TaxCodeRef: { + value: QboTaxId, + }, + Qty: 1, + }, + }); + } else { + InvoiceLineAdd.push({ + ItemRef: { + FullName: + bodyshop.md_responsibility_centers.taxes[`tax_ty${tyCounter}`] + .accountitem, + }, + Desc: bodyshop.md_responsibility_centers.taxes[`tax_ty${tyCounter}`] + .accountdesc, + Amount: taxAmount.toFormat(DineroQbFormat), + }); + } + } + } if (!qbo && InvoiceLineAdd.length === 0) { //Handle the scenario where there is a $0 sale invoice. @@ -832,17 +827,19 @@ exports.createMultiQbPayerLines = function ({ }; function CheckQBOUSATaxID({ jobline, job, type }) { - if (type === "labor") { - return jobline.lbr_tax ? "TAX" : "NON"; - } else if (type === "part") { - return jobline.tax_part ? "TAX" : "NON"; - } else if (type === "materials") { - return job.tax_paint_mat_rt > 0 ? "TAX" : "NON"; - } else if (type === " towing") { - return true ? "TAX" : "NON"; - } else if (type === "adjustment") { - return false ? "TAX" : "NON"; - } else { - throw new Error(`Unknown type to calculate tax id: ${type} `); - } + //Replacing this to be all non-taxable items with the refactor of parts tax rates. + return "NON"; + // if (type === "labor") { + // return jobline.lbr_tax ? "TAX" : "NON"; + // } else if (type === "part") { + // return jobline.tax_part ? "TAX" : "NON"; + // } else if (type === "materials") { + // return job.tax_paint_mat_rt > 0 ? "TAX" : "NON"; + // } else if (type === " towing") { + // return true ? "TAX" : "NON"; + // } else if (type === "adjustment") { + // return false ? "TAX" : "NON"; + // } else { + // throw new Error(`Unknown type to calculate tax id: ${type} `); + // } } diff --git a/server/accounting/qbo/qbo-receivables.js b/server/accounting/qbo/qbo-receivables.js index 8471f80be..04be93cb9 100644 --- a/server/accounting/qbo/qbo-receivables.js +++ b/server/accounting/qbo/qbo-receivables.js @@ -242,6 +242,11 @@ exports.default = async (req, res) => { (error && error.authResponse && error.authResponse.body) || (error && error.message), }); + console.log(error); + logger.log("qbo-receivable-create-error", "ERROR", req.user.email, { + error: error.message, + stack: error.stack, + }); //Add the export log error. if (elgen) { const result = await client diff --git a/server/cdk/cdk-calculate-allocations.js b/server/cdk/cdk-calculate-allocations.js index cd75dc6ec..042e09abf 100644 --- a/server/cdk/cdk-calculate-allocations.js +++ b/server/cdk/cdk-calculate-allocations.js @@ -25,26 +25,40 @@ exports.default = async function (socket, jobid) { const { bodyshop } = job; const taxAllocations = { - local: { - center: bodyshop.md_responsibility_centers.taxes.local.name, - sale: Dinero(job.job_totals.totals.local_tax), + tax_ty1: { + center: bodyshop.md_responsibility_centers.taxes[`tax_ty1`].name, + sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty1Tax`]), cost: Dinero(), - profitCenter: bodyshop.md_responsibility_centers.taxes.local, - costCenter: bodyshop.md_responsibility_centers.taxes.local, + profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty1`], + costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty1`], }, - state: { - center: bodyshop.md_responsibility_centers.taxes.state.name, - sale: Dinero(job.job_totals.totals.state_tax), + tax_ty2: { + center: bodyshop.md_responsibility_centers.taxes[`tax_ty2`].name, + sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty2Tax`]), cost: Dinero(), - profitCenter: bodyshop.md_responsibility_centers.taxes.state, - costCenter: bodyshop.md_responsibility_centers.taxes.state, + profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty2`], + costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty2`], }, - federal: { - center: bodyshop.md_responsibility_centers.taxes.federal.name, - sale: Dinero(job.job_totals.totals.federal_tax), + tax_ty3: { + center: bodyshop.md_responsibility_centers.taxes[`tax_ty3`].name, + sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty3Tax`]), cost: Dinero(), - profitCenter: bodyshop.md_responsibility_centers.taxes.federal, - costCenter: bodyshop.md_responsibility_centers.taxes.federal, + profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty3`], + costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty3`], + }, + tax_ty4: { + center: bodyshop.md_responsibility_centers.taxes[`tax_ty4`].name, + sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty4Tax`]), + cost: Dinero(), + profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty4`], + costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty4`], + }, + tax_ty5: { + center: bodyshop.md_responsibility_centers.taxes[`tax_ty5`].name, + sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown[`ty5Tax`]), + cost: Dinero(), + profitCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty5`], + costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty5`], }, }; @@ -328,6 +342,30 @@ exports.default = async function (socket, jobid) { } } + //profile level adjustments + Object.keys(job.job_totals.parts.adjustments).forEach((key) => { + const accountName = selectedDmsAllocationConfig.profits[key]; + + const otherAccount = bodyshop.md_responsibility_centers.profits.find( + (c) => c.name === accountName + ); + + if (otherAccount) { + if (!profitCenterHash[accountName]) + profitCenterHash[accountName] = Dinero(); + + profitCenterHash[accountName] = profitCenterHash[accountName].add( + Dinero(job.job_totals.parts.adjustments[key]) + ); + } else { + CdkBase.createLogEvent( + socket, + "ERROR", + `Error encountered in CdkCalculateAllocations. Unable to find adjustment account. ${error}` + ); + } + }); + const jobAllocations = _.union( Object.keys(profitCenterHash), Object.keys(costCenterHash) diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 3bba58cf4..b46a1a2ed 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -1168,10 +1168,8 @@ exports.GET_JOB_BY_PK = `query GET_JOB_BY_PK($id: uuid!) { shopid est_ct_ln cieca_pfl -vehicle{ - id - notes -} + cieca_pft + cieca_pfo est_ph1 est_ea selling_dealer @@ -1292,19 +1290,6 @@ vehicle{ prt_dsmk_m misc_amt misc_tax - parts_order_lines { - id - parts_order { - id - order_number - order_date - user_email - vendor { - id - name - } - } - } } } }`; diff --git a/server/job/job-costing.js b/server/job/job-costing.js index e7568fa72..c4603110d 100644 --- a/server/job/job-costing.js +++ b/server/job/job-costing.js @@ -972,6 +972,8 @@ const getAdditionalCostCenter = (jl, profitCenters) => { return profitCenters["ATS"]; } else if (lineDesc.includes("towing")) { return profitCenters["TOW"]; + } else if (jl.act_price > 0) { + ret.profitcenter_part = defaults.profits["PAO"]; } else { return null; } diff --git a/server/job/job-totals.js b/server/job/job-totals.js index 9c825ada4..cc350e88a 100644 --- a/server/job/job-totals.js +++ b/server/job/job-totals.js @@ -1,6 +1,8 @@ const Dinero = require("dinero.js"); const queries = require("../graphql-client/queries"); const GraphQLClient = require("graphql-request").GraphQLClient; +const adminClient = require("../graphql-client/graphql-client").client; +const _ = require("lodash"); const logger = require("../utils/logger"); // Dinero.defaultCurrency = "USD"; // Dinero.globalLocale = "en-CA"; @@ -59,9 +61,9 @@ async function TotalsServerSide(req, res) { try { let ret = { rates: await CalculateRatesTotals({ job, client }), - parts: CalculatePartsTotals(job.joblines, job.parts_tax_rates), - additional: CalculateAdditional(job), + parts: CalculatePartsTotals(job.joblines, job.parts_tax_rates, job), }; + ret.additional = CalculateAdditional(job); ret.totals = CalculateTaxesTotals(job, ret); return ret; @@ -93,9 +95,9 @@ async function Totals(req, res) { try { let ret = { rates: await CalculateRatesTotals({ job, client }), - parts: CalculatePartsTotals(job.joblines, job.parts_tax_rates), - additional: CalculateAdditional(job), + parts: CalculatePartsTotals(job.joblines, job.parts_tax_rates, job), }; + ret.additional = CalculateAdditional(job); ret.totals = CalculateTaxesTotals(job, ret); res.status(200).json(ret); @@ -259,8 +261,9 @@ async function CalculateRatesTotals({ job, client }) { let hasMapaLine = false; let hasMashLine = false; let hasMahwLine = false; - let mapaOpCodes = ParseCalopCode(job.materials["mapa"]?.cal_opcode); - let mashOpCodes = ParseCalopCode(job.materials["mash"]?.cal_opcode); + let hasCustomMahwLine; + let mapaOpCodes = ParseCalopCode(job.materials["MAPA"]?.cal_opcode); + let mashOpCodes = ParseCalopCode(job.materials["MASH"]?.cal_opcode); jobLines.forEach((item) => { //IO-1317 Use the lines on the estimate if they exist instead. @@ -277,9 +280,21 @@ async function CalculateRatesTotals({ job, client }) { amount: Math.round((item.act_price || 0) * 100), }); } - if (item.line_desc?.toLowerCase().includes("hazardous waste")) { + //We might add a hazardous waste line. So we'll need to make sure we only pick up the CCC one. + if ( + item.line_desc?.toLowerCase().includes("hazardous waste") && + !item.manual_line && + item.part_type === null && + item.lbr_op !== "OP16" //Seems to be that it is OP16 for sublet lines. + ) { hasMahwLine = item; } + if ( + item.line_desc?.toLowerCase().includes("hazardous waste") && + item.manual_line + ) { + hasCustomMahwLine = item; + } if (item.mod_lbr_ty) { //Check to see if it has 0 hours and a price instead. @@ -337,16 +352,18 @@ async function CalculateRatesTotals({ job, client }) { } let threshold; //Check if there is a max for this type. - if (job.materials && job.materials[property]) { + if (job.materials && job.materials[property.toUpperCase()]) { // if ( - job.materials[property].cal_maxdlr !== undefined && - job.materials[property].cal_maxdlr >= 0 + job.materials[property.toUpperCase()].cal_maxdlr !== undefined && + job.materials[property.toUpperCase()].cal_maxdlr >= 0 ) { //It has an upper threshhold. threshold = Dinero({ - amount: Math.round(job.materials[property].cal_maxdlr * 100), + amount: Math.round( + job.materials[property.toUpperCase()].cal_maxdlr * 100 + ), }); } } @@ -375,34 +392,56 @@ async function CalculateRatesTotals({ job, client }) { stlMahw.ttl_amt !== 0 && (!hasMahwLine || hasMahwLine.act_price !== stlMahw.ttl_amt) ) { + //The Mahw line that has been added doesn't match with what we have in the STL. Add/update the adjusting line so that the balance is correct. + //Add a hazardous waste material line in case there isn't one on the estimate. - const newMahwLine = { - line_desc: "Hazardous Waste Removal*", - part_type: "PAS", - oem_partno: null, - db_price: 0, - act_price: stlMahw.ttl_amt, - part_qty: 1, - //mod_lbr_ty: "LAB", - db_hrs: 0, - mod_lb_hrs: 0, - lbr_op: "OP11", - lbr_amt: 0, - op_code_desc: "REMOVE / REPLACE", - tax_part: hasMahwLine.tax_amt > 0 ? true : false, - db_ref: null, - manual_line: true, - jobid: job.id, - }; - job.joblines.push(newMahwLine); - await client.request(queries.INSERT_NEW_JOB_LINE, { - lineInput: [newMahwLine], - }); + let newPrice = stlMahw.ttl_amt; + if (hasCustomMahwLine) { + //Update it + job.joblines.forEach((jl) => { + if (jl.id === hasCustomMahwLine.id) { + jl.act_price = newPrice; + jl.manual_line = true; + jl.tax_part = stlMahw.tax_amt > 0 ? true : false; + } + }); + await client.request(queries.UPDATE_JOB_LINE, { + lineId: hasCustomMahwLine.id, + line: { + act_price: newPrice, + manual_line: true, + tax_part: stlMahw.tax_amt > 0 ? true : false, + }, + }); + } else { + const newMahwLine = { + line_desc: "Hazardous Waste Removal*", + part_type: null, + oem_partno: null, + db_price: 0, + act_price: newPrice, + part_qty: 1, + mod_lbr_ty: "LAB", + db_hrs: 0, + mod_lb_hrs: 0, + lbr_op: "OP0", + lbr_amt: 0, + op_code_desc: "REMOVE / REPLACE", + tax_part: stlMahw.tax_amt > 0 ? true : false, + db_ref: null, + manual_line: true, + jobid: job.id, + }; + job.joblines.push(newMahwLine); + await client.request(queries.INSERT_NEW_JOB_LINE, { + lineInput: [newMahwLine], + }); + } } //Materials Scrubbing as required by CCC. - let matTotalLine = job.cieca_stl.data.find((l) => l.ttl_typecd === "MAT"); - let shopMatLine = job.cieca_stl.data.find((l) => l.ttl_typecd === "MASH"); + let matTotalLine = job.cieca_stl?.data?.find((l) => l.ttl_typecd === "MAT"); + let shopMatLine = job.cieca_stl?.data?.find((l) => l.ttl_typecd === "MASH"); if (matTotalLine && shopMatLine) { //Check to see if theyre different @@ -427,57 +466,8 @@ async function CalculateRatesTotals({ job, client }) { return ret; } -function CalculatePartsTotals(jobLines, parts_tax_rates) { +function CalculatePartsTotals(jobLines, parts_tax_rates, job) { const jl = jobLines.filter((jl) => !jl.removed); - // jl.forEach((line) => { - // //Some profile based estimates don't automatically add the discount to the line, some do. - // //Clean up the ones that don't to add it in. - - // //Apply a discount to the line if there is a profile discount, but it isn't added to the line itself. - // const partTax = parts_tax_rates[line.part_type]; - // if ( - // line.act_price > 0 && - // partTax && - // partTax.prt_discp && - // partTax.prt_discp > 0 - // ) { - // //apply a discount - // const discount = Dinero({ - // amount: Math.round(line.act_price * 100), - // }).percentage( - // Math.abs(partTax.prt_discp) > 1 - // ? partTax.prt_discp - // : partTax.prt_discp * 100 - // ); - // line.prt_dsmk_m = discount.toFormat("0.0"); - // line.prt_dsmk_p = partTax.prt_discp; - // line.act_price = Dinero({ - // amount: Math.round(line.act_price * 100), - // }) - // .subtract(discount) - // .toFormat("0.0"); - // } else if ( - // line.act_price > 0 && - // partTax && - // partTax.prt_mkupp && - // partTax.prt_mkupp > 0 - // ) { - // //apply a mark up - // const markup = Dinero({ - // amount: Math.round(line.act_price * 100), - // }).percentage( - // Math.abs(partTax.prt_mkupp) > 1 - // ? partTax.prt_mkupp - // : partTax.prt_mkupp * 100 - // ); - // line.prt_dsmk_m = markup.toFormat("0.0"); - // line.prt_dsmk_p = partTax.prt_mkupp; - // line.act_price = Dinero({ amount: Math.round(line.act_price * 100) }) - // .add(markup) - // .toFormat("0.0"); - // } - // }); - const ret = jl.reduce( (acc, value) => { switch (value.part_type) { @@ -517,24 +507,26 @@ function CalculatePartsTotals(jobLines, parts_tax_rates) { value.db_ref !== "900511" ) return acc; + + const discountAmount = + ((value.prt_dsmk_m && value.prt_dsmk_m !== 0) || + (value.prt_dsmk_p && value.prt_dsmk_p !== 0)) && + DiscountNotAlreadyCounted(value, jl) + ? value.prt_dsmk_m + ? Dinero({ amount: Math.round(value.prt_dsmk_m * 100) }) + : Dinero({ + amount: Math.round(value.act_price * 100), + }) + .multiply(value.part_qty || 0) + .percentage(Math.abs(value.prt_dsmk_p || 0)) + .multiply(value.prt_dsmk_p > 0 ? 1 : -1) + : Dinero(); + return { ...acc, parts: { ...acc.parts, - prt_dsmk_total: acc.parts.prt_dsmk_total.add( - ((value.prt_dsmk_m && value.prt_dsmk_m !== 0) || - (value.prt_dsmk_p && value.prt_dsmk_p !== 0)) && - DiscountNotAlreadyCounted(value, jl) - ? value.prt_dsmk_m - ? Dinero({ amount: Math.round(value.prt_dsmk_m * 100) }) - : Dinero({ - amount: Math.round(value.act_price * 100), - }) - .multiply(value.part_qty || 0) - .percentage(Math.abs(value.prt_dsmk_p || 0)) - .multiply(value.prt_dsmk_p > 0 ? 1 : -1) - : Dinero() - ), + prt_dsmk_total: acc.parts.prt_dsmk_total.add(discountAmount), ...(value.part_type ? { list: { @@ -543,20 +535,24 @@ function CalculatePartsTotals(jobLines, parts_tax_rates) { acc.parts.list[value.part_type] && acc.parts.list[value.part_type].total ? { - total: acc.parts.list[value.part_type].total.add( - Dinero({ - amount: Math.round( - (value.act_price || 0) * 100 - ), - }).multiply(value.part_qty || 0) - ), + total: acc.parts.list[value.part_type].total + .add( + Dinero({ + amount: Math.round( + (value.act_price || 0) * 100 + ), + }).multiply(value.part_qty || 0) + ) + .add(discountAmount), } : { total: Dinero({ amount: Math.round( (value.act_price || 0) * 100 ), - }).multiply(value.part_qty || 0), + }) + .multiply(value.part_qty || 0) + .add(discountAmount), }, }, } @@ -600,36 +596,29 @@ function CalculatePartsTotals(jobLines, parts_tax_rates) { } ); - //Apply insurance based parts discuounts/markups. - let adjustments = { - PAA: Dinero(), - PAC: Dinero(), - PAG: Dinero(), - PAL: Dinero(), - PAN: Dinero(), - PAO: Dinero(), - PAP: Dinero(), - PAR: Dinero(), - PAS: Dinero(), - PAT: Dinero(), - }; + //Apply insurance based parts discounts/markups. + let adjustments = {}; + //Track all adjustments that need to be made. + + const linesToAdjustForDiscount = []; Object.keys(parts_tax_rates).forEach((key) => { //Check if there's a discount or a mark up. let disc = Dinero(), markup = Dinero(); + + let discountRate, markupRate; if ( parts_tax_rates[key].prt_discp !== undefined && parts_tax_rates[key].prt_discp >= 0 ) { //Check if there's any parts in this part type. if (ret.parts.list[key] !== undefined) { - disc = ret.parts.list[key].total - .percentage( - Math.abs(parts_tax_rates[key].prt_discp) > 1 - ? parts_tax_rates[key].prt_discp - : parts_tax_rates[key].prt_discp * 100 - ) - .multiply(-1); + discountRate = + Math.abs(parts_tax_rates[key].prt_discp) > 1 + ? parts_tax_rates[key].prt_discp + : parts_tax_rates[key].prt_discp * 100; + + disc = ret.parts.list[key].total.percentage(discountRate).multiply(-1); } } if ( @@ -638,26 +627,33 @@ function CalculatePartsTotals(jobLines, parts_tax_rates) { ) { //Check if there's any parts in this part type. if (ret.parts.list[key] !== undefined) { - markup = ret.parts.list[key].total.percentage( + markupRate = Math.abs(parts_tax_rates[key].prt_mkupp) > 1 ? parts_tax_rates[key].prt_mkupp - : parts_tax_rates[key].prt_mkupp * 100 //Seems that mark up is written as decimal not %. - ); + : parts_tax_rates[key].prt_mkupp * 100; //Seems that mark up is written as decimal not %. + + markup = ret.parts.list[key].total.percentage(markupRate); } } - let adjustment = disc.add(markup); - adjustments[key] = adjustment; - }); + const correspondingCiecaStlTotalLine = job.cieca_stl?.data.find( + (c) => c.ttl_typecd === key + ); - //Temporarily commenting this out since these totals appear to be already included in the calculation. - // Object.keys(adjustments).forEach((key) => { - // if (ret.parts.list[key] !== undefined) { - // ret.parts.list[key].total = ret.parts.list[key].total.add( - // adjustments[key] - // ); - // ret.parts.subtotal = ret.parts.subtotal.add(adjustments[key]); - // } - // }); + //If the difference is greater than a penny, fix it. + //This usually ties into whether or not the profile has part type discounts overall in the PFP. + if ( + correspondingCiecaStlTotalLine && + Math.abs( + ret.parts.list[key]?.total.getAmount() - + correspondingCiecaStlTotalLine.ttl_amt * 100 + ) > 1 + ) { + let adjustment = disc.add(markup); + adjustments[key] = adjustment; + ret.parts.subtotal = ret.parts.subtotal.add(adjustment); + ret.parts.total = ret.parts.total.add(adjustment); + } + }); return { adjustments, @@ -691,6 +687,9 @@ function IsAdditionalCost(jobLine) { } function CalculateAdditional(job) { + const stlTowing = job.cieca_stl?.data.find((c) => c.ttl_type === "OTTW"); + const stlStorage = job.cieca_stl?.data.find((c) => c.ttl_type === "OTST"); + let ret = { additionalCosts: null, additionalCostItems: [], @@ -701,9 +700,11 @@ function CalculateAdditional(job) { pvrt: null, total: null, }; - ret.towing = Dinero({ - amount: Math.round((job.towing_payable || 0) * 100), - }); + ret.towing = stlTowing + ? Dinero({ amount: Math.round(stlTowing.ttl_amt * 100) }) + : Dinero({ + amount: Math.round((job.towing_payable || 0) * 100), + }); ret.additionalCosts = job.joblines .filter((jl) => !jl.removed && IsAdditionalCost(jl)) @@ -728,9 +729,11 @@ function CalculateAdditional(job) { ret.adjustments = Dinero({ amount: Math.round((job.adjustment_bottom_line || 0) * 100), }); - ret.storage = Dinero({ - amount: Math.round((job.storage_payable || 0) * 100), - }); + ret.storage = stlStorage + ? Dinero({ amount: Math.round(stlStorage.ttl_amt * 100) }) + : Dinero({ + amount: Math.round((job.storage_payable || 0) * 100), + }); ret.pvrt = Dinero({ amount: Math.round((job.ca_bc_pvrt || 0) * 100), }); @@ -738,7 +741,6 @@ function CalculateAdditional(job) { .add(ret.adjustments) //IO-813 Adjustment takes care of GST & PST at labor rate. .add(ret.towing) .add(ret.storage); - //.add(ret.pvrt); return ret; } @@ -749,14 +751,12 @@ function CalculateTaxesTotals(job, otherTotals) { .add(otherTotals.rates.subtotal) //No longer using just rates subtotal to include mapa/mash. .add(otherTotals.additional.total); - // .add(Dinero({ amount: (job.towing_payable || 0) * 100 })) - // .add(Dinero({ amount: (job.storage_payable || 0) * 100 })); - //Potential issue here with Sublet Calculation. Sublets are calculated under labor in Mitchell, but it's done in IO //Under the parts rates. let statePartsTax = Dinero(); let additionalItemsTax = Dinero(); + let us_sales_tax_breakdown; //Audatex sends additional glass part types. IO-774 const BackupGlassTax = @@ -767,92 +767,288 @@ function CalculateTaxesTotals(job, otherTotals) { job.parts_tax_rates.PAGQ || job.parts_tax_rates.PAGR); + const taxableAmounts = { + PAA: Dinero(), + PAN: Dinero(), + PAL: Dinero(), + PAR: Dinero(), + PAC: Dinero(), + PAG: Dinero(), + PAO: Dinero(), + PAS: Dinero(), + PAP: Dinero(), + PAM: Dinero(), + + LA1: Dinero(), + LA2: Dinero(), + LA3: Dinero(), + LA4: Dinero(), + LAU: Dinero(), + LAA: Dinero(), + LAB: Dinero(), + LAD: Dinero(), + LAE: Dinero(), + LAF: Dinero(), + LAG: Dinero(), + LAM: Dinero(), + LAR: Dinero(), + LAS: Dinero(), + + MAPA: Dinero(), + MASH: Dinero(), + TOW: Dinero(), + STOR: Dinero(), + }; + + //For each line, determine if it's taxable, and if it is, add the line amount to the taxable amounts total. job.joblines .filter((jl) => !jl.removed) .forEach((val) => { if (!val.tax_part) return; if (!val.part_type && IsAdditionalCost(val)) { - additionalItemsTax = additionalItemsTax.add( - Dinero({ amount: Math.round((val.act_price || 0) * 100) }) - .multiply(val.part_qty || 0) - .percentage( - ((job.parts_tax_rates && - job.parts_tax_rates["PAN"] && - job.parts_tax_rates["PAN"].prt_tax_rt) || - 0) * 100 - ) + taxableAmounts.PAO = taxableAmounts.PAO.add( + Dinero({ amount: Math.round((val.act_price || 0) * 100) }).multiply( + val.part_qty || 0 + ) ); + } else if (!val.part_type) { + //Do nothing for now. } else { - statePartsTax = statePartsTax.add( - Dinero({ amount: Math.round((val.act_price || 0) * 100) }) - .multiply(val.part_qty || 0) - .add( - val.prt_dsmk_m && - val.prt_dsmk_m !== 0 && - DiscountNotAlreadyCounted(val, job.joblines) - ? val.prt_dsmk_m - ? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) }) - : Dinero({ - amount: Math.round(val.act_price * 100), - }) - .multiply(val.part_qty || 0) - .percentage(Math.abs(val.prt_dsmk_p || 0)) - .multiply(val.prt_dsmk_p > 0 ? 1 : -1) - : Dinero() - ) - .percentage( - ((job.parts_tax_rates && - job.parts_tax_rates[val.part_type] && - job.parts_tax_rates[val.part_type].prt_tax_rt) || - (val.part_type && - val.part_type.startsWith("PAG") && - BackupGlassTax && - BackupGlassTax.prt_tax_rt) || - (!val.part_type && - val.db_ref === "900510" && - job.parts_tax_rates["PAN"] && - job.parts_tax_rates["PAN"].prt_tax_rt) || - 0) * 100 - ) + const typeOfPart = val.part_type; + + const discMarkupAmount = + val.prt_dsmk_m && + val.prt_dsmk_m !== 0 && + DiscountNotAlreadyCounted(val, job.joblines) // DO WE NEED TO COUNT PFP DISCOUNT HERE? + ? val.prt_dsmk_m + ? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) }) + : Dinero({ + amount: Math.round(val.act_price * 100), + }) + .multiply(val.part_qty || 0) + .percentage(Math.abs(val.prt_dsmk_p || 0)) + .multiply(val.prt_dsmk_p > 0 ? 1 : -1) + : Dinero(); + + const partAmount = Dinero({ + amount: Math.round((val.act_price || 0) * 100), + }) + .multiply(val.part_qty || 0) + .add(discMarkupAmount); + taxableAmounts[typeOfPart] = taxableAmounts[typeOfPart].add(partAmount); + } + }); + + //Check in the PFL file which types of labor are taxable. Add the amount that is considered taxable to the taxable amounts total. + Object.keys(taxableAmounts) + .filter((key) => key.startsWith("LA")) + .map((key) => { + const isLaborTypeTaxable = job.cieca_pfl[key]?.lbr_tax_in; + if (isLaborTypeTaxable) { + taxableAmounts[key] = taxableAmounts[key].add( + otherTotals.rates[key.toLowerCase()].total ); } }); + Object.keys(taxableAmounts) + .filter((key) => key.startsWith("MA")) + .map((key) => { + const isTypeTaxable = job.materials[key]?.tax_ind; + if (isTypeTaxable) { + taxableAmounts[key] = taxableAmounts[key].add( + otherTotals.rates[key.toLowerCase()].total + ); + } + }); + //Add towing and storage taxable amounts + const stlTowing = job.cieca_stl?.data.find((c) => c.ttl_typecd === "OTTW"); + const stlStorage = job.cieca_stl?.data.find((c) => c.ttl_typecd === "OTST"); + + if (stlTowing) + taxableAmounts.TOW = Dinero({ + amount: Math.round(stlTowing.t_amt * 100), + }); + if (stlStorage) + taxableAmounts.TOW = Dinero({ + amount: Math.round(stlStorage.t_amt * 100), + }); + + const pfp = job.parts_tax_rates; + + //For any profile level markups/discounts, add them in now as well. + Object.keys(otherTotals.parts.adjustments).forEach((key) => { + const adjustmentAmount = otherTotals.parts.adjustments[key]; + if (adjustmentAmount.getAmount() !== 0 && pfp[key]?.prt_tax_in) { + taxableAmounts[key] = taxableAmounts[key].add(adjustmentAmount); + } + }); + + // console.log("*** Taxable Amounts***"); + // console.table(JSON.parse(JSON.stringify(taxableAmounts))); + + //For the taxable amounts, figure out which tax type applies. + //Then sum up the total of that tax type and then calculate the thresholds. + + const taxableAmountsByTier = { + ty1Tax: Dinero(), + ty2Tax: Dinero(), + ty3Tax: Dinero(), + ty4Tax: Dinero(), + ty5Tax: Dinero(), + ty6Tax: Dinero(), + }; + const totalTaxByTier = { + ty1Tax: Dinero(), + ty2Tax: Dinero(), + ty3Tax: Dinero(), + ty4Tax: Dinero(), + ty5Tax: Dinero(), + ty6Tax: Dinero(), + }; + + const pfl = job.cieca_pfl; + const pfm = job.materials; + const pfo = job.cieca_pfo; + Object.keys(taxableAmounts).map((key) => { + try { + if (key.startsWith("PA")) { + const typeOfPart = key; // === "PAM" ? "PAC" : key; + //At least one of these scenarios must be taxable. + for (let tyCounter = 1; tyCounter <= 5; tyCounter++) { + if (IsTrueOrYes(pfp[typeOfPart][`prt_tx_in${tyCounter}`])) { + //This amount is taxable for this type. + taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[ + `ty${tyCounter}Tax` + ].add(taxableAmounts[typeOfPart]); + } + } + } else if (key.startsWith("MA")) { + //Materials Handling + for (let tyCounter = 1; tyCounter <= 5; tyCounter++) { + if (IsTrueOrYes(pfm[key][`mat_tx_in${tyCounter}`])) { + //This amount is taxable for this type. + taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[ + `ty${tyCounter}Tax` + ].add(taxableAmounts[key]); + } + } + } else if (key.startsWith("LA")) { + //Labor. + for (let tyCounter = 1; tyCounter <= 5; tyCounter++) { + if (IsTrueOrYes(pfl[key][`lbr_tx_in${tyCounter}`])) { + //This amount is taxable for this type. + taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[ + `ty${tyCounter}Tax` + ].add(taxableAmounts[key]); + } + } + } else if (key === "TOW") { + for (let tyCounter = 1; tyCounter <= 5; tyCounter++) { + if (IsTrueOrYes(pfo[`tow_t_in${tyCounter}`])) { + //This amount is taxable for this type. + taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[ + `ty${tyCounter}Tax` + ].add(taxableAmounts[key]); + } + } + } else if (key === "STOR") { + for (let tyCounter = 1; tyCounter <= 5; tyCounter++) { + if (IsTrueOrYes(pfo[`stor_t_in${tyCounter}`])) { + //This amount is taxable for this type. + taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[ + `ty${tyCounter}Tax` + ].add(taxableAmounts[key]); + } + } + } + } catch (error) { + console.error("Key with issue", key); + } + }); + + const remainingTaxableAmounts = taxableAmountsByTier; + // console.log("*** Taxable Amounts by Tier***"); + // console.table(JSON.parse(JSON.stringify(taxableAmountsByTier))); + + Object.keys(taxableAmountsByTier).forEach((taxTierKey) => { + try { + let tyCounter = taxTierKey[2]; //Get the number from the key. + //i represents the tax number. If we got here, this type of tax is applicable. Now we need to add based on the thresholds. + for (let threshCounter = 1; threshCounter <= 5; threshCounter++) { + const thresholdAmount = parseFloat( + job.cieca_pft[`ty${tyCounter}_thres${threshCounter}`] + ); + const thresholdTaxRate = parseFloat( + job.cieca_pft[`ty${tyCounter}_rate${threshCounter}`] + ); + + let taxableAmountInThisThreshold; + if (thresholdAmount === 9999.99) { + // THis is the last threshold. Tax the entire remaining amount. + taxableAmountInThisThreshold = remainingTaxableAmounts[taxTierKey]; + remainingTaxableAmounts[taxTierKey] = Dinero(); + } else { + if ( + thresholdAmount >= + remainingTaxableAmounts[taxTierKey].getAmount() / 100 + ) { + //This threshold is bigger than the remaining taxable balance. Add it all. + taxableAmountInThisThreshold = remainingTaxableAmounts[taxTierKey]; + remainingTaxableAmounts[taxTierKey] = Dinero(); + } else { + //Take the size of the threshold from the remaining amount, tax it, and do it all over. + taxableAmountInThisThreshold = Dinero({ + amount: Math.round(thresholdAmount * 100), + }); + remainingTaxableAmounts[taxTierKey] = remainingTaxableAmounts[ + taxTierKey + ].subtract( + Dinero({ + amount: Math.round(taxableAmountInThisThreshold * 100), + }) + ); + } + } + + const taxAmountToAdd = + taxableAmountInThisThreshold.percentage(thresholdTaxRate); + + totalTaxByTier[taxTierKey] = + totalTaxByTier[taxTierKey].add(taxAmountToAdd); + } + } catch (error) { + console.error("PFP Calculation error", error); + } + }); + + // console.log("*** Total Tax by Tier Amounts***"); + // console.table(JSON.parse(JSON.stringify(totalTaxByTier))); + + statePartsTax = statePartsTax + .add(totalTaxByTier.ty1Tax) + .add(totalTaxByTier.ty2Tax) + .add(totalTaxByTier.ty3Tax) + .add(totalTaxByTier.ty4Tax) + .add(totalTaxByTier.ty5Tax) + .add(totalTaxByTier.ty6Tax); + us_sales_tax_breakdown = totalTaxByTier; + //console.log("Tiered Taxes Total for Parts/Labor", statePartsTax.toFormat()); + let laborTaxTotal = Dinero(); if (Object.keys(job.cieca_pfl).length > 0) { - //Do it by labor type - const types = [ - "la1", - "la2", - "la3", - "la4", - "lau", - "laa", - "lab", - "lad", - "lae", - "laf", - "lag", - "lam", - "lar", - "las", - ]; - types.forEach((type) => { - laborTaxTotal = laborTaxTotal.add( - otherTotals.rates[type].total.percentage( - job.cieca_pfl[type.toUpperCase()] - ? job.cieca_pfl[type.toUpperCase()].lbr_taxp - : (job.tax_lbr_rt || 0) * 100 - ) - ); - }); + //Ignore it now, we have calculated it above. + //This was previously used for JCS before parts were also calculated at a different rate. } else { //We don't have it, just add in how it was before. laborTaxTotal = otherTotals.rates.subtotal.percentage( (job.tax_lbr_rt || 0) * 100 ); // THis is currently using the lbr tax rate from PFH not PFL. } + + //console.log("Labor Tax Total", laborTaxTotal.toFormat()); + let ret = { subtotal: subtotal, federal_tax: subtotal @@ -863,35 +1059,8 @@ function CalculateTaxesTotals(job, otherTotals) { ) ), statePartsTax, - state_tax: statePartsTax - .add(laborTaxTotal) - .add( - otherTotals.additional.adjustments.percentage( - (job.tax_lbr_rt || 0) * 100 - ) - ) - .add( - otherTotals.additional.towing.percentage((job.tax_tow_rt || 0) * 100) - ) - .add( - otherTotals.additional.storage.percentage((job.tax_str_rt || 0) * 100) - ) - .add(additionalItemsTax) - .add( - otherTotals.rates.mapa.hasMapaLine === false //If parts and materials were not added as lines, we must calculate the taxes on them. - ? otherTotals.rates.mapa.total.percentage( - (job.tax_paint_mat_rt || 0) * 100 - ) - : Dinero() - ) - .add( - otherTotals.rates.mash.hasMashLine === false //If parts and materials were not added as lines, we must calculate the taxes on them. - ? otherTotals.rates.mash.total.percentage( - (job.tax_shop_mat_rt || 0) * 100 - ) - : Dinero() - ), - + us_sales_tax_breakdown, + state_tax: statePartsTax, local_tax: subtotal.percentage((job.local_tax_rate || 0) * 100), }; ret.total_repairs = ret.subtotal @@ -928,34 +1097,7 @@ function CalculateTaxesTotals(job, otherTotals) { exports.default = Totals; function DiscountNotAlreadyCounted(jobline, joblines) { - //CCC already factors in the discount. If the difference between the 2 is exactly the discount, it's all good. - if ( - Math.round( - (jobline.prt_dsmk_m / (jobline.act_price - jobline.prt_dsmk_m)) * 100 - ) === Math.abs(jobline.prt_dsmk_p) - ) { - return false; - } - - //Check it against the database price too? If it's an OE part. - if ( - Math.abs(jobline.db_price - jobline.act_price) - - Math.abs(jobline.prt_dsmk_m) < - 0.01 - ) { - return false; - } - - if ( - //If it's not a discount line, then it definitely hasn't been counted yet. - jobline.db_ref !== "900510" && - jobline.db_ref !== "900511" - ) - return true; - - const ParentLine = joblines.find((j) => j.unq_seq === jobline.line_ref); - - return ParentLine && !(ParentLine.prt_dsmk_m && ParentLine.prt_dsmk_m !== 0); + return false; } exports.DiscountNotAlreadyCounted = DiscountNotAlreadyCounted; @@ -964,3 +1106,35 @@ function ParseCalopCode(opcode) { if (!opcode) return []; return opcode.trim().split(" "); } + +function IsTrueOrYes(value) { + return value === true || value === "Y" || value === "y"; +} + +async function UpdateJobLines(joblinesToUpdate) { + if (joblinesToUpdate.length === 0) return; + const updateQueries = joblinesToUpdate.map((line, index) => + generateUpdateQuery(_.pick(line, ["id", "prt_dsmk_m", "prt_dsmk_p"]), index) + ); + const query = ` + mutation UPDATE_EST_LINES{ + ${updateQueries} + } + `; + + const result = await adminClient.request(query); +} + +const generateUpdateQuery = (lineToUpdate, index) => { + return ` + update_joblines${index}: update_joblines(where: { id: { _eq: "${ + lineToUpdate.id + }" } }, _set: ${JSON.stringify(lineToUpdate).replace( + /"(\w+)"\s*:/g, + "$1:" + )}) { + returning { + id + } + }`; +}; diff --git a/setadmin.js b/setadmin.js index c62f8c0a2..1f4f9dc0b 100644 --- a/setadmin.js +++ b/setadmin.js @@ -1,6 +1,6 @@ var { admin } = require("./server/firebase/firebase-handler"); -const uidToMakeAdmin = "yTvpfkcNnGckLd1JnoXC7bTdvtu1"; +const uidToMakeAdmin = "fIaZcVQQfUR12Fu14I2fyA5vXbp1"; admin .auth()