580 lines
22 KiB
JavaScript
580 lines
22 KiB
JavaScript
// Schema: knowledge/original/경쟁사 프로그램/STmate 분석/30_원자료/조달청_공사원가호환규정.xsd (1.0.5).
|
|
const EstC3r = (() => {
|
|
const kinds = {
|
|
5: "자재",
|
|
6: "노무",
|
|
21: "표준시장단가",
|
|
101: "경비항목",
|
|
103: "일위대가",
|
|
104: "중기단가산출",
|
|
105: "기계경비",
|
|
};
|
|
const esc = (value) => {
|
|
if (/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(String(value ?? "")))
|
|
throw Error("XML에 사용할 수 없는 제어문자가 있습니다.");
|
|
return String(value ?? "").replace(
|
|
/[&<>"']/g,
|
|
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c],
|
|
);
|
|
};
|
|
const tag = (name, value) => `<${name}>${esc(value)}</${name}>`;
|
|
const raw = (name, value) => `<${name}>${value}</${name}>`;
|
|
const n = (value) => {
|
|
const number = Number(value || 0);
|
|
if (!Number.isFinite(number)) throw Error("XML 수치 오류");
|
|
return number;
|
|
};
|
|
function text(value, length, label) {
|
|
const s = String(value ?? "").replace(/[\r\n\t]/g, " ");
|
|
if (s.length > length) throw Error(`${label}: XML 길이 ${length}자 초과`);
|
|
return s;
|
|
}
|
|
function code(value, length = 17) {
|
|
const s = String(value || "");
|
|
if (!s || s.length > length || !/^[A-Z0-9]+$/.test(s))
|
|
throw Error(`XML 코드 형식 오류: ${s} (영문 대문자/숫자 ${length}자 이내)`);
|
|
return s;
|
|
}
|
|
const name = (row) =>
|
|
tag("품명", text(row.Name, 50, "품명")) +
|
|
tag("규격", text(row.Size, 100, "규격")) +
|
|
tag("단위", text(row.Unite, 10, "단위"));
|
|
const amounts = (row) =>
|
|
tag("재료비", n(row.GC)) + tag("노무비", n(row.NC)) + tag("경비", n(row.JC) + n(row.SC));
|
|
const prices = (row) =>
|
|
tag("재료비단가", n(row.GU)) +
|
|
tag("노무비단가", n(row.NU)) +
|
|
tag("경비단가", n(row.JU) + n(row.SU));
|
|
const remarks = (row) => tag("비고", text(row.Bigo || row.Bigo1 || "", 50, "비고"));
|
|
const flag = (value) => (n(value) >= 2 ? "true" : "false");
|
|
function output(project, meta) {
|
|
if (!meta?.company?.trim() || !meta?.kind)
|
|
throw Error("XML 작성업체명과 공사구분을 입력하세요.");
|
|
if (
|
|
![
|
|
"토목공사",
|
|
"건축공사",
|
|
"설비공사",
|
|
"전기공사",
|
|
"통신공사",
|
|
"조경공사",
|
|
"소방공사",
|
|
"문화재수리공사",
|
|
"국가유산수리공사",
|
|
].includes(meta.kind)
|
|
)
|
|
throw Error("XML 공사구분 오류");
|
|
const date = meta.date || new Date().toISOString().slice(0, 10);
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || Number.isNaN(Date.parse(date)))
|
|
throw Error("XML 제작일 오류");
|
|
const source = project.tables,
|
|
generated = [],
|
|
warnings = [];
|
|
const available = new Map(
|
|
Object.keys(kinds).map((kind) => [
|
|
Number(kind),
|
|
new Set((source[EstNative.sourceTables[kind]] || []).map((row) => row.Code)),
|
|
]),
|
|
);
|
|
function requireSource(kind, key) {
|
|
if (!available.get(n(kind))?.has(key)) throw Error(`XML 연결 자료 없음: ${kind}/${key}`);
|
|
}
|
|
const expenseCodes = new Set((source.EstiMateDat || []).map((r) => r.Code));
|
|
let serial = 0;
|
|
function expense(row, unit = false) {
|
|
let value;
|
|
do {
|
|
value = "X" + String(++serial).padStart(10, "0");
|
|
} while (expenseCodes.has(value));
|
|
expenseCodes.add(value);
|
|
generated.push({
|
|
Code: value,
|
|
Name: row.Name,
|
|
Size: row.Size,
|
|
Unite: row.Unite,
|
|
NC: unit ? n(row.NU) : n(row.NC),
|
|
GC: unit ? n(row.GU) : n(row.GC),
|
|
JC: unit ? n(row.JU) + n(row.SU) : n(row.JC) + n(row.SC),
|
|
});
|
|
return value;
|
|
}
|
|
function detail(row, index) {
|
|
let kind = kinds[n(row.LinkIndex)],
|
|
key = row.Code,
|
|
gn = n(row.GN),
|
|
current = { ...row };
|
|
if (!kind || !key || [1, 3].includes(n(row.CalcIndex))) {
|
|
kind = "경비항목";
|
|
key = expense(row);
|
|
gn = 1;
|
|
current.GU = n(row.GC);
|
|
current.NU = n(row.NC);
|
|
current.JU = n(row.JC) + n(row.SC);
|
|
current.SU = 0;
|
|
warnings.push(`${row.Code || row.Name || index}: 독립 금액 항목으로 변환`);
|
|
} else requireSource(row.LinkIndex, key);
|
|
code(key);
|
|
return raw(
|
|
"세부공종",
|
|
tag("세부공종코드", key) +
|
|
tag("자원구분", kind) +
|
|
tag("계산제외", flag(row.CalcIndex)) +
|
|
tag("세부공종번호", index) +
|
|
tag("세부공종명", text(row.Name, 50, "세부공종명")) +
|
|
tag("규격", text(row.Size, 100, "규격")) +
|
|
tag("단위", text(row.Unite, 10, "단위")) +
|
|
tag("수량", gn) +
|
|
prices(current) +
|
|
amounts(row) +
|
|
tag("관급", "false") +
|
|
tag("자재구분", "구분없음") +
|
|
tag("세부공종구분", "일반내역") +
|
|
remarks(row),
|
|
);
|
|
}
|
|
let active = null;
|
|
const groups = [],
|
|
stack = [],
|
|
siblings = new Map();
|
|
function addGroup(name, level, header = null) {
|
|
while (stack.length && stack.at(-1).level <= level) stack.pop();
|
|
const parent = stack.at(-1)?.code || "",
|
|
number = (siblings.get(parent) || 0) + 1;
|
|
if (number > 99 || parent.length >= 20)
|
|
throw Error("XML 공종 계층/동일 단계 공종 수 한도 초과");
|
|
siblings.set(parent, number);
|
|
active = { name, level, header, code: parent + String(number).padStart(2, "0"), rows: [] };
|
|
groups.push(active);
|
|
stack.push(active);
|
|
}
|
|
for (const row of source.EstMainDat) {
|
|
if (n(row.LinkIndex) === 54) break;
|
|
if (n(row.Level) > 0) {
|
|
addGroup(row.Name, n(row.Level), row);
|
|
continue;
|
|
}
|
|
if (!active) addGroup(project.name, 10);
|
|
active.rows.push(row);
|
|
}
|
|
if (!groups.length) addGroup(project.name, 10);
|
|
const boq = groups
|
|
.map((group, i) =>
|
|
raw(
|
|
"내역서",
|
|
tag("내역서번호", group.code) +
|
|
tag("공종구분", "일반공종") +
|
|
tag("계산제외", "false") +
|
|
tag("공종번호", i + 1) +
|
|
tag("공종명", text(group.name, 50, "공종명")) +
|
|
amounts(
|
|
group.header || {
|
|
GC: group.rows.reduce((s, r) => s + (n(r.CalcIndex) < 2 ? n(r.GC) : 0), 0),
|
|
NC: group.rows.reduce((s, r) => s + (n(r.CalcIndex) < 2 ? n(r.NC) : 0), 0),
|
|
JC: group.rows.reduce(
|
|
(s, r) => s + (n(r.CalcIndex) < 2 ? n(r.JC) + n(r.SC) : 0),
|
|
0,
|
|
),
|
|
},
|
|
) +
|
|
tag("비고", "") +
|
|
group.rows.map((r, j) => detail(r, j + 1)).join(""),
|
|
),
|
|
)
|
|
.join("");
|
|
const head = [...EstNative.groups(source.HeadMainDat || []).values()]
|
|
.map((group) => {
|
|
const title =
|
|
source.HeadTitleDat?.find((r) => r.Code === group.rows[0].Code) || group.rows[0];
|
|
return raw(
|
|
"일위대가",
|
|
tag("일위대가코드", code(group.rows[0].Code, 12)) +
|
|
name(title) +
|
|
amounts(title) +
|
|
tag("비목구분", "합계") +
|
|
remarks(title) +
|
|
group.rows
|
|
.slice(1)
|
|
.map((row) => {
|
|
let kind = kinds[n(row.LinkIndex)],
|
|
key = row.Code,
|
|
current = { ...row };
|
|
if (!kind || !key || [1, 3].includes(n(row.CalcIndex))) {
|
|
kind = "경비항목";
|
|
key = expense(row);
|
|
current.GN = 1;
|
|
current.GU = n(row.GC);
|
|
current.NU = n(row.NC);
|
|
current.JU = n(row.JC) + n(row.SC);
|
|
current.SU = 0;
|
|
warnings.push(`${group.rows[0].Code}: 연결 없는 구성 항목을 독립 단가로 변환`);
|
|
} else requireSource(row.LinkIndex, key);
|
|
return raw(
|
|
"일위대가상세",
|
|
tag("자원구분", kind) +
|
|
tag("자원코드", code(key)) +
|
|
tag("계산제외", flag(row.CalcIndex)) +
|
|
name(row) +
|
|
tag("수량", n(current.GN)) +
|
|
prices(current) +
|
|
amounts(row) +
|
|
tag("관급", "false") +
|
|
remarks(row),
|
|
);
|
|
})
|
|
.join(""),
|
|
);
|
|
})
|
|
.join("");
|
|
const costs = [...EstNative.groups(source.CostMainDat || []).values()]
|
|
.map((group) => {
|
|
const title =
|
|
source.CostTitleDat?.find((r) => r.Code === group.rows[0].Code) || group.rows[0];
|
|
return raw(
|
|
"중기단가산출",
|
|
tag("중기단가산출코드", code(title.Code, 12)) +
|
|
name(title) +
|
|
amounts(title) +
|
|
tag("비목구분", "합계") +
|
|
remarks(title) +
|
|
tag(
|
|
"중기단가산출식",
|
|
group.rows
|
|
.slice(1)
|
|
.map((r) => EstText.clean(r.LineData || ""))
|
|
.join("\n"),
|
|
),
|
|
);
|
|
})
|
|
.join("");
|
|
const machines = [...EstNative.groups(source.MachMainDat || []).values()]
|
|
.map((group) => {
|
|
const title =
|
|
source.MachTitleDat?.find((r) => r.Code === group.rows[0].Code) || group.rows[0];
|
|
warnings.push(`${title.Code}: 기계경비 상세는 경비항목 단가로 전개`);
|
|
return raw(
|
|
"기계경비",
|
|
tag("기계경비코드", code(title.Code)) +
|
|
name(title) +
|
|
tag("작업시간", "E0") +
|
|
tag("작업장소", "") +
|
|
amounts(title) +
|
|
remarks(title) +
|
|
group.rows
|
|
.slice(1)
|
|
.filter((r) => n(r.Index) === 0)
|
|
.map((row) => {
|
|
const key = expense(row);
|
|
return raw(
|
|
"기계경비상세",
|
|
tag("자원구분", "경비항목") +
|
|
tag("자원코드", key) +
|
|
tag("계산제외", flag(row.CalcIndex)) +
|
|
name(row) +
|
|
tag("수량", 1) +
|
|
prices({ GU: n(row.GC), NU: n(row.NC), JU: n(row.JC) + n(row.SC) }) +
|
|
amounts(row) +
|
|
remarks(row),
|
|
);
|
|
})
|
|
.join(""),
|
|
);
|
|
})
|
|
.join("");
|
|
const materials = (source.StuffcDat || [])
|
|
.map((row) =>
|
|
raw(
|
|
"자재",
|
|
tag("자재코드", code(row.Code, 16)) +
|
|
name(row) +
|
|
tag("재료비단가", n(row.TC)) +
|
|
remarks(row),
|
|
),
|
|
)
|
|
.join("");
|
|
const labor = (source.LaborcDat || [])
|
|
.map((row) =>
|
|
raw(
|
|
"노무",
|
|
tag("노무코드", code(row.Code, 16)) +
|
|
name(row) +
|
|
tag("노무비단가", n(row.TC)) +
|
|
remarks(row),
|
|
),
|
|
)
|
|
.join("");
|
|
const expenses = [...(source.EstiMateDat || []), ...generated]
|
|
.map((row) =>
|
|
raw(
|
|
"경비항목",
|
|
tag("경비항목코드", code(row.Code, 16)) +
|
|
tag("경비구분", "견적항목") +
|
|
name(row) +
|
|
tag("통화구분", "") +
|
|
tag("재료비단가", n(row.GC)) +
|
|
tag("노무비단가", n(row.NC)) +
|
|
tag("경비단가", n(row.JC) + n(row.SC)) +
|
|
remarks(row),
|
|
),
|
|
)
|
|
.join("");
|
|
const market = (source.SilJukDat || [])
|
|
.map((row) => {
|
|
const key = code(row.Code, 12);
|
|
if (!/^[ACEMT]/.test(key)) throw Error(`표준시장단가 코드 접두어 오류: ${key}`);
|
|
return raw(
|
|
"실적단가",
|
|
tag("항목구분", "표준시장단가") +
|
|
tag("공종구분", meta.kind === "건축공사" ? "건축공사" : "토목공사") +
|
|
tag("실적공사비코드", key) +
|
|
name(row) +
|
|
tag("재료비단가", n(row.GC)) +
|
|
tag("노무비단가", n(row.NC)) +
|
|
tag("경비단가", n(row.JC) + n(row.SC)) +
|
|
remarks(row),
|
|
);
|
|
})
|
|
.join("");
|
|
const fees = (source.Wonga || [])
|
|
.map((row) =>
|
|
raw(
|
|
"제경비항목",
|
|
tag("세부공종코드", code(row.Gubun, 12)) +
|
|
tag("집계항목여부", row.BaseField ? "true" : "false") +
|
|
tag("비목명", text(row.Name, 50, "비목명")) +
|
|
tag("적용산식", text(row.VSusic, 200, "원가 수식")) +
|
|
tag("적용요율", n(String(row.Youl || 0).replace("%", ""))) +
|
|
tag("추가금액", 0) +
|
|
tag("산출금액", n(row.Cost)) +
|
|
tag("적용산식설명", ""),
|
|
),
|
|
)
|
|
.join("");
|
|
const body =
|
|
raw("공사제경비", fees) +
|
|
raw(
|
|
"단일공사내역서",
|
|
raw("단일공사정보", tag("공사구분", meta.kind) + tag("공사설명", "")) +
|
|
boq +
|
|
head +
|
|
costs +
|
|
machines +
|
|
materials +
|
|
labor +
|
|
expenses +
|
|
market,
|
|
);
|
|
return {
|
|
xml:
|
|
'<?xml version="1.0" encoding="UTF-8"?>' +
|
|
raw(
|
|
"공사원가호환규정",
|
|
tag("스키마버전", "1.0.5") +
|
|
raw(
|
|
"작성정보",
|
|
tag("작성업체명", text(meta.company, 50, "작성업체명")) +
|
|
tag("프로그램명", "EST 독립 웹앱") +
|
|
tag("프로그램버전", "est-web-2") +
|
|
tag("파일제작일", date) +
|
|
tag("파일검증여부", "false") +
|
|
tag("건축공사사업내용작성여부", "false"),
|
|
) +
|
|
raw(
|
|
"공사정보",
|
|
tag("공사관리번호", text(meta.number || "", 20, "관리번호")) +
|
|
tag("주공사", meta.kind) +
|
|
tag("공사명", text(project.name, 100, "공사명")) +
|
|
tag("비고", ""),
|
|
) +
|
|
raw("공사내역서", body),
|
|
),
|
|
warnings: [...new Set(warnings)],
|
|
};
|
|
}
|
|
function read(source) {
|
|
if (/<!DOCTYPE|<!ENTITY/i.test(source)) throw Error("XML 외부 선언은 허용하지 않습니다.");
|
|
const doc = new DOMParser().parseFromString(source, "application/xml");
|
|
if (doc.querySelector("parsererror") || doc.documentElement.localName !== "공사원가호환규정")
|
|
throw Error("공사원가호환규정 XML 형식이 아닙니다.");
|
|
const children = (node, name) => [...node.children].filter((c) => c.localName === name);
|
|
const value = (node, name) => children(node, name)[0]?.textContent || "";
|
|
const amount = (node, name) => {
|
|
const raw = value(node, name);
|
|
if (raw.trim() === "" || !Number.isFinite(Number(raw))) throw Error(`XML 숫자 오류: ${name}`);
|
|
return Number(raw);
|
|
};
|
|
const rows = (node, name) => [...node.getElementsByTagName(name)];
|
|
const result = EstNative.empty();
|
|
result.options.SourceRefresh = false;
|
|
result.options.AutoCalc = false; // Imported monetary values are authoritative until explicitly recalculated.
|
|
const info = children(doc.documentElement, "공사정보")[0];
|
|
if (!info) throw Error("XML 공사정보 없음");
|
|
result.name = value(info, "공사명");
|
|
result.exportMeta = {
|
|
company: value(children(doc.documentElement, "작성정보")[0], "작성업체명"),
|
|
kind: value(info, "주공사"),
|
|
number: value(info, "공사관리번호"),
|
|
};
|
|
const fields = (node) => ({
|
|
Name: value(node, "품명"),
|
|
Size: value(node, "규격"),
|
|
Unite: value(node, "단위"),
|
|
Bigo: value(node, "비고"),
|
|
});
|
|
const money = (node) => ({
|
|
GC: amount(node, "재료비"),
|
|
NC: amount(node, "노무비"),
|
|
JC: amount(node, "경비"),
|
|
SC: 0,
|
|
TC: amount(node, "재료비") + amount(node, "노무비") + amount(node, "경비"),
|
|
});
|
|
const unit = (node) => ({
|
|
GU: amount(node, "재료비단가"),
|
|
NU: amount(node, "노무비단가"),
|
|
JU: amount(node, "경비단가"),
|
|
SU: 0,
|
|
TU: amount(node, "재료비단가") + amount(node, "노무비단가") + amount(node, "경비단가"),
|
|
});
|
|
const linked = (node) => ({
|
|
Code: code(value(node, "자원코드")),
|
|
LinkIndex: Number(
|
|
Object.entries(kinds).find(([, v]) => v === value(node, "자원구분"))?.[0] || 0,
|
|
),
|
|
GN: amount(node, "수량"),
|
|
CalcIndex: ["true", "1"].includes(value(node, "계산제외")) ? 2 : 0,
|
|
...fields(node),
|
|
...money(node),
|
|
...unit(node),
|
|
});
|
|
let pk = 0;
|
|
const contracts = rows(doc.documentElement, "단일공사내역서");
|
|
if (contracts.length !== 1) throw Error("복합공사 XML은 단일공사별 파일로 나눈 뒤 불러오세요.");
|
|
const contract = contracts[0];
|
|
const contractGroups = children(contract, "내역서"),
|
|
maxDepth = Math.max(
|
|
1,
|
|
...contractGroups.map((group) => value(group, "내역서번호").length / 2),
|
|
);
|
|
for (const group of contractGroups) {
|
|
result.tables.EstMainDat.push({
|
|
PkID: ++pk,
|
|
Level: Math.max(1, maxDepth - value(group, "내역서번호").length / 2 + 1),
|
|
Code: value(group, "내역서번호"),
|
|
Name: value(group, "공종명"),
|
|
...money(group),
|
|
});
|
|
for (const row of children(group, "세부공종")) {
|
|
const next = {
|
|
PkID: ++pk,
|
|
Code: value(row, "세부공종코드"),
|
|
Name: value(row, "세부공종명"),
|
|
Size: value(row, "규격"),
|
|
Unite: value(row, "단위"),
|
|
GN: amount(row, "수량"),
|
|
...money(row),
|
|
...unit(row),
|
|
CalcIndex: ["true", "1"].includes(value(row, "계산제외")) ? 2 : 0,
|
|
LinkIndex: Number(
|
|
Object.entries(kinds).find(([, v]) => v === value(row, "자원구분"))?.[0] || 0,
|
|
),
|
|
};
|
|
result.tables.EstMainDat.push(next);
|
|
}
|
|
}
|
|
const direct = result.tables.EstMainDat.filter((r) => !r.Level && r.CalcIndex < 2);
|
|
result.tables.EstMainDat.push({
|
|
PkID: ++pk,
|
|
Name: "순공사비",
|
|
LinkIndex: 54,
|
|
...Object.fromEntries(
|
|
[...EstMath.components, "TC"].map((k) => [k, direct.reduce((s, r) => s + n(r[k]), 0)]),
|
|
),
|
|
});
|
|
for (const [element, table, key] of [
|
|
["자재", "StuffcDat", "자재코드"],
|
|
["노무", "LaborcDat", "노무코드"],
|
|
["경비항목", "EstiMateDat", "경비항목코드"],
|
|
["실적단가", "SilJukDat", "실적공사비코드"],
|
|
])
|
|
children(contract, element).forEach((row, i) => {
|
|
const amounts =
|
|
element === "자재"
|
|
? { GC: amount(row, "재료비단가") }
|
|
: element === "노무"
|
|
? { NC: amount(row, "노무비단가") }
|
|
: {
|
|
GC: amount(row, "재료비단가"),
|
|
NC: amount(row, "노무비단가"),
|
|
JC: amount(row, "경비단가"),
|
|
};
|
|
const tc = Object.values(amounts).reduce((a, b) => a + b, 0);
|
|
result.tables[table].push({
|
|
PkID: i + 1,
|
|
Code: code(value(row, key)),
|
|
...fields(row),
|
|
...amounts,
|
|
TC: tc,
|
|
InputTC: tc,
|
|
ValueIndex: 4,
|
|
});
|
|
});
|
|
for (const [element, main, title, key] of [
|
|
["일위대가", "HeadMainDat", "HeadTitleDat", "일위대가코드"],
|
|
["기계경비", "MachMainDat", "MachTitleDat", "기계경비코드"],
|
|
])
|
|
children(contract, element).forEach((row, i) => {
|
|
const header = { Code: code(value(row, key)), Index: i + 1, ...fields(row), ...money(row) };
|
|
result.tables[title].push({ ...header });
|
|
result.tables[main].push({ PkID: result.tables[main].length + 1, ...header });
|
|
children(row, element + "상세").forEach((child) =>
|
|
result.tables[main].push({
|
|
PkID: result.tables[main].length + 1,
|
|
Index: 0,
|
|
TitleCode: header.Code,
|
|
...linked(child),
|
|
}),
|
|
);
|
|
});
|
|
children(contract, "중기단가산출").forEach((row, i) => {
|
|
const header = {
|
|
Code: code(value(row, "중기단가산출코드")),
|
|
Index: i + 1,
|
|
...fields(row),
|
|
...money(row),
|
|
};
|
|
result.tables.CostTitleDat.push({ ...header });
|
|
result.tables.CostMainDat.push({ PkID: result.tables.CostMainDat.length + 1, ...header });
|
|
result.tables.CostMainDat.push({
|
|
PkID: result.tables.CostMainDat.length + 1,
|
|
Index: 0,
|
|
TitleCode: header.Code,
|
|
LineData: value(row, "중기단가산출식"),
|
|
...money(row),
|
|
CalcIndex: 1,
|
|
});
|
|
});
|
|
const fees = children(doc.documentElement, "공사내역서")[0];
|
|
for (const row of children(children(fees, "공사제경비")[0] || fees, "제경비항목"))
|
|
result.tables.Wonga.push({
|
|
Gubun: value(row, "세부공종코드"),
|
|
Name: value(row, "비목명"),
|
|
Youl: value(row, "적용요율"),
|
|
VSusic: value(row, "적용산식"),
|
|
Cost: amount(row, "산출금액"),
|
|
});
|
|
result.originalC3r = source;
|
|
result.importWarnings = [
|
|
"XML의 저장 금액을 보존하기 위해 자동계산을 껐습니다. 재계산 전 원본 상세·산식을 확인하세요.",
|
|
"XML 원문은 프로젝트에 보존됩니다. 제출기관 인증/등록 검사는 별도입니다.",
|
|
];
|
|
result.calculation = {
|
|
errors: [],
|
|
warnings: result.importWarnings,
|
|
direct: result.tables.EstMainDat.at(-1),
|
|
total: result.tables.Wonga.at(-1)?.Cost ?? result.tables.EstMainDat.at(-1).TC,
|
|
};
|
|
return EstModel.validate(result);
|
|
}
|
|
return { output, read };
|
|
})();
|
|
if (typeof module !== "undefined" && module.exports) module.exports = EstC3r;
|