Add remaining EMS file parsing.

This commit is contained in:
Patrick Fic
2025-03-19 14:52:29 -07:00
parent e67309ed4d
commit 2e5fe7c99d
8 changed files with 281 additions and 76 deletions

View File

@@ -0,0 +1,4 @@
export interface DecodedEnv {
est_system: string;
estfile_id: string;
}

View File

@@ -0,0 +1,42 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedEnv } from "./decode-env.interface";
const DecodeEnv = async (
extensionlessFilePath: string
): Promise<DecodedEnv> => {
let dbf: DBFFile | null = null;
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.ENV`);
} catch (error) {
log.error("Error opening ENV File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any ENV files at ${extensionlessFilePath}`);
throw new Error(`Could not find any ENV files at ${extensionlessFilePath}`);
}
const rawDBFRecord = await dbf.readRecords(1);
//AD2 will always have only 1 row.
//TODO: Determine if there's any value to capture the whole ENV file.
const rawEnvData: DecodedEnv = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
//TODO: Several of these fields will fail. Should extend schema to capture them.
"EST_SYSTEM",
"ESTFILE_ID",
])
);
//Apply business logic transfomrations.
return rawEnvData;
};
export default DecodeEnv;

View File

@@ -8,6 +8,7 @@ import {
DecodedPfmLine,
JobMaterialRateFields,
} from "./decode-pfm.interface";
import YNBoolConverter from "../../util/ynBoolConverter";
const DecodePfm = async (
extensionlessFilePath: string
@@ -45,40 +46,42 @@ const DecodePfm = async (
};
const rawPfmData: DecodedPfmLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedPfmLine = deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"MATL_TYPE",
"CAL_CODE",
"CAL_DESC",
"CAL_MAXDLR",
"CAL_PRIP",
const singleLineData: DecodedPfmLine = YNBoolConverter(
deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"MATL_TYPE",
"CAL_CODE",
"CAL_DESC",
"CAL_MAXDLR",
"CAL_PRIP",
"CAL_SECP",
"MAT_CALP",
"CAL_PRETHR", //Mitchell here
"CAL_PSTTHR",
"CAL_THRAMT",
"CAL_SECP",
"MAT_CALP",
"CAL_PRETHR", //Mitchell here
"CAL_PSTTHR",
"CAL_THRAMT",
"CAL_LBRMIN",
"CAL_LBRMIN",
"CAL_LBRRTE", //Audatex puts it here
"CAL_OPCODE",
"CAL_LBRRTE", //Audatex puts it here
"CAL_OPCODE",
"TAX_IND",
"MAT_TAXP",
"MAT_ADJP",
"MAT_TX_TY1",
"MAT_TX_IN1",
"MAT_TX_TY2",
"MAT_TX_IN2",
"MAT_TX_TY3",
"MAT_TX_IN3",
"MAT_TX_TY4",
"MAT_TX_IN4",
"MAT_TX_TY5",
"MAT_TX_IN5",
])
"TAX_IND",
"MAT_TAXP",
"MAT_ADJP",
"MAT_TX_TY1",
"MAT_TX_IN1",
"MAT_TX_TY2",
"MAT_TX_IN2",
"MAT_TX_TY3",
"MAT_TX_IN3",
"MAT_TX_TY4",
"MAT_TX_IN4",
"MAT_TX_TY5",
"MAT_TX_IN5",
])
)
);
//Also capture the whole object.
@@ -89,15 +92,44 @@ const DecodePfm = async (
});
//Apply line by line adjustments.
const materialsLine: DecodedPfmLine | undefined = rawPfmData.find(
const mapaLine: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === "MAPA"
);
if (materialsLine) {
if (mapaLine) {
jobMaterialRates.rate_mapa =
materialsLine.cal_lbrrte || materialsLine.cal_prethr || 0;
mapaLine.cal_lbrrte || mapaLine.cal_prethr || 0;
jobMaterialRates.tax_paint_mat_rt = mapaLine.mat_taxp ?? 0 / 100;
}
const mashLine: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === "MASH"
);
if (mashLine) {
jobMaterialRates.rate_mash =
mashLine.cal_lbrrte || mashLine.cal_prethr || 0;
jobMaterialRates.tax_shop_mat_rt = mashLine.mat_taxp ?? 0 / 100;
}
const mahwLine: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === "MAHW"
);
if (mahwLine) {
jobMaterialRates.rate_mahw =
mahwLine.cal_lbrrte || mahwLine.cal_prethr || 0;
jobMaterialRates.tax_levies_rt = mahwLine.mat_taxp ?? 0 / 100;
}
const additionalMaterials = ["MA2S", "MA2T", "MA3S", "MACS", "MABL"];
additionalMaterials.forEach((type) => {
const line: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === type
);
if (line) {
jobMaterialRates[`rate_${type.toLowerCase()}`] =
line.cal_lbrrte || line.cal_prethr || 0;
}
});
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`

View File

@@ -4,6 +4,7 @@ import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedPfo } from "./decode-pfo.interface";
import YNBoolConverter from "../../util/ynBoolConverter";
const DecodePfo = async (
extensionlessFilePath: string
@@ -27,36 +28,38 @@ const DecodePfo = async (
//PFO will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawPfoData: DecodedPfo = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"TX_TOW_TY",
"TOW_T_TY1",
"TOW_T_IN1",
"TOW_T_TY2",
"TOW_T_IN2",
"TOW_T_TY3",
"TOW_T_IN3",
"TOW_T_TY4",
"TOW_T_IN4",
"TOW_T_TY5",
"TOW_T_IN5",
"TOW_T_TY6",
"TOW_T_IN6",
"TX_STOR_TY",
"STOR_T_TY1",
"STOR_T_IN1",
"STOR_T_TY2",
"STOR_T_IN2",
"STOR_T_TY3",
"STOR_T_IN3",
"STOR_T_TY4",
"STOR_T_IN4",
"STOR_T_TY5",
"STOR_T_IN5",
"STOR_T_TY6",
"STOR_T_IN6",
])
const rawPfoData: DecodedPfo = YNBoolConverter(
deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"TX_TOW_TY",
"TOW_T_TY1",
"TOW_T_IN1",
"TOW_T_TY2",
"TOW_T_IN2",
"TOW_T_TY3",
"TOW_T_IN3",
"TOW_T_TY4",
"TOW_T_IN4",
"TOW_T_TY5",
"TOW_T_IN5",
"TOW_T_TY6",
"TOW_T_IN6",
"TX_STOR_TY",
"STOR_T_TY1",
"STOR_T_IN1",
"STOR_T_TY2",
"STOR_T_IN2",
"STOR_T_TY3",
"STOR_T_IN3",
"STOR_T_TY4",
"STOR_T_IN4",
"STOR_T_TY5",
"STOR_T_IN5",
"STOR_T_TY6",
"STOR_T_IN6",
])
)
);
//Apply business logic transfomrations.

View File

@@ -0,0 +1,33 @@
export interface DecodedPfpLine {
prt_type: string;
prt_tax_in: boolean;
prt_tax_rt: number;
prt_mkupp: number;
prt_mktyp: string;
prt_discp: number;
prt_tx_ty1: string;
prt_tx_in1: boolean;
prt_tx_ty2: string;
prt_tx_in2: boolean;
prt_tx_ty3: string;
prt_tx_in3: boolean;
prt_tx_ty4: string;
prt_tx_in4: boolean;
prt_tx_ty5: string;
prt_tx_in5: boolean;
}
export interface DecodedPfp {
PAA: DecodedPfpLine;
PAC: DecodedPfpLine;
PAL: DecodedPfpLine;
PAG: DecodedPfpLine;
PAM: DecodedPfpLine;
PAP: DecodedPfpLine;
PAN: DecodedPfpLine;
PAO: DecodedPfpLine;
PAR: DecodedPfpLine;
PAS: DecodedPfpLine;
PASL: DecodedPfpLine;
PAT: DecodedPfpLine;
}

View File

@@ -0,0 +1,71 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import YNBoolConverter from "../../util/ynBoolConverter";
import { DecodedPfp, DecodedPfpLine } from "./decode-pfp.interface";
const DecodePfp = async (
extensionlessFilePath: string
): Promise<DecodedPfp> => {
let dbf: DBFFile | null = null;
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFP`);
} catch (error) {
//PFP File only has 1 location.
log.error("Error opening PFP File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any PFP files at ${extensionlessFilePath}`);
throw new Error(`Could not find any PFP files at ${extensionlessFilePath}`);
}
const rawDBFRecord = await dbf.readRecords();
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawPfpData: DecodedPfpLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedPfpLine = deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"PRT_TYPE",
"PRT_TAX_IN",
"PRT_TAX_RT",
"PRT_MKUPP",
"PRT_MKTYP",
"PRT_DISCP",
"PRT_TX_TY1",
"PRT_TX_IN1",
"PRT_TX_TY2",
"PRT_TX_IN2",
"PRT_TX_TY3",
"PRT_TX_IN3",
"PRT_TX_TY4",
"PRT_TX_IN4",
"PRT_TX_TY5",
"PRT_TX_IN5",
])
);
singleLineData.prt_tax_rt = singleLineData.prt_tax_rt / 100;
return YNBoolConverter(singleLineData);
});
//Apply business logic transfomrations.
//Convert array of lines to a hash object.
const parsedPfpFile: DecodedPfp = rawPfpData.reduce(
(acc: DecodedPfp, line: DecodedPfpLine) => {
acc[line.prt_type] = line;
return acc;
},
{} as DecodedPfp
);
return parsedPfpFile;
};
export default DecodePfp;

View File

@@ -15,6 +15,8 @@ import DecodePfm from "./decode-pfm";
import { DecodedPfm } from "./decode-pfm.interface";
import DecodePfo from "./decode-pfo";
import { DecodedPfo } from "./decode-pfo.interface";
import DecodePfp from "./decode-pfp";
import { DecodedPfp } from "./decode-pfp.interface";
import DecodePft from "./decode-pft";
import { DecodedPft } from "./decode-pft.interface";
import DecodeStl from "./decode-stl";
@@ -23,6 +25,8 @@ import DecodeTtl from "./decode-ttl";
import { DecodedTtl } from "./decode-ttl.interface";
import DecodeVeh from "./decode-veh";
import { DecodedVeh } from "./decode-veh.interface";
import { DecodedEnv } from "./decode-env.interface";
import DecodeEnv from "./decode-env";
async function ImportJob(filepath: string): Promise<void> {
const parsedFilePath = path.parse(filepath);
@@ -35,6 +39,7 @@ async function ImportJob(filepath: string): Promise<void> {
try {
//The below all end up returning parts of the job object.
//Some of them return additional info - e.g. owner or vehicle record data at both the job and corresponding table level.
const env: DecodedEnv = await DecodeEnv(extensionlessFilePath);
const ad1: DecodedAd1 = await DecodeAD1(extensionlessFilePath);
const ad2: DecodedAD2 = await DecodeAD2(extensionlessFilePath);
const veh: DecodedVeh = await DecodeVeh(extensionlessFilePath);
@@ -46,19 +51,22 @@ async function ImportJob(filepath: string): Promise<void> {
const pfo: DecodedPfo = await DecodePfo(extensionlessFilePath); // TODO: This will be the `cieca_pfo` object
const stl: DecodedStl[] = await DecodeStl(extensionlessFilePath); // TODO: This will be the `cieca_stl` object
const ttl: DecodedTtl = await DecodeTtl(extensionlessFilePath);
const pfp: DecodedPfp = await DecodePfp(extensionlessFilePath);
log.debug("EMS Object", {
ad1,
ad2,
veh,
lin,
pfh,
pfl,
pft,
pfm,
pfo,
stl,
ttl,
log.debug("Job Object", {
...env,
...ad1,
...ad2,
...veh,
joblines: { data: lin },
...pfh,
cieca_pfl: pfl,
cieca_pft: pft,
materials: pfm,
cieca_pfo: pfo,
...stl,
...ttl,
parts_tax_rates: pfp,
});
} catch (error) {
log.error("Error encountered while decoding job. ", errorTypeCheck(error));

View File

@@ -0,0 +1,12 @@
const YNBoolConverter = <T extends object>(original: T): T => {
Object.keys(original).forEach((key) => {
if (original[key] === "Y") {
original[key] = true;
} else if (original[key] === "N") {
original[key] = false;
}
});
return original;
};
export default YNBoolConverter;