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, DecodedPfmLine,
JobMaterialRateFields, JobMaterialRateFields,
} from "./decode-pfm.interface"; } from "./decode-pfm.interface";
import YNBoolConverter from "../../util/ynBoolConverter";
const DecodePfm = async ( const DecodePfm = async (
extensionlessFilePath: string extensionlessFilePath: string
@@ -45,40 +46,42 @@ const DecodePfm = async (
}; };
const rawPfmData: DecodedPfmLine[] = rawDBFRecord.map((record) => { const rawPfmData: DecodedPfmLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedPfmLine = deepLowerCaseKeys( const singleLineData: DecodedPfmLine = YNBoolConverter(
_.pick(record, [ deepLowerCaseKeys(
//TODO: Add typings for EMS File Formats. _.pick(record, [
"MATL_TYPE", //TODO: Add typings for EMS File Formats.
"CAL_CODE", "MATL_TYPE",
"CAL_DESC", "CAL_CODE",
"CAL_MAXDLR", "CAL_DESC",
"CAL_PRIP", "CAL_MAXDLR",
"CAL_PRIP",
"CAL_SECP", "CAL_SECP",
"MAT_CALP", "MAT_CALP",
"CAL_PRETHR", //Mitchell here "CAL_PRETHR", //Mitchell here
"CAL_PSTTHR", "CAL_PSTTHR",
"CAL_THRAMT", "CAL_THRAMT",
"CAL_LBRMIN", "CAL_LBRMIN",
"CAL_LBRRTE", //Audatex puts it here "CAL_LBRRTE", //Audatex puts it here
"CAL_OPCODE", "CAL_OPCODE",
"TAX_IND", "TAX_IND",
"MAT_TAXP", "MAT_TAXP",
"MAT_ADJP", "MAT_ADJP",
"MAT_TX_TY1", "MAT_TX_TY1",
"MAT_TX_IN1", "MAT_TX_IN1",
"MAT_TX_TY2", "MAT_TX_TY2",
"MAT_TX_IN2", "MAT_TX_IN2",
"MAT_TX_TY3", "MAT_TX_TY3",
"MAT_TX_IN3", "MAT_TX_IN3",
"MAT_TX_TY4", "MAT_TX_TY4",
"MAT_TX_IN4", "MAT_TX_IN4",
"MAT_TX_TY5", "MAT_TX_TY5",
"MAT_TX_IN5", "MAT_TX_IN5",
]) ])
)
); );
//Also capture the whole object. //Also capture the whole object.
@@ -89,15 +92,44 @@ const DecodePfm = async (
}); });
//Apply line by line adjustments. //Apply line by line adjustments.
const materialsLine: DecodedPfmLine | undefined = rawPfmData.find( const mapaLine: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === "MAPA" (line) => line.matl_type === "MAPA"
); );
if (mapaLine) {
if (materialsLine) {
jobMaterialRates.rate_mapa = 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. //Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated` //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 deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck"; import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedPfo } from "./decode-pfo.interface"; import { DecodedPfo } from "./decode-pfo.interface";
import YNBoolConverter from "../../util/ynBoolConverter";
const DecodePfo = async ( const DecodePfo = async (
extensionlessFilePath: string extensionlessFilePath: string
@@ -27,36 +28,38 @@ const DecodePfo = async (
//PFO will always have only 1 row. //PFO will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields. //Commented lines have been cross referenced with existing partner fields.
const rawPfoData: DecodedPfo = deepLowerCaseKeys( const rawPfoData: DecodedPfo = YNBoolConverter(
_.pick(rawDBFRecord[0], [ deepLowerCaseKeys(
//TODO: Add typings for EMS File Formats. _.pick(rawDBFRecord[0], [
"TX_TOW_TY", //TODO: Add typings for EMS File Formats.
"TOW_T_TY1", "TX_TOW_TY",
"TOW_T_IN1", "TOW_T_TY1",
"TOW_T_TY2", "TOW_T_IN1",
"TOW_T_IN2", "TOW_T_TY2",
"TOW_T_TY3", "TOW_T_IN2",
"TOW_T_IN3", "TOW_T_TY3",
"TOW_T_TY4", "TOW_T_IN3",
"TOW_T_IN4", "TOW_T_TY4",
"TOW_T_TY5", "TOW_T_IN4",
"TOW_T_IN5", "TOW_T_TY5",
"TOW_T_TY6", "TOW_T_IN5",
"TOW_T_IN6", "TOW_T_TY6",
"TX_STOR_TY", "TOW_T_IN6",
"STOR_T_TY1", "TX_STOR_TY",
"STOR_T_IN1", "STOR_T_TY1",
"STOR_T_TY2", "STOR_T_IN1",
"STOR_T_IN2", "STOR_T_TY2",
"STOR_T_TY3", "STOR_T_IN2",
"STOR_T_IN3", "STOR_T_TY3",
"STOR_T_TY4", "STOR_T_IN3",
"STOR_T_IN4", "STOR_T_TY4",
"STOR_T_TY5", "STOR_T_IN4",
"STOR_T_IN5", "STOR_T_TY5",
"STOR_T_TY6", "STOR_T_IN5",
"STOR_T_IN6", "STOR_T_TY6",
]) "STOR_T_IN6",
])
)
); );
//Apply business logic transfomrations. //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 { DecodedPfm } from "./decode-pfm.interface";
import DecodePfo from "./decode-pfo"; import DecodePfo from "./decode-pfo";
import { DecodedPfo } from "./decode-pfo.interface"; import { DecodedPfo } from "./decode-pfo.interface";
import DecodePfp from "./decode-pfp";
import { DecodedPfp } from "./decode-pfp.interface";
import DecodePft from "./decode-pft"; import DecodePft from "./decode-pft";
import { DecodedPft } from "./decode-pft.interface"; import { DecodedPft } from "./decode-pft.interface";
import DecodeStl from "./decode-stl"; import DecodeStl from "./decode-stl";
@@ -23,6 +25,8 @@ import DecodeTtl from "./decode-ttl";
import { DecodedTtl } from "./decode-ttl.interface"; import { DecodedTtl } from "./decode-ttl.interface";
import DecodeVeh from "./decode-veh"; import DecodeVeh from "./decode-veh";
import { DecodedVeh } from "./decode-veh.interface"; import { DecodedVeh } from "./decode-veh.interface";
import { DecodedEnv } from "./decode-env.interface";
import DecodeEnv from "./decode-env";
async function ImportJob(filepath: string): Promise<void> { async function ImportJob(filepath: string): Promise<void> {
const parsedFilePath = path.parse(filepath); const parsedFilePath = path.parse(filepath);
@@ -35,6 +39,7 @@ async function ImportJob(filepath: string): Promise<void> {
try { try {
//The below all end up returning parts of the job object. //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. //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 ad1: DecodedAd1 = await DecodeAD1(extensionlessFilePath);
const ad2: DecodedAD2 = await DecodeAD2(extensionlessFilePath); const ad2: DecodedAD2 = await DecodeAD2(extensionlessFilePath);
const veh: DecodedVeh = await DecodeVeh(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 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 stl: DecodedStl[] = await DecodeStl(extensionlessFilePath); // TODO: This will be the `cieca_stl` object
const ttl: DecodedTtl = await DecodeTtl(extensionlessFilePath); const ttl: DecodedTtl = await DecodeTtl(extensionlessFilePath);
const pfp: DecodedPfp = await DecodePfp(extensionlessFilePath);
log.debug("EMS Object", { log.debug("Job Object", {
ad1, ...env,
ad2, ...ad1,
veh, ...ad2,
lin, ...veh,
pfh, joblines: { data: lin },
pfl, ...pfh,
pft, cieca_pfl: pfl,
pfm, cieca_pft: pft,
pfo, materials: pfm,
stl, cieca_pfo: pfo,
ttl, ...stl,
...ttl,
parts_tax_rates: pfp,
}); });
} catch (error) { } catch (error) {
log.error("Error encountered while decoding job. ", errorTypeCheck(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;