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", () => {