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 if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) { res.sendStatus(401); return; } 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: "", 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_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 || 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, }; };