feat(est-test): 적용대가 보기 변경·수량불러오기·적용대가집계표
- 물가변동 품목조정 화면을 native-applied-cost.js 로 옮김(native-extra.js 700줄 한계) - 보기 변경: 단가 밴드 숨기고 계약·구분별 수량 7종만 - 수량불러오기: 고른 내역서 행 i 수량을 구분 칸에, 원가 행 i Cost 를 Es*Cost 에(위치 짝) - 적용대가집계표: 원가 행마다 계약·적용제외·적용대가 금액 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ for (const file of [
|
||||
"native-verify.js",
|
||||
"native-block-actions.js",
|
||||
"native-extra.js",
|
||||
"native-applied-cost.js",
|
||||
"native-business-actions.js",
|
||||
"native-forms.js",
|
||||
"native-excel-to-est.js",
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""적용대가 분류 내역서: 수량불러오기(위치 짝)·적용대가집계표·보기 변경."""
|
||||
|
||||
from test_est_rea_parity import run
|
||||
|
||||
|
||||
def test_gn_load_copies_quantity_by_position_and_wonga_cost():
|
||||
result = run(
|
||||
"math",
|
||||
"""(()=>{
|
||||
const p=EstNative.empty();p.options.SourceRefresh=false;
|
||||
p.tables.EstMainDat=[{Code:'A',GN:10,GU:80},{Code:'B',GN:20,GU:50},{Code:'C',GN:5,GU:10}];
|
||||
p.tables.Wonga=[{Name:'재료비',Cost:0},{Name:'노무비',Cost:0}];
|
||||
const src={tables:{EstMainDat:[{GN:3},{GN:7}],Wonga:[{Cost:111},{Cost:222},{Cost:333}]}};
|
||||
const n=EstAppliedCost.load(p,3,src);
|
||||
const q=EstBusiness.baseline(EstNative.calculate(structuredClone(p)));
|
||||
EstAppliedCost.load(q,0,{tables:{EstMainDat:[{GN:9}],Wonga:[{Cost:500}]}});
|
||||
const r=structuredClone(p);delete r.baseline;
|
||||
EstAppliedCost.load(r,0,{tables:{EstMainDat:[{GN:4}]}});
|
||||
return {n,actual:p.tables.EstMainDat.map(x=>x.EscActual??null),
|
||||
work:p.tables.Wonga.map(x=>x.EsWorkCost),base:q.baseline.tables.EstMainDat[0].GN,
|
||||
contract:q.tables.Wonga[0].EsContractCost,old:r.tables.EstMainDat[0].GN_};
|
||||
})()""",
|
||||
)
|
||||
assert result == {
|
||||
"n": 2,
|
||||
"actual": [3, 7, None],
|
||||
"work": [111, 222],
|
||||
"base": 9,
|
||||
"contract": 500,
|
||||
"old": 4,
|
||||
}
|
||||
|
||||
|
||||
def test_summary_uses_loaded_contract_cost_or_contract_calculation():
|
||||
result = run(
|
||||
"math",
|
||||
"""(()=>{
|
||||
const p=EstNative.empty();p.options.SourceRefresh=false;p.options.EscDesignTotal=1000;
|
||||
p.tables.EstMainDat=[{Code:'A',Name:'a',GN:10,GU:100},{LinkIndex:54}];
|
||||
p.tables.Wonga=[{Name:'재료비',VSusic:'',BaseField:'GC',LinkIndex:1}];
|
||||
const q=EstBusiness.baseline(EstNative.calculate(p));
|
||||
q.tables.EstMainDat[0].EscActual=4;
|
||||
const a=EstAppliedCost.summary(q).map(x=>[x.contract,x.except,x.apply]);
|
||||
q.tables.Wonga[0].EsContractCost=2000;
|
||||
const b=EstAppliedCost.summary(q).map(x=>[x.contract,x.except,x.apply]);
|
||||
return {a,b};
|
||||
})()""",
|
||||
)
|
||||
contract, except_, apply = result["a"][0]
|
||||
assert apply == contract - except_
|
||||
assert result["b"][0][0] == 2000
|
||||
assert result["b"][0][2] == 2000 - result["b"][0][1]
|
||||
|
||||
|
||||
def test_style_toggle_shows_quantity_band_only():
|
||||
result = run(
|
||||
"math",
|
||||
"""(()=>{
|
||||
const p=EstNative.empty();p.options.SourceRefresh=false;
|
||||
p.tables.EstMainDat=[{Code:'A',Name:'a',GN:10,GU:100,EscPS:2}];
|
||||
const q=EstBusiness.baseline(EstNative.calculate(p));
|
||||
const state={project:q,view:'escalation',escalationPage:0,selection:new Set()};
|
||||
const box=(t,tools,body)=>tools+body,button=(a,t)=>`<button data-action="${a}">${t}</button>`,
|
||||
table=(h,rows)=>JSON.stringify({h,rows});
|
||||
const g=globalThis;g.EstApp={...(g.EstApp||{}),esc:String,number:String,render(){}};g.EstViews={cell:(t,i,k,v)=>String(v)};
|
||||
const plain=EstAppliedCost.view(state,q,{box,button,table});
|
||||
EstAppliedCost.handle('applied-style-toggle',state);
|
||||
const band=EstAppliedCost.view(state,q,{box,button,table});
|
||||
const head=(s)=>JSON.parse(s.slice(s.indexOf('{"h"'))).h;
|
||||
return {plain:head(plain).length,band:head(band)};
|
||||
})()""",
|
||||
)
|
||||
assert result["plain"] == 18
|
||||
assert result["band"] == [
|
||||
"코드",
|
||||
"품명",
|
||||
"계약 수량",
|
||||
"시공중 수량",
|
||||
"예정공정 수량",
|
||||
"실행공정 수량",
|
||||
"기성 수량",
|
||||
"PS 수량",
|
||||
"선시공 수량",
|
||||
"적용제외",
|
||||
"적용수량",
|
||||
]
|
||||
@@ -21,6 +21,7 @@
|
||||
<script defer src="native-misc-tools.js"></script>
|
||||
<script defer src="native-excel-to-est.js"></script>
|
||||
<script defer src="native-other-load.js"></script>
|
||||
<script defer src="native-applied-cost.js"></script>
|
||||
<script defer src="native-actions-more.js"></script>
|
||||
<script defer src="native-wonga.js"></script>
|
||||
<script defer src="native-jungsang.js"></script>
|
||||
|
||||
@@ -7,6 +7,7 @@ const EstActionsMore = (() => {
|
||||
typeof EstMiscTools !== "undefined" && EstMiscTools,
|
||||
typeof EstExcelToEst !== "undefined" && EstExcelToEst,
|
||||
typeof EstOtherLoad !== "undefined" && EstOtherLoad,
|
||||
typeof EstAppliedCost !== "undefined" && EstAppliedCost,
|
||||
].filter(Boolean);
|
||||
function handle(name, state, node) {
|
||||
for (const module of modules()) if (module.handle(name, state, node)) return true;
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
// 원본 「물가변동 적용대가 분류 내역서」(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 };
|
||||
})();
|
||||
@@ -229,81 +229,7 @@ const EstExtra = (() => {
|
||||
"전회는 직전 확정 회차 누계를 읽습니다. 금회·사정은 수량·금액·비율 중 하나를 입력해 계산합니다.",
|
||||
);
|
||||
} else if (view === "escalation") {
|
||||
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 rows = page.rows.map(({ row, index }) => {
|
||||
const result = output.rows[index];
|
||||
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),
|
||||
];
|
||||
});
|
||||
html = box(
|
||||
"물가변동 품목조정",
|
||||
`<select id="escalation-component">${choices}</select>` +
|
||||
button("capture-bid", "현재 단가를 입찰단가로 기록") +
|
||||
button("load-bid", "입찰단가 불러오기") +
|
||||
button("load-contract", "계약 내역 불러오기") +
|
||||
button("load-progress-exclusion", "기성 누계수량 불러오기") +
|
||||
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(
|
||||
[
|
||||
"코드",
|
||||
"품명",
|
||||
"입찰 당시",
|
||||
"계약 단가",
|
||||
"변동 당시",
|
||||
"등락률 %",
|
||||
"등락폭",
|
||||
"조정단가",
|
||||
"예정공정수량",
|
||||
"실행공정수량",
|
||||
"기성수량",
|
||||
"적용제외",
|
||||
"적용수량",
|
||||
"제외 자동",
|
||||
"수동 제외수량",
|
||||
"시공중 수량(참고)",
|
||||
"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(" / "))}`,
|
||||
);
|
||||
html = EstAppliedCost.view(state, project, { box, button, table });
|
||||
} else if (view === "annual") {
|
||||
const output = EstBusiness.annual(project, false);
|
||||
state.business = output;
|
||||
|
||||
@@ -104,6 +104,7 @@ const allowed = new Set([
|
||||
"native-misc-tools.js",
|
||||
"native-excel-to-est.js",
|
||||
"native-other-load.js",
|
||||
"native-applied-cost.js",
|
||||
"native-actions-more.js",
|
||||
"native-wonga-tools.js",
|
||||
"native-estimate-print.js",
|
||||
|
||||
Reference in New Issue
Block a user