diff --git a/resources/tester/helper_est_native.cjs b/resources/tester/helper_est_native.cjs index 5297e0b8a..bd585437c 100644 --- a/resources/tester/helper_est_native.cjs +++ b/resources/tester/helper_est_native.cjs @@ -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", diff --git a/resources/tester/test_est_applied_cost.py b/resources/tester/test_est_applied_cost.py new file mode 100644 index 000000000..9e26b5e68 --- /dev/null +++ b/resources/tester/test_est_applied_cost.py @@ -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)=>``, + 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 수량", + "선시공 수량", + "적용제외", + "적용수량", + ] diff --git a/tools/est-test/index.html b/tools/est-test/index.html index aed780765..1a89de7d0 100644 --- a/tools/est-test/index.html +++ b/tools/est-test/index.html @@ -21,6 +21,7 @@ + diff --git a/tools/est-test/native-actions-more.js b/tools/est-test/native-actions-more.js index 0314ab929..9d783f46c 100644 --- a/tools/est-test/native-actions-more.js +++ b/tools/est-test/native-actions-more.js @@ -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; diff --git a/tools/est-test/native-applied-cost.js b/tools/est-test/native-applied-cost.js new file mode 100644 index 000000000..095995623 --- /dev/null +++ b/tools/est-test/native-applied-cost.js @@ -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) => + ``, + ) + .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), + ``, + 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( + "물가변동 품목조정", + `` + + 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" : "") + + `${state.escalationPage + 1} / ${page.pages}쪽 · ${page.total}행` + + button( + "escalation-next", + "다음", + state.escalationPage + 1 >= page.pages ? "disabled" : "", + ) + + ``, + 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( + "적용대가집계표", + `
| 비목 | 약어 | 계약금액 | 적용제외금액 | 적용대가금액 | 산출근거 |
|---|---|---|---|---|---|
| ${esc([row.Bimog1, row.Bimog2, row.Name].filter(Boolean).join(" "))} | ${esc(row.SName ?? "")} | ${number(contract)} | ${number(except)} | ${number(apply)} | ${esc(row.VSusic ?? "")} |