diff --git a/server.js b/server.js index f16d583d5..a64545b72 100644 --- a/server.js +++ b/server.js @@ -224,6 +224,7 @@ app.post("/qbo/payments", fb.validateFirebaseIdToken, qbo.payments); var data = require("./server/data/data"); app.post("/data/ah", data.autohouse); app.post("/data/cc", data.claimscorp); +app.post("/data/kaizen", data.kaizen); app.post("/record-handler/arms", data.arms); var taskHandler = require("./server/tasks/tasks"); diff --git a/server/data/claimscorp.js b/server/data/claimscorp.js index 60943aed3..1768f6eb1 100644 --- a/server/data/claimscorp.js +++ b/server/data/claimscorp.js @@ -507,7 +507,7 @@ const CreateRepairOrderTag = (job, errorCallback) => { Body: repairCosts.BodyLaborTotalCost.toFormat(CCDineroFormat), Paint: repairCosts.RefinishLaborTotalCost.toFormat(CCDineroFormat), Prep: Dinero().toFormat(CCDineroFormat), - Frame: Dinero(job.job_totals.rates.laf.total).toFormat(CCDineroFormat), + Frame: repairCosts.FrameLaborTotalCost.toFormat(CCDineroFormat), Mech: repairCosts.MechanicalLaborTotalCost.toFormat(CCDineroFormat), Glass: repairCosts.GlassLaborTotalCost.toFormat(CCDineroFormat), Elec: repairCosts.ElectricalLaborTotalCost.toFormat(CCDineroFormat), diff --git a/server/data/data.js b/server/data/data.js index 077f9f134..18ec4c321 100644 --- a/server/data/data.js +++ b/server/data/data.js @@ -1,3 +1,4 @@ +exports.arms = require("./arms").default; exports.autohouse = require("./autohouse").default; exports.claimscorp = require("./claimscorp").default; -exports.arms = require("./arms").default; \ No newline at end of file +exports.kaizen = require("./kaizen").default; \ No newline at end of file diff --git a/server/data/kaizen.js b/server/data/kaizen.js new file mode 100644 index 000000000..57c354e4e --- /dev/null +++ b/server/data/kaizen.js @@ -0,0 +1,830 @@ +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"], + }, +}; + +exports.default = async (req, res) => { + //Query for the List of Bodyshop Clients. + logger.log("kaizen-start", "DEBUG", "api", null, null); + const kaizenShopsNames = ["SUMMIT", "STRATHMORE", "SUNRIDGE"]; + + const { bodyshops } = await client.request(queries.GET_KAIZEN_SHOPS, { + shopname: kaizenShopsNames, + }); + + 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("day") + : moment().subtract(5, "days").startOf("day"), + ...(end && { end: moment(end).endOf("day") }), + } + ); + + 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); + } +}; + +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), + }; +}; diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index a27ac8bd0..554388afe 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -1070,6 +1070,183 @@ query ENTEGRAL_EXPORT($bodyshopid: uuid!) { } }`; +exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) { + bodyshops_by_pk(id: $bodyshopid){ + id + shopname + address1 + city + state + zip_post + country + phone + last_name_first + md_ro_statuses + md_order_statuses + md_responsibility_centers + jc_hourly_rates + cdk_dealerid + pbs_serialnumber + use_paint_scale_data + timezone + } + jobs(where: {_and: [{updated_at: {_gt: $start}}, {updated_at: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}]}) { + actual_completion + actual_delivery + actual_in + asgn_date + bills { + billlines { + actual_cost + cost_center + id + quantity + } + federal_tax_rate + id + is_credit_memo + local_tax_rate + state_tax_rate + } + created_at + clm_no + date_estimated + date_exported + date_invoiced + date_open + date_repairstarted + employee_body_rel { + first_name + last_name + employee_number + id + } + employee_csr_rel { + first_name + last_name + employee_number + id + } + employee_prep_rel { + first_name + last_name + employee_number + id + } + employee_refinish_rel { + first_name + last_name + employee_number + id + } + est_ct_fn + est_ct_ln + id + ins_co_nm + joblines(where: {removed: {_eq: false}}) { + act_price + billlines(order_by: {bill: {date: desc_nulls_last}} limit: 1) { + actual_cost + actual_price + quantity + bill { + vendor { + name + } + invoice_number + date + } + } + db_price + id + lbr_op + line_desc + line_ind + line_no + mod_lb_hrs + mod_lbr_ty + parts_order_lines(order_by: {parts_order: {order_date: desc_nulls_last}} limit: 1){ + parts_order{ + id + order_date + } + } + part_qty + part_type + profitcenter_part + profitcenter_labor + prt_dsmk_m + prt_dsmk_p + oem_partno + status + } + job_totals + loss_date + mixdata(limit: 1, order_by: {updated_at: desc}) { + jobid + totalliquidcost + } + ownr_addr1 + ownr_addr2 + ownr_city + ownr_co_nm + ownr_fn + ownr_ln + ownr_st + ownr_zip + parts_orders(limit: 1, order_by: {created_at: desc}) { + created_at + } + parts_tax_rates + plate_no + rate_la1 + rate_la2 + rate_la3 + rate_la4 + rate_laa + rate_lab + rate_lad + rate_lae + rate_laf + rate_lag + rate_lam + rate_lar + rate_las + rate_lau + rate_ma2s + rate_ma2t + rate_ma3s + rate_mabl + rate_macs + rate_mahw + rate_matd + rate_mapa + rate_mash + ro_number + scheduled_completion + scheduled_delivery + scheduled_in + status + timetickets { + id + rate + cost_center + actualhrs + productivehrs + flat_rate + } + tlos_ind + v_color + v_model_yr + v_model_desc + v_make_desc + v_vin + vehicle { + v_bstyle + } + } +}`; + exports.UPDATE_JOB = ` mutation UPDATE_JOB($jobId: uuid!, $job: jobs_set_input!) { update_jobs(where: { id: { _eq: $jobId } }, _set: $job) { @@ -1542,7 +1719,7 @@ exports.GET_CLAIMSCORP_SHOPS = `query GET_CLAIMSCORP_SHOPS { } }`; -exports.GET_ENTEGRAL_SHOPS = `query GET_AUTOHOUSE_SHOPS { +exports.GET_ENTEGRAL_SHOPS = `query GET_ENTEGRAL_SHOPS { bodyshops(where: {entegral_id: {_is_null: false}, _or: {entegral_id: {_neq: ""}}}){ id shopname @@ -1562,6 +1739,26 @@ exports.GET_ENTEGRAL_SHOPS = `query GET_AUTOHOUSE_SHOPS { } }`; +exports.GET_KAIZEN_SHOPS = `query GET_KAIZEN_SHOPS($shopname: [String]) { + bodyshops(where: {shopname: {_in: $shopname}}){ + id + shopname + address1 + city + state + zip_post + country + phone + md_ro_statuses + md_order_statuses + autohouseid + md_responsibility_centers + jc_hourly_rates + imexshopid + timezone + } +}`; + exports.DELETE_ALL_DMS_VEHICLES = `mutation DELETE_ALL_DMS_VEHICLES{ delete_dms_vehicles(where: {}) { affected_rows