507 lines
18 KiB
JavaScript
507 lines
18 KiB
JavaScript
/**
|
|
* STMATE w 8.0 — SIxMemo(COSTN.SMT) 산근 연결 · 5단 연쇄재계산(Act_Calc_All) · 다중시트 엑셀/조달XML/JDL 변환기 (smt_engine.js)
|
|
* 근거: 17_단가조립_검증.md, 18_누락_계산로직_점검.md, 23_COSTN_SMT_전수파싱.md, 28_변환_수량산출_식언어.md, 31_일위대가_레시피_사전.md
|
|
*/
|
|
(function (global) {
|
|
"use strict";
|
|
|
|
const Calc = global.STMATE_CALC;
|
|
const { fmt, num } = Calc;
|
|
|
|
const SmtEval = global.STMATE_SMT_EVAL;
|
|
const rowCode = (r) => String(r.CODE || "").trim();
|
|
// 낙찰률(NAGCD) 단가는 2칸 원단가로 산근을 셈 — 1칸은 원단가 × 낙찰률
|
|
const costSlot = (r) => (num(r.NAGCD) > 0 ? 2 : 1);
|
|
|
|
function costnLookup(ctx) {
|
|
const byCode = {};
|
|
((ctx.dbfData && ctx.dbfData.costn_rows) || []).forEach((r) => (byCode[rowCode(r)] = r));
|
|
const lookup = (code) => {
|
|
const r = byCode[code];
|
|
if (!r) return null;
|
|
const s = costSlot(r);
|
|
const out = { M: num(r[`MAMT${s}`]), L: num(r[`LAMT${s}`]), S: num(r[`SAMT${s}`]) };
|
|
return Object.assign(out, { unit: String(r.UNIT || "").trim(), name: String(r.NAME || "") });
|
|
};
|
|
return { byCode, lookup };
|
|
}
|
|
|
|
// 결과금액조정(COSTN S_ADJ · ADJ_M/L/S) — 표시된 것만
|
|
const adjOf = (row) =>
|
|
String(row.S_ADJ || "").trim() === "T"
|
|
? { M: num(row.ADJ_M), L: num(row.ADJ_L), S: num(row.ADJ_S) }
|
|
: null;
|
|
|
|
// 수량변수 ~Q00001.QTY1~ — QCODE 행(QTY0~9 직접 · 그 밖은 XCODE 「이름=값」 줄 · 빈 값 = 0)
|
|
function qtyResolver(ctx) {
|
|
const rows = (ctx.dbfData && ctx.dbfData.qcode_rows) || [];
|
|
const by = {};
|
|
rows.forEach(
|
|
(r) =>
|
|
(by[
|
|
String(r.CODE || "")
|
|
.trim()
|
|
.toUpperCase()
|
|
] = r),
|
|
);
|
|
return (code, field) => {
|
|
const r = by[code];
|
|
if (!r) return { error: `${code} 는 등록된 수량코드가 아님` };
|
|
if (/^QTY\d$/.test(field)) return { value: num(r[field]) };
|
|
for (const ln of String(r.XCODE || "").split(/\r?\n/)) {
|
|
const at = ln.indexOf("=");
|
|
if (at > 0 && ln.slice(0, at).trim().toUpperCase() === field)
|
|
return { value: num(ln.slice(at + 1).trim() || "0") };
|
|
}
|
|
return { error: `${code} 에 수량변수 「${field}」 없음` };
|
|
};
|
|
}
|
|
|
|
// 소수 자리 설정 — Start8x.Ini 0·1·2 자리(22 §3.4) · 비거나 이상하면 1(실무 7건 값)
|
|
const digitOf = (v, def = 1) => (/^[0-2]$/.test(String(v ?? "").trim()) ? Number(v) : def);
|
|
|
|
// 외산장비 환율 — 단가의 조사시점(ARENM 1 당초 · 2 변경)의 EXLNM·EXHNM·EXEUR·EXYEN(REA 14 cmBx_ARE)
|
|
function fxOf(ini) {
|
|
const n = String(ini.ARENM || "1").trim() === "2" ? 2 : 1;
|
|
return {
|
|
usd: num(ini[`EXLNM${n}`]),
|
|
usdHigh: num(ini[`EXHNM${n}`]),
|
|
eur: num(ini[`EXEUR${n}`]),
|
|
yen: num(ini[`EXYEN${n}`]),
|
|
};
|
|
}
|
|
|
|
// 산근 계산 설정 — Start8x.Ini(CHGNM · OPTNM · ZCDNM · ACDNM) · COSTN(SBMOK · S_ADJ)
|
|
function smtOptions(ctx, code) {
|
|
const ini = (ctx.dbfData && ctx.dbfData.start_ini) || {};
|
|
const row = costnLookup(ctx).byCode[code] || {};
|
|
return {
|
|
totalFirst: String(ini.CHGNM ?? "1") === "0",
|
|
order: ini.OPTNM || "SML",
|
|
assignDigits: ini.ZCDNM !== undefined && ini.ZCDNM !== "" ? num(ini.ZCDNM) : 2,
|
|
lineDigits: digitOf(ini.ACDNM),
|
|
sbmok: String(row.SBMOK || "").trim(),
|
|
adj: adjOf(row),
|
|
qty: qtyResolver(ctx),
|
|
selfCode: code,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 1. COSTN.SMT 산근 평가 — smt_eval.js 위임 · 계산 설정은 smtOptions
|
|
*/
|
|
function evaluateSmtFormulaText(smtText, ctx, codeHint) {
|
|
const { lookup } = costnLookup(ctx);
|
|
const ev = SmtEval.evaluate(smtText, lookup, smtOptions(ctx, codeHint));
|
|
const linesOut = ev.lines.map((ln) => {
|
|
const out = { lineNo: ln.lineNo, raw: ln.raw, assigned: (ln.assigned || []).join(", ") };
|
|
Object.assign(out, {
|
|
kind: ln.kind,
|
|
errors: ln.errors || [],
|
|
excluded: Boolean(ln.excluded),
|
|
});
|
|
if (ln.kind !== "amount") return Object.assign(out, { refInfo: null });
|
|
const tag = "MLS".split("").find((k) => ln.amounts[k]) || "M";
|
|
const refName = (lookup(ln.ref.replace(/[LMS]$/, "")) || {}).name || ln.ref;
|
|
const calcAmt = ln.amounts.M + ln.amounts.L + ln.amounts.S;
|
|
const refInfo = { refCode: ln.ref, compTag: tag, refName, baseUnitPrice: ln.base, calcAmt };
|
|
return Object.assign(out, { assigned: ln.label ? `→ ${ln.label}` : "", refInfo });
|
|
});
|
|
const res = { vars: ev.vars, linesOut, sumM: ev.M, sumL: ev.L, sumS: ev.S, sumT: ev.T };
|
|
return Object.assign(res, { errors: ev.errors, labels: ev.labels });
|
|
}
|
|
|
|
// 노임 환산 갈래 H0~H5 — 직종 묶음(CONLD · CONTD · CON1D~4D) ↔ 환산식(RXXNM · RXTNM · RX1NM~4NM)
|
|
// 근거: 22 §3.4 cmBx_Sel_RX · 30 §4 시간당노임산출 · TWM_EDIT_ILWI wcBx_RXX(0 개별 · 1 기본 · 2 기타 · 3~6 기타-1~4)
|
|
const RX_SETS = [
|
|
["CONLD", "RXXNM"],
|
|
["CONTD", "RXTNM"],
|
|
["CON1D", "RX1NM"],
|
|
["CON2D", "RX2NM"],
|
|
["CON3D", "RX3NM"],
|
|
["CON4D", "RX4NM"],
|
|
];
|
|
function laborMap(ini, rxxSel, fallback) {
|
|
const m = new Map();
|
|
const base = ini.RXXNM || fallback;
|
|
for (const [set, key] of RX_SETS)
|
|
for (const c of String(ini[set] || "").match(/.{1,6}/g) || [])
|
|
if (!m.has(c)) m.set(c, ini[key] || base);
|
|
const sel = RX_SETS[(Number(rxxSel) || 0) - 1];
|
|
if (sel) for (const c of m.keys()) m.set(c, ini[sel[1]] || base);
|
|
return m;
|
|
}
|
|
|
|
// 일위대가·중기 구성 계산 설정 — Start8x.Ini(CHBNM · OPTNM · CON*D · RX*NM · 환율 · 자리) · COSTN RXX
|
|
// 손료 갈래(E0 주간 · E1 야간 · E2 2교대 · E3 3교대, 30 §4) — 계수는 사용자 입력(sysConfig.shiftFactors)
|
|
function compositionOptions(ctx, code, row) {
|
|
const ini = (ctx.dbfData && ctx.dbfData.start_ini) || {};
|
|
const sys = (ctx.state && ctx.state.sysConfig) || {};
|
|
const shift = String(row.SHIFT || sys.shiftMode || "E0");
|
|
return {
|
|
totalFirst: code[0] === "B" && String(ini.CHBNM ?? "1") === "0",
|
|
order: ini.OPTNM || "SML",
|
|
sbmok: String(row.SBMOK || "").trim(),
|
|
adj: adjOf(row),
|
|
labor: code[0] === "X" ? laborMap(ini, row.RXX, sys.rxFormula) : null,
|
|
rx: ini.RXXNM || sys.rxFormula,
|
|
usdRate: num(ini.EXLNM1),
|
|
fx: fxOf(ini),
|
|
// 줄 금액 자리 = 중기 KCD · 일위대가 BPN · 중기 노임 단가 JCD · $장비 단가 ECD(22 §3.4 · REA 14 안내문)
|
|
lineDigits: digitOf(code[0] === "X" ? ini.KCDNM : ini.BPNNM),
|
|
wageDigits: digitOf(ini.JCDNM, 0),
|
|
fxDigits: digitOf(ini.ECDNM, 0),
|
|
shiftFactor:
|
|
code[0] === "X" && shift !== "E0" ? num((sys.shiftFactors || {})[shift]) || 1 : 1,
|
|
};
|
|
}
|
|
|
|
// 참조 그래프 순환 찾기 — 구성(BDQTY)·산근(&코드&) · 경로를 6자리 코드로(REA 30 mode1_circular_reference)
|
|
function findCycles(deps, codes) {
|
|
const state = {},
|
|
cycles = [],
|
|
path = [];
|
|
const visit = (c) => {
|
|
if (state[c] === 2) return;
|
|
if (state[c] === 1) {
|
|
cycles.push([...path.slice(path.indexOf(c)), c]);
|
|
return;
|
|
}
|
|
state[c] = 1;
|
|
path.push(c);
|
|
for (const d of deps(c)) if (codes.has(d)) visit(d);
|
|
path.pop();
|
|
state[c] = 2;
|
|
};
|
|
[...codes].forEach(visit);
|
|
return cycles;
|
|
}
|
|
|
|
/**
|
|
* 2. 연쇄 재계산(Act_Calc_All) — 중기(X)·산근(D)·일위대가(B)를 참조 순서대로 다시 셈
|
|
* 구성: X·B = BDQTY 구성행(bdqty.py 복호) · D = COSTN.SMT 식 · 결과는 COSTN 단가 칸에 씀
|
|
*/
|
|
function cascadeRecalculateAll(ctx) {
|
|
const count = { xCount: 0, dCount: 0, bCount: 0 };
|
|
if (!ctx.dbfData || !ctx.dbfData.costn_rows) return count;
|
|
const crows = ctx.dbfData.costn_rows;
|
|
const memos = ctx.dbfData.costn_memos || {};
|
|
const syncUnit = global.STMATE_VIEW_PRJ_BOQ.syncCostnUnitPrice;
|
|
const { byCode, lookup } = costnLookup(ctx);
|
|
const groups = {};
|
|
for (const r of ctx.dbfData.bdqty_rows || []) {
|
|
if (/^[BX]/.test(r.BDCODE || "") && byCode[r.BDCODE])
|
|
(groups[r.BDCODE] = groups[r.BDCODE] || []).push(r);
|
|
}
|
|
const memoText = (code) =>
|
|
(ctx.uiState.editedSmtMap && ctx.uiState.editedSmtMap[code]) ||
|
|
(memos[code] && memos[code].text) ||
|
|
"";
|
|
const deps = (code) =>
|
|
groups[code]
|
|
? groups[code].map((r) => String(r.CODE || "").trim())
|
|
: [...memoText(code).matchAll(/&([A-Z]\d{4,5})[LMS]?(?::[LMS])?&/g)].map((m) => m[1]);
|
|
const order = [],
|
|
seen = new Set();
|
|
const visit = (code) => {
|
|
if (seen.has(code) || !(groups[code] || memoText(code).trim())) return;
|
|
seen.add(code);
|
|
deps(code).forEach(visit);
|
|
order.push(code);
|
|
};
|
|
[...Object.keys(groups), ...Object.keys(memos)].forEach(visit);
|
|
// 순환 참조 — 계산은 저장 단가로 이어 가고 경로를 알림(F2-13)
|
|
count.cycles = findCycles(deps, new Set(order)).map((p) => p.join(" → "));
|
|
if (ctx.uiState) ctx.uiState.cycleErrors = count.cycles;
|
|
|
|
for (const code of order) {
|
|
const row = byCode[code];
|
|
if (!row) continue;
|
|
const ev = groups[code]
|
|
? SmtEval.evaluateComposition(groups[code], lookup, compositionOptions(ctx, code, row))
|
|
: (({ sumM, sumL, sumS, sumT }) => ({ M: sumM, L: sumL, S: sumS, T: sumT }))(
|
|
evaluateSmtFormulaText(memoText(code), ctx, code),
|
|
);
|
|
if (!(ev.T > 0 || num(row.TAMT1) === 0)) continue; // 계산 실패로 저장 단가를 지우지 않음
|
|
const nag = num(row.NAGCD);
|
|
if (nag > 0) {
|
|
Object.assign(row, { MAMT2: String(ev.M), LAMT2: String(ev.L) });
|
|
Object.assign(row, { SAMT2: String(ev.S), TAMT2: String(ev.T) });
|
|
const b = (v) => SmtEval.bidRate(v, nag);
|
|
syncUnit(ctx, code, b(ev.M), b(ev.L), b(ev.S));
|
|
} else syncUnit(ctx, code, ev.M, ev.L, ev.S);
|
|
count[code[0] === "X" ? "xCount" : code[0] === "D" ? "dCount" : "bCount"] += 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* 3. 선택 호표(B/X)의 표준 레시피 구성행 조회
|
|
*/
|
|
function getRecipeBreakdown(ctx, code) {
|
|
const crows = (ctx.dbfData && ctx.dbfData.costn_rows) || [];
|
|
const row = crows.find((r) => String(r.CODE || "").trim() === code);
|
|
if (!row) return [];
|
|
const nm = String(row.NAME || "").trim();
|
|
const sz = String(row.SIZE || "").trim();
|
|
const p = code[0].toUpperCase();
|
|
const recipes = (ctx.catalogData && ctx.catalogData.recipes) || [];
|
|
const kind = p === "X" ? "중기" : p === "B" ? "일위대가" : "단가산출";
|
|
const pool = recipes.filter((r) => r["종류"] === kind);
|
|
const rec =
|
|
pool.find((r) => r["명칭"].trim() === nm && r["규격"].trim() === sz) ||
|
|
pool.find((r) => r["명칭"].trim() === nm);
|
|
return rec && rec["구성"] ? rec["구성"] : [];
|
|
}
|
|
|
|
/**
|
|
* 4. 다중 시트 Excel SpreadsheetML (.xls) 통합 워크북 생성기 (갑지·원가계산서·설계내역서·일위대가·단가산출·중기·단가목록)
|
|
*/
|
|
function buildMultiSheetExcelXml(ctx) {
|
|
const esc = (v) =>
|
|
String(v ?? "")
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
const kj = ctx.computed.kanjubComputed;
|
|
const boqRows = global.STMATE_VIEW_PRJ_BOQ.evaluateFullBoqRows(ctx);
|
|
const costnRows = (ctx.dbfData && ctx.dbfData.costn_rows) || [];
|
|
const memos = (ctx.dbfData && ctx.dbfData.costn_memos) || {};
|
|
|
|
const mkSheet = (name, headers, rows) => `
|
|
<Worksheet ss:Name="${esc(name)}">
|
|
<Table>
|
|
<Row>${headers.map((h) => `<Cell><Data ss:Type="String">${esc(h)}</Data></Cell>`).join("")}</Row>
|
|
${rows
|
|
.map(
|
|
(r) =>
|
|
`<Row>${r
|
|
.map((c) => {
|
|
const isN = typeof c === "number" && Number.isFinite(c);
|
|
return `<Cell><Data ss:Type="${isN ? "Number" : "String"}">${esc(c)}</Data></Cell>`;
|
|
})
|
|
.join("")}</Row>`,
|
|
)
|
|
.join("\n")}
|
|
</Table>
|
|
</Worksheet>`;
|
|
|
|
const shWon = mkSheet(
|
|
"공사원가계산서",
|
|
["비목", "산정기초(원)", "요율(%)", "금액(원)", "산출근거"],
|
|
kj.lines.map((l) => [l.label, l.base || 0, l.rate || 0, l.amount || 0, l.formula || ""]),
|
|
);
|
|
const shBoq = mkSheet(
|
|
"설계내역서",
|
|
[
|
|
"공종번호",
|
|
"코드",
|
|
"공종명",
|
|
"규격",
|
|
"단위",
|
|
"수량",
|
|
"재료비단가",
|
|
"노무비단가",
|
|
"경비단가",
|
|
"합계단가",
|
|
"합계금액",
|
|
],
|
|
boqRows.map((r) => [
|
|
r.ITNUM || "",
|
|
r.BDCODE1 || "",
|
|
r.NAME1 || "",
|
|
r.SIZE1 || "",
|
|
r.UNIT1 || "",
|
|
num(r.QTY1),
|
|
num(r.MAMT1),
|
|
num(r.LAMT1),
|
|
num(r.SAMT1),
|
|
num(r.TAMT1),
|
|
num(r.T_AMT1),
|
|
]),
|
|
);
|
|
const shIlwi = mkSheet(
|
|
"일위대가목록표",
|
|
["코드", "품명", "규격", "단위", "재료비", "노무비", "경비", "합계단가"],
|
|
costnRows
|
|
.filter((r) => String(r.CODE || "").startsWith("B"))
|
|
.map((r) => [
|
|
r.CODE,
|
|
r.NAME,
|
|
r.SIZE,
|
|
r.UNIT,
|
|
num(r.MAMT1),
|
|
num(r.LAMT1),
|
|
num(r.SAMT1),
|
|
num(r.TAMT1),
|
|
]),
|
|
);
|
|
const shSan = mkSheet(
|
|
"단가산출근거",
|
|
[
|
|
"코드",
|
|
"품명",
|
|
"규격",
|
|
"단위",
|
|
"재료비",
|
|
"노무비",
|
|
"경비",
|
|
"합계단가",
|
|
"산출근거(COSTN.SMT)",
|
|
],
|
|
costnRows
|
|
.filter((r) => String(r.CODE || "").startsWith("D") || memos[r.CODE])
|
|
.map((r) => [
|
|
r.CODE,
|
|
r.NAME,
|
|
r.SIZE,
|
|
r.UNIT,
|
|
num(r.MAMT1),
|
|
num(r.LAMT1),
|
|
num(r.SAMT1),
|
|
num(r.TAMT1),
|
|
memos[r.CODE] ? memos[r.CODE].text.replace(/\r?\n/g, " | ") : "",
|
|
]),
|
|
);
|
|
const shJoong = mkSheet(
|
|
"중기사용료",
|
|
[
|
|
"코드",
|
|
"장비명",
|
|
"규격",
|
|
"단위",
|
|
"재료비(연료/잡품)",
|
|
"노무비(운전원)",
|
|
"경비(손료)",
|
|
"시간당사용료",
|
|
],
|
|
costnRows
|
|
.filter((r) => String(r.CODE || "").startsWith("X"))
|
|
.map((r) => [
|
|
r.CODE,
|
|
r.NAME,
|
|
r.SIZE,
|
|
r.UNIT,
|
|
num(r.MAMT1),
|
|
num(r.LAMT1),
|
|
num(r.SAMT1),
|
|
num(r.TAMT1),
|
|
]),
|
|
);
|
|
const shDan = mkSheet(
|
|
"단가목록표",
|
|
[
|
|
"코드",
|
|
"구분",
|
|
"품명",
|
|
"규격",
|
|
"단위",
|
|
"재료비",
|
|
"노무비",
|
|
"경비",
|
|
"적용단가",
|
|
"조달단가",
|
|
"물가자료",
|
|
],
|
|
costnRows
|
|
.filter((r) => /^[LMS]/.test(String(r.CODE || "")))
|
|
.map((r) => [
|
|
r.CODE,
|
|
r.CODE[0],
|
|
r.NAME,
|
|
r.SIZE,
|
|
r.UNIT,
|
|
num(r.MAMT1),
|
|
num(r.LAMT1),
|
|
num(r.SAMT1),
|
|
num(r.TAMT1),
|
|
num(r.AMT_11),
|
|
num(r.AMT_21),
|
|
]),
|
|
);
|
|
|
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
<?mso-application progid="Excel.Sheet"?>
|
|
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
|
|
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
|
|
${shWon}
|
|
${shBoq}
|
|
${shIlwi}
|
|
${shSan}
|
|
${shJoong}
|
|
${shDan}
|
|
</Workbook>`;
|
|
}
|
|
|
|
/**
|
|
* 5. 조달청 공사원가 호환규정 V1.0.5 (Valid_Xml.vsl · T3_C3B) 전체 계층 XML 생성기
|
|
*/
|
|
function buildFullPpsXml(ctx) {
|
|
const esc = (v) =>
|
|
String(v ?? "")
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
const kj = ctx.computed.kanjubComputed;
|
|
const bt = ctx.computed.boqComputed.totals;
|
|
const boqRows = global.STMATE_VIEW_PRJ_BOQ.evaluateFullBoqRows(ctx);
|
|
const costnRows = (ctx.dbfData && ctx.dbfData.costn_rows) || [];
|
|
|
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
<T3_C3B xmlns="http://www.pps.go.kr/cost/v105" schemaVersion="1.0.5">
|
|
<Header>
|
|
<Program>STMATE w 8.0 (경영정보)</Program>
|
|
<ProjectCode>${esc(ctx.state.projectMeta.pcode)}</ProjectCode>
|
|
<ProjectName>${esc(ctx.state.projectMeta.pname)}</ProjectName>
|
|
<PriceStandard>${esc(ctx.state.projectMeta.pnum)}</PriceStandard>
|
|
<DirectMaterial>${bt.directM}</DirectMaterial>
|
|
<DirectLabor>${bt.directL}</DirectLabor>
|
|
<DirectExpense>${bt.directS}</DirectExpense>
|
|
<DirectCostTotal>${bt.directT}</DirectCostTotal>
|
|
<TotalSupplyAmount>${kj.FINAL_SUPPLY}</TotalSupplyAmount>
|
|
<GovMaterialAmount>${kj.GOV_MAT}</GovMaterialAmount>
|
|
<GrandTotalCost>${kj.GRAND_TOTAL}</GrandTotalCost>
|
|
</Header>
|
|
<CostSummary>
|
|
${kj.lines
|
|
.map(
|
|
(ln) =>
|
|
` <CostLine name="${esc(ln.label)}" base="${ln.base || 0}" rate="${ln.rate || 0}" amount="${ln.amount || 0}" />`,
|
|
)
|
|
.join("\n")}
|
|
</CostSummary>
|
|
<BoqItems count="${boqRows.length}">
|
|
${boqRows
|
|
.map(
|
|
(r) =>
|
|
` <BoqRow recno="${r._recno}" itnum="${esc(r.ITNUM)}" code="${esc(r.BDCODE1)}" name="${esc(r.NAME1)}" spec="${esc(r.SIZE1)}" unit="${esc(r.UNIT1)}" qty="${num(r.QTY1)}" mUnit="${num(r.MAMT1)}" lUnit="${num(r.LAMT1)}" sUnit="${num(r.SAMT1)}" tAmt="${num(r.T_AMT1)}" />`,
|
|
)
|
|
.join("\n")}
|
|
</BoqItems>
|
|
<UnitCostCatalog count="${costnRows.length}">
|
|
${costnRows
|
|
.map(
|
|
(c) =>
|
|
` <CostItem code="${esc(c.CODE)}" jdcode="${esc(c.JDCODE)}" name="${esc(c.NAME)}" spec="${esc(c.SIZE)}" unit="${esc(c.UNIT)}" m="${num(c.MAMT1)}" l="${num(c.LAMT1)}" s="${num(c.SAMT1)}" t="${num(c.TAMT1)}" />`,
|
|
)
|
|
.join("\n")}
|
|
</UnitCostCatalog>
|
|
</T3_C3B>`;
|
|
}
|
|
|
|
global.STMATE_ENGINE = {
|
|
evaluateSmtFormulaText,
|
|
cascadeRecalculateAll,
|
|
costnLookup,
|
|
smtOptions,
|
|
compositionOptions,
|
|
qtyResolver,
|
|
laborMap,
|
|
findCycles,
|
|
getRecipeBreakdown,
|
|
buildMultiSheetExcelXml,
|
|
buildFullPpsXml,
|
|
};
|
|
})(window);
|