Files
bodyshop/server/data/autohouse.js
2021-07-22 16:29:41 -07:00

703 lines
22 KiB
JavaScript

const path = require("path");
const queries = require("../graphql-client/queries");
const Dinero = require("dinero.js");
const moment = require("moment");
var builder = require("xmlbuilder2");
const _ = require("lodash");
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 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: console.log,
algorithms: {
serverHostKey: ["ssh-rsa", "ssh-dss"],
},
};
exports.default = async (req, res) => {
//Query for the List of Bodyshop Clients.
console.log("Starting Autohouse datapump request.");
const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS);
const allxmlsToUpload = [];
const allErrors = [];
try {
for (const bodyshop of bodyshops) {
console.log("Starting extract for ", bodyshop.shopname);
const erroredJobs = [];
try {
const { jobs } = await client.request(queries.AUTOHOUSE_QUERY, {
bodyshopid: bodyshop.id,
});
const autoHouseObject = {
AutoHouseExport: {
RepairOrder: jobs.map((j) =>
CreateRepairOrderTag(
{ ...j, bodyshop },
function ({ job, error }) {
erroredJobs.push({ job: job, error: error.toString() });
}
)
),
},
};
console.log(
bodyshop.shopname,
"***Number of Failed jobs***: ",
erroredJobs.length,
JSON.stringify(erroredJobs.map((j) => j.job.ro_number))
);
var ret = builder
.create(autoHouseObject, {
version: "1.0",
encoding: "UTF-8",
})
.end({ pretty: true, allowEmptyTags: true });
allxmlsToUpload.push({
xml: ret,
filename: `IM_${bodyshop.imexshopid}_${moment().format(
"DDMMYYYY_HHMMSS"
)}.xml`,
});
console.log("Finished extract for shop ", bodyshop.shopname);
} catch (error) {
//Error at the shop level.
console.log("Error at shop level", bodyshop.shopname, error);
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
fatal: true,
errors: [error.toString()],
});
} finally {
allErrors.push({
bodyshopid: bodyshop.id,
imexshopid: bodyshop.imexshopid,
errors: erroredJobs,
});
}
}
let sftp = new Client();
sftp.on("error", (errors) =>
console.log("Error in FTP client", JSON.stringify(errors))
);
try {
//Connect to the FTP and upload all.
await sftp.connect(ftpSetup);
for (const xmlObj of allxmlsToUpload) {
console.log("Uploading", xmlObj.filename);
const uploadResult = await sftp.put(
Buffer.from(xmlObj.xml),
`/${xmlObj.filename}`
);
console.log(
"🚀 ~ file: autohouse.js ~ line 94 ~ uploadResult",
uploadResult
);
}
//***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml
} catch (error) {
console.log("Error when connecting to FTP", error);
} finally {
sftp.end();
}
res.sendStatus(200);
} catch (error) {
res.status(200).json(error);
}
};
const CreateRepairOrderTag = (job, errorCallback) => {
//Level 2
const repairCosts = CreateCosts(job);
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,
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,
Make: job.v_make_desc,
Model: job.v_model_desc,
VIN: job.v_vin,
License: job.plate_no,
MileageIn: job.kmin,
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: null,
ClaimType: null,
LossType: null,
Policy: null,
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: null,
EstComplete: null,
CustomerAuthorizationDate: null,
InsuranceAuthorizationDate: null,
DateOpened: job.date_open && moment(job.date_open).format(AhDateFormat),
ScheduledArrivalDate:
job.scheduled_in && moment(job.scheduled_in).format(AhDateFormat),
CarinShop: job.actual_in && moment(job.actual_in).format(AhDateFormat),
InsInspDate: null,
StartDate: null,
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).format(AhDateFormat),
DeliveryAppointmentDate:
job.scheduled_delivery &&
moment(job.scheduled_delivery).format(AhDateFormat),
DateClosed:
job.date_invoiced && moment(job.date_invoiced).format(AhDateFormat),
CustomerPaidInFullDate: null,
InsurancePaidInFullDate: null,
CustPickup:
job.actual_delivery &&
moment(job.actual_delivery).format(AhDateFormat),
AccountPostedDate:
job.date_exported && moment(job.date_exported).format(AhDateFormat),
CSIProcessedDate: null,
ThankYouLetterSent: null,
AdditionalFollowUpDate: null,
},
Rates: {
BodyRate: job.rate_lab,
RefinishRate: job.rate_lar,
MechanicalRate: job.rate_lam,
StructuralRate: job.rate_las,
PMRate: job.rate_mapa,
BMRate: job.rate_mash,
TaxRate: null,
StorageRateperDay: null,
DaysStored: null,
},
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,
BodyRepairHours: job.joblines
.filter((line) => repairOpCodes.includes(line.lbr_op))
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
BodyReplaceHours: job.joblines
.filter((line) => replaceOpCodes.includes(line.lbr_op))
.reduce((acc, val) => acc + val.mod_lb_hrs, 0),
RefinishHours: job.job_totals.rates.lar.hours,
MechanicalHours: job.job_totals.rates.lam.hours,
StructuralHours: job.job_totals.rates.las.hours,
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: null,
PartsReconditionedCost:
repairCosts.PartsReconditionedCost.toFormat(AHDineroFormat),
PartsRecycled: Dinero(
job.job_totals.parts.parts.list.PAR &&
job.job_totals.parts.parts.list.PAR.total
).toFormat(AHDineroFormat),
PartsRecycledCost: null,
PartsOther: Dinero(
job.job_totals.parts.parts.list.PAO &&
job.job_totals.parts.parts.list.PAO.total
).toFormat(AHDineroFormat),
PartsOtherCost: null,
SubletTotal: Dinero(job.job_totals.parts.sublets.total).toFormat(
AHDineroFormat
),
SubletTotalCost: 0,
BodyLaborTotal: Dinero(job.job_totals.rates.lab.total).toFormat(
AHDineroFormat
),
BodyLaborTotalCost: 0,
RefinishLaborTotal: Dinero(job.job_totals.rates.lar.total).toFormat(
AHDineroFormat
),
RefinishLaborTotalCost: 0,
MechanicalLaborTotal: Dinero(job.job_totals.rates.lam.total).toFormat(
AHDineroFormat
),
MechanicalLaborTotalCost: 0,
StructuralLaborTotal: Dinero(job.job_totals.rates.las.total).toFormat(
AHDineroFormat
),
StructuralLaborTotalCost: null,
MiscellaneousChargeTotal: null,
MiscellaneousChargeTotalCost: null,
PMTotal: Dinero(job.job_totals.rates.mapa.total).toFormat(
AHDineroFormat
),
PMTotalCost: 0,
BMTotal: Dinero(job.job_totals.rates.mash.total).toFormat(
AHDineroFormat
),
BMTotalCost: 0,
MiscTotal: 0,
MiscTotalCost: 0,
TowingTotal: Dinero(job.job_totals.additional.towing).toFormat(
AHDineroFormat
),
TowingTotalCost: null,
StorageTotal: Dinero(job.job_totals.additional.storage).toFormat(
AHDineroFormat
),
StorageTotalCost: null,
DetailTotal: null,
DetailTotalCost: null,
SalesTaxTotal: Dinero(job.job_totals.totals.local_tax)
.add(Dinero(job.job_totals.totals.state_tax))
.add(Dinero(job.job_totals.totals.federal_tax))
.toFormat(AHDineroFormat),
SalesTaxTotalCost: null,
GrossTotal: Dinero(job.job_totals.totals.net_repairs).toFormat(
AHDineroFormat
),
DeductibleTotal: job.ded_amt,
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: 0,
Deposit: 0,
AmountDue: 0,
},
Misc: {
ProductionStatus: null,
StatusDescription: null,
Hub50Comment: null,
DateofChange: null,
BodyTechName: null,
TotalLossYN: null,
InsScreenCommentsLine1: null,
InsScreenCommentsLine2: null,
AssignmentCaller: null,
AssignmentDivision: null,
LocationofPrimaryImpact: "12",
LocationofSecondaryImpact: null,
PaintTechID: null,
PaintTechName: null,
ImportType: null,
ImportFile: null,
GSTTax: null,
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(jl, job.bodyshop.md_order_statuses)
)
: [generateNullDetailLine()],
},
};
return ret;
} catch (error) {
console.log("Error calculating job", error);
errorCallback({ job, error });
}
};
const CreateCosts = (job) => {
//Create a mapping based on AH Requirements
const billTotalsByCostCenters = job.bills.reduce((bill_acc, bill_val) => {
//At the bill level.
bill_val.billlines.map((line_val) => {
//At the bill line level.
//console.log("JobCostingPartsTable -> line_val", line_val);
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;
}, {});
const materialsHours = { mapaHrs: 0, mashHrs: 0 };
//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(materialsHours.mapaHrs)
);
}
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;
},
{}
);
const defaultCosts = job.bodyshop.md_responsibility_centers.defaults.costs;
return {
PartsTotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => {
return acc.add(billTotalsByCostCenters[key]);
}, Dinero()),
PartsOemCost: (billTotalsByCostCenters[defaultCosts.PAN] || Dinero()).add(
billTotalsByCostCenters[defaultCosts.PAP] || Dinero()
),
PartsAMCost: billTotalsByCostCenters[defaultCosts.PAA] || Dinero(),
PartsReconditionedCost: Dinero(),
PartsRecycledCost: billTotalsByCostCenters[defaultCosts.PAR] || Dinero(),
PartsOtherCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(),
SubletTotalCost: billTotalsByCostCenters[defaultCosts.PAS] || Dinero(),
BodyLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAB] || Dinero(),
RefinishLaborTotalCost:
ticketTotalsByCostCenter[defaultCosts.LAR] || Dinero(),
MechanicalLaborTotalCost:
ticketTotalsByCostCenter[defaultCosts.LAM] || Dinero(),
StructuralLaborTotalCost:
ticketTotalsByCostCenter[defaultCosts.LAS] || 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) => {
//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 "CLO";
else if (md_ro_statuses.production_statuses.includes(status)) return "IPR";
else return "UNDEFINED";
// default: return "UNDEFINED"
};
const GenerateDetailLines = (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),
Critical: null,
Description: line.line_desc,
DiscountMarkup: null,
InvoiceNumber: line.billlines[0] && line.billlines[0].bill.invoice_number,
IOUPart: null,
LineNumber: line.line_no,
MarkUp: null,
OrderedOn: null,
OriginalCost: null,
OriginalInvoiceNumber: null,
PriceEach: line.billlines[0] && line.billlines[0].actual_cost,
PartNumber: _.escape(line.oem_partno),
ProfitPercent: null,
PurchaseOrderNumber: null,
Qty: line.part_qty,
Status: line.status,
SupplementNumber: null,
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,
Deleted: null,
ExpectedOn: null,
ReceivedOn: null,
OrderedBy: null,
ShipVia: null,
VendorContact: null,
EstimateAmount: line.act_price,
};
return ret;
};
const generateNullDetailLine = () => {
return {
BackOrdered: "0",
Cost: 0,
Critical: null,
Description: "No Lines on Estimate",
DiscountMarkup: null,
InvoiceNumber: null,
IOUPart: null,
LineNumber: 0,
MarkUp: null,
OrderedOn: null,
OriginalCost: null,
OriginalInvoiceNumber: null,
PriceEach: 0,
PartNumber: 0,
ProfitPercent: null,
PurchaseOrderNumber: null,
Qty: 0,
Status: null,
SupplementNumber: null,
Type: null,
Vendor: null,
VendorPaid: null,
VendorPrice: null,
Deleted: null,
ExpectedOn: null,
ReceivedOn: null,
OrderedBy: null,
ShipVia: null,
VendorContact: null,
EstimateAmount: null,
};
};