From 8d5202f46d486e5c1d5709b036e5544ef9572c4f Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 31 Oct 2022 10:42:42 -0700 Subject: [PATCH] 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.`);