- 물가변동 품목조정 화면을 native-applied-cost.js 로 옮김(native-extra.js 700줄 한계) - 보기 변경: 단가 밴드 숨기고 계약·구분별 수량 7종만 - 수량불러오기: 고른 내역서 행 i 수량을 구분 칸에, 원가 행 i Cost 를 Es*Cost 에(위치 짝) - 적용대가집계표: 원가 행마다 계약·적용제외·적용대가 금액 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
237 lines
9.8 KiB
JavaScript
237 lines
9.8 KiB
JavaScript
// 원본 「물가변동 적용대가 분류 내역서」(TfrmAppliedCost — spec_pb1 §5·§6) 화면: 품목조정 표(native-extra.js 에서 옮김),
|
|
// 「적용대가 보기 변경」(cmStyleToggle 0x10b5918 → e43b48: 단가 밴드를 숨기고 계약·구분별 수량 7종만 — 값 변화 없음),
|
|
// 「수량불러오기」(cmGNLoad 0x10b57a4 → 10b6630·10b5934·10b617c: 고른 내역서의 행 i 수량 GN 을 이 내역 행 i 의 구분 칸에,
|
|
// 그 파일 원가 행 i 의 Cost 를 원가 행 i 의 Es*Cost 에 — 위치 짝, 원본 행이 없으면 건너뜀),
|
|
// 「적용대가집계표」(mnuESWonga → TfrmESPWonga: 원가 행마다 계약금액·적용제외금액·적용대가금액 — 계산은 품목조정과 같은 값).
|
|
const EstAppliedCost = (() => {
|
|
const num = (v) => Number(v) || 0;
|
|
const QTY = ["EscConstruction", "EscPlanned", "EscActual", "EscProgress", "EscPS", "EscBefore"];
|
|
const QTY_LABELS = [
|
|
"시공중 수량",
|
|
"예정공정 수량",
|
|
"실행공정 수량",
|
|
"기성 수량",
|
|
"PS 수량",
|
|
"선시공 수량",
|
|
];
|
|
const KINDS = [
|
|
"계약분 수량",
|
|
"시공중인공사계약분 수량",
|
|
"예정공정율분 수량",
|
|
"실행공정율분 수량",
|
|
"기성율분 수량",
|
|
"PS분 수량",
|
|
"신시공부분 수량",
|
|
];
|
|
const COST = [
|
|
"EsContractCost",
|
|
"EsSiGongCost",
|
|
"EsSchduleCost",
|
|
"EsWorkCost",
|
|
"EsACCost",
|
|
"EsPSCost",
|
|
"EsBeforeCost",
|
|
];
|
|
|
|
function view(state, project, { box, button, table }) {
|
|
const { esc, number } = EstApp;
|
|
const output = EstBusiness.escalation(project);
|
|
state.business = output;
|
|
const comp = state.escalationComponent || "NU";
|
|
const choices = EstMath.units
|
|
.map(
|
|
(key, i) =>
|
|
`<option value="${key}" ${key === comp ? "selected" : ""}>${["노무", "재료", "경비", "실적"][i]}</option>`,
|
|
)
|
|
.join("");
|
|
const page = EstBlockActions.escalationPage(state, project.tables.EstMainDat);
|
|
const qty = state.escQtyView;
|
|
const rows = page.rows.map(({ row, index }) => {
|
|
const result = output.rows[index];
|
|
if (qty)
|
|
return [
|
|
esc(row.Code),
|
|
esc(row.Name),
|
|
number(num(result.EscApplied) + num(result.EscExcluded)),
|
|
...QTY.map((k) => EstViews.cell("EstMainDat", index, k, row[k] ?? 0)),
|
|
number(result.EscExcluded),
|
|
number(result.EscApplied),
|
|
];
|
|
return [
|
|
esc(row.Code),
|
|
esc(row.Name),
|
|
EstViews.cell("EstMainDat", index, `A${comp}`, row[`A${comp}`]),
|
|
number(result[`Contract${comp}`]),
|
|
EstViews.cell("EstMainDat", index, comp, row[comp]),
|
|
number(result[`D${comp}`]),
|
|
number(result[`E${comp}`]),
|
|
number(result[`F${comp}`]),
|
|
EstViews.cell("EstMainDat", index, "EscPlanned", row.EscPlanned || 0),
|
|
EstViews.cell("EstMainDat", index, "EscActual", row.EscActual || 0),
|
|
EstViews.cell("EstMainDat", index, "EscProgress", row.EscProgress || 0),
|
|
number(result.EscExcluded),
|
|
number(result.EscApplied),
|
|
`<input type="checkbox" data-esc-auto="${index}" ${row.EscAuto !== false ? "checked" : ""} aria-label="적용제외 자동계산">`,
|
|
EstViews.cell("EstMainDat", index, "EscManualQuantity", row.EscManualQuantity ?? 0),
|
|
EstViews.cell("EstMainDat", index, "EscConstruction", row.EscConstruction ?? 0),
|
|
EstViews.cell("EstMainDat", index, "EscPS", row.EscPS ?? 0),
|
|
EstViews.cell("EstMainDat", index, "EscBefore", row.EscBefore ?? 0),
|
|
];
|
|
});
|
|
return box(
|
|
"물가변동 품목조정",
|
|
`<select id="escalation-component">${choices}</select>` +
|
|
button("capture-bid", "현재 단가를 입찰단가로 기록") +
|
|
button("load-bid", "입찰단가 불러오기") +
|
|
button("load-contract", "계약 내역 불러오기") +
|
|
button("load-progress-exclusion", "기성 누계수량 불러오기") +
|
|
button(
|
|
"applied-style-toggle",
|
|
qty ? "적용대가 보기 변경(풀기)" : "적용대가 보기 변경",
|
|
qty ? 'aria-pressed="true"' : "",
|
|
) +
|
|
button("applied-gn-load", "수량불러오기") +
|
|
button("es-wonga", "적용대가집계표") +
|
|
button("reset-escalation", "물가변동 입력 초기화") +
|
|
button("capture-baseline", "계약 내역 확정") +
|
|
button("escalation-prev", "이전", state.escalationPage === 0 ? "disabled" : "") +
|
|
`<span>${state.escalationPage + 1} / ${page.pages}쪽 · ${page.total}행</span>` +
|
|
button(
|
|
"escalation-next",
|
|
"다음",
|
|
state.escalationPage + 1 >= page.pages ? "disabled" : "",
|
|
) +
|
|
`<label>조사 도급액(0=단가로 계산) <input type="number" step="any" data-option="EscDesignTotal" value="${project.options.EscDesignTotal ?? 0}"></label>`,
|
|
table(
|
|
qty
|
|
? ["코드", "품명", "계약 수량", ...QTY_LABELS, "적용제외", "적용수량"]
|
|
: [
|
|
"코드",
|
|
"품명",
|
|
"입찰 당시",
|
|
"계약 단가",
|
|
"변동 당시",
|
|
"등락률 %",
|
|
"등락폭",
|
|
"조정단가",
|
|
"예정공정수량",
|
|
"실행공정수량",
|
|
"기성수량",
|
|
"적용제외",
|
|
"적용수량",
|
|
"제외 자동",
|
|
"수동 제외수량",
|
|
"시공중 수량(참고)",
|
|
"PS 수량(참고)",
|
|
"선시공 수량(참고)",
|
|
],
|
|
rows,
|
|
),
|
|
`조사 도급액 ${number(output.designTotal)} · 계약 도급액 ${number(output.contractTotal)} · 조정 도급액 ${number(output.adjustedTotal)} · 적용제외(간접비 포함) ${number(output.excludedTotal)} · 적용대가 ${number(output.applied)} · 조정률 ${number(output.rate)}% · 조정금액 ${number(output.adjustment)}. 계약·입찰 파일은 동일 행 순서로 불러옵니다. ${esc(output.errors.join(" / "))}`,
|
|
);
|
|
}
|
|
|
|
// 수량불러오기 한 벌 적용: kind 0 = 계약 수량(계약 내역 확정본, 없으면 당초 칸 GN_), 1~6 = 구분 칸
|
|
function load(project, kind, source) {
|
|
const rows = project.tables.EstMainDat,
|
|
from = source.tables?.EstMainDat || [],
|
|
base = project.baseline?.tables?.EstMainDat;
|
|
let count = 0;
|
|
rows.forEach((row, i) => {
|
|
const src = from[i];
|
|
if (!src) return;
|
|
if (kind === 0) {
|
|
if (base?.[i]) base[i].GN = num(src.GN);
|
|
else row.GN_ = num(src.GN);
|
|
} else row[QTY[kind - 1]] = num(src.GN);
|
|
count++;
|
|
});
|
|
const wonga = project.tables.Wonga || [],
|
|
fromWonga = source.tables?.Wonga || [];
|
|
wonga.forEach((row, i) => fromWonga[i] && (row[COST[kind]] = num(fromWonga[i].Cost)));
|
|
return count;
|
|
}
|
|
|
|
function summary(project) {
|
|
const out = EstBusiness.escalation(project),
|
|
wonga = project.tables.Wonga || [];
|
|
return wonga.map((row, i) => {
|
|
const contract =
|
|
row.EsContractCost != null ? num(row.EsContractCost) : num(out.contractWonga?.[i]?.Cost),
|
|
except = num(out.excludedWonga?.[i]?.Cost);
|
|
return { row, contract, except, apply: contract - except };
|
|
});
|
|
}
|
|
function summaryDialog(state) {
|
|
const { esc, number } = EstApp,
|
|
list = summary(state.project);
|
|
if (!list.length) throw Error("원가계산서가 없습니다.");
|
|
EstApp.showDialog(
|
|
"적용대가집계표",
|
|
`<div style="max-height:480px;overflow:auto"><table class="sheet"><thead><tr><th>비목</th><th>약어</th><th>계약금액</th><th>적용제외금액</th><th>적용대가금액</th><th>산출근거</th></tr></thead><tbody>${list
|
|
.map(
|
|
({ row, contract, except, apply }) =>
|
|
`<tr><td style="text-align:left">${esc([row.Bimog1, row.Bimog2, row.Name].filter(Boolean).join(" "))}</td><td>${esc(row.SName ?? "")}</td><td>${number(contract)}</td><td>${number(except)}</td><td>${number(apply)}</td><td style="text-align:left">${esc(row.VSusic ?? "")}</td></tr>`,
|
|
)
|
|
.join("")}</tbody></table></div>`,
|
|
);
|
|
}
|
|
|
|
async function gnLoad(state) {
|
|
const kind = await new Promise((resolve) =>
|
|
EstActions.form(
|
|
"수량불러오기",
|
|
[
|
|
{
|
|
name: "kind",
|
|
label: "불러올 수량분",
|
|
value: "",
|
|
choices: [["", "선택"], ...KINDS.map((k, i) => [i, k])],
|
|
},
|
|
],
|
|
(values) => resolve(values.kind),
|
|
),
|
|
);
|
|
if (kind === "" || kind == null) throw Error("불러올 수량분을 선택하세요..");
|
|
const file = await new Promise((resolve) => {
|
|
const picker = document.createElement("input");
|
|
picker.type = "file";
|
|
picker.accept = ".estx,.json";
|
|
picker.addEventListener("change", () => resolve(picker.files[0]));
|
|
picker.click();
|
|
});
|
|
if (!file) return;
|
|
const name = KINDS[Number(kind)];
|
|
let source;
|
|
try {
|
|
source = /\.estx$/i.test(file.name)
|
|
? await EstApp.api(`/api/import?name=${encodeURIComponent(file.name)}`, {
|
|
method: "POST",
|
|
body: await file.arrayBuffer(),
|
|
})
|
|
: EstModel.legacy(JSON.parse(await file.text()));
|
|
} catch {
|
|
throw Error(`${name} 를 불러오지 못했습니다...`);
|
|
}
|
|
let count = 0;
|
|
EstApp.change((project) => (count = load(project, Number(kind), source)));
|
|
state.notice = `${name} ${count}행을 불러왔습니다.`;
|
|
EstApp.render();
|
|
}
|
|
|
|
function handle(name, state) {
|
|
if (name === "applied-style-toggle") {
|
|
state.escQtyView = !state.escQtyView;
|
|
EstApp.render();
|
|
} else if (name === "applied-gn-load")
|
|
gnLoad(state).catch((error) => {
|
|
state.notice = error.message;
|
|
EstApp.render();
|
|
});
|
|
else if (name === "es-wonga") summaryDialog(state);
|
|
else return false;
|
|
return true;
|
|
}
|
|
return { view, load, summary, handle, KINDS };
|
|
})();
|