diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 3970446ba..1c0e48ea3 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -1085,6 +1085,48 @@ + + responsibilitycenter_accountdesc + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + responsibilitycenter_accountitem + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + responsibilitycenter_accountname false @@ -1127,9 +1169,72 @@ + + responsibilitycenter_rate + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + responsibilitycenters + + ap + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + ar + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + atp false diff --git a/client/src/components/_test/test.component.jsx b/client/src/components/_test/test.component.jsx index 37810cb23..3c5d96715 100644 --- a/client/src/components/_test/test.component.jsx +++ b/client/src/components/_test/test.component.jsx @@ -16,10 +16,6 @@ export default connect( mapDispatchToProps )(function Test({ bodyshop }) { const handle = async () => { - console.log( - "await auth.currentUser.getIdToken(true)", - await auth.currentUser.getIdToken(true) - ); const response = await axios.post( "/accounting/iif/receivables", { jobId: "661dd1d5-bf06-426f-8bd2-bd9e41de8eb1" }, 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 e5d2305b7..4e4f653e2 100644 --- a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx +++ b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx @@ -1,5 +1,5 @@ import { DeleteFilled } from "@ant-design/icons"; -import { Button, Form, Input, Select } from "antd"; +import { Button, Form, Input, Select, InputNumber } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import styled from "styled-components"; @@ -100,6 +100,38 @@ export default function ShopInfoResponsibilityCenterComponent({ form }) { > + + + + + + { remove(field.name); @@ -179,6 +211,38 @@ export default function ShopInfoResponsibilityCenterComponent({ form }) { > + + + + + + { remove(field.name); @@ -1028,7 +1092,7 @@ export default function ShopInfoResponsibilityCenterComponent({ form }) { "md_responsibility_centers", "taxes", "federal", - "account_name", + "accountname", ]} > @@ -1045,108 +1109,371 @@ export default function ShopInfoResponsibilityCenterComponent({ form }) { "md_responsibility_centers", "taxes", "federal", - "account_number", + "accountnumber", ]} > + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + +
- - - - - - - - - -
-
- - - - - - - - - -
+ + + + + + + + + + + + + + + + diff --git a/client/src/redux/user/user.sagas.js b/client/src/redux/user/user.sagas.js index 76d010241..8611eef0c 100644 --- a/client/src/redux/user/user.sagas.js +++ b/client/src/redux/user/user.sagas.js @@ -140,7 +140,7 @@ export function* checkInstanceIdSaga({ payload: uid }) { } catch (error) { console.log("error", error); //TODO error handling - } + } } export function* onSignInSuccess() { diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 2301a3bb6..47c730d2d 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -86,9 +86,14 @@ "invoice_state_tax_rate": "Invoices - State Tax Rate %", "logo_img_path": "Shop Logo", "responsibilitycenter": "Responsibility Center", + "responsibilitycenter_accountdesc": "Account Description", + "responsibilitycenter_accountitem": "Item", "responsibilitycenter_accountname": "Account Name", "responsibilitycenter_accountnumber": "Account Number", + "responsibilitycenter_rate": "Rate", "responsibilitycenters": { + "ap": "Accounts Payable", + "ar": "Accounts Receivable", "atp": "ATP", "federal_tax": "Federal Tax", "lab": "Body", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 7311933a7..8f7d5efdd 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -86,9 +86,14 @@ "invoice_state_tax_rate": "", "logo_img_path": "", "responsibilitycenter": "", + "responsibilitycenter_accountdesc": "", + "responsibilitycenter_accountitem": "", "responsibilitycenter_accountname": "", "responsibilitycenter_accountnumber": "", + "responsibilitycenter_rate": "", "responsibilitycenters": { + "ap": "", + "ar": "", "atp": "", "federal_tax": "", "lab": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 85da028cf..2a5f31046 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -86,9 +86,14 @@ "invoice_state_tax_rate": "", "logo_img_path": "", "responsibilitycenter": "", + "responsibilitycenter_accountdesc": "", + "responsibilitycenter_accountitem": "", "responsibilitycenter_accountname": "", "responsibilitycenter_accountnumber": "", + "responsibilitycenter_rate": "", "responsibilitycenters": { + "ap": "", + "ar": "", "atp": "", "federal_tax": "", "lab": "", diff --git a/server/accounting/iif/iif-receivables.js b/server/accounting/iif/iif-receivables.js index 80e38db72..eae100216 100644 --- a/server/accounting/iif/iif-receivables.js +++ b/server/accounting/iif/iif-receivables.js @@ -7,6 +7,7 @@ require("dotenv").config({ ), }); const queries = require("../../graphql-client/queries"); +const Dinero = require("dinero.js"); exports.default = async (req, res) => { const BearerToken = req.headers.authorization; @@ -23,14 +24,89 @@ exports.default = async (req, res) => { .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_JOBS_FOR_RECEIVABLES_EXPORT, { id: jobId }); + const { jobs_by_pk } = result; + const { bodyshop } = jobs_by_pk; + //Build the IIF file. const response = []; response.push(TRNS_HEADER); - response.push(generateInvoiceHeader(result.jobs_by_pk)); + response.push( + generateInvoiceHeader(jobs_by_pk, bodyshop.md_responsibility_centers.ar) + ); + + //Allocations + const invoice_allocation = jobs_by_pk.invoice_allocation; + Object.keys(invoice_allocation.partsAllocations).forEach( + (partsAllocationKey) => { + if ( + !!!invoice_allocation.partsAllocations[partsAllocationKey].allocations + ) + return; + + invoice_allocation.partsAllocations[ + partsAllocationKey + ].allocations.forEach((alloc) => { + response.push( + generateInvoiceLine( + jobs_by_pk, + alloc, + bodyshop.md_responsibility_centers + ) + ); + }); + } + ); + Object.keys(invoice_allocation.labMatAllocations).forEach( + (AllocationKey) => { + if (!!!invoice_allocation.labMatAllocations[AllocationKey].allocations) + return; + + invoice_allocation.labMatAllocations[AllocationKey].allocations.forEach( + (alloc) => { + response.push( + generateInvoiceLine( + jobs_by_pk, + alloc, + bodyshop.md_responsibility_centers + ) + ); + } + ); + } + ); + //End Allocations + + //Taxes + const taxMapping = bodyshop.md_responsibility_centers.taxes; + const { federal_tax, state_tax, local_tax } = JSON.parse( + jobs_by_pk.job_totals + ).totals; + const federal_tax_dinero = Dinero(federal_tax); + const state_tax_dinero = Dinero(state_tax); + const local_tax_dinero = Dinero(local_tax); + + if (federal_tax_dinero.getAmount() > 0) { + response.push( + generateTaxLine(jobs_by_pk, federal_tax_dinero, "federal", taxMapping) + ); + } + if (state_tax_dinero.getAmount() > 0) { + response.push( + generateTaxLine(jobs_by_pk, state_tax_dinero, "state", taxMapping) + ); + } + if (local_tax_dinero.getAmount() > 0) { + response.push( + generateTaxLine(jobs_by_pk, local_tax_dinero, "local", taxMapping) + ); + } + //End Taxes + response.push(END_TRNS); + //Prep the response and send it. res.setHeader("Content-type", "application/octet-stream"); res.setHeader("Content-disposition", "attachment; filename=file.txt"); - res.setHeader("filename", `${result.jobs_by_pk.ro_number}-RECEIVABLES.iif`); + res.setHeader("filename", `${jobs_by_pk.ro_number}-RECEIVABLES.iif`); res.send(response.join("\n")); } catch (error) { console.log("error", error); @@ -42,17 +118,51 @@ const TRNS_HEADER = `!TRNS TRNSID TRNSTYPE DATE ACCNT NAME CLASS AMOUNT DOCNUM M !SPL SPLID TRNSTYPE DATE ACCNT NAME CLASS AMOUNT DOCNUM MEMO CLEAR QNTY PRICE INVITEM PAYMETH TAXABLE VALADJ SERVICEDATE OTHER2 EXTRA !ENDTRNS`; -const generateInvoiceHeader = (job) => - `TRNS INVOICE ${new Date(job.date_invoiced).getMonth() + 1}/${new Date( - job.date_invoiced - ).getDate()}/${new Date( - job.date_invoiced - ).getFullYear()} Accounts Receivable GUO DA Acct.# ${job.ownr_id}:${ - job.ro_number - } 0100 ${job.clm_total} ${job.ro_number} N N Y GUO DA Acct.# ${job.ownr_id}:${ - job.ro_number - } ${job.ownr_addr1} ${job.ownr_city} ${job.ownr_st} ${job.ownr_zip} `; -const generateInvoiceLine = (line) => - `SPL INVOICE 05/21/2020 Sales:Total Labour:Sales, Body Shop Labour 0100 -572.60 114519 Labor Body N 572.60 Total Labour:4015 Y N `; +const generateInvoiceHeader = (job, arMapping) => + `TRNS INVOICE ${generateJobInvoiceDate(job)} ${arMapping.name} GUO DA Acct.# ${ + job.ownerid + }:${job.ro_number} 0100 ${job.clm_total} ${job.ro_number} N N Y GUO DA Acct.# ${ + job.ownr_id + }:${job.ro_number} ${job.ownr_addr1} ${job.ownr_city} ${job.ownr_st} ${ + job.ownr_zip + } `; + +const generateInvoiceLine = (job, allocation, responsibilityCenters) => { + const { amount, center } = allocation; + const DineroAmount = Dinero(amount); + const account = responsibilityCenters.profits.find( + (i) => i.name.toLowerCase() === center.toLowerCase() + ); + + if (!!!account) { + throw new Error( + `A matching account does not exist for the allocation. Center: ${center}` + ); + } + + return `SPL INVOICE ${generateJobInvoiceDate(job)} ${ + account.accountname + } 0100 ${DineroAmount.multiply(-1).toFormat(DineroQbFormat)} ${job.ro_number} ${ + account.accountdesc + } N ${DineroAmount.toFormat(DineroQbFormat)} ${account.accountitem} Y N `; +}; + +const generateTaxLine = (job, amount, type, taxMapping) => { + return `SPL INVOICE ${generateJobInvoiceDate(job)} ${ + taxMapping[type].accountname + } ${taxMapping[type].accountdesc} 0100 ${amount + .multiply(-1) + .toFormat(DineroQbFormat)} ${job.ro_number} N ${taxMapping[type].rate.toFixed( + 2 + )}% ${taxMapping[type].accountitem} N N AUTOSTAX `; +}; const END_TRNS = `ENDTRNS`; + +const DineroQbFormat = "0,0.0"; + +const generateJobInvoiceDate = (job) => { + return `${new Date(job.date_invoiced).getMonth() + 1}/${new Date( + job.date_invoiced + ).getDate()}/${new Date(job.date_invoiced).getFullYear()}`; +}; diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index f54ed1e5a..29bb68eb3 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -41,7 +41,7 @@ mutation UPDATE_MESSAGE($msid: String!, $fields: messages_set_input!) { `; exports.QUERY_JOBS_FOR_RECEIVABLES_EXPORT = ` -query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($id: uuid!){ +query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($id: uuid!) { jobs_by_pk(id: $id) { id job_totals @@ -55,11 +55,10 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($id: uuid!){ ownr_zip ownr_city ownr_st - - } - bodyshops{ - id - md_responsibility_centers + bodyshop { + id + md_responsibility_centers + } } } `;