Files
bodyshop/server/data/autohouse.js
2023-01-25 11:20:51 -08:00

1028 lines
34 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"],
},
};
exports.default = async (req, res) => {
//Query for the List of Bodyshop Clients.
logger.log("autohouse-start", "DEBUG", "api", null, null);
const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS);
const specificShopIds = req.body.bodyshopIds; // ['uuid]
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
const allxmlsToUpload = [];
const allErrors = [];
try {
for (const bodyshop of specificShopIds
? bodyshops.filter((b) => specificShopIds.includes(b.id))
: bodyshops) {
logger.log("autohouse-start-shop-extract", "DEBUG", "api", bodyshop.id, {
shopname: bodyshop.shopname,
});
const erroredJobs = [];
try {
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).startOf("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)),
});
}
var ret = builder
.create(
{
// version: "1.0",
// encoding: "UTF-8",
//keepNullNodes: true,
},
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,
});
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
autuhouseid: bodyshop.autuhouseid,
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,
})),
});
}
}
if (skipUpload) {
for (const xmlObj of allxmlsToUpload) {
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
}
res.json(allxmlsToUpload);
sendServerEmail({
subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
`,
});
return;
}
let sftp = new Client();
sftp.on("error", (errors) =>
logger.log("autohouse-sftp-error", "ERROR", "api", null, {
...errors,
})
);
try {
//Connect to the FTP and upload all.
await sftp.connect(ftpSetup);
for (const xmlObj of allxmlsToUpload) {
logger.log("autohouse-sftp-upload", "DEBUG", "api", null, {
filename: xmlObj.filename,
});
const uploadResult = await sftp.put(
Buffer.from(xmlObj.xml),
`/${xmlObj.filename}`
);
logger.log("autohouse-sftp-upload-result", "DEBUG", "api", null, {
uploadResult,
});
}
//***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml
} catch (error) {
logger.log("autohouse-sftp-error", "ERROR", "api", null, {
...error,
});
} finally {
sftp.end();
}
sendServerEmail({
subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
`,
});
res.sendStatus(200);
} catch (error) {
res.status(200).json(error);
}
};
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);
if (job.ro_number === "QBD209") {
console.log("Stop here");
}
//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: job.ownr_fn || "",
LastName: job.ownr_ln || "",
Street: job.ownr_addr1 || "",
City: job.ownr_city || "",
State: job.ownr_st || "",
Zip: (job.ownr_zip && job.ownr_zip.substring(0, 3)) || "",
Phone1: job.ownr_ph1 || "",
Phone2: null,
Phone2Extension: null,
Phone3: null,
Phone3Extension: null,
FileComments: null,
Source: null,
Email: job.ownr_ea || "",
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_addr1 || "",
State: job.ins_city || "",
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,
});
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) {
if (
!billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
]
)
billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = Dinero();
billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
] = billTotalsByCostCenters[
job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
].add(
Dinero({
amount:
(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:
(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.actualhrs || ticket_val.productivehrs || 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,
};
};