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 DineroFormat = "0,0.00"; const DateFormat = "MM/DD/YYYY"; const repairOpCodes = ["OP4", "OP9", "OP10"]; const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"]; const ftpSetup = { host: process.env.KAIZEN_HOST, port: process.env.KAIZEN_PORT, username: process.env.KAIZEN_USER, password: process.env.KAIZEN_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", ], }, }; exports.default = async (req, res) => { //Query for the List of Bodyshop Clients. logger.log("kaizen-start", "DEBUG", "api", null, null); const kaizenShopsIDs = ["SUMMIT", "STRATHMORE", "SUNRIDGE"]; const {bodyshops} = await client.request(queries.GET_KAIZEN_SHOPS, { imexshopid: kaizenShopsIDs, }); 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("kaizen-start-shop-extract", "DEBUG", "api", bodyshop.id, { shopname: bodyshop.shopname, }); const erroredJobs = []; try { const {jobs, bodyshops_by_pk} = await client.request( queries.KAIZEN_QUERY, { bodyshopid: bodyshop.id, start: start ? moment(start).startOf("hours") : moment().subtract(2, "hours").startOf("hour"), ...(end && {end: moment(end).endOf("hours")}), } ); const kaizenObject = { DataFeed: { ShopInfo: { ShopName: bodyshops_by_pk.shopname, Jobs: 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("kaizen-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, }, kaizenObject ) .end({allowEmptyTags: true}); allxmlsToUpload.push({ count: kaizenObject.DataFeed.ShopInfo.Jobs.length, xml: ret, filename: `${bodyshop.shopname}-${moment().format( "YYYYMMDDTHHMMss" )}.xml`, }); logger.log("kaizen-end-shop-extract", "DEBUG", "api", bodyshop.id, { shopname: bodyshop.shopname, }); } catch (error) { //Error at the shop level. logger.log("kaizen-error-shop", "ERROR", "api", bodyshop.id, { ...error, }); allErrors.push({ bodyshopid: bodyshop.id, imexshopid: bodyshop.imexshopid, shopname: bodyshop.shopname, fatal: true, errors: [error.toString()], }); } finally { allErrors.push({ bodyshopid: bodyshop.id, imexshopid: bodyshop.imexshopid, shopname: bodyshop.shopname, 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: `Kaizen 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("kaizen-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("kaizen-sftp-upload", "DEBUG", "api", null, { filename: xmlObj.filename, }); const uploadResult = await sftp.put( Buffer.from(xmlObj.xml), `/${xmlObj.filename}` ); logger.log("kaizen-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("kaizen-sftp-error", "ERROR", "api", null, { ...error, }); } finally { sftp.end(); } // sendServerEmail({ // subject: `Kaizen 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); sendServerEmail({ subject: `Kaizen Report ${moment().format("MM-DD-YY @ HH:mm:ss")}`, text: `Errors: JSON.stringify(error)} All Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}`, }); } }; 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); try { const ret = { JobID: job.id, RoNumber: job.ro_number, JobStatus: job.tlos_ind ? "Total Loss" : job.ro_number ? job.status : "Estimate", Customer: { CompanyName: job.ownr_co_nm?.trim() || "", FirstName: job.ownr_fn?.trim() || "", LastName: job.ownr_ln?.trim() || "", Address1: job.ownr_addr1?.trim() || "", Address2: job.ownr_addr2?.trim() || "", City: job.ownr_city?.trim() || "", State: job.ownr_st?.trim() || "", Zip: job.ownr_zip?.trim() || "", }, Vehicle: { 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 || "", BodyStyle: job.vehicle?.v_bstyle || "", Color: job.v_color || "", VIN: job.v_vin || "", PlateNo: job.plate_no || "", }, InsuranceCompany: job.ins_co_nm || "", Claim: job.clm_no || "", Contacts: { CSR: job.employee_csr_rel ? `${ job.employee_csr_rel.last_name ? job.employee_csr_rel.last_name : "" }${job.employee_csr_rel.last_name ? ", " : ""}${ job.employee_csr_rel.first_name ? job.employee_csr_rel.first_name : "" }` : "", Estimator: `${job.est_ct_ln ? job.est_ct_ln : ""}${ job.est_ct_ln ? ", " : "" }${job.est_ct_fn ? job.est_ct_fn : ""}`, }, Dates: { DateEstimated: (job.date_estimated && moment(job.date_estimated).format(DateFormat)) || "", DateOpened: (job.date_opened && moment(job.date_opened).format(DateFormat)) || "", DateScheduled: (job.scheduled_in && moment(job.scheduled_in) .tz(job.bodyshop.timezone) .format(DateFormat)) || "", DateArrived: (job.actual_in && moment(job.actual_in) .tz(job.bodyshop.timezone) .format(DateFormat)) || "", DateStart: job.date_repairstarted ? (job.date_repairstarted && moment(job.date_repairstarted) .tz(job.bodyshop.timezone) .format(DateFormat)) || "" : (job.actual_in && moment(job.actual_in) .tz(job.bodyshop.timezone) .format(DateFormat)) || "", DateScheduledCompletion: (job.scheduled_completion && moment(job.scheduled_completion) .tz(job.bodyshop.timezone) .format(DateFormat)) || "", DateCompleted: (job.actual_completion && moment(job.actual_completion) .tz(job.bodyshop.timezone) .format(DateFormat)) || "", DateScheduledDelivery: (job.scheduled_delivery && moment(job.scheduled_delivery) .tz(job.bodyshop.timezone) .format(DateFormat)) || "", DateDelivered: (job.actual_delivery && moment(job.actual_delivery) .tz(job.bodyshop.timezone) .format(DateFormat)) || "", DateInvoiced: (job.date_invoiced && moment(job.date_invoiced) .tz(job.bodyshop.timezone) .format(DateFormat)) || "", DateExported: (job.date_exported && moment(job.date_exported) .tz(job.bodyshop.timezone) .format(DateFormat)) || "", }, Sales: { Labour: { Aluminum: Dinero(job.job_totals.rates.laa.total).toFormat( DineroFormat ), Body: Dinero(job.job_totals.rates.lab.total).toFormat(DineroFormat), Diagnostic: Dinero(job.job_totals.rates.lad.total).toFormat( DineroFormat ), Electrical: Dinero(job.job_totals.rates.lae.total).toFormat( DineroFormat ), Frame: Dinero(job.job_totals.rates.laf.total).toFormat(DineroFormat), Glass: Dinero(job.job_totals.rates.lag.total).toFormat(DineroFormat), Mechanical: Dinero(job.job_totals.rates.lam.total).toFormat( DineroFormat ), OtherLabour: 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)) .toFormat(DineroFormat), Refinish: Dinero(job.job_totals.rates.lar.total).toFormat( DineroFormat ), Structural: Dinero(job.job_totals.rates.las.total).toFormat( DineroFormat ), }, Materials: { Body: Dinero(job.job_totals.rates.mash.total).toFormat(DineroFormat), Refinish: Dinero(job.job_totals.rates.mapa.total).toFormat( DineroFormat ), }, Parts: { Aftermarket: Dinero( job.job_totals.parts.parts.list.PAA && job.job_totals.parts.parts.list.PAA.total ).toFormat(DineroFormat), LKQ: Dinero( job.job_totals.parts.parts.list.PAL && job.job_totals.parts.parts.list.PAL.total ).toFormat(DineroFormat), OEM: 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(DineroFormat), OtherParts: Dinero( job.job_totals.parts.parts.list.PAO && job.job_totals.parts.parts.list.PAO.total ).toFormat(DineroFormat), Reconditioned: Dinero( job.job_totals.parts.parts.list.PAM && job.job_totals.parts.parts.list.PAM.total ).toFormat(DineroFormat), TotalParts: Dinero( job.job_totals.parts.parts.list.PAA && job.job_totals.parts.parts.list.PAA.total ) .add( Dinero( job.job_totals.parts.parts.list.PAL && job.job_totals.parts.parts.list.PAL.total ) ) .add( Dinero( job.job_totals.parts.parts.list.PAN && job.job_totals.parts.parts.list.PAN.total ) ) .add( Dinero( job.job_totals.parts.parts.list.PAO && job.job_totals.parts.parts.list.PAO.total ) ) .add( Dinero( job.job_totals.parts.parts.list.PAM && job.job_totals.parts.parts.list.PAM.total ) ) .toFormat(DineroFormat), }, OtherSales: Dinero(job.job_totals.additional.storage).toFormat( DineroFormat ), Sublet: Dinero(job.job_totals.parts.sublets.total).toFormat( DineroFormat ), Towing: Dinero(job.job_totals.additional.towing).toFormat(DineroFormat), ATS: job.job_totals.additional.additionalCostItems.includes( "ATS Amount" ) === true ? Dinero( job.job_totals.additional.additionalCostItems[ job.job_totals.additional.additionalCostItems.indexOf( "ATS Amount" ) ].total ).toFormat(DineroFormat) : Dinero().toFormat(DineroFormat), SaleSubtotal: Dinero(job.job_totals.totals.subtotal).toFormat( DineroFormat ), Tax: 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(DineroFormat), SaleTotal: Dinero(job.job_totals.totals.total_repairs).toFormat( DineroFormat ), }, SaleHours: { Aluminum: job.job_totals.rates.laa.hours.toFixed(2), Body: job.job_totals.rates.lab.hours.toFixed(2), Diagnostic: job.job_totals.rates.lad.hours.toFixed(2), Electrical: job.job_totals.rates.lae.hours.toFixed(2), Frame: job.job_totals.rates.laf.hours.toFixed(2), Glass: job.job_totals.rates.lag.hours.toFixed(2), Mechanical: job.job_totals.rates.lam.hours.toFixed(2), Other: ( 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 ).toFixed(2), Refinish: job.job_totals.rates.lar.hours.toFixed(2), Structural: job.job_totals.rates.las.hours.toFixed(2), TotalHours: job.joblines .reduce((acc, val) => acc + val.mod_lb_hrs, 0) .toFixed(2), }, Costs: { Labour: { Aluminum: repairCosts.AluminumLabourTotalCost.toFormat(DineroFormat), Body: repairCosts.BodyLabourTotalCost.toFormat(DineroFormat), Diagnostic: repairCosts.DiagnosticLabourTotalCost.toFormat(DineroFormat), Electrical: repairCosts.ElectricalLabourTotalCost.toFormat(DineroFormat), Frame: repairCosts.FrameLabourTotalCost.toFormat(DineroFormat), Glass: repairCosts.GlassLabourTotalCost.toFormat(DineroFormat), Mechancial: repairCosts.MechanicalLabourTotalCost.toFormat(DineroFormat), OtherLabour: repairCosts.LabourMiscTotalCost.toFormat(DineroFormat), Refinish: repairCosts.RefinishLabourTotalCost.toFormat(DineroFormat), Structural: repairCosts.StructuralLabourTotalCost.toFormat(DineroFormat), TotalLabour: repairCosts.LabourTotalCost.toFormat(DineroFormat), }, Materials: { Body: repairCosts.BMTotalCost.toFormat(DineroFormat), Refinish: repairCosts.PMTotalCost.toFormat(DineroFormat), }, Parts: { Aftermarket: repairCosts.PartsAMCost.toFormat(DineroFormat), LKQ: repairCosts.PartsRecycledCost.toFormat(DineroFormat), OEM: repairCosts.PartsOemCost.toFormat(DineroFormat), OtherCost: repairCosts.PartsOtherCost.toFormat(DineroFormat), Reconditioned: repairCosts.PartsReconditionedCost.toFormat(DineroFormat), TotalParts: repairCosts.PartsAMCost.add(repairCosts.PartsRecycledCost) .add(repairCosts.PartsReconditionedCost) .add(repairCosts.PartsOemCost) .add(repairCosts.PartsOtherCost) .toFormat(DineroFormat), }, Sublet: repairCosts.SubletTotalCost.toFormat(DineroFormat), Towing: repairCosts.TowingTotalCost.toFormat(DineroFormat), ATS: Dinero().toFormat(DineroFormat), Storage: repairCosts.StorageTotalCost.toFormat(DineroFormat), CostTotal: repairCosts.TotalCost.toFormat(DineroFormat), }, CostHours: { Aluminum: repairCosts.AluminumLabourTotalHrs.toFixed(2), Body: repairCosts.BodyLabourTotalHrs.toFixed(2), Diagnostic: repairCosts.DiagnosticLabourTotalHrs.toFixed(2), Refinish: repairCosts.RefinishLabourTotalHrs.toFixed(2), Frame: repairCosts.FrameLabourTotalHrs.toFixed(2), Mechanical: repairCosts.MechanicalLabourTotalHrs.toFixed(2), Glass: repairCosts.GlassLabourTotalHrs.toFixed(2), Electrical: repairCosts.ElectricalLabourTotalHrs.toFixed(2), Structural: repairCosts.StructuralLabourTotalHrs.toFixed(2), Other: repairCosts.LabourMiscTotalHrs.toFixed(2), CostTotalHours: repairCosts.TotalHrs.toFixed(2), }, }; return ret; } catch (error) { logger.log("kaizen-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 Labour 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; }, {} ); const ticketHrsByCostCenter = job.timetickets.reduce( (ticket_acc, ticket_val) => { //At the invoice level. if (!ticket_acc[ticket_val.cost_center]) ticket_acc[ticket_val.cost_center] = 0; ticket_acc[ticket_val.cost_center] = ticket_acc[ticket_val.cost_center] + (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()), AluminumLabourTotalCost: ticketTotalsByCostCenter[defaultCosts.LAA] || Dinero(), AluminumLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAA] || 0, BodyLabourTotalCost: ticketTotalsByCostCenter[defaultCosts.LAB] || Dinero(), BodyLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAB] || 0, DiagnosticLabourTotalCost: ticketTotalsByCostCenter[defaultCosts.LAD] || Dinero(), DiagnosticLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAD] || 0, ElectricalLabourTotalCost: ticketTotalsByCostCenter[defaultCosts.LAE] || Dinero(), ElectricalLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAE] || 0, FrameLabourTotalCost: ticketTotalsByCostCenter[defaultCosts.LAF] || Dinero(), FrameLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAF] || 0, GlassLabourTotalCost: ticketTotalsByCostCenter[defaultCosts.LAG] || Dinero(), GlassLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAG] || 0, LabourMiscTotalCost: ( 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()), LabourMiscTotalHrs: (ticketHrsByCostCenter[defaultCosts.LA1] || 0) + (ticketHrsByCostCenter[defaultCosts.LA2] || 0) + (ticketHrsByCostCenter[defaultCosts.LA3] || 0) + (ticketHrsByCostCenter[defaultCosts.LA4] || 0) + (ticketHrsByCostCenter[defaultCosts.LAU] || 0), MechanicalLabourTotalCost: ticketTotalsByCostCenter[defaultCosts.LAM] || Dinero(), MechanicalLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAM] || 0, RefinishLabourTotalCost: ticketTotalsByCostCenter[defaultCosts.LAR] || Dinero(), RefinishLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAR] || 0, StructuralLabourTotalCost: ticketTotalsByCostCenter[defaultCosts.LAS] || Dinero(), StructuralLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAS] || 0, 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(), LabourTotalCost: Object.keys(ticketTotalsByCostCenter).reduce( (acc, key) => { return acc.add(ticketTotalsByCostCenter[key]); }, Dinero() ), TotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => { return acc.add(billTotalsByCostCenters[key]); }, Dinero()), TotalHrs: job.timetickets.reduce((acc, ticket_val) => { return ( acc + (ticket_val.flat_rate ? ticket_val.productivehrs : ticket_val.actualhrs) || 0 ); }, 0), }; };