- 보고서: 원본 STMATE 엑셀과 셀 대조(목록표·자재단가대비·중기·일위대가·산근 목록·설계내역서·원가계산서) · 조달청 XML 공식 XSD 검증 · PM_Prt 화면 - 제잡비: 원본 계산 순서 4형식(T/S/TA/SA) · 실무 7건 저장 결과키 전수 1원 · 요율표 조회 · 선택지 계산 · 제잡비·제비율 화면 - 식언어·수량: 함수·조건식·금액변수·문법 검사·수량변수·QCODE · 산근·구성·중기공식 편집 화면 · 순환 알림 - 화면: 원본 메뉴 체계·단축키 · 설계내역서 편집(행 넣기·지우기·이동·레벨·보관함) · 초기값 · 단가목록표 - 저장: 초기값(Start8x.Ini) 바뀐 키 저장 · 노임 환산 직종 = 초기값 CONLD · 산근·구성 결과에 적용단가 칸 채우지 않음(원본 0) - DBF 글 칸 앞 빈칸 보존 · 시험 도우미 표준입력 utf8(한글 조각 깨짐 원인) - 실무 5건 재계산 후 저장 = 바뀐 칸 0 · 시험 332건 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
4.9 KiB
JavaScript
111 lines
4.9 KiB
JavaScript
/* -*- coding: utf-8 -*- */
|
|
/**
|
|
* 자재 후보단가 채택 (scr_edit_price.js) — 가격지 1~5단가(AMT_1n~AMT_5n) 중 최소단가 자동채택 · 지정 단가 ·
|
|
* 보호표시(UC=T) 수동 고정 우선 · 0원 후보 제외
|
|
* 근거: 지식DB STmate/old/20_분석 17 §5.5(최저가 채택) · 22 §3.1(보호표시·0원 수정 안 됨) ·
|
|
* ui_form_catalog wM_Boxa RG_MComp(변동없음 · 1~5 단가 · 최소단가) · 13 T10(수동값 우선)
|
|
*/
|
|
(function (global) {
|
|
"use strict";
|
|
const num = (v) => Number(v) || 0;
|
|
|
|
/**
|
|
* rows = COSTN 행 · opts { slot: 차수 1~4, mode: "min" | 1~5 | "keep", kinds: "M" }
|
|
* 돌려줌 { changed: [{code, from, to, src}], kept: [보호표시 코드] } · rows 는 그대로(적용은 부르는 쪽)
|
|
*/
|
|
function adoptCandidates(rows, opts = {}) {
|
|
const slot = Number(opts.slot) || 1,
|
|
mode = opts.mode ?? "min",
|
|
kinds = opts.kinds || "M";
|
|
const changed = [],
|
|
kept = [];
|
|
if (mode === "keep") return { changed, kept };
|
|
for (const r of rows || []) {
|
|
const code = String(r.CODE || "").trim();
|
|
if (!code || !kinds.includes(code[0])) continue;
|
|
const cand = [1, 2, 3, 4, 5].map((i) => num(r[`AMT_${i}${slot}`]));
|
|
let to = null,
|
|
src = 0;
|
|
if (mode === "min") {
|
|
cand.forEach((v, i) => v > 0 && (to === null || v < to) && ((to = v), (src = i + 1)));
|
|
} else if (cand[Number(mode) - 1] > 0) [to, src] = [cand[Number(mode) - 1], Number(mode)];
|
|
if (to === null) continue;
|
|
if (String(r.UC || "").trim() === "T") {
|
|
kept.push(code);
|
|
continue;
|
|
}
|
|
const from = num(r[`MAMT${slot}`]);
|
|
if (from !== to) changed.push({ code, from, to, src });
|
|
}
|
|
return { changed, kept };
|
|
}
|
|
|
|
const api = { adoptCandidates };
|
|
global.STMATE_PRICE_ADOPT = api;
|
|
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
|
|
|
// 화면 — 단가표 메뉴에 「자재 후보단가 채택」 창
|
|
if (typeof document === "undefined") return;
|
|
const P = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || {
|
|
screens: {},
|
|
actions: {},
|
|
menus: [],
|
|
});
|
|
P.actions.Act_Qty_AdoptPrice = (ctx, app) => {
|
|
if (!ctx.dbfData) return alert("먼저 프로젝트를 여세요.");
|
|
const opts = [
|
|
"min:최소단가(0원 제외)",
|
|
"1:1 단가",
|
|
"2:2 단가",
|
|
"3:3 단가",
|
|
"4:4 단가",
|
|
"5:5 단가",
|
|
"keep:변동없음",
|
|
];
|
|
app.openModal(
|
|
"자재 후보단가 채택 (wM_Boxa RG_MComp)",
|
|
`<div style="display:flex;flex-direction:column;gap:8px;">
|
|
<p style="margin:0;">가격지 1~5단가 중 고른 단가를 재료비 적용단가로 · 보호표시(UC=T) 자재는 수동값 그대로 · 0원 후보는 제외</p>
|
|
<label>채택 기준 <select id="adopt-mode">${opts.map((o) => `<option value="${o.split(":")[0]}">${o.split(":")[1]}</option>`).join("")}</select></label>
|
|
<label>차수 칸 <select id="adopt-slot">${[1, 2, 3, 4].map((s) => `<option value="${s}">${s}</option>`).join("")}</select></label>
|
|
<div id="adopt-preview" style="max-height:260px;overflow:auto;font-size:12px;"></div>
|
|
<div style="text-align:right;"><button class="win-btn" id="adopt-run-preview">미리보기</button> <button class="win-btn primary" id="adopt-run-apply">적용</button></div>
|
|
</div>`,
|
|
);
|
|
const $ = (id) => document.getElementById(id);
|
|
const run = () =>
|
|
adoptCandidates(ctx.dbfData.costn_rows, {
|
|
mode: $("adopt-mode").value,
|
|
slot: $("adopt-slot").value,
|
|
});
|
|
$("adopt-run-preview").onclick = () => {
|
|
const res = run();
|
|
$("adopt-preview").innerHTML =
|
|
`<div>바뀔 자재 ${res.changed.length}건 · 보호표시로 그대로 ${res.kept.length}건</div>` +
|
|
res.changed
|
|
.map((c) => `<div>${c.code}: ${app.fmt(c.from)} → ${app.fmt(c.to)} (${c.src}단가)</div>`)
|
|
.join("");
|
|
};
|
|
$("adopt-run-apply").onclick = () => {
|
|
const res = run();
|
|
const slot = Number($("adopt-slot").value) || 1;
|
|
const by = Object.fromEntries(ctx.dbfData.costn_rows.map((r) => [String(r.CODE).trim(), r]));
|
|
const sync = global.STMATE_VIEW_PRJ_BOQ && global.STMATE_VIEW_PRJ_BOQ.syncCostnUnitPrice;
|
|
for (const c of res.changed) {
|
|
const r = by[c.code];
|
|
if (slot === 1 && sync) sync(ctx, c.code, c.to, num(r.LAMT1), num(r.SAMT1));
|
|
else
|
|
Object.assign(r, {
|
|
[`MAMT${slot}`]: String(c.to),
|
|
[`TAMT${slot}`]: String(c.to + num(r[`LAMT${slot}`]) + num(r[`SAMT${slot}`])),
|
|
});
|
|
}
|
|
app.closeModal();
|
|
app.cascadeAndRender();
|
|
alert(`자재 ${res.changed.length}건 채택 · 보호표시 ${res.kept.length}건 그대로`);
|
|
};
|
|
};
|
|
if (!P.menus.some((m) => m.act === "Act_Qty_AdoptPrice"))
|
|
P.menus.push({ menu: "m2", label: "자재 후보단가 채택(최소단가)", act: "Act_Qty_AdoptPrice" });
|
|
})(typeof window !== "undefined" ? window : globalThis);
|