RO into IO merge as of 02/05/2024.
This commit is contained in:
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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} `);
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
),
|
||||
},
|
||||
} })
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
45
server/ccc/partspricechange.js
Normal file
45
server/ccc/partspricechange.js
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -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),
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
12
server/firebase/rome-prod-firebase-adminsdk.json
Normal file
12
server/firebase/rome-prod-firebase-adminsdk.json
Normal 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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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
1148
server/job/job-totals-USA.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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";
|
||||
|
||||
@@ -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");
|
||||
|
||||
127
server/payroll/calculate-totals.js
Normal file
127
server/payroll/calculate-totals.js
Normal 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);
|
||||
};
|
||||
107
server/payroll/claim-task.js
Normal file
107
server/payroll/claim-task.js
Normal 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
326
server/payroll/pay-all.js
Normal 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;
|
||||
3
server/payroll/payroll.js
Normal file
3
server/payroll/payroll.js
Normal file
@@ -0,0 +1,3 @@
|
||||
exports.calculatelabor = require("./calculate-totals").calculatelabor;
|
||||
exports.payall = require("./pay-all").payall;
|
||||
exports.claimtask = require("./claim-task").claimtask;
|
||||
15
server/routes/payrollRoutes.js
Normal file
15
server/routes/payrollRoutes.js
Normal 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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user