RO into IO merge as of 02/05/2024.

This commit is contained in:
Patrick Fic
2024-02-12 12:22:05 -08:00
211 changed files with 31134 additions and 25729 deletions

View File

@@ -16,6 +16,7 @@ const CalculateAllocations =
const CdkBase = require("../../web-sockets/web-socket");
const moment = require("moment-timezone");
const Dinero = require("dinero.js");
const { default: InstanceManager } = require("../../utils/instanceMgr");
const axios = AxiosLib.create();
axios.interceptors.request.use((x) => {
@@ -660,7 +661,7 @@ async function InsertAccountPostingData(socket) {
.toISOString(), //"0001-01-01T00:00:00.0000000Z",
Description: socket.txEnvelope.story,
//AdditionalInfo: "String",
Source: "ImEX Online",
Source: InstanceManager({imex: "ImEX Online", rome:"Rome Online"}),
Lines: wips,
},
},

View File

@@ -3,6 +3,7 @@ const DineroQbFormat = require("./accounting-constants").DineroQbFormat;
const Dinero = require("dinero.js");
const {DiscountNotAlreadyCounted} = require("../job/job-totals");
const logger = require("../utils/logger");
const InstanceManager = require("../utils/instanceMgr");
exports.default = function ({
bodyshop,
@@ -34,12 +35,8 @@ 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(
//TODO:AIO This appears to be a net 0 change exept for default quantity as 0 instead of 1 for imex. Need to verify.
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)
@@ -51,8 +48,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()
@@ -78,7 +80,7 @@ exports.default = function ({
const taxAccountCode = findTaxCode(
{
local: false,
federal: true,
federal: InstanceManager({imex: true, rome: false}),
state:
jobs_by_pk.state_tax_rate === 0
? false
@@ -93,7 +95,11 @@ exports.default = function ({
bodyshop.md_responsibility_centers.sales_tax_codes
);
const QboTaxId = taxCodes[taxAccountCode];
const QboTaxId = InstanceManager({imex:taxCodes[taxAccountCode], rome: CheckQBOUSATaxID({
jobline: jobline,
type: "part",
job: jobs_by_pk,
}) })
if (!invoiceLineHash[account.name]) invoiceLineHash[account.name] = {};
if (!invoiceLineHash[account.name][QboTaxId]) {
invoiceLineHash[account.name][QboTaxId] = {
@@ -123,9 +129,15 @@ exports.default = function ({
Desc: account.accountdesc,
Quantity: 1, //jobline.part_qty,
Amount: DineroAmount, //.toFormat(DineroQbFormat),
SalesTaxCodeRef: {
SalesTaxCodeRef:
InstanceManager({imex: {
FullName: "E",
},
}, rome:{
FullName:
bodyshop.md_responsibility_centers.taxes.itemexemptcode ||
"NON",
} })
,
};
} else {
invoiceLineHash[account.name].Amount =
@@ -156,13 +168,13 @@ exports.default = function ({
const taxAccountCode = findTaxCode(
{
local: false,
federal: true,
federal: InstanceManager({imex: true, rome: false}),
state: jobs_by_pk.state_tax_rate === 0 ? false : true,
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
const QboTaxId = taxCodes[taxAccountCode];
const QboTaxId = InstanceManager({imex: taxCodes[taxAccountCode], rome: CheckQBOUSATaxID({jobline: jobline, type: "labor"})})
if (!invoiceLineHash[account.name]) invoiceLineHash[account.name] = {};
if (!invoiceLineHash[account.name][QboTaxId]) {
invoiceLineHash[account.name][QboTaxId] = {
@@ -193,9 +205,18 @@ exports.default = function ({
Quantity: 1, // jobline.mod_lb_hrs,
Amount: DineroAmount,
//Amount: DineroAmount.toFormat(DineroQbFormat),
SalesTaxCodeRef: {
FullName: "E",
SalesTaxCodeRef:
InstanceManager({imex: {
FullName:"E"
},
rome: {
FullName:
bodyshop.md_responsibility_centers.taxes.itemexemptcode ||
"NON",
}
})
,
};
} else {
invoiceLineHash[account.name].Amount =
@@ -221,13 +242,17 @@ exports.default = function ({
const taxAccountCode = findTaxCode(
{
local: false,
federal: true,
federal: InstanceManager({imex: true, rome: false}),
state: jobs_by_pk.state_tax_rate === 0 ? false : true,
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
const QboTaxId = taxCodes[taxAccountCode];
const QboTaxId = InstanceManager({imex:taxCodes[taxAccountCode] , rome: CheckQBOUSATaxID({
// jobline: jobline,
job: jobs_by_pk,
type: "materials",
})});
if (!invoiceLineHash[mapaAccount.name])
invoiceLineHash[mapaAccount.name] = {};
if (!invoiceLineHash[mapaAccount.name][QboTaxId]) {
@@ -262,9 +287,16 @@ exports.default = function ({
Amount: Dinero(jobs_by_pk.job_totals.rates.mapa.total).toFormat(
DineroQbFormat
),
SalesTaxCodeRef: {
SalesTaxCodeRef:
InstanceManager({imex: {
FullName: "E",
},
rome: {
FullName:
bodyshop.md_responsibility_centers.taxes.itemexemptcode || "NON",
}
})
,
});
}
} else {
@@ -287,13 +319,17 @@ exports.default = function ({
const taxAccountCode = findTaxCode(
{
local: false,
federal: true,
federal: InstanceManager({imex: true, rome: false}),
state: jobs_by_pk.state_tax_rate === 0 ? false : true,
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
const QboTaxId = taxCodes[taxAccountCode];
const QboTaxId = InstanceManager({imex:taxCodes[taxAccountCode], rome: CheckQBOUSATaxID({
// jobline: jobline,
job: jobs_by_pk,
type: "materials",
})});
if (!invoiceLineHash[mashAccount.name])
invoiceLineHash[mashAccount.name] = {};
if (!invoiceLineHash[mashAccount.name][QboTaxId]) {
@@ -328,9 +364,17 @@ exports.default = function ({
Amount: Dinero(jobs_by_pk.job_totals.rates.mash.total).toFormat(
DineroQbFormat
),
SalesTaxCodeRef: {
FullName: "E",
},
SalesTaxCodeRef:
InstanceManager({imex:
{
FullName: "E",
},
rome: {
FullName:
bodyshop.md_responsibility_centers.taxes.itemexemptcode || "NON",
}
})
,
});
}
} else {
@@ -363,13 +407,14 @@ exports.default = function ({
//Add Towing, storage and adjustment lines.
if (jobs_by_pk.towing_payable && jobs_by_pk.towing_payable !== 0) {
///TODO:AIO Check if this towing check works for imex and not just Rome.
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(
{
local: false,
federal: true,
federal: InstanceManager({imex:true, rome:false}) ,
state: jobs_by_pk.state_tax_rate === 0 ? false : true,
},
bodyshop.md_responsibility_centers.sales_tax_codes
@@ -377,12 +422,18 @@ exports.default = function ({
const account = responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits["TOW"]
);
const QboTaxId = taxCodes[taxAccountCode];
const QboTaxId = InstanceManager({imex:taxCodes[taxAccountCode], rome: CheckQBOUSATaxID({
// jobline: jobline,
job: jobs_by_pk,
type: "towing",
}) })
InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail",
Amount: Dinero({
amount: Math.round((jobs_by_pk.towing_payable || 0) * 100),
}).toFormat(DineroQbFormat),
///TODO:AIO Check if this towing check works for imex and not just Rome.
Amount: Dinero(jobs_by_pk.job_totals.additional.towing).toFormat(
DineroQbFormat
),
SalesItemLineDetail: {
...(jobs_by_pk.class
? {ClassRef: {value: classes[jobs_by_pk.class]}}
@@ -405,22 +456,29 @@ exports.default = function ({
},
Desc: "Towing",
Quantity: 1,
Amount: Dinero({
amount: Math.round((jobs_by_pk.towing_payable || 0) * 100),
}).toFormat(DineroQbFormat),
SalesTaxCodeRef: {
///TODO:AIO Check if this towing check works for imex and not just Rome.
Amount: Dinero(jobs_by_pk.job_totals.additional.towing).toFormat(
DineroQbFormat
),
SalesTaxCodeRef:
InstanceManager({imex: {
FullName: "E",
},
rome: {
FullName:
bodyshop.md_responsibility_centers.taxes.itemexemptcode || "NON",
}}),
});
}
}
if (jobs_by_pk.storage_payable && jobs_by_pk.storage_payable !== 0) {
///TODO:AIO Check if this storage check works for imex and not just Rome.
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(
{
local: false,
federal: true,
federal: InstanceManager({imex: true, rome:false }) ,
state: jobs_by_pk.state_tax_rate === 0 ? false : true,
},
bodyshop.md_responsibility_centers.sales_tax_codes
@@ -428,12 +486,18 @@ exports.default = function ({
const account = responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits["TOW"]
);
const QboTaxId = taxCodes[taxAccountCode];
const QboTaxId = InstanceManager({imex:taxCodes[taxAccountCode], rome: CheckQBOUSATaxID({
// jobline: jobline,
job: jobs_by_pk,
type: "storage",
}) })
InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail",
Amount: Dinero({
amount: Math.round((jobs_by_pk.storage_payable || 0) * 100),
}).toFormat(DineroQbFormat),
///TODO:AIO Check if this storage check works for imex and not just Rome.
Amount: Dinero(
jobs_by_pk.job_totals.additional.storage.amount
).toFormat(DineroQbFormat),
SalesItemLineDetail: {
...(jobs_by_pk.class
? {ClassRef: {value: classes[jobs_by_pk.class]}}
@@ -456,12 +520,19 @@ exports.default = function ({
},
Desc: "Storage",
Quantity: 1,
Amount: Dinero({
amount: Math.round((jobs_by_pk.storage_payable || 0) * 100),
}).toFormat(DineroQbFormat),
SalesTaxCodeRef: {
///TODO:AIO Check if this storage check works for imex and not just Rome.
Amount: Dinero(
jobs_by_pk.job_totals.additional.storage.amount
).toFormat(DineroQbFormat),
SalesTaxCodeRef:
InstanceManager({imex:{
FullName: "E",
},
}, rome: {
FullName:
bodyshop.md_responsibility_centers.taxes.itemexemptcode || "NON",
} })
,
});
}
}
@@ -474,13 +545,18 @@ exports.default = function ({
const taxAccountCode = findTaxCode(
{
local: false,
federal: true,
federal: InstanceManager({imex: true, rome: false}) ,
state: jobs_by_pk.state_tax_rate === 0 ? false : true,
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
const QboTaxId = taxCodes[taxAccountCode];
const QboTaxId = InstanceManager({imex: taxCodes[taxAccountCode], rome: CheckQBOUSATaxID({
// jobline: jobline,
type: "adjustment",
job: jobs_by_pk,
})})
InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail",
Amount: Dinero({
@@ -509,109 +585,118 @@ exports.default = function ({
Amount: Dinero({
amount: Math.round((jobs_by_pk.adjustment_bottom_line || 0) * 100),
}).toFormat(DineroQbFormat),
SalesTaxCodeRef: {
SalesTaxCodeRef:
InstanceManager({imex:{
FullName: "E",
},
rome: {
FullName:
bodyshop.md_responsibility_centers.taxes.itemexemptcode || "NON",
}
})
,
});
}
}
//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) {
if (qbo) {
// do qbo
} else {
InvoiceLineAdd.push({
ItemRef: {
FullName:
bodyshop.md_responsibility_centers.taxes.federal.accountitem,
},
Desc: bodyshop.md_responsibility_centers.taxes.federal.accountdesc,
Amount: federal_tax.toFormat(DineroQbFormat),
});
}
}
const RulesetToUse = InstanceManager({imex:"CANADA",rome: "US"})
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) {
InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail",
Amount: Dinero({amount: (ca_bc_pvrt || 0) * 100}).toFormat(
DineroQbFormat
),
SalesItemLineDetail: {
...(jobs_by_pk.class
? {ClassRef: {value: classes[jobs_by_pk.class]}}
: {}),
if(RulesetToUse = "CANADA"){
if (federal_tax.getAmount() > 0) {
if (qbo) {
// do qbo
} else {
InvoiceLineAdd.push({
ItemRef: {
value: items["PVRT"],
FullName:
bodyshop.md_responsibility_centers.taxes.federal.accountitem,
},
Qty: 1,
TaxCodeRef: {
value:
taxCodes[
findTaxCode(
{
local: false,
federal: true,
state: false,
},
bodyshop.md_responsibility_centers.sales_tax_codes
)
],
},
},
});
} else {
InvoiceLineAdd.push({
ItemRef: {
FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem,
},
Desc: "PVRT",
Amount: Dinero({amount: (ca_bc_pvrt || 0) * 100}).toFormat(
DineroQbFormat
),
});
Desc: bodyshop.md_responsibility_centers.taxes.federal.accountdesc,
Amount: federal_tax.toFormat(DineroQbFormat),
});
}
}
}
//QB USA with GST
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) {
InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail",
Amount: Dinero({amount: (ca_bc_pvrt || 0) * 100}).toFormat(
DineroQbFormat
),
SalesItemLineDetail: {
...(jobs_by_pk.class
? {ClassRef: {value: classes[jobs_by_pk.class]}}
: {}),
ItemRef: {
value: items["PVRT"],
},
Qty: 1,
TaxCodeRef: {
value:
taxCodes[
findTaxCode(
{
local: false,
federal: true,
state: false,
},
bodyshop.md_responsibility_centers.sales_tax_codes
)
],
},
},
});
} else {
InvoiceLineAdd.push({
ItemRef: {
FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem,
},
Desc: "PVRT",
Amount: Dinero({amount: (ca_bc_pvrt || 0) * 100}).toFormat(
DineroQbFormat
),
});
}
}
//QB USA with GST
//This was required for the No. 1 Collision Group.
if (
bodyshop.accountingconfig &&
@@ -636,6 +721,120 @@ exports.default = function ({
},
});
}
}
else{
//Handle insurance profile adjustments
Object.keys(job_totals.parts.adjustments).forEach((key) => {
if (qbo) {
//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,
},
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(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[account.accountitem],
},
TaxCodeRef: {
value: QboTaxId,
},
Qty: 1,
},
});
} else {
InvoiceLineAdd.push({
ItemRef: {
FullName: responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits[key]
).accountitem,
},
Desc: "Storage",
Quantity: 1,
Amount: Dinero(job_totals.parts.adjustments[key]).toFormat(
DineroQbFormat
),
SalesTaxCodeRef: {
FullName:
bodyshop.md_responsibility_centers.taxes.itemexemptcode || "NON",
},
});
}
});
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.
@@ -767,11 +966,34 @@ exports.createMultiQbPayerLines = function ({
Amount: Dinero({
amount: Math.round((payer.amount || 0) * 100),
}).toFormat(DineroQbFormat),
SalesTaxCodeRef: {
SalesTaxCodeRef:
InstanceManager({imex: {
FullName: "E",
},
}, rome:{
FullName:
bodyshop.md_responsibility_centers.taxes.itemexemptcode || "NON",
} })
});
}
return InvoiceLineAdd;
};
function CheckQBOUSATaxID({jobline, job, 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} `);
// }
}

View File

@@ -10,6 +10,7 @@ const OAuthClient = require("intuit-oauth");
const client = require("../../graphql-client/graphql-client").client;
const queries = require("../../graphql-client/queries");
const {parse, stringify} = require("querystring");
const InstanceManager = require("../../utils/instanceMgr");
const oauthClient = new OAuthClient({
clientId: process.env.QBO_CLIENT_ID,
@@ -21,10 +22,10 @@ const oauthClient = new OAuthClient({
let url;
if (process.env.NODE_ENV === "production") {
url = `https://imex.online`;
if (process.env.NODE_ENV === "production") { //TODO:AIO Add in QBO callbacks.
url = InstanceManager({imex: `https://imex.online`, rome: `https://romeonline.io`,});
} else if (process.env.NODE_ENV === "test") {
url = `https://test.imex.online`;
url = InstanceManager({imex: `https://test.imex.online`,rome: `https://test.romeonline.io`});
} else {
url = `http://localhost:3000`;
}

View File

@@ -18,6 +18,7 @@ const {
const OAuthClient = require("intuit-oauth");
const CreateInvoiceLines = require("../qb-receivables-lines").default;
const moment = require("moment-timezone");
const GraphQLClient = require("graphql-request").GraphQLClient;
const {generateOwnerTier} = require("../qbxml/qbxml-utils");
const {createMultiQbPayerLines} = require("../qb-receivables-lines");
@@ -240,6 +241,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
@@ -642,8 +648,7 @@ async function InsertInvoice(
],
...(bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
bodyshop.accountingconfig.qbo_usa &&
bodyshop.region_config.includes("CA_") && {
bodyshop.accountingconfig.qbo_usa && {
TxnTaxDetail: {
TxnTaxCodeRef: {
value:

View File

@@ -7,6 +7,7 @@ const builder = require("xmlbuilder2");
const QbXmlUtils = require("./qbxml-utils");
const moment = require("moment-timezone");
const logger = require('../../utils/logger');
const InstanceManager = require("../../utils/instanceMgr");
require("dotenv").config({
path: path.resolve(
@@ -121,9 +122,14 @@ const generateBillLine = (billLine, responsibilityCenters, jobClass) => {
.multiply(billLine.quantity || 1)
.toFormat(DineroQbFormat),
...(jobClass ? {ClassRef: {FullName: jobClass}} : {}),
SalesTaxCodeRef: {
FullName: findTaxCode(billLine, responsibilityCenters.sales_tax_codes),
},
...InstanceManager({imex:{
SalesTaxCodeRef: {
FullName: findTaxCode(
billLine,
responsibilityCenters.sales_tax_codes
),
},
} })
};
};

View File

@@ -7,6 +7,7 @@ const builder = require("xmlbuilder2");
const QbXmlUtils = require("./qbxml-utils");
const CreateInvoiceLines = require("../qb-receivables-lines").default;
const logger = require('../../utils/logger');
const InstanceManager = require('../../utils/instanceMgr');
require("dotenv").config({
path: path.resolve(
@@ -280,6 +281,12 @@ const generateInvoiceQbxml = (
PostalCode: jobs_by_pk.ownr_zip,
},
PONumber: jobs_by_pk.clm_no,
...InstanceManager({rome: {
ItemSalesTaxRef: {
FullName:
bodyshop.md_responsibility_centers.taxes.invoiceexemptcode,
},
}}),
IsToBePrinted: bodyshop.accountingconfig.printlater,
...(jobs_by_pk.ownr_ea
? {IsToBeEmailed: bodyshop.accountingconfig.emaillater}

View File

@@ -0,0 +1,45 @@
const path = require("path");
const _ = require("lodash");
const logger = require("../utils/logger");
const queries = require("../graphql-client/queries");
const GraphQLClient = require("graphql-request").GraphQLClient;
const moment = require("moment-timezone");
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
});
exports.generatePpc = async (req, res) => {
const {jobid} = req.body;
const BearerToken = req.headers.authorization;
logger.log("generate-ppc", "DEBUG", req.user.email, jobid, null);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
try {
const {jobs_by_pk: job} = await client
.setHeaders({Authorization: BearerToken})
.request(queries.GET_JOB_FOR_PPC, {
jobid: jobid,
});
const ReturnVal = {
...job,
trans_type: "P",
create_dt: moment().tz(job.bodyshop.timezone).format("yyyyMMDD"),
create_tm: moment().tz(job.bodyshop.timezone).format("HHmmSS"),
incl_est: true,
joblines: job.joblines.map((jl) => ({...jl, tran_code: 2})),
};
res.json(ReturnVal);
} catch (error) {
res.status(400).json(error);
}
};

View File

@@ -13,6 +13,7 @@ const CdkBase = require("../web-sockets/web-socket");
const Dinero = require("dinero.js");
const _ = require("lodash");
const {DiscountNotAlreadyCounted} = require("../job/job-totals");
const InstanceManager = require('../utils/instanceMgr');
exports.default = async function (socket, jobid) {
try {
@@ -24,7 +25,8 @@ exports.default = async function (socket, jobid) {
const job = await QueryJobData(socket, jobid);
const {bodyshop} = job;
const taxAllocations = {
const taxAllocations =
InstanceManager({imex: {
local: {
center: bodyshop.md_responsibility_centers.taxes.local.name,
sale: Dinero(job.job_totals.totals.local_tax),
@@ -46,7 +48,44 @@ exports.default = async function (socket, jobid) {
profitCenter: bodyshop.md_responsibility_centers.taxes.federal,
costCenter: bodyshop.md_responsibility_centers.taxes.federal,
},
};
}, rome:{
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[`tax_ty1`],
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty1`],
},
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[`tax_ty2`],
costCenter: bodyshop.md_responsibility_centers.taxes[`tax_ty2`],
},
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[`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`],
},
} })
//Determine if there are MAPA and MASH lines already on the estimate.
//If there are, don't do anything extra (mitchell estimate)
@@ -327,6 +366,32 @@ exports.default = async function (socket, jobid) {
// console.log("NO MASH ACCOUNT FOUND!!");
}
}
if(InstanceManager({rome:true})){
//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),

View File

@@ -8,8 +8,8 @@ require("dotenv").config({
const axios = require("axios");
let nodemailer = require("nodemailer");
let aws = require("@aws-sdk/client-ses");
let {defaultProvider} = require("@aws-sdk/credential-provider-node");
let { defaultProvider } = require("@aws-sdk/credential-provider-node");
const InstanceManager = require("../utils/instanceMgr");
const logger = require("../utils/logger");
const client = require("../graphql-client/graphql-client").client;
const queries = require("../graphql-client/queries");
@@ -18,21 +18,28 @@ const ses = new aws.SES({
// The key apiVersion is no longer supported in v3, and can be removed.
// @deprecated The client uses the "latest" apiVersion.
apiVersion: "latest",
region: "ca-central-1",
defaultProvider
defaultProvider,
region: InstanceManager({
imex: "ca-central-1",
rome: "us-east-2",
}),
});
let transporter = nodemailer.createTransport({
SES: {ses, aws},
SES: { ses, aws },
});
exports.sendServerEmail = async function ({subject, text}) {
exports.sendServerEmail = async function ({ subject, text }) {
if (process.env.NODE_ENV === undefined) return;
try {
transporter.sendMail(
{
from: `ImEX Online API - ${process.env.NODE_ENV} <noreply@imex.online>`,
to: ["patrick@imexsystems.ca", "support@thinkimex.com"],
from: InstanceManager({
imex: `ImEX Online API - ${process.env.NODE_ENV} <noreply@imex.online>`,
rome: `Rome Online API - ${process.env.NODE_ENV} <noreply@romeonline.io>`,
promanager: `ProManager API - ${process.env.NODE_ENV} <noreply@promanager.web-est.com>`,
}),
to: ["patrick@imexsystems.ca"],
subject: subject,
text: text,
ses: {
@@ -54,11 +61,15 @@ exports.sendServerEmail = async function ({subject, text}) {
logger.log("server-email-failure", "error", null, null, error);
}
};
exports.sendTaskEmail = async function ({to, subject, text, attachments}) {
exports.sendTaskEmail = async function ({ to, subject, text, attachments }) {
try {
transporter.sendMail(
{
from: `ImEX Online <noreply@imex.online>`,
from: InstanceManager({
imex: `ImEX Online <noreply@imex.online>`,
rome: `Rome Online <noreply@romeonline.io>`,
promanager: `ProManager <noreply@promanager.web-est.com>`,
}),
to: to,
subject: subject,
text: text,
@@ -90,14 +101,20 @@ exports.sendEmail = async (req, res) => {
try {
return getImage(m);
} catch (error) {
logger.log("send-email-error", "ERROR", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
error,
});
logger.log(
"send-email-error",
"ERROR",
req.user.email,
null,
{
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
error,
}
);
}
})
);
@@ -113,12 +130,12 @@ exports.sendEmail = async (req, res) => {
attachments:
[
...((req.body.attachments &&
req.body.attachments.map((a) => {
return {
filename: a.filename,
path: a.path,
};
})) ||
req.body.attachments.map((a) => {
return {
filename: a.filename,
path: a.path,
};
})) ||
[]),
...downloadedMedia.map((a) => {
return {
@@ -140,14 +157,20 @@ exports.sendEmail = async (req, res) => {
(err, info) => {
console.log(err || info);
if (info) {
logger.log("send-email-success", "DEBUG", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
// info,
});
logger.log(
"send-email-success",
"DEBUG",
req.user.email,
null,
{
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
// info,
}
);
logEmail(req, {
to: req.body.to,
cc: req.body.cc,
@@ -158,28 +181,34 @@ exports.sendEmail = async (req, res) => {
success: true, //response: info
});
} else {
logger.log("send-email-failure", "ERROR", req.user.email, null, {
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
error: err,
});
logger.log(
"send-email-failure",
"ERROR",
req.user.email,
null,
{
from: `${req.body.from.name} <${req.body.from.address}>`,
replyTo: req.body.ReplyTo.Email,
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
error: err,
}
);
logEmail(req, {
to: req.body.to,
cc: req.body.cc,
subject: req.body.subject,
bodyshopid: req.body.bodyshopid,
});
res.status(500).json({success: false, error: err});
res.status(500).json({ success: false, error: err });
}
}
);
};
async function getImage(imageUrl) {
let image = await axios.get(imageUrl, {responseType: "arraybuffer"});
let image = await axios.get(imageUrl, { responseType: "arraybuffer" });
let raw = Buffer.from(image.data).toString("base64");
return "data:" + image.headers["content-type"] + ";base64," + raw;
}
@@ -230,7 +259,14 @@ exports.emailBounce = async function (req, res) {
}
});
messageId = message.mail.messageId;
if (replyTo === "noreply@imex.online") {
if (
replyTo ===
InstanceManager({
imex: "noreply@imex.online",
rome: "noreply@romeonline.io",
promanager: "noreply@promanager.web-est.com",
})
) {
res.sendStatus(200);
return;
}
@@ -242,17 +278,28 @@ exports.emailBounce = async function (req, res) {
});
transporter.sendMail(
{
from: `ImEX Online <noreply@imex.online>`,
from: InstanceMgr({
imex: `ImEX Online <noreply@imex.online>`,
rome: `Rome Online <noreply@romeonline.io>`,
}),
to: replyTo,
//bcc: "patrick@snapt.ca",
subject: `ImEX Online Bounced Email - RE: ${subject}`,
text: `ImEX Online has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error.
subject: `${InstanceMgr({
imex: "ImEX Online",
rome: "Rome Online",
promanager: "ProManager",
})} Bounced Email - RE: ${subject}`,
text: `${InstanceMgr({
imex: "ImEX Online",
rome: "Rome Online",
promanager: "ProManager",
})} has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error.
${body.bounce?.bouncedRecipients.map(
(r) =>
`Recipient: ${r.emailAddress} | Status: ${r.action} | Code: ${r.diagnosticCode}
(r) =>
`Recipient: ${r.emailAddress} | Status: ${r.action} | Code: ${r.diagnosticCode}
`
)}
)}
`,
},
(err, info) => {

View File

@@ -0,0 +1,12 @@
{
"type": "service_account",
"project_id": "rome-prod-1",
"private_key_id": "38f0f0ac354b923dd8b00fda24a21b1e3980401e",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDdKIa203i75Dxl\nCUlYXHIEkbK5V7ffU248NIR0OK5AqTn2TUbCdwaLYfz7cr33B81IHMP0Aq/H3Yzx\npvmIEaNW6y1Ge3mYfObDVh+ZKY4tjFtlqANnc8UV7aEuNP53Vy4GVWK9mB1wecgQ\na6lAW1JwkIQkPRNVrkC6vKLcjWGN9bBrr2ByfbZT0qx9OcbcoJ3XIqDnK6dQz6V0\nsskS9Magl6RBZeYTYxgf54b1xVbeho+d5xW5GIluzkiVFyHodoVhGgDIWE5ddP8w\n8Iw4RGlkQBDHhmpaZd6DgqSM4wPJXonke3q6E4ySPPuIBRNiNUuEaxEKmq5oK8CH\n8OgXZnTHAgMBAAECggEAT1Ng9qTlkmdsLkVldHc8Ql1MQOSwxD71tEyWEeXewryw\nWKMhNVFiHI6aIkrmznuS60G+G4D3MfZKvsbIjEDfWKbkR3q0g7iRQRFcJiDcqYPF\nqLHZ/rpsv8/LV3qUp5Oyo3zu/NhZ/uT/mLw1KitXZ56+dw0dKUdmWlSdCgUAL918\nWhit+ITOnKjGd4O4eKqN5iXmw0d0yE8hw1stB45KFZNyPTLApXBZe6b4QLCNDcoo\n+aB6g7T89FSE95XxdzeiFvlGEJMO/J25cvwDScMcx6CyOHErvJY1fxhi2eee7Ts9\n+rEoMteGhHaOUXGY0Ck4Y3E4JTOC977B/uuHWUgWcQKBgQD8h7BtwDtDo5JLqemp\ngZxovL3gk3gioJk5LPlw1kkF1KUP1xGH2HL/CV6KlXslTbqlKniV73fNcGHEso+f\n4qQUCgSU9Wrm+ppeAFahX/A7k4xAwiPq/J/58vSWDxlzqaPiU/PNSWjJvG0J/tD+\nwAbjmo1f/09rvIhKggh6WZkjmQKBgQDgMnuC6lYlswkCnBMu5U1XTVdFHH7c2TRz\ndmabNzjlcbnXyX1fPa6rY6fiA6XGB8WDzPIQcUIGQ0G8C56jdFYi/PvRR8IIQyT3\ndKZwNgOr4pF83xyZ5dwWJS1W/ENUgDG3eeMt+qw/x9QHnyM4MXcuJ5jNtsF5PUR/\n8p2IXAuXXwKBgQC1SZPWtlHVVPAJcYlVLr7iUdzeBAASm8hjy22nG66AiQ+WN4dW\nRoUHoepFAtrNBOgg+kRnHuqaiTsmwilpVoMD/80aQrTj7LQ1F3kZkI4dtubQ9o5i\ne8k83rXHpD9ZUUddi3dSwIiBisuciVnwCFrpumITsG2LomUVWBROAVR2wQKBgAxJ\nuAtM3LvkPDIwa7y+RKbsTkQzc6CXJxDNBIKtXCj3OsBhAsYdk11BcQoqOQPJmUHI\nEdxk2MGPHiM4X9GFptL0Grk1vaTGSVhmxFiSHVFmcaLud5rXxmBQWVPTL72J6S+t\nNo5mltpIEY75YezKiUW2VeGwipoiiYaZvZijst7xAoGAG5oRAyvAoL1MXMXdSY4h\nDFE9hjlIsWioUi3LVXIyuRe9fS7yomgD6im3m015DCgkfmBFHNigzjcpr0kdsZZt\nnuu4mWE0iDWH4EHYytHQRvQk6ih1n+rXXf3cZxbs+xFyMsNl/LmYUw/5pnBdy+s0\nBmyrA1pzoVrofYYtOOFXE64=\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-dxdb4@rome-prod-1.iam.gserviceaccount.com",
"client_id": "117707985682955455586",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-dxdb4%40rome-prod-1.iam.gserviceaccount.com"
}

View File

@@ -206,6 +206,8 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
adjustment_bottom_line
state_tax_rate
qb_multiple_payers
tax_paint_mat_rt
tax_lbr_rt
owner {
accountingid
}
@@ -224,6 +226,7 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
prt_dsmk_p
prt_dsmk_m
tax_part
lbr_tax
line_ref
unq_seq
lbr_op
@@ -440,7 +443,7 @@ query QUERY_BILLS_FOR_PAYABLES_EXPORT($bills: [uuid!]!) {
federal_tax_rate
invoice_number
is_credit_memo
invoice_number
invoice_number
job {
id
ro_number
@@ -1279,6 +1282,7 @@ exports.UPDATE_JOB = `
exports.GET_JOB_BY_PK = `query GET_JOB_BY_PK($id: uuid!) {
jobs_by_pk(id: $id) {
cieca_stl
updated_at
alt_transport
intakechecklist
@@ -1323,6 +1327,9 @@ exports.GET_JOB_BY_PK = `query GET_JOB_BY_PK($id: uuid!) {
est_ct_fn
shopid
est_ct_ln
cieca_pfl
cieca_pft
cieca_pfo
vehicle {
id
notes
@@ -1445,22 +1452,12 @@ exports.GET_JOB_BY_PK = `query GET_JOB_BY_PK($id: uuid!) {
manual_line
prt_dsmk_p
prt_dsmk_m
parts_order_lines {
id
parts_order {
id
order_number
order_date
user_email
vendor {
id
name
}
}
}
misc_amt
misc_tax
}
}
}`;
//TODO:AIO The above query used to have parts order lines in it. Validate that this doesn't need it.
exports.QUERY_JOB_COSTING_DETAILS = ` query QUERY_JOB_COSTING_DETAILS($id: uuid!) {
jobs_by_pk(id: $id) {
@@ -2127,6 +2124,24 @@ query GET_PBS_AP_ALLOCATIONS($billids: [uuid!]) {
}
}`;
exports.GET_JOB_FOR_PPC = `query GET_JOB_FOR_PPC($jobid: uuid!) {
jobs_by_pk(id: $jobid) {
id
ciecaid
ro_number
joblines(where: {removed: {_eq: false}, act_price_before_ppc: {_is_null: false}}) {
id
act_price
unq_seq
}
bodyshop {
timezone
}
}
}
`;
exports.QUERY_PARTS_SCAN = `query QUERY_PARTS_SCAN ($id: uuid!) {
jobs_by_pk(id: $id) {
bodyshop {
@@ -2156,3 +2171,104 @@ exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) {
shopid
}
}`;
exports.QUERY_JOB_PAYROLL_DATA = `query QUERY_JOB_PAYROLL_DATA($id: uuid!) {
jobs_by_pk(id: $id) {
bodyshop{
id
md_responsibility_centers
md_tasks_presets
employee_teams{
id
name
employee_team_members{
id
employee{
id
first_name
last_name
}
percentage
labor_rates
}
}
}
timetickets{
id
employeeid
rate
productivehrs
actualhrs
ciecacode
}
lbr_adjustments
ro_number
id
job_totals
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_mahw
rate_mapa
rate_mash
rate_matd
status
materials
completed_tasks
joblines(where: { removed: { _eq: false } }){
id
line_no
unq_seq
line_ind
line_desc
part_type
line_ref
oem_partno
db_price
act_price
part_qty
mod_lbr_ty
db_hrs
mod_lb_hrs
lbr_op
lbr_amt
op_code_desc
status
notes
location
tax_part
db_ref
manual_line
prt_dsmk_p
prt_dsmk_m
misc_amt
misc_tax
assigned_team
convertedtolbr
convertedtolbr_data
}
}
}`;
exports.INSERT_TIME_TICKETS = `mutation INSERT_TIMETICKETS($timetickets: [timetickets_insert_input!]!) {
insert_timetickets(objects: $timetickets) {
affected_rows
}
}
`;

View File

@@ -967,6 +967,8 @@ const getAdditionalCostCenter = (jl, profitCenters) => {
return profitCenters["ATS"];
} else if (lineDesc.includes("towing")) {
return profitCenters["TOW"];
} else if (jl.act_price > 0) { //TODO:AIO Ensure that this is tested.
ret.profitcenter_part = defaults.profits["PAO"];
} else {
return null;
}

1148
server/job/job-totals-USA.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,20 @@
const Dinero = require("dinero.js");
const queries = require("../graphql-client/queries");
const logger = require('../utils/logger');
const adminClient = require("../graphql-client/graphql-client").client;
const _ = require("lodash");
const logger = require("../utils/logger");
//****************************************************** */
//****************************************************** */
//****************************************************** */
//****************************************************** */
//****************************************************** */
//THIS IS THE CANADIAN/IMEX REQUIRED JOB TOTALS CALCULATION.
//****************************************************** */
//****************************************************** */
//****************************************************** */
//****************************************************** */
// Dinero.defaultCurrency = "USD";
// Dinero.globalLocale = "en-CA";

View File

@@ -1,6 +1,11 @@
exports.totals = require("./job-totals").default;
const RenderInstanceManager = require("../utils/instanceMgr");
exports.totals = RenderInstanceManager({
imex: require("./job-totals").default,
rome: require("./job-totals-USA").default,
});
exports.totalsSsu = require("./job-totals").totalsSsu;
exports.costing = require("./job-costing").JobCosting;
exports.costingmulti = require("./job-costing").JobCostingMulti;
exports.statustransition = require("./job-status-transition").statustransition;
exports.lifecycle = require('./job-lifecycle');
exports.lifecycle = require("./job-lifecycle");

View File

@@ -0,0 +1,127 @@
const Dinero = require("dinero.js");
const queries = require("../graphql-client/queries");
const GraphQLClient = require("graphql-request").GraphQLClient;
const logger = require("../utils/logger");
const {
CalculateExpectedHoursForJob,
CalculateTicketsHoursForJob,
} = require("./pay-all");
// Dinero.defaultCurrency = "USD";
// Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN";
exports.calculatelabor = async function (req, res) {
const {jobid, calculateOnly} = req.body;
logger.log("job-payroll-calculate-labor", "DEBUG", req.user.email, jobid, null);
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
try {
const {jobs_by_pk: job} = await client
.setHeaders({Authorization: BearerToken})
.request(queries.QUERY_JOB_PAYROLL_DATA, {
id: jobid,
});
//iterate over each ticket, building a hash of team -> employee to calculate total assigned hours.
const {employeeHash, assignmentHash} = CalculateExpectedHoursForJob(job);
const ticketHash = CalculateTicketsHoursForJob(job);
const totals = [];
//Iteratively go through all 4 levels of the object and create an array that can be presented.
// use the employee hash as the golden record (i.e. what they should have), and add what they've claimed.
//While going through, delete items from ticket hash.
//Anything left in ticket hash is an extra entered item.
Object.keys(employeeHash).forEach((employeeIdKey) => {
//At the employee level.
Object.keys(employeeHash[employeeIdKey]).forEach((laborTypeKey) => {
//At the labor level
Object.keys(employeeHash[employeeIdKey][laborTypeKey]).forEach(
(rateKey) => {
//At the rate level.
const expectedHours =
employeeHash[employeeIdKey][laborTypeKey][rateKey];
//Will the following line fail? Probably if it doesn't exist.
const claimedHours = get(
ticketHash,
`${employeeIdKey}.${laborTypeKey}.${rateKey}`
);
if (claimedHours) {
delete ticketHash[employeeIdKey][laborTypeKey][rateKey];
}
totals.push({
employeeid: employeeIdKey,
rate: rateKey,
mod_lbr_ty: laborTypeKey,
expectedHours,
claimedHours: claimedHours || 0,
});
}
);
});
});
Object.keys(ticketHash).forEach((employeeIdKey) => {
//At the employee level.
Object.keys(ticketHash[employeeIdKey]).forEach((laborTypeKey) => {
//At the labor level
Object.keys(ticketHash[employeeIdKey][laborTypeKey]).forEach(
(rateKey) => {
//At the rate level.
const expectedHours = 0;
//Will the following line fail? Probably if it doesn't exist.
const claimedHours = get(
ticketHash,
`${employeeIdKey}.${laborTypeKey}.${rateKey}`
);
if (claimedHours) {
delete ticketHash[employeeIdKey][laborTypeKey][rateKey];
}
totals.push({
employeeid: employeeIdKey,
rate: rateKey,
mod_lbr_ty: laborTypeKey,
expectedHours,
claimedHours: claimedHours || 0,
});
}
);
});
});
if (assignmentHash.unassigned > 0) {
totals.push({
employeeid: undefined,
//rate: rateKey,
//mod_lbr_ty: laborTypeKey,
expectedHours: assignmentHash.unassigned,
claimedHours: 0,
});
}
res.json(totals);
//res.json(assignmentHash);
} catch (error) {
logger.log(
"job-payroll-calculate-labor-error",
"ERROR",
req.user.email,
jobid,
{
jobid: jobid,
error,
}
);
res.status(503).send();
}
};
get = function (obj, key) {
return key.split(".").reduce(function (o, x) {
return typeof o == "undefined" || o === null ? o : o[x];
}, obj);
};

View File

@@ -0,0 +1,107 @@
const Dinero = require("dinero.js");
const queries = require("../graphql-client/queries");
const GraphQLClient = require("graphql-request").GraphQLClient;
const logger = require("../utils/logger");
const {
CalculateExpectedHoursForJob,
CalculateTicketsHoursForJob,
} = require("./pay-all");
const moment = require("moment");
// Dinero.defaultCurrency = "USD";
// Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN";
exports.claimtask = async function (req, res) {
const {jobid, task, calculateOnly, employee} = req.body;
logger.log("job-payroll-pay-all", "DEBUG", req.user.email, jobid, null);
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
try {
const {jobs_by_pk: job} = await client
.setHeaders({Authorization: BearerToken})
.request(queries.QUERY_JOB_PAYROLL_DATA, {
id: jobid,
});
const theTaskPreset = job.bodyshop.md_tasks_presets.presets.find(
(tp) => tp.name === task
);
if (!theTaskPreset) {
res
.status(400)
.json({success: false, error: "Provided task preset not found."});
return;
}
//Get all of the assignments that are filtered.
const {assignmentHash, employeeHash} = CalculateExpectedHoursForJob(
job,
theTaskPreset.hourstype
);
const ticketsToInsert = [];
//Then add them in based on a percentage to each employee.
Object.keys(employeeHash).forEach((employeeIdKey) => {
//At the employee level.
Object.keys(employeeHash[employeeIdKey]).forEach((laborTypeKey) => {
//At the labor level
Object.keys(employeeHash[employeeIdKey][laborTypeKey]).forEach(
(rateKey) => {
//At the rate level.
const expectedHours =
employeeHash[employeeIdKey][laborTypeKey][rateKey] *
(theTaskPreset.percent / 100);
ticketsToInsert.push({
task_name: task,
jobid: job.id,
bodyshopid: job.bodyshop.id,
employeeid: employeeIdKey,
productivehrs: expectedHours,
rate: rateKey,
ciecacode: laborTypeKey,
flat_rate: true,
cost_center:
job.bodyshop.md_responsibility_centers.defaults.costs[
laborTypeKey
],
memo: `*Flagged Task* ${theTaskPreset.memo}`,
});
}
);
});
});
if (!calculateOnly) {
//Insert the time ticekts if we're not just calculating them.
const insertResult = await client.request(queries.INSERT_TIME_TICKETS, {
timetickets: ticketsToInsert.filter(
(ticket) => ticket.productivehrs !== 0
),
});
const updateResult = await client.request(queries.UPDATE_JOB, {
jobId: job.id,
job: {
status: theTaskPreset.nextstatus,
completed_tasks: [
...job.completed_tasks,
{
name: task,
completedat: moment(),
completed_by: employee,
useremail: req.user.email,
},
],
},
});
}
res.json({unassignedHours: assignmentHash.unassigned, ticketsToInsert});
} catch (error) {
logger.log("job-payroll-claim-task-error", "ERROR", req.user.email, jobid, {
jobid: jobid,
error,
});
res.status(503).send();
}
};

326
server/payroll/pay-all.js Normal file
View File

@@ -0,0 +1,326 @@
const Dinero = require("dinero.js");
const queries = require("../graphql-client/queries");
const GraphQLClient = require("graphql-request").GraphQLClient;
const _ = require("lodash");
const rdiff = require("recursive-diff");
const logger = require("../utils/logger");
const {json} = require("body-parser");
// Dinero.defaultCurrency = "USD";
// Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN";
exports.payall = async function (req, res) {
const {jobid, calculateOnly} = req.body;
logger.log("job-payroll-pay-all", "DEBUG", req.user.email, jobid, null);
const BearerToken = req.BearerToken;
const client = req.userGraphQLClient;
try {
const {jobs_by_pk: job} = await client
.setHeaders({Authorization: BearerToken})
.request(queries.QUERY_JOB_PAYROLL_DATA, {
id: jobid,
});
//iterate over each ticket, building a hash of team -> employee to calculate total assigned hours.
const {employeeHash, assignmentHash} = CalculateExpectedHoursForJob(job);
const ticketHash = CalculateTicketsHoursForJob(job);
if (assignmentHash.unassigned > 0) {
res.json({success: false, error: "Not all hours have been assigned."});
return;
}
//Calculate how much time each tech should have by labor type.
//Doing this order creates a diff of changes on the ticket hash to make it the same as the employee hash.
const recursiveDiff = rdiff.getDiff(ticketHash, employeeHash, true);
const ticketsToInsert = [];
recursiveDiff.forEach((diff) => {
//Every iteration is what we would need to insert into the time ticket hash
//so that it would match the employee hash exactly.
const path = diffParser(diff);
if (diff.op === "add") {
console.log(Object.keys(diff.val));
if (typeof diff.val === "object" && Object.keys(diff.val).length > 1) {
//Multiple values to add.
Object.keys(diff.val).forEach((key) => {
console.log("Hours", diff.val[key][Object.keys(diff.val[key])[0]]);
console.log("Rate", Object.keys(diff.val[key])[0]);
ticketsToInsert.push({
task_name: "Pay All",
jobid: job.id,
bodyshopid: job.bodyshop.id,
employeeid: path.employeeid,
productivehrs: diff.val[key][Object.keys(diff.val[key])[0]],
rate: Object.keys(diff.val[key])[0],
ciecacode: key,
cost_center:
job.bodyshop.md_responsibility_centers.defaults.costs[key],
flat_rate: true,
memo: `Add unflagged hours. (${req.user.email})`,
});
});
} else {
//Only the 1 value to add.
ticketsToInsert.push({
task_name: "Pay All",
jobid: job.id,
bodyshopid: job.bodyshop.id,
employeeid: path.employeeid,
productivehrs: path.hours,
rate: path.rate,
ciecacode: path.mod_lbr_ty,
flat_rate: true,
cost_center:
job.bodyshop.md_responsibility_centers.defaults.costs[
path.mod_lbr_ty
],
memo: `Add unflagged hours. (${req.user.email})`,
});
}
} else if (diff.op === "update") {
//An old ticket amount isn't sufficient
//We can't modify the existing ticket, it might already be committed. So let's add a new one instead.
ticketsToInsert.push({
task_name: "Pay All",
jobid: job.id,
bodyshopid: job.bodyshop.id,
employeeid: path.employeeid,
productivehrs: diff.val - diff.oldVal,
rate: path.rate,
ciecacode: path.mod_lbr_ty,
flat_rate: true,
cost_center:
job.bodyshop.md_responsibility_centers.defaults.costs[
path.mod_lbr_ty
],
memo: `Adjust flagged hours per assignment. (${req.user.email})`,
});
} else {
//Has to be a delete
if (
typeof diff.oldVal === "object" &&
Object.keys(diff.oldVal).length > 1
) {
//Multiple oldValues to add.
Object.keys(diff.oldVal).forEach((key) => {
ticketsToInsert.push({
task_name: "Pay All",
jobid: job.id,
bodyshopid: job.bodyshop.id,
employeeid: path.employeeid,
productivehrs:
diff.oldVal[key][Object.keys(diff.oldVal[key])[0]] * -1,
rate: Object.keys(diff.oldVal[key])[0],
ciecacode: key,
cost_center:
job.bodyshop.md_responsibility_centers.defaults.costs[key],
flat_rate: true,
memo: `Remove flagged hours per assignment. (${req.user.email})`,
});
});
} else {
//Only the 1 value to add.
ticketsToInsert.push({
task_name: "Pay All",
jobid: job.id,
bodyshopid: job.bodyshop.id,
employeeid: path.employeeid,
productivehrs: path.hours * -1,
rate: path.rate,
ciecacode: path.mod_lbr_ty,
cost_center:
job.bodyshop.md_responsibility_centers.defaults.costs[
path.mod_lbr_ty
],
flat_rate: true,
memo: `Remove flagged hours per assignment. (${req.user.email})`,
});
}
}
});
const insertResult = await client.request(queries.INSERT_TIME_TICKETS, {
timetickets: ticketsToInsert.filter(
(ticket) => ticket.productivehrs !== 0
),
});
res.json(ticketsToInsert.filter((ticket) => ticket.productivehrs !== 0));
} catch (error) {
logger.log(
"job-payroll-labor-totals-error",
"ERROR",
req.user.email,
jobid,
{
jobid: jobid,
error: JSON.stringify(error),
}
);
res.status(400).json({error: error.message});
}
};
function diffParser(diff) {
const type = typeof diff.oldVal;
let mod_lbr_ty, rate, hours;
if (diff.path.length === 1) {
if (diff.op === "add") {
mod_lbr_ty = Object.keys(diff.val)[0];
rate = Object.keys(diff.val[mod_lbr_ty])[0];
// hours = diff.oldVal[mod_lbr_ty][rate];
} else {
mod_lbr_ty = Object.keys(diff.oldVal)[0];
rate = Object.keys(diff.oldVal[mod_lbr_ty])[0];
// hours = diff.oldVal[mod_lbr_ty][rate];
}
} else if (diff.path.length === 2) {
mod_lbr_ty = diff.path[1];
if (diff.op === "add") {
rate = Object.keys(diff.val)[0];
} else {
rate = Object.keys(diff.oldVal)[0];
}
} else if (diff.path.length === 3) {
mod_lbr_ty = diff.path[1];
rate = diff.path[2];
//hours = 0;
}
//Set the hours
if (
typeof diff.val === "number" &&
diff.val !== null &&
diff.val !== undefined
) {
hours = diff.val;
} else if (diff.val !== null && diff.val !== undefined) {
if (diff.path.length === 1) {
hours =
diff.val[Object.keys(diff.val)[0]][
Object.keys(diff.val[Object.keys(diff.val)[0]])
];
} else {
hours = diff.val[Object.keys(diff.val)[0]];
}
} else if (
typeof diff.oldVal === "number" &&
diff.oldVal !== null &&
diff.oldVal !== undefined
) {
hours = diff.oldVal;
} else {
hours = diff.oldVal[Object.keys(diff.oldVal)[0]];
}
const ret = {
multiVal: false,
employeeid: diff.path[0], // Always True
mod_lbr_ty,
rate,
hours,
};
return ret;
}
function CalculateExpectedHoursForJob(job, filterToLbrTypes) {
const assignmentHash = {unassigned: 0};
const employeeHash = {}; // employeeid => Cieca labor type => rate => hours. Contains how many hours each person should be paid.
job.joblines
.filter((jobline) => {
if (!filterToLbrTypes) return true;
else {
return (
filterToLbrTypes.includes(jobline.mod_lbr_ty) ||
(jobline.convertedtolbr &&
filterToLbrTypes.includes(jobline.convertedtolbr_data.mod_lbr_ty))
);
}
})
.forEach((jobline) => {
if (jobline.convertedtolbr) {
// Line has been converte to labor. Temporarily re-assign the hours.
jobline.mod_lbr_ty = jobline.convertedtolbr_data.mod_lbr_ty;
jobline.mod_lb_hrs += jobline.convertedtolbr_data.mod_lb_hrs;
}
if (jobline.mod_lb_hrs != 0) {
//Check if the line is assigned. If not, keep track of it as an unassigned line by type.
if (jobline.assigned_team === null) {
assignmentHash.unassigned =
assignmentHash.unassigned + jobline.mod_lb_hrs;
} else {
//Line is assigned.
if (!assignmentHash[jobline.assigned_team]) {
assignmentHash[jobline.assigned_team] = 0;
}
assignmentHash[jobline.assigned_team] =
assignmentHash[jobline.assigned_team] + jobline.mod_lb_hrs;
//Create the assignment breakdown.
const theTeam = job.bodyshop.employee_teams.find(
(team) => team.id === jobline.assigned_team
);
theTeam.employee_team_members.forEach((tm) => {
//Figure out how many hours they are owed at this line, and at what rate.
if (!employeeHash[tm.employee.id]) {
employeeHash[tm.employee.id] = {};
}
if (!employeeHash[tm.employee.id][jobline.mod_lbr_ty]) {
employeeHash[tm.employee.id][jobline.mod_lbr_ty] = {};
}
if (
!employeeHash[tm.employee.id][jobline.mod_lbr_ty][
tm.labor_rates[jobline.mod_lbr_ty]
]
) {
employeeHash[tm.employee.id][jobline.mod_lbr_ty][
tm.labor_rates[jobline.mod_lbr_ty]
] = 0;
}
const hoursOwed = (tm.percentage * jobline.mod_lb_hrs) / 100;
employeeHash[tm.employee.id][jobline.mod_lbr_ty][
tm.labor_rates[jobline.mod_lbr_ty]
] =
employeeHash[tm.employee.id][jobline.mod_lbr_ty][
tm.labor_rates[jobline.mod_lbr_ty]
] + hoursOwed;
});
}
}
});
return {assignmentHash, employeeHash};
}
function CalculateTicketsHoursForJob(job) {
const ticketHash = {}; // employeeid => Cieca labor type => rate => hours.
//Calculate how much each employee has been paid so far.
job.timetickets.forEach((ticket) => {
if (!ticketHash[ticket.employeeid]) {
ticketHash[ticket.employeeid] = {};
}
if (!ticketHash[ticket.employeeid][ticket.ciecacode]) {
ticketHash[ticket.employeeid][ticket.ciecacode] = {};
}
if (!ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate]) {
ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate] = 0;
}
ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate] =
ticketHash[ticket.employeeid][ticket.ciecacode][ticket.rate] +
ticket.productivehrs;
});
return ticketHash;
}
exports.CalculateExpectedHoursForJob = CalculateExpectedHoursForJob;
exports.CalculateTicketsHoursForJob = CalculateTicketsHoursForJob;

View File

@@ -0,0 +1,3 @@
exports.calculatelabor = require("./calculate-totals").calculatelabor;
exports.payall = require("./pay-all").payall;
exports.claimtask = require("./claim-task").claimtask;

View File

@@ -0,0 +1,15 @@
const express = require('express');
const router = express.Router();
const payroll = require('../payroll/payroll');
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
router.use(validateFirebaseIdTokenMiddleware);
router.use(withUserGraphQLClientMiddleware);
router.post("/calculatelabor", payroll.calculatelabor);
router.post("/payall", payroll.payall);
router.post("/claimtask", payroll.claimtask);
module.exports = router;

View File

@@ -11,6 +11,8 @@ const queries = require("../graphql-client/queries");
const {phone} = require("phone");
const {admin} = require("../firebase/firebase-handler");
const logger = require("../utils/logger");
const InstanceRenderManager = require("../../client/src/utils/instanceRenderMgr");
exports.receive = async (req, res) => {
//Perform request validation
@@ -103,9 +105,15 @@ exports.receive = async (req, res) => {
const fcmresp = await admin.messaging().send({
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
notification: {
title: `ImEX Online Message - ${data.phone_num}`,
title:
InstanceRenderManager({
imex:`ImEX Online Message - ${data.phone_num}` ,
rome: `Rome Online Message - ${data.phone_num}`,
promanager: `Pro Manager Message - ${data.phone_num}`
})
,
body: message.image_path ? `Image ${message.text}` : message.text,
//imageUrl: "https://thinkimex.com/img/io-fcm.png",
//imageUrl: "https://thinkimex.com/img/io-fcm.png", //TODO:AIO Resolve addresses for other instances
},
data,
});

View File

@@ -4,11 +4,11 @@
* Default is to return the ImEX Prop
* @typedef {Object} InstanceManagerObject
* @property { string | object | function } rome Return this prop if Rome.
* @property { string | object | function } proman Return this prop if Rome.
* @property { string | object | function } promanager Return this prop if Rome.
* @property { string | object | function } imex Return this prop if Rome.
*/
function InstanceManager({ rome, proman, imex }) {
export default function InstanceManager({ rome, promanager, imex }) {
let propToReturn = null;
switch (process.env.INSTANCE) {
@@ -19,16 +19,16 @@ function InstanceManager({ rome, proman, imex }) {
propToReturn = rome;
break;
case "PROMANAGER":
propToReturn = proman;
propToReturn = promanager;
break;
default:
propToReturn = imex;
break;
}
if (!propToReturn) {
throw new Error(
`Prop to return is not valid for this instance (${process.env.INSTANCE}).`
);
}
// if (!propToReturn) {
// throw new Error(
// `Prop to return is not valid for this instance (${process.env.INSTANCE}).`
// );
// }
return propToReturn;
}