Files
bodyshop/server/data/autohouse.js
Allan Carr 78678dd3dc IO-3027 Datapumps Refactor
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-15 10:04:03 -08:00

883 lines
33 KiB
JavaScript

const path = require("path");
const queries = require("../graphql-client/queries");
const Dinero = require("dinero.js");
const moment = require("moment-timezone");
var builder = require("xmlbuilder2");
const _ = require("lodash");
const logger = require("../utils/logger");
const fs = require("fs");
require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
let Client = require("ssh2-sftp-client");
const client = require("../graphql-client/graphql-client").client;
const { sendServerEmail } = require("../email/sendemail");
const AHDineroFormat = "0.00";
const AhDateFormat = "MMDDYYYY";
const repairOpCodes = ["OP4", "OP9", "OP10"];
const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"];
const ftpSetup = {
host: process.env.AUTOHOUSE_HOST,
port: process.env.AUTOHOUSE_PORT,
username: process.env.AUTOHOUSE_USER,
password: process.env.AUTOHOUSE_PASSWORD,
debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data),
algorithms: {
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
}
};
const allxmlsToUpload = [];
const allErrors = [];
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
res.sendStatus(403);
return;
}
// Only process if the appropriate token is provided.
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
res.sendStatus(401);
return;
}
// Send immediate response and continue processing.
res.status(202).json({
success: true,
message: "Processing request ...",
timestamp: new Date().toISOString()
});
try {
logger.log("autohouse-start", "DEBUG", "api", null, null);
const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS); //Query for the List of Bodyshop Clients.
const specificShopIds = req.body.bodyshopIds; // ['uuid];
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
const batchSize = 10;
const shopsToProcess =
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
logger.log("autohouse-shopsToProcess-generated", "DEBUG", "api", null, null);
if (shopsToProcess.length === 0) {
logger.log("autohouse-shopsToProcess-empty", "DEBUG", "api", null, null);
return;
}
const batchPromises = [];
for (let i = 0; i < shopsToProcess.length; i += batchSize) {
const batch = shopsToProcess.slice(i, i + batchSize);
const batchPromise = (async () => {
await processBatch(batch, start, end);
if (skipUpload) {
for (const xmlObj of allxmlsToUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
}
} else {
await uploadViaSFTP(allxmlsToUpload);
}
})();
batchPromises.push(batchPromise);
}
await Promise.all(batchPromises);
await sendServerEmail({
subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count, result: x.result })),
null,
2
)}`
});
logger.log("autohouse-end", "DEBUG", "api", null, null);
} catch (error) {
logger.log("autohouse-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
}
};
async function processBatch(batch, start, end) {
for (const bodyshop of batch) {
const erroredJobs = [];
try {
logger.log("autohouse-start-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
const { jobs, bodyshops_by_pk } = await client.request(queries.AUTOHOUSE_QUERY, {
bodyshopid: bodyshop.id,
start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"),
...(end && { end: moment(end).endOf("day") })
});
const autoHouseObject = {
AutoHouseExport: {
RepairOrder: jobs.map((j) =>
CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) {
erroredJobs.push({ job: job, error: error.toString() });
})
)
}
};
if (erroredJobs.length > 0) {
logger.log("autohouse-failed-jobs", "ERROR", "api", bodyshop.id, {
count: erroredJobs.length,
jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number))
});
}
const ret = builder.create({}, autoHouseObject).end({ allowEmptyTags: true });
allxmlsToUpload.push({
count: autoHouseObject.AutoHouseExport.RepairOrder.length,
xml: ret,
filename: `IM_${bodyshop.autohouseid}_${moment().format("DDMMYYYY_HHMMss")}.xml`
});
logger.log("autohouse-end-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname
});
} catch (error) {
//Error at the shop level.
logger.log("autohouse-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack });
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
autohouseid: bodyshop.autohouseid,
fatal: true,
errors: [error.toString()]
});
} finally {
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
autohouseid: bodyshop.autohouseid,
errors: erroredJobs.map((ej) => ({
ro_number: ej.job?.ro_number,
jobid: ej.job?.id,
error: ej.error
}))
});
}
}
}
async function uploadViaSFTP(allxmlsToUpload) {
const sftp = new Client();
sftp.on("error", (errors) =>
logger.log("autohouse-sftp-connection-error", "ERROR", "api", null, { error: errors.message, stack: errors.stack })
);
try {
//Connect to the FTP and upload all.
await sftp.connect(ftpSetup);
for (const xmlObj of allxmlsToUpload) {
try {
xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
logger.log("autohouse-sftp-upload", "DEBUG", "api", null, {
filename: xmlObj.filename,
result: xmlObj.result
});
} catch (error) {
logger.log("autohouse-sftp-upload-error", "ERROR", "api", null, {
filename: xmlObj.filename,
error: error.message,
stack: error.stack
});
throw error;
}
}
} catch (error) {
logger.log("autohouse-sftp-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
throw error;
} finally {
sftp.end();
}
}
const CreateRepairOrderTag = (job, errorCallback) => {
//Level 2
if (!job.job_totals) {
errorCallback({
jobid: job.id,
job: job,
ro_number: job.ro_number,
error: { toString: () => "No job totals for RO." }
});
return {};
}
const repairCosts = CreateCosts(job);
//Calculate detail only lines.
const detailAdjustments = job.joblines
.filter((jl) => jl.ah_detail_line && jl.mod_lbr_ty)
.reduce(
(acc, val) => {
return {
hours: acc.hours + val.mod_lb_hrs,
amount: acc.amount.add(
Dinero({
amount: Math.round((job.job_totals.rates[val.mod_lbr_ty.toLowerCase()].rate || 0) * val.mod_lb_hrs * 100)
})
)
};
},
{ hours: 0, amount: Dinero() }
);
try {
const ret = {
RepairOrderInformation: {
ShopInternalName: job.bodyshop.autohouseid,
ID: parseInt(job.ro_number.match(/\d/g).join(""), 10),
RO: job.ro_number,
Est: parseInt(job.ro_number.match(/\d/g).join(""), 10), //We no longer use estimate id.
GUID: job.id,
TransType: StatusMapping(job.status, job.bodyshop.md_ro_statuses),
ShopName: job.bodyshop.shopname,
ShopAddress: job.bodyshop.address1,
ShopCity: job.bodyshop.city,
ShopState: job.bodyshop.state,
ShopZip: job.bodyshop.zip_post,
ShopPhone: job.bodyshop.phone,
EstimatorID: `${job.est_ct_ln ? job.est_ct_ln : ""}${
job.est_ct_ln ? ", " : ""
}${job.est_ct_fn ? job.est_ct_fn : ""}`,
EstimatorName: `${job.est_ct_ln ? job.est_ct_ln : ""}${
job.est_ct_ln ? ", " : ""
}${job.est_ct_fn ? job.est_ct_fn : ""}`
},
CustomerInformation: {
FirstName: "",
LastName: "",
Street: "",
City: "",
State: "",
Zip: (job.ownr_zip && job.ownr_zip.substring(0, 3)) || "",
Phone1: "",
Phone2: null,
Phone2Extension: null,
Phone3: null,
Phone3Extension: null,
FileComments: null,
Source: null,
Email: "",
RetWhsl: null,
Cat: null,
InsuredorClaimantFlag: null
},
VehicleInformation: {
Year: job.v_model_yr
? parseInt(job.v_model_yr.match(/\d/g))
? parseInt(job.v_model_yr.match(/\d/g).join(""), 10)
: ""
: "",
Make: job.v_make_desc || "",
Model: job.v_model_desc || "",
VIN: job.v_vin || "",
License: job.plate_no,
MileageIn: job.kmin || 0,
Vehiclecolor: job.v_color,
VehicleProductionDate: null,
VehiclePaintCode: null,
VehicleTrimCode: null,
VehicleBodyStyle: null,
DriveableFlag: job.driveable ? "Y" : "N"
},
InsuranceInformation: {
InsuranceCo: job.ins_co_nm || "",
CompanyName: job.ins_co_nm || "",
Address: job.ins_addr1 || "",
City: job.ins_city || "",
State: job.ins_st || "",
Zip: job.ins_zip || "",
Phone: job.ins_ph1 || "",
Fax: job.ins_fax || "",
ClaimType: null,
LossType: job.loss_type || "",
Policy: job.policy_no || "",
Claim: job.clm_no || "",
InsuredLastName: null,
InsuredFirstName: null,
ClaimantLastName: null,
ClaimantFirstName: null,
Assignment: null,
InsuranceAgentLastName: null,
InsuranceAgentFirstName: null,
InsAgentPhone: null,
InsideAdjuster: null,
OutsideAdjuster: null
},
Dates: {
DateofLoss: (job.loss_date && moment(job.loss_date).format(AhDateFormat)) || "",
InitialCustomerContactDate: null,
FirstFollowUpDate: null,
ReferralDate: null,
EstimateAppointmentDate: null,
SecondFollowUpDate: null,
AssignedDate: (job.asgn_date && moment(job.asgn_date).format(AhDateFormat)) || "",
EstComplete: null,
CustomerAuthorizationDate: null,
InsuranceAuthorizationDate: null,
DateOpened:
(job.date_open && moment(job.date_open).tz(job.bodyshop.timezone).format(AhDateFormat)) ||
(job.created_at && moment(job.created_at).tz(job.bodyshop.timezone).format(AhDateFormat)) ||
"",
ScheduledArrivalDate:
(job.scheduled_in && moment(job.scheduled_in).tz(job.bodyshop.timezone).format(AhDateFormat)) || "",
CarinShop: (job.actual_in && moment(job.actual_in).tz(job.bodyshop.timezone).format(AhDateFormat)) || "",
InsInspDate: null,
StartDate: job.date_repairstarted
? (job.date_repairstarted && moment(job.date_repairstarted).tz(job.bodyshop.timezone).format(AhDateFormat)) ||
""
: (job.date_repairstarted && moment(job.actual_in).tz(job.bodyshop.timezone).format(AhDateFormat)) || "",
PartsOrder: null,
TeardownHold: null,
SupplementSubmittedDate: null,
SupplementApprovedDate: null,
AssntoBody: null,
AssntoMech: null,
AssntoPaint: null,
AssntoDetail: null,
// PromiseDate:
// (job.scheduled_completion &&
// moment(job.scheduled_completion).format(AhDateFormat)) ||
// "",
//InsuranceTargetOut: null,
CarComplete:
(job.actual_completion && moment(job.actual_completion).tz(job.bodyshop.timezone).format(AhDateFormat)) || "",
DeliveryAppointmentDate:
// (job.scheduled_delivery &&
// moment(job.scheduled_delivery)
// .tz(job.bodyshop.timezone)
// .format(AhDateFormat)) ||
(job.scheduled_completion &&
moment(job.scheduled_completion).tz(job.bodyshop.timezone).format(AhDateFormat)) ||
"",
DateClosed:
(job.date_invoiced && moment(job.date_invoiced).tz(job.bodyshop.timezone).format(AhDateFormat)) || "",
CustomerPaidInFullDate: null,
InsurancePaidInFullDate: null,
CustPickup:
(job.actual_delivery && moment(job.actual_delivery).tz(job.bodyshop.timezone).format(AhDateFormat)) || "",
AccountPostedDate:
job.date_exported && moment(job.date_exported).tz(job.bodyshop.timezone).format(AhDateFormat),
CSIProcessedDate: null,
ThankYouLetterSent: null,
AdditionalFollowUpDate: null
},
Rates: {
BodyRate: job.rate_lab || 0,
RefinishRate: job.rate_lar || 0,
MechanicalRate: job.rate_lam || 0,
StructuralRate: job.rate_las || 0,
ElectricalRate: job.rate_lae || 0,
FrameRate: job.rate_laf || 0,
GlassRate: job.rate_lag || 0,
DetailRate: 0, // job.rate_lad || 0,
LaborMiscRate: 0,
PMRate: job.rate_mapa || 0,
BMRate: job.rate_mash || 0,
TaxRate: (job.parts_tax_rates && job.parts_tax_rates.PAN && job.parts_tax_rates.PAN.prt_tax_rt) || 0,
StorageRateperDay: 0,
DaysStored: 0
},
// EstimateTotals: {
// BodyHours: null,
// RefinishHours: null,
// MechanicalHours: null,
// StructuralHours: null,
// PartsTotal: null,
// PartsOEM: null,
// PartsAM: null,
// PartsReconditioned: null,
// PartsRecycled: null,
// PartsOther: null,
// SubletTotal: null,
// BodyLaborTotal: null,
// RefinishLaborTotal: null,
// MechanicalLaborTotal: null,
// StructuralLaborTotal: null,
// MiscellaneousChargeTotal: null,
// PMTotal: null,
// BMTotal: null,
// MiscTotal: null,
// TowingTotal: null,
// StorageTotal: null,
// DetailTotal: null,
// SalesTaxTotal: null,
// GrossTotal: null,
// DeductibleTotal: null,
// DepreciationTotal: null,
// Discount: null,
// CustomerPay: null,
// InsurancePay: null,
// Deposit: null,
// AmountDue: null,
// },
// SupplementTotals: {
// BodyHours: null,
// RefinishHours: null,
// MechanicalHours: null,
// StructuralHours: null,
// PartsTotal: null,
// PartsOEM: null,
// PartsAM: null,
// PartsReconditioned: null,
// PartsRecycled: null,
// PartsOther: null,
// SubletTotal: null,
// BodyLaborTotal: null,
// RefinishLaborTotal: null,
// MechanicalLaborTotal: null,
// StructuralLaborTotal: null,
// MiscellaneousChargeTotal: null,
// PMTotal: null,
// BMTotal: null,
// MiscTotal: null,
// TowingTotal: null,
// StorageTotal: null,
// DetailTotal: null,
// SalesTaxTotal: null,
// GrossTotal: null,
// DeductibleTotal: null,
// DepreciationTotal: null,
// Discount: null,
// CustomerPay: null,
// InsurancePay: null,
// Deposit: null,
// AmountDue: null,
// },
RevisedTotals: {
BodyHours: job.job_totals.rates.lab.hours.toFixed(2),
BodyRepairHours: job.joblines
.filter((line) => repairOpCodes.includes(line.lbr_op))
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
.toFixed(2),
BodyReplaceHours: job.joblines
.filter((line) => replaceOpCodes.includes(line.lbr_op))
.reduce((acc, val) => acc + val.mod_lb_hrs, 0)
.toFixed(2),
RefinishHours: job.job_totals.rates.lar.hours.toFixed(2),
MechanicalHours: job.job_totals.rates.lam.hours.toFixed(2),
StructuralHours: job.job_totals.rates.las.hours.toFixed(2),
ElectricalHours: job.job_totals.rates.lae.hours.toFixed(2),
FrameHours: job.job_totals.rates.laf.hours.toFixed(2),
GlassHours: job.job_totals.rates.lag.hours.toFixed(2),
DetailHours: detailAdjustments.hours, //job.job_totals.rates.lad.hours.toFixed(2),
LaborMiscHours: (
job.job_totals.rates.la1.hours +
job.job_totals.rates.la2.hours +
job.job_totals.rates.la3.hours +
job.job_totals.rates.la4.hours +
job.job_totals.rates.lau.hours -
detailAdjustments.hours
).toFixed(2),
PartsTotal: Dinero(job.job_totals.parts.parts.total).toFormat(AHDineroFormat),
PartsTotalCost: repairCosts.PartsTotalCost.toFormat(AHDineroFormat),
PartsOEM: Dinero(job.job_totals.parts.parts.list.PAN && job.job_totals.parts.parts.list.PAN.total)
.add(Dinero(job.job_totals.parts.parts.list.PAP && job.job_totals.parts.parts.list.PAP.total))
.toFormat(AHDineroFormat),
PartsOEMCost: repairCosts.PartsOemCost.toFormat(AHDineroFormat),
PartsAM: Dinero(job.job_totals.parts.parts.list.PAA && job.job_totals.parts.parts.list.PAA.total).toFormat(
AHDineroFormat
),
PartsAMCost: repairCosts.PartsAMCost.toFormat(AHDineroFormat),
PartsReconditioned: repairCosts.PartsReconditionedCost.toFormat(AHDineroFormat),
PartsReconditionedCost: repairCosts.PartsReconditionedCost.toFormat(AHDineroFormat),
PartsRecycled: Dinero(
job.job_totals.parts.parts.list.PAL && job.job_totals.parts.parts.list.PAL.total
).toFormat(AHDineroFormat),
PartsRecycledCost: repairCosts.PartsRecycledCost.toFormat(AHDineroFormat),
PartsOther: Dinero(job.job_totals.parts.parts.list.PAO && job.job_totals.parts.parts.list.PAO.total).toFormat(
AHDineroFormat
),
PartsOtherCost: repairCosts.PartsOtherCost.toFormat(AHDineroFormat),
SubletTotal: Dinero(job.job_totals.parts.sublets.total).toFormat(AHDineroFormat),
SubletTotalCost: repairCosts.SubletTotalCost.toFormat(AHDineroFormat),
BodyLaborTotal: Dinero(job.job_totals.rates.lab.total).toFormat(AHDineroFormat),
BodyLaborTotalCost: repairCosts.BodyLaborTotalCost.toFormat(AHDineroFormat),
RefinishLaborTotal: Dinero(job.job_totals.rates.lar.total).toFormat(AHDineroFormat),
RefinishLaborTotalCost: repairCosts.RefinishLaborTotalCost.toFormat(AHDineroFormat),
MechanicalLaborTotal: Dinero(job.job_totals.rates.lam.total).toFormat(AHDineroFormat),
MechanicalLaborTotalCost: repairCosts.MechanicalLaborTotalCost.toFormat(AHDineroFormat),
StructuralLaborTotal: Dinero(job.job_totals.rates.las.total).toFormat(AHDineroFormat),
StructuralLaborTotalCost: repairCosts.StructuralLaborTotalCost.toFormat(AHDineroFormat),
ElectricalLaborTotal: Dinero(job.job_totals.rates.lae.total).toFormat(AHDineroFormat),
ElectricalLaborTotalCost: repairCosts.ElectricalLaborTotalCost.toFormat(AHDineroFormat),
FrameLaborTotal: Dinero(job.job_totals.rates.laf.total).toFormat(AHDineroFormat),
FrameLaborTotalCost: repairCosts.FrameLaborTotalCost.toFormat(AHDineroFormat),
GlassLaborTotal: Dinero(job.job_totals.rates.lag.total).toFormat(AHDineroFormat),
GlassLaborTotalCost: repairCosts.GlassLaborTotalCost.toFormat(AHDineroFormat),
DetailLaborTotal: detailAdjustments.amount.toFormat(AHDineroFormat),
// Dinero(job.job_totals.rates.lad.total).toFormat(
// AHDineroFormat
// ),
DetailLaborTotalCost: Dinero().toFormat(AHDineroFormat),
// repairCosts.DetailLaborTotalCost.toFormat(AHDineroFormat),
LaborMiscTotal: Dinero(job.job_totals.rates.la1.total)
.add(Dinero(job.job_totals.rates.la2.total))
.add(Dinero(job.job_totals.rates.la3.total))
.add(Dinero(job.job_totals.rates.la4.total))
.add(Dinero(job.job_totals.rates.lau.total))
.subtract(detailAdjustments.amount)
.toFormat(AHDineroFormat),
LaborMiscTotalCost: 0,
MiscellaneousChargeTotal: 0,
MiscellaneousChargeTotalCost: 0,
PMTotal: Dinero(job.job_totals.rates.mapa.total).toFormat(AHDineroFormat),
PMTotalCost: repairCosts.PMTotalCost.toFormat(AHDineroFormat),
BMTotal: Dinero(job.job_totals.rates.mash.total).toFormat(AHDineroFormat),
BMTotalCost: repairCosts.BMTotalCost.toFormat(AHDineroFormat),
MiscTotal: Dinero(job.job_totals.additional.additionalCosts).toFormat(AHDineroFormat),
MiscTotalCost: 0,
TowingTotal: Dinero(job.job_totals.additional.towing).toFormat(AHDineroFormat),
TowingTotalCost: repairCosts.TowingTotalCost.toFormat(AHDineroFormat),
StorageTotal: Dinero(job.job_totals.additional.storage).toFormat(AHDineroFormat),
StorageTotalCost: repairCosts.StorageTotalCost.toFormat(AHDineroFormat),
DetailTotal: detailAdjustments.amount.toFormat(AHDineroFormat),
DetailTotalCost: 0,
SalesTaxTotal: Dinero(job.job_totals.totals.local_tax)
.add(Dinero(job.job_totals.totals.state_tax))
.add(Dinero(job.job_totals.totals.federal_tax))
.add(Dinero(job.job_totals.additional.pvrt))
.toFormat(AHDineroFormat),
SalesTaxTotalCost: 0,
GrossTotal: Dinero(job.job_totals.totals.total_repairs).toFormat(AHDineroFormat),
DeductibleTotal: Dinero({
amount: Math.round((job.ded_amt || 0) * 100)
}).toFormat(AHDineroFormat),
DepreciationTotal: Dinero(job.job_totals.totals.custPayable.dep_taxes).toFormat(AHDineroFormat),
Discount: Dinero(job.job_totals.additional.adjustments).toFormat(AHDineroFormat),
CustomerPay: Dinero(job.job_totals.totals.custPayable.total).toFormat(AHDineroFormat),
InsurancePay: Dinero(job.job_totals.totals.total_repairs)
.subtract(Dinero(job.job_totals.totals.custPayable.total))
.toFormat(AHDineroFormat),
Deposit: 0,
AmountDue: 0
},
Misc: {
ProductionStatus: null,
StatusDescription: null,
Hub50Comment: null,
DateofChange: null,
BodyTechName: null,
TotalLossYN: job.tlos_ind ? "Y" : "N",
InsScreenCommentsLine1: null,
InsScreenCommentsLine2: null,
AssignmentCaller: null,
AssignmentDivision: null,
LocationofPrimaryImpact: (job.area_of_damage && job.area_of_damage.impact1) || 0,
LocationofSecondaryImpact: (job.area_of_damage && job.area_of_damage.impact2) || 0,
PaintTechID: null,
PaintTechName: null,
ImportType: null,
ImportFile: null,
GSTTax: Dinero(job.job_totals.totals.federal_tax).toFormat(AHDineroFormat),
RepairDelayStatusCode: null,
RepairDelaycomment: null,
AgentMktgID: null,
AgentCity: null,
Picture1: null,
Picture2: null,
ExtNoteDate: null,
RentalOrdDate: null,
RentalPUDate: null,
RentalDueDate: null,
RentalActRetDate: null,
RentalCompanyID: null,
// CSIID: null,
InsGroupCode: null
},
DetailLines: {
DetailLine:
job.joblines.length > 0
? job.joblines.map((jl) => GenerateDetailLines(job, jl, job.bodyshop.md_order_statuses))
: [generateNullDetailLine()]
}
};
return ret;
} catch (error) {
logger.log("autohouse-job-calculate-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
errorCallback({ jobid: job.id, ro_number: job.ro_number, error });
}
};
const CreateCosts = (job) => {
//Create a mapping based on AH Requirements
//For DMS, the keys in the object below are the CIECA part types.
const billTotalsByCostCenters = job.bills.reduce((bill_acc, bill_val) => {
//At the bill level.
bill_val.billlines.map((line_val) => {
//At the bill line level.
if (!bill_acc[line_val.cost_center]) bill_acc[line_val.cost_center] = Dinero();
bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add(
Dinero({
amount: Math.round((line_val.actual_cost || 0) * 100)
})
.multiply(line_val.quantity)
.multiply(bill_val.is_credit_memo ? -1 : 1)
);
return null;
});
return bill_acc;
}, {});
//If the hourly rates for job costing are set, add them in.
if (
job.bodyshop.jc_hourly_rates &&
(job.bodyshop.jc_hourly_rates.mapa ||
typeof job.bodyshop.jc_hourly_rates.mapa === "number" ||
isNaN(job.bodyshop.jc_hourly_rates.mapa) === false)
) {
if (!billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MAPA])
billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MAPA] = Dinero();
if (job.bodyshop.use_paint_scale_data === true) {
if (job.mixdata.length > 0) {
billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MAPA] = Dinero({
amount: Math.round(((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100)
});
} else {
billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MAPA] = billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
].add(
Dinero({
amount: Math.round((job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mapa * 100) || 0)
}).multiply(job.job_totals.rates.mapa.hours)
);
}
} else {
billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MAPA] = billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
].add(
Dinero({
amount: Math.round((job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mapa * 100) || 0)
}).multiply(job.job_totals.rates.mapa.hours)
);
}
}
if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash) {
if (!billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MASH])
billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MASH] = Dinero();
billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MASH] = billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MASH
].add(
Dinero({
amount: Math.round((job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash * 100) || 0)
}).multiply(job.job_totals.rates.mash.hours)
);
}
//Uses CIECA Labor types.
const ticketTotalsByCostCenter = job.timetickets.reduce((ticket_acc, ticket_val) => {
//At the invoice level.
if (!ticket_acc[ticket_val.cost_center]) ticket_acc[ticket_val.cost_center] = Dinero();
ticket_acc[ticket_val.cost_center] = ticket_acc[ticket_val.cost_center].add(
Dinero({
amount: Math.round((ticket_val.rate || 0) * 100)
}).multiply((ticket_val.flat_rate ? ticket_val.productivehrs : ticket_val.actualhrs) || 0)
);
return ticket_acc;
}, {});
//CIECA STANDARD MAPPING OBJECT.
const ciecaObj = {
ATS: "ATS",
LA1: "LA1",
LA2: "LA2",
LA3: "LA3",
LA4: "LA4",
LAA: "LAA",
LAB: "LAB",
LAD: "LAD",
LAE: "LAE",
LAF: "LAF",
LAG: "LAG",
LAM: "LAM",
LAR: "LAR",
LAS: "LAS",
LAU: "LAU",
PAA: "PAA",
PAC: "PAC",
PAG: "PAG",
PAL: "PAL",
PAM: "PAM",
PAN: "PAN",
PAO: "PAO",
PAP: "PAP",
PAR: "PAR",
PAS: "PAS",
TOW: "TOW",
MAPA: "MAPA",
MASH: "MASH",
PASL: "PASL"
};
const defaultCosts =
job.bodyshop.cdk_dealerid || job.bodyshop.pbs_serialnumber
? ciecaObj
: job.bodyshop.md_responsibility_centers.defaults.costs;
return {
PartsTotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => {
if (
key !== defaultCosts.PAS &&
key !== defaultCosts.PASL &&
key !== defaultCosts.MAPA &&
key !== defaultCosts.MASH &&
key !== defaultCosts.TOW
)
return acc.add(billTotalsByCostCenters[key]);
return acc;
}, Dinero()),
PartsOemCost: (billTotalsByCostCenters[defaultCosts.PAN] || Dinero()).add(
billTotalsByCostCenters[defaultCosts.PAP] || Dinero()
),
PartsAMCost: billTotalsByCostCenters[defaultCosts.PAA] || Dinero(),
PartsReconditionedCost: billTotalsByCostCenters[defaultCosts.PAM] || Dinero(),
PartsRecycledCost: billTotalsByCostCenters[defaultCosts.PAL] || Dinero(),
PartsOtherCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(),
SubletTotalCost:
billTotalsByCostCenters[defaultCosts.PAS] || Dinero(billTotalsByCostCenters[defaultCosts.PASL] || Dinero()),
BodyLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAB] || Dinero(),
RefinishLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAR] || Dinero(),
MechanicalLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAM] || Dinero(),
StructuralLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAS] || Dinero(),
ElectricalLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAE] || Dinero(),
FrameLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAF] || Dinero(),
GlassLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAG] || Dinero(),
DetailLaborTotalCost: Dinero(),
// ticketTotalsByCostCenter[defaultCosts.LAD] || Dinero(),
LaborMiscTotalCost: (ticketTotalsByCostCenter[defaultCosts.LA1] || Dinero())
.add(ticketTotalsByCostCenter[defaultCosts.LA2] || Dinero())
.add(ticketTotalsByCostCenter[defaultCosts.LA2] || Dinero())
.add(ticketTotalsByCostCenter[defaultCosts.LA3] || Dinero())
.add(ticketTotalsByCostCenter[defaultCosts.LA4] || Dinero())
.add(ticketTotalsByCostCenter[defaultCosts.LAU] || Dinero()),
PMTotalCost: billTotalsByCostCenters[defaultCosts.MAPA] || Dinero(),
BMTotalCost: billTotalsByCostCenters[defaultCosts.MASH] || Dinero(),
MiscTotalCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(),
TowingTotalCost: billTotalsByCostCenters[defaultCosts.TOW] || Dinero(),
StorageTotalCost: Dinero(),
DetailTotal: Dinero(),
DetailTotalCost: Dinero(),
SalesTaxTotalCost: Dinero()
};
};
const StatusMapping = (status, md_ro_statuses) => {
//Possible return statuses EST, SCH, ARR, IPR, RDY, DEL, CLO, CAN, UNDEFINED.
const {
default_imported,
default_open,
default_scheduled,
default_arrived,
default_completed,
default_delivered,
default_invoiced,
default_exported,
default_void
} = md_ro_statuses;
if (status === default_open || status === default_imported) return "EST";
else if (status === default_scheduled) return "SCH";
else if (status === default_arrived) return "ARR";
else if (status === default_completed) return "RDY";
else if (status === default_delivered) return "DEL";
else if (status === default_invoiced || status === default_exported) return "CLO";
else if (status === default_void) return "VOID";
else if (md_ro_statuses.production_statuses.includes(status)) return "IPR";
else return "UNDEFINED";
};
const GenerateDetailLines = (job, line, statuses) => {
const ret = {
BackOrdered: line.status === statuses.default_bo ? "1" : "0",
Cost: (line.billlines[0] && (line.billlines[0].actual_cost * line.billlines[0].quantity).toFixed(2)) || 0,
//Critical: null,
Description: line.line_desc ? line.line_desc.replace(/[^\x00-\x7F]/g, "") : "",
DiscountMarkup: line.prt_dsmk_m || 0,
InvoiceNumber: line.billlines[0] && line.billlines[0].bill.invoice_number,
IOUPart: 0,
LineNumber: line.line_no || 0,
MarkUp: null,
OrderedOn:
(line.parts_order_lines[0] && moment(line.parts_order_lines[0].parts_order.order_date).format(AhDateFormat)) ||
"",
OriginalCost: null,
OriginalInvoiceNumber: null,
PriceEach: line.act_price || 0,
PartNumber: line.oem_partno ? line.oem_partno.replace(/[^\x00-\x7F]/g, "") : "",
ProfitPercent: null,
PurchaseOrderNumber: null,
Qty: line.part_qty || 0,
Status: line.status || "",
SupplementNumber: line.line_ind ? line.line_ind.replace(/[^\d.-]/g, "") : 0,
Type: line.part_type || "",
Vendor: (line.billlines[0] && line.billlines[0].bill.vendor.name) || "",
VendorPaid: null,
VendorPrice: (line.billlines[0] && line.billlines[0].actual_price.toFixed(2)) || 0,
Deleted: null,
ExpectedOn: null,
ReceivedOn: line.billlines[0] && moment(line.billlines[0].bill.date).format(AhDateFormat),
OrderedBy: null,
ShipVia: null,
VendorContact: null,
EstimateAmount: (line.act_price * line.part_qty).toFixed(2) || 0 //Rebecca
};
return ret;
};
const generateNullDetailLine = () => {
return {
BackOrdered: "0",
Cost: 0,
Critical: null,
Description: "No Lines on Estimate",
DiscountMarkup: 0,
InvoiceNumber: null,
IOUPart: 0,
LineNumber: 0,
MarkUp: null,
OrderedOn: "",
OriginalCost: null,
OriginalInvoiceNumber: null,
PriceEach: 0,
PartNumber: 0,
ProfitPercent: null,
PurchaseOrderNumber: null,
Qty: 0,
Status: "",
SupplementNumber: 0,
Type: "",
Vendor: "",
VendorPaid: null,
VendorPrice: 0,
Deleted: 0,
ExpectedOn: "",
ReceivedOn: "",
OrderedBy: "",
ShipVia: "",
VendorContact: "",
EstimateAmount: 0
};
};