From 8d5202f46d486e5c1d5709b036e5544ef9572c4f Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 31 Oct 2022 10:42:42 -0700 Subject: [PATCH 1/5] WIP PBS AP. --- .eslintrc.json | 4 +- bodyshop_translations.babel | 21 +++ .../dms-allocations-summary-ap.component.jsx | 138 ++++++++++++++ .../components/header/header.component.jsx | 10 +- .../payable-export-all-button.component.jsx | 8 + .../payable-export-button.component.jsx | 8 + ...p-info.responsibilitycenters.component.jsx | 121 +++++++++++- .../dms-payables/dms-payables.container.jsx | 178 ++++++++++++++++++ .../pages/manage/manage.page.component.jsx | 4 + client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + server.js | 6 +- server/accounting/pbs/pbs-ap-allocations.js | 138 ++++++++++++++ server/graphql-client/queries.js | 42 +++++ server/web-sockets/web-socket.js | 21 ++- 16 files changed, 690 insertions(+), 12 deletions(-) create mode 100644 client/src/components/dms-allocations-summary-ap/dms-allocations-summary-ap.component.jsx create mode 100644 client/src/pages/dms-payables/dms-payables.container.jsx create mode 100644 server/accounting/pbs/pbs-ap-allocations.js diff --git a/.eslintrc.json b/.eslintrc.json index 348764d46..3b158ef3e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -16,6 +16,6 @@ "rules": { "no-console": "off" }, - "settings": {}, - "plugins": ["cypress"] + "settings": {} + //"plugins": ["cypress"] } diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index bd442b8d8..b3627be24 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -6921,6 +6921,27 @@ + + federal_tax_itc + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + gst_override false diff --git a/client/src/components/dms-allocations-summary-ap/dms-allocations-summary-ap.component.jsx b/client/src/components/dms-allocations-summary-ap/dms-allocations-summary-ap.component.jsx new file mode 100644 index 000000000..7c4ca3b36 --- /dev/null +++ b/client/src/components/dms-allocations-summary-ap/dms-allocations-summary-ap.component.jsx @@ -0,0 +1,138 @@ +import { SyncOutlined } from "@ant-design/icons"; +import { Button, Card, Table, Typography } from "antd"; +import Dinero from "dinero.js"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; + +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser + bodyshop: selectBodyshop, +}); + +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(DmsAllocationsSummaryAp); + +export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) { + const { t } = useTranslation(); + const [allocationsSummary, setAllocationsSummary] = useState([]); + + useEffect(() => { + if (socket.connected) { + socket.emit("pbs-calculate-allocations-ap", billids, (ack) => { + setAllocationsSummary(ack); + socket.allocationsSummary = ack; + }); + } + }, [socket, socket.connected, billids]); + + const columns = [ + { + title: t("jobs.fields.dms.center"), + dataIndex: "center", + key: "center", + }, + { + title: t("jobs.fields.dms.sale"), + dataIndex: "sale", + key: "sale", + render: (text, record) => Dinero(record.sale).toFormat(), + }, + { + title: t("jobs.fields.dms.cost"), + dataIndex: "cost", + key: "cost", + render: (text, record) => Dinero(record.cost).toFormat(), + }, + { + title: t("jobs.fields.dms.sale_dms_acctnumber"), + dataIndex: "sale_dms_acctnumber", + key: "sale_dms_acctnumber", + render: (text, record) => + record.profitCenter && record.profitCenter.dms_acctnumber, + }, + { + title: t("jobs.fields.dms.cost_dms_acctnumber"), + dataIndex: "cost_dms_acctnumber", + key: "cost_dms_acctnumber", + render: (text, record) => + record.costCenter && record.costCenter.dms_acctnumber, + }, + { + title: t("jobs.fields.dms.dms_wip_acctnumber"), + dataIndex: "dms_wip_acctnumber", + key: "dms_wip_acctnumber", + render: (text, record) => + record.costCenter && record.costCenter.dms_wip_acctnumber, + }, + ]; + + return ( + { + socket.emit("pbs-calculate-allocations-ap", billids, (ack) => + setAllocationsSummary(ack) + ); + }} + > + + + } + > + { + const totals = + allocationsSummary && + allocationsSummary.reduce( + (acc, val) => { + return { + totalSale: acc.totalSale.add(Dinero(val.sale)), + totalCost: acc.totalCost.add(Dinero(val.cost)), + }; + }, + { + totalSale: Dinero(), + totalCost: Dinero(), + } + ); + + return ( + + + + {t("general.labels.totals")} + + + + {totals && totals.totalSale.toFormat()} + + + { + // totals.totalCost.toFormat() + } + + + + + ); + }} + /> + + ); +} diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 85aa0c200..7da393683 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -89,6 +89,11 @@ function Header({ {}, bodyshop && bodyshop.imexshopid ); + const { DmsAp } = useTreatments( + ["DmsAp"], + {}, + bodyshop && bodyshop.imexshopid + ); const { t } = useTranslation(); @@ -264,10 +269,11 @@ function Header({ {t("menus.header.accounting-receivables")} - {!( + {(!( (bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber) - ) && ( + ) || + DmsAp.treatment === "on") && ( {t("menus.header.accounting-payables")} 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 fdebc2335..c01853414 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 @@ -14,6 +14,7 @@ import { import { logImEXEvent } from "../../firebase/firebase.utils"; import _ from "lodash"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; +import { Link } from "react-router-dom"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -176,6 +177,13 @@ export function PayableExportAll({ setLoading(false); }; + if (bodyshop.pbs_serialnumber) + return ( + + + + ); + return ( + + ); + return ( + + + + {/* */} + + + + + +
+ + + + + + } + > + + +
+ + + + ); +} + +export const determineDmsType = (bodyshop) => { + if (bodyshop.cdk_dealerid) return "cdk"; + else { + return "pbs"; + } +}; diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx index 97f8cb71c..548e9dcf2 100644 --- a/client/src/pages/manage/manage.page.component.jsx +++ b/client/src/pages/manage/manage.page.component.jsx @@ -164,6 +164,9 @@ const EmailTest = lazy(() => ); const Dashboard = lazy(() => import("../dashboard/dashboard.container")); const Dms = lazy(() => import("../dms/dms.container")); +const DmsPayables = lazy(() => + import("../dms-payables/dms-payables.container") +); const { Content, Footer } = Layout; @@ -391,6 +394,7 @@ export function Manage({ match, conflict, bodyshop }) { + ); diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 6996dc40d..11718ed7f 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -437,6 +437,7 @@ "ar": "Accounts Receivable", "ats": "ATS", "federal_tax": "Federal Tax", + "federal_tax_itc": "Federal Tax Credit", "gst_override": "GST Override Account #", "la1": "LA1", "la2": "LA2", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 0fe3570c6..984d5d8e7 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -437,6 +437,7 @@ "ar": "", "ats": "", "federal_tax": "", + "federal_tax_itc": "", "gst_override": "", "la1": "", "la2": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index c4bb4a09c..ae70715af 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -437,6 +437,7 @@ "ar": "", "ats": "", "federal_tax": "", + "federal_tax_itc": "", "gst_override": "", "la1": "", "la2": "", diff --git a/server.js b/server.js index 1780ab1d5..327008c39 100644 --- a/server.js +++ b/server.js @@ -64,11 +64,7 @@ app.use( //Email Based Paths. var sendEmail = require("./server/email/sendemail.js"); app.post("/sendemail", fb.validateFirebaseIdToken, sendEmail.sendEmail); -app.post( - "/emailbounce", -bodyParser.text(), - sendEmail.emailBounce -); +app.post("/emailbounce", bodyParser.text(), sendEmail.emailBounce); //Test route to ensure Express is responding. app.get("/test", async function (req, res) { diff --git a/server/accounting/pbs/pbs-ap-allocations.js b/server/accounting/pbs/pbs-ap-allocations.js new file mode 100644 index 000000000..ebf52cac7 --- /dev/null +++ b/server/accounting/pbs/pbs-ap-allocations.js @@ -0,0 +1,138 @@ +const path = require("path"); +require("dotenv").config({ + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), +}); +const GraphQLClient = require("graphql-request").GraphQLClient; + +const queries = require("../../graphql-client/queries"); +const CdkBase = require("../../web-sockets/web-socket"); +const moment = require("moment"); +const Dinero = require("dinero.js"); + +exports.default = async function (socket, billids) { + try { + CdkBase.createLogEvent( + socket, + "DEBUG", + `Received request to calculate allocations for ${billids}` + ); + const { bills, bodyshops } = await QueryBillData(socket, billids); + const bodyshop = bodyshops[0]; + const transactionLines = []; + + bills.forEach((bill) => { + //Keep the allocations at the bill level. + const billHash = { + [bodyshop.md_responsibility_centers.taxes.federal_itc.name]: { + Account: + bodyshop.md_responsibility_centers.taxes.federal_itc.dms_acctnumber, + //ControlNumber: "String", //need to figure this out still? + Amount: Dinero(), + // Comment: "String", + //AdditionalInfo: "String", + InvoiceNumber: bill.invoice_number, + InvoiceDate: moment(bill.date).tz(bodyshop.timezone).toISOString(), + }, + [bodyshop.md_responsibility_centers.taxes.state.name]: { + Account: + bodyshop.md_responsibility_centers.taxes.state.dms_acctnumber, + //ControlNumber: "String", //need to figure this out still? + Amount: Dinero(), + // Comment: "String", + //AdditionalInfo: "String", + InvoiceNumber: bill.invoice_number, + InvoiceDate: moment(bill.date).tz(bodyshop.timezone).toISOString(), + }, + }; + + bill.billlines.forEach((bl) => { + let lineDinero = Dinero({ + amount: Math.round((bl.actual_cost || 0) * 100), + }) + .multiply(bl.quantity) + .multiply(bill.is_credit_memo ? -1 : 1); + const cc = getCostAccount(bl, bodyshop.md_responsibility_centers); + + if (!billHash[cc.name]) { + billHash[cc.name] = { + Account: cc.dms_acctnumber, + //ControlNumber: "String", //need to figure this out still? + Amount: Dinero(), + // Comment: "String", + //AdditionalInfo: "String", + InvoiceNumber: bill.invoice_number, + InvoiceDate: moment(bill.date).tz(bodyshop.timezone).toISOString(), + }; + } + + //Add the line amount. + + billHash[cc.name] = { + ...billHash[cc.name], + Amount: billHash[cc.name].Amount.add(lineDinero), + }; + + //Does the line have taxes? + if (bl.applicable_taxes.federal) { + billHash[bodyshop.md_responsibility_centers.taxes.federal_itc.name] = + { + ...bodyshop.md_responsibility_centers.taxes.federal_itc.name, + Amount: billHash[ + bodyshop.md_responsibility_centers.taxes.federal_itc.name + ].Amount.add(lineDinero.percentage(bl.federal_tax_rate || 0)), + }; + } + if (bl.applicable_taxes.state) { + billHash[bodyshop.md_responsibility_centers.taxes.state.name] = { + ...bodyshop.md_responsibility_centers.taxes.state.name, + Amount: billHash[ + bodyshop.md_responsibility_centers.taxes.state.name + ].Amount.add(lineDinero.percentage(bl.state_tax_rate || 0)), + }; + } + }); + + Object.keys(billHash).map((key) => { + transactionLines.push(billHash[key]); + }); + }); + + return transactionLines; + } catch (error) { + CdkBase.createLogEvent( + socket, + "ERROR", + `Error encountered in CdkCalculateAllocations. ${error}` + ); + } +}; + +async function QueryBillData(socket, billids) { + CdkBase.createLogEvent( + socket, + "DEBUG", + `Querying bill data for id(s) ${billids}` + ); + const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {}); + const result = await client + .setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` }) + .request(queries.GET_PBS_AP_ALLOCATIONS, { billids: billids }); + CdkBase.createLogEvent( + socket, + "TRACE", + `Bill data query result ${JSON.stringify(result, null, 2)}` + ); + return result; +} + +//@returns the account object. +function getCostAccount(billline, respcenters) { + if (!billline.cost_center) return null; + + const acctName = respcenters.defaults.costs[billline.cost_center]; + + return respcenters.costs.find((c) => c.name === acctName); +} diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index dc10f8e83..9dc38386f 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -1633,3 +1633,45 @@ mutation ($sesid: String!, $status: String, $context: jsonb) { } } }`; + +exports.GET_PBS_AP_ALLOCATIONS = ` +query GET_PBS_AP_ALLOCATIONS($billids: [uuid!]) { + bodyshops(where: {associations: {active: {_eq: true}}}) { + md_responsibility_centers + timezone + } + bills(where: {id: {_in: $billids}}) { + id + date + isinhouse + invoice_number + federal_tax_rate + is_credit_memo + jobid + job { + id + ro_number + } + local_tax_rate + state_tax_rate + total + vendorid + vendor { + id + name + } + billlines { + id + actual_cost + actual_price + applicable_taxes + cost_center + deductedfromlbr + lbr_adjustment + quantity + } + } +} + + +`; diff --git a/server/web-sockets/web-socket.js b/server/web-sockets/web-socket.js index 53076f0cc..11b16cfbe 100644 --- a/server/web-sockets/web-socket.js +++ b/server/web-sockets/web-socket.js @@ -22,6 +22,9 @@ const { PbsSelectedCustomer, } = require("../accounting/pbs/pbs-job-export"); +const PbsCalculateAllocationsAp = + require("../accounting/pbs/pbs-ap-allocations").default; + io.use(function (socket, next) { try { if (socket.handshake.auth.token) { @@ -101,7 +104,7 @@ io.on("connection", (socket) => { }); //END CDK - //PBS + //PBS AR socket.on("pbs-calculate-allocations", async (jobid, callback) => { const allocations = await CdkCalculateAllocations(socket, jobid); createLogEvent(socket, "DEBUG", `Allocations calculated.`); @@ -125,7 +128,21 @@ io.on("connection", (socket) => { socket.selectedCustomerId = selectedCustomerId; PbsSelectedCustomer(socket, selectedCustomerId); }); - //End PBS + //End PBS AR + + //PBS AP + socket.on("pbs-calculate-allocations-ap", async (billids, callback) => { + const allocations = await PbsCalculateAllocationsAp(socket, billids); + createLogEvent(socket, "DEBUG", `AP Allocations calculated.`); + createLogEvent( + socket, + "TRACE", + `Allocations calculated. ${JSON.stringify(allocations, null, 2)}` + ); + + callback(allocations); + }); + //END PBS AP socket.on("disconnect", () => { createLogEvent(socket, "DEBUG", `User disconnected.`); From ede1cdb89bae93f93ca8af1ab452d64549d88f72 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 31 Oct 2022 15:59:00 -0700 Subject: [PATCH 2/5] WIP PBS AP. --- bodyshop_translations.babel | 63 +++++++ .../dms-allocations-summary-ap.component.jsx | 128 ++++++------- ...p-info.responsibilitycenters.component.jsx | 127 ++++++------- .../vendors-form/vendors-form.component.jsx | 31 +++- client/src/graphql/vendors.queries.js | 1 + .../dms-payables/dms-payables.container.jsx | 19 +- client/src/translations/en_us/common.json | 3 + client/src/translations/es/common.json | 3 + client/src/translations/fr/common.json | 3 + hasura/metadata/tables.yaml | 3 + .../down.sql | 4 + .../up.sql | 2 + server/accounting/pbs/pbs-ap-allocations.js | 169 ++++++++++++++++-- server/accounting/pbs/pbs-job-export.js | 2 + server/graphql-client/queries.js | 20 +++ server/web-sockets/web-socket.js | 14 +- 16 files changed, 418 insertions(+), 174 deletions(-) create mode 100644 hasura/migrations/1667251913323_alter_table_public_vendors_add_column_dmsid/down.sql create mode 100644 hasura/migrations/1667251913323_alter_table_public_vendors_add_column_dmsid/up.sql diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index b3627be24..d9875fede 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -22351,6 +22351,27 @@ + + amount + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + center false @@ -22540,6 +22561,27 @@ + + lines + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + name1 false @@ -47259,6 +47301,27 @@ + + dmsid + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + due_date false diff --git a/client/src/components/dms-allocations-summary-ap/dms-allocations-summary-ap.component.jsx b/client/src/components/dms-allocations-summary-ap/dms-allocations-summary-ap.component.jsx index 7c4ca3b36..3dccf03f8 100644 --- a/client/src/components/dms-allocations-summary-ap/dms-allocations-summary-ap.component.jsx +++ b/client/src/components/dms-allocations-summary-ap/dms-allocations-summary-ap.component.jsx @@ -1,5 +1,5 @@ import { SyncOutlined } from "@ant-design/icons"; -import { Button, Card, Table, Typography } from "antd"; +import { Button, Card, Form, Input, Table } from "antd"; import Dinero from "dinero.js"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -29,52 +29,49 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) { if (socket.connected) { socket.emit("pbs-calculate-allocations-ap", billids, (ack) => { setAllocationsSummary(ack); + socket.allocationsSummary = ack; }); } }, [socket, socket.connected, billids]); - + console.log(allocationsSummary); const columns = [ { - title: t("jobs.fields.dms.center"), - dataIndex: "center", - key: "center", + title: t("jobs.fields.ro_number"), + dataIndex: ["Posting", "Reference"], + key: "reference", }, + { - title: t("jobs.fields.dms.sale"), - dataIndex: "sale", - key: "sale", - render: (text, record) => Dinero(record.sale).toFormat(), - }, - { - title: t("jobs.fields.dms.cost"), - dataIndex: "cost", - key: "cost", - render: (text, record) => Dinero(record.cost).toFormat(), - }, - { - title: t("jobs.fields.dms.sale_dms_acctnumber"), - dataIndex: "sale_dms_acctnumber", - key: "sale_dms_acctnumber", - render: (text, record) => - record.profitCenter && record.profitCenter.dms_acctnumber, - }, - { - title: t("jobs.fields.dms.cost_dms_acctnumber"), - dataIndex: "cost_dms_acctnumber", - key: "cost_dms_acctnumber", - render: (text, record) => - record.costCenter && record.costCenter.dms_acctnumber, - }, - { - title: t("jobs.fields.dms.dms_wip_acctnumber"), - dataIndex: "dms_wip_acctnumber", - key: "dms_wip_acctnumber", - render: (text, record) => - record.costCenter && record.costCenter.dms_wip_acctnumber, + title: t("jobs.fields.dms.lines"), + dataIndex: "Lines", + key: "Lines", + render: (text, record) => ( +
+ + + + + + {record.Posting.Lines.map((l, idx) => ( + + + + + + ))} +
{t("bills.fields.invoice_number")}{t("bodyshop.fields.dms.dms_acctnumber")}{t("jobs.fields.dms.amount")}
{l.InvoiceNumber}{l.Account}{Dinero(l.Amount).toFormat()}
+ ), }, ]; + const handleFinish = async (values) => { + socket.emit(`pbs-export-ap`, { + billids, + txEnvelope: values, + }); + }; + return ( `${record.InvoiceNumber}${record.Account}`} dataSource={allocationsSummary} locale={{ emptyText: t("dms.labels.refreshallocations") }} - summary={() => { - const totals = - allocationsSummary && - allocationsSummary.reduce( - (acc, val) => { - return { - totalSale: acc.totalSale.add(Dinero(val.sale)), - totalCost: acc.totalCost.add(Dinero(val.cost)), - }; - }, - { - totalSale: Dinero(), - totalCost: Dinero(), - } - ); - - return ( - - - - {t("general.labels.totals")} - - - - {totals && totals.totalSale.toFormat()} - - - { - // totals.totalCost.toFormat() - } - - - - - ); - }} /> +
+ + + + +
); } diff --git a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx index 315add12b..6753df364 100644 --- a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx +++ b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx @@ -4536,68 +4536,71 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { */} - {/* - - - - - - - - - - - - - - - - */} + + {DmsAp.treatment === "on" && ( + + {/* + + */} + {/* + + */} + + + + + + + + + + + )} Refund}> {/* ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(VendorsFormComponent); + +export function VendorsFormComponent({ + bodyshop, form, formLoading, handleDelete, @@ -29,6 +46,12 @@ export default function VendorsFormComponent({ }) { const { t } = useTranslation(); const client = useApolloClient(); + const { DmsAp } = useTreatments( + ["DmsAp"], + {}, + bodyshop && bodyshop.imexshopid + ); + const { getFieldValue } = form; return (
@@ -184,6 +207,12 @@ export default function VendorsFormComponent({ // } + + {DmsAp.treatment === "on" && ( + + + + )} {t("vendors.labels.preferredmakes")} {(fields, { add, remove }) => { diff --git a/client/src/graphql/vendors.queries.js b/client/src/graphql/vendors.queries.js index f18b124ff..7fddbba5b 100644 --- a/client/src/graphql/vendors.queries.js +++ b/client/src/graphql/vendors.queries.js @@ -18,6 +18,7 @@ export const QUERY_VENDOR_BY_ID = gql` street1 active phone + dmsid } } `; diff --git a/client/src/pages/dms-payables/dms-payables.container.jsx b/client/src/pages/dms-payables/dms-payables.container.jsx index c9a55bc31..3e38428e1 100644 --- a/client/src/pages/dms-payables/dms-payables.container.jsx +++ b/client/src/pages/dms-payables/dms-payables.container.jsx @@ -1,5 +1,4 @@ import { Button, Card, Col, notification, Row, Select, Space } from "antd"; -import queryString from "query-string"; import React, { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -7,9 +6,7 @@ import { useHistory, useLocation } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import SocketIO from "socket.io-client"; import DmsAllocationsSummaryApComponent from "../../components/dms-allocations-summary-ap/dms-allocations-summary-ap.component"; -import DmsCustomerSelector from "../../components/dms-customer-selector/dms-customer-selector.component"; import DmsLogEvents from "../../components/dms-log-events/dms-log-events.component"; -import DmsPostForm from "../../components/dms-post-form/dms-post-form.component"; import { auth } from "../../firebase/firebase.utils"; import { setBreadcrumbs, @@ -47,10 +44,9 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { const [logLevel, setLogLevel] = useState("DEBUG"); const history = useHistory(); const [logs, setLogs] = useState([]); - const search = queryString.parse(useLocation().search); + const { state } = useLocation(); - const { jobId } = search; const logsRef = useRef(null); useEffect(() => { @@ -109,24 +105,13 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { return (
- DMS PAYABLES SCREEN - + - - {/* */} - - -
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 11718ed7f..473f2a232 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1355,6 +1355,7 @@ "depreciation_taxes": "Depreciation/Taxes", "dms": { "address": "Customer Address", + "amount": "Amount", "center": "Center", "cost": "Cost", "cost_dms_acctnumber": "Cost DMS Acct #", @@ -1364,6 +1365,7 @@ "id": "DMS ID", "inservicedate": "In Service Date", "journal": "Journal #", + "lines": "Posting Lines", "name1": "Customer Name", "payer": { "amount": "Amount", @@ -2801,6 +2803,7 @@ "country": "Country", "discount": "Discount % (as decimal)", "display_name": "Display Name", + "dmsid": "DMS ID", "due_date": "Payment Due Date (# of days)", "email": "Contact Email", "favorite": "Favorite?", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 984d5d8e7..7df3d4062 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -1355,6 +1355,7 @@ "depreciation_taxes": "Depreciación / Impuestos", "dms": { "address": "", + "amount": "", "center": "", "cost": "", "cost_dms_acctnumber": "", @@ -1364,6 +1365,7 @@ "id": "", "inservicedate": "", "journal": "", + "lines": "", "name1": "", "payer": { "amount": "", @@ -2801,6 +2803,7 @@ "country": "País", "discount": "% De descuento", "display_name": "Nombre para mostrar", + "dmsid": "", "due_date": "Fecha de vencimiento del pago", "email": "Email de contacto", "favorite": "¿Favorito?", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index ae70715af..a17359079 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -1355,6 +1355,7 @@ "depreciation_taxes": "Amortissement / taxes", "dms": { "address": "", + "amount": "", "center": "", "cost": "", "cost_dms_acctnumber": "", @@ -1364,6 +1365,7 @@ "id": "", "inservicedate": "", "journal": "", + "lines": "", "name1": "", "payer": { "amount": "", @@ -2801,6 +2803,7 @@ "country": "Pays", "discount": "Remise %", "display_name": "Afficher un nom", + "dmsid": "", "due_date": "Date limite de paiement", "email": "Email du contact", "favorite": "Préféré?", diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 2a338443a..00be05ed1 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -5396,6 +5396,7 @@ - country - created_at - discount + - dmsid - due_date - email - favorite @@ -5418,6 +5419,7 @@ - country - created_at - discount + - dmsid - due_date - email - favorite @@ -5450,6 +5452,7 @@ - country - created_at - discount + - dmsid - due_date - email - favorite diff --git a/hasura/migrations/1667251913323_alter_table_public_vendors_add_column_dmsid/down.sql b/hasura/migrations/1667251913323_alter_table_public_vendors_add_column_dmsid/down.sql new file mode 100644 index 000000000..e7d1107de --- /dev/null +++ b/hasura/migrations/1667251913323_alter_table_public_vendors_add_column_dmsid/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"."vendors" add column "dmsid" text +-- null; diff --git a/hasura/migrations/1667251913323_alter_table_public_vendors_add_column_dmsid/up.sql b/hasura/migrations/1667251913323_alter_table_public_vendors_add_column_dmsid/up.sql new file mode 100644 index 000000000..25b9e8232 --- /dev/null +++ b/hasura/migrations/1667251913323_alter_table_public_vendors_add_column_dmsid/up.sql @@ -0,0 +1,2 @@ +alter table "public"."vendors" add column "dmsid" text + null; diff --git a/server/accounting/pbs/pbs-ap-allocations.js b/server/accounting/pbs/pbs-ap-allocations.js index ebf52cac7..036b0c538 100644 --- a/server/accounting/pbs/pbs-ap-allocations.js +++ b/server/accounting/pbs/pbs-ap-allocations.js @@ -11,8 +11,47 @@ const queries = require("../../graphql-client/queries"); const CdkBase = require("../../web-sockets/web-socket"); const moment = require("moment"); const Dinero = require("dinero.js"); +const AxiosLib = require("axios").default; +const axios = AxiosLib.create(); +const { PBS_ENDPOINTS, PBS_CREDENTIALS } = require("./pbs-constants"); +const { CheckForErrors } = require("./pbs-job-export"); +const uuid = require("uuid").v4; +axios.interceptors.request.use((x) => { + const socket = x.socket; -exports.default = async function (socket, billids) { + const headers = { + ...x.headers.common, + ...x.headers[x.method], + ...x.headers, + }; + const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${ + x.url + } | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`; + console.log(printable); + + CdkBase.createJsonEvent(socket, "TRACE", `Raw Request: ${printable}`, x.data); + + return x; +}); + +axios.interceptors.response.use((x) => { + const socket = x.config.socket; + + const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify( + x.data + )}`; + console.log(printable); + CdkBase.createJsonEvent( + socket, + "TRACE", + `Raw Response: ${printable}`, + x.data + ); + + return x; +}); + +async function PbsCalculateAllocationsAp(socket, billids) { try { CdkBase.createLogEvent( socket, @@ -21,28 +60,48 @@ exports.default = async function (socket, billids) { ); const { bills, bodyshops } = await QueryBillData(socket, billids); const bodyshop = bodyshops[0]; - const transactionLines = []; + socket.bodyshop = bodyshop; + socket.bills = bills; + + //Each bill will enter it's own top level transaction. + + const transactionlist = []; bills.forEach((bill) => { //Keep the allocations at the bill level. + + const transactionObject = { + SerialNumber: socket.bodyshop.pbs_serialnumber, + billid: bill.id, + Posting: { + Reference: bill.job.ro_number, + JournalCode: socket.txEnvelope ? socket.txEnvelope.journal : null, + TransactionDate: moment().tz(socket.bodyshop.timezone).toISOString(), //"0001-01-01T00:00:00.0000000Z", + //Description: "Bulk AP posting.", + //AdditionalInfo: "String", + Source: "ImEX Online", + Lines: [], //socket.apAllocations, + }, + }; + const billHash = { [bodyshop.md_responsibility_centers.taxes.federal_itc.name]: { Account: bodyshop.md_responsibility_centers.taxes.federal_itc.dms_acctnumber, - //ControlNumber: "String", //need to figure this out still? + ControlNumber: bill.vendor.dmsid, Amount: Dinero(), // Comment: "String", - //AdditionalInfo: "String", + AdditionalInfo: bill.vendor.name, InvoiceNumber: bill.invoice_number, InvoiceDate: moment(bill.date).tz(bodyshop.timezone).toISOString(), }, [bodyshop.md_responsibility_centers.taxes.state.name]: { Account: bodyshop.md_responsibility_centers.taxes.state.dms_acctnumber, - //ControlNumber: "String", //need to figure this out still? + ControlNumber: bill.vendor.dmsid, Amount: Dinero(), // Comment: "String", - //AdditionalInfo: "String", + AdditionalInfo: bill.vendor.name, InvoiceNumber: bill.invoice_number, InvoiceDate: moment(bill.date).tz(bodyshop.timezone).toISOString(), }, @@ -59,17 +118,16 @@ exports.default = async function (socket, billids) { if (!billHash[cc.name]) { billHash[cc.name] = { Account: cc.dms_acctnumber, - //ControlNumber: "String", //need to figure this out still? + ControlNumber: bill.vendor.dmsid, Amount: Dinero(), // Comment: "String", - //AdditionalInfo: "String", + AdditionalInfo: bill.vendor.name, InvoiceNumber: bill.invoice_number, InvoiceDate: moment(bill.date).tz(bodyshop.timezone).toISOString(), }; } //Add the line amount. - billHash[cc.name] = { ...billHash[cc.name], Amount: billHash[cc.name].Amount.add(lineDinero), @@ -79,36 +137,57 @@ exports.default = async function (socket, billids) { if (bl.applicable_taxes.federal) { billHash[bodyshop.md_responsibility_centers.taxes.federal_itc.name] = { - ...bodyshop.md_responsibility_centers.taxes.federal_itc.name, + ...billHash[ + bodyshop.md_responsibility_centers.taxes.federal_itc.name + ], Amount: billHash[ bodyshop.md_responsibility_centers.taxes.federal_itc.name - ].Amount.add(lineDinero.percentage(bl.federal_tax_rate || 0)), + ].Amount.add(lineDinero.percentage(bill.federal_tax_rate || 0)), }; } if (bl.applicable_taxes.state) { billHash[bodyshop.md_responsibility_centers.taxes.state.name] = { - ...bodyshop.md_responsibility_centers.taxes.state.name, + ...billHash[bodyshop.md_responsibility_centers.taxes.state.name], Amount: billHash[ bodyshop.md_responsibility_centers.taxes.state.name - ].Amount.add(lineDinero.percentage(bl.state_tax_rate || 0)), + ].Amount.add(lineDinero.percentage(bill.state_tax_rate || 0)), }; } + //End tax check + }); + + let APAmount = Dinero(); + Object.keys(billHash).map((key) => { + if (billHash[key].Amount.getAmount() > 0) { + transactionObject.Posting.Lines.push(billHash[key]); + APAmount = APAmount.add(billHash[key].Amount); //Calculate the total expense for the bill iteratively to create the corresponding credit to AP. + } }); - Object.keys(billHash).map((key) => { - transactionLines.push(billHash[key]); + transactionObject.Posting.Lines.push({ + Account: bodyshop.md_responsibility_centers.ap.dms_acctnumber, + ControlNumber: bill.vendor.dmsid, + Amount: APAmount.multiply(-1), + // Comment: "String", + AdditionalInfo: bill.vendor.name, + InvoiceNumber: bill.invoice_number, + InvoiceDate: moment(bill.date).tz(bodyshop.timezone).toISOString(), }); + + transactionlist.push(transactionObject); }); - return transactionLines; + return transactionlist; } catch (error) { CdkBase.createLogEvent( socket, "ERROR", - `Error encountered in CdkCalculateAllocations. ${error}` + `Error encountered in PbsCalculateAllocationsAp. ${error}` ); } -}; +} + +exports.PbsCalculateAllocationsAp = PbsCalculateAllocationsAp; async function QueryBillData(socket, billids) { CdkBase.createLogEvent( @@ -125,6 +204,7 @@ async function QueryBillData(socket, billids) { "TRACE", `Bill data query result ${JSON.stringify(result, null, 2)}` ); + return result; } @@ -136,3 +216,56 @@ function getCostAccount(billline, respcenters) { return respcenters.costs.find((c) => c.name === acctName); } + +exports.PbsExportAp = async function (socket, { billids, txEnvelope }) { + CdkBase.createLogEvent(socket, "DEBUG", `Exporting selected AP.`); + + //apAllocations has the same shap as the lines key for the accounting posting to PBS. + socket.apAllocations = await PbsCalculateAllocationsAp(socket, billids); + socket.txEnvelope = txEnvelope; + for (const allocation of socket.apAllocations) { + const { billid, ...restAllocation } = allocation; + const { data: AccountPostingChange } = await axios.post( + PBS_ENDPOINTS.AccountingPostingChange, + restAllocation, + { auth: PBS_CREDENTIALS, socket } + ); + + CheckForErrors(socket, AccountPostingChange); + + if (AccountPostingChange.WasSuccessful) { + CdkBase.createLogEvent(socket, "DEBUG", `Marking bill as exported.`); + await MarkApExported(socket, [billid]); + + // socket.emit("export-success", billids); + } else { + CdkBase.createLogEvent(socket, "ERROR", `Export was not succesful.`); + } + } +}; + +async function MarkApExported(socket, billids) { + CdkBase.createLogEvent( + socket, + "DEBUG", + `Marking bills as exported for id ${billids}` + ); + const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {}); + const result = await client + .setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` }) + .request(queries.MARK_BILLS_EXPORTED, { + billids, + bill: { + exported: true, + exported_at: new Date(), + }, + logs: socket.bills.map((bill) => ({ + bodyshopid: socket.bodyshop.id, + billid: bill.id, + successful: true, + useremail: socket.user.email, + })), + }); + + return result; +} diff --git a/server/accounting/pbs/pbs-job-export.js b/server/accounting/pbs/pbs-job-export.js index 58173be58..ee0b3c8a8 100644 --- a/server/accounting/pbs/pbs-job-export.js +++ b/server/accounting/pbs/pbs-job-export.js @@ -182,6 +182,8 @@ async function CheckForErrors(socket, response) { } } +exports.CheckForErrors = CheckForErrors; + async function QueryJobData(socket, jobid) { CdkBase.createLogEvent(socket, "DEBUG", `Querying job data for id ${jobid}`); const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {}); diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index 9dc38386f..0320aa3d5 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -1506,6 +1506,23 @@ mutation MARK_JOB_EXPORTED($jobId: uuid!, $job: jobs_set_input!, $log: exportlog } `; +exports.MARK_BILLS_EXPORTED = ` +mutation UPDATE_BILLS($billids: [uuid!]!, $bill: bills_set_input!, $logs: [exportlog_insert_input!]!) { + update_bills(where: {id: {_in: $billids}}, _set: $bill) { + returning { + id + exported + exported_at + } + } + insert_exportlog(objects: $logs) { +returning{ + id +} + } +} +`; + exports.INSERT_EXPORT_LOG = ` mutation INSERT_EXPORT_LOG($log: exportlog_insert_input!) { insert_exportlog_one(object: $log) { @@ -1639,6 +1656,8 @@ query GET_PBS_AP_ALLOCATIONS($billids: [uuid!]) { bodyshops(where: {associations: {active: {_eq: true}}}) { md_responsibility_centers timezone + pbs_serialnumber + id } bills(where: {id: {_in: $billids}}) { id @@ -1659,6 +1678,7 @@ query GET_PBS_AP_ALLOCATIONS($billids: [uuid!]) { vendor { id name + dmsid } billlines { id diff --git a/server/web-sockets/web-socket.js b/server/web-sockets/web-socket.js index 11b16cfbe..67886077e 100644 --- a/server/web-sockets/web-socket.js +++ b/server/web-sockets/web-socket.js @@ -22,8 +22,10 @@ const { PbsSelectedCustomer, } = require("../accounting/pbs/pbs-job-export"); -const PbsCalculateAllocationsAp = - require("../accounting/pbs/pbs-ap-allocations").default; +const { + PbsCalculateAllocationsAp, + PbsExportAp, +} = require("../accounting/pbs/pbs-ap-allocations"); io.use(function (socket, next) { try { @@ -139,9 +141,15 @@ io.on("connection", (socket) => { "TRACE", `Allocations calculated. ${JSON.stringify(allocations, null, 2)}` ); - + socket.apAllocations = allocations; callback(allocations); }); + + socket.on("pbs-export-ap", ({ billids, txEnvelope }) => { + socket.txEnvelope = txEnvelope; + PbsExportAp(socket, { billids, txEnvelope }); + }); + //END PBS AP socket.on("disconnect", () => { From 6354ccee875267710c06b5399476bafd9e3eb461 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Tue, 1 Nov 2022 14:26:13 -0700 Subject: [PATCH 3/5] AP changes based on validation testing. --- server/accounting/pbs/pbs-ap-allocations.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/server/accounting/pbs/pbs-ap-allocations.js b/server/accounting/pbs/pbs-ap-allocations.js index 036b0c538..b4c6bf514 100644 --- a/server/accounting/pbs/pbs-ap-allocations.js +++ b/server/accounting/pbs/pbs-ap-allocations.js @@ -74,7 +74,7 @@ async function PbsCalculateAllocationsAp(socket, billids) { SerialNumber: socket.bodyshop.pbs_serialnumber, billid: bill.id, Posting: { - Reference: bill.job.ro_number, + Reference: bill.invoice_number, JournalCode: socket.txEnvelope ? socket.txEnvelope.journal : null, TransactionDate: moment().tz(socket.bodyshop.timezone).toISOString(), //"0001-01-01T00:00:00.0000000Z", //Description: "Bulk AP posting.", @@ -159,7 +159,10 @@ async function PbsCalculateAllocationsAp(socket, billids) { let APAmount = Dinero(); Object.keys(billHash).map((key) => { if (billHash[key].Amount.getAmount() > 0) { - transactionObject.Posting.Lines.push(billHash[key]); + transactionObject.Posting.Lines.push({ + ...billHash[key], + Amount: billHash[key].Amount.toFormat("0.00"), + }); APAmount = APAmount.add(billHash[key].Amount); //Calculate the total expense for the bill iteratively to create the corresponding credit to AP. } }); @@ -167,7 +170,7 @@ async function PbsCalculateAllocationsAp(socket, billids) { transactionObject.Posting.Lines.push({ Account: bodyshop.md_responsibility_centers.ap.dms_acctnumber, ControlNumber: bill.vendor.dmsid, - Amount: APAmount.multiply(-1), + Amount: APAmount.multiply(-1).toFormat("0.00"), // Comment: "String", AdditionalInfo: bill.vendor.name, InvoiceNumber: bill.invoice_number, From b3ed9106f03987738858c5c718b592d41eb69523 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Thu, 3 Nov 2022 08:25:44 -0700 Subject: [PATCH 4/5] Add notification of complete export. --- bodyshop_translations.babel | 44 +++++++- .../dms-allocations-summary-ap.component.jsx | 32 +++++- .../dms-payables/dms-payables.container.jsx | 106 +++++++++--------- client/src/translations/en_us/common.json | 2 + client/src/translations/es/common.json | 2 + client/src/translations/fr/common.json | 2 + server/accounting/pbs/pbs-ap-allocations.js | 15 ++- server/graphql-client/queries.js | 4 +- 8 files changed, 147 insertions(+), 60 deletions(-) diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index d9875fede..fbf938dff 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -1,4 +1,4 @@ - +