feat(est-test): 정산원가계산서 작성과 당초금액 초기화
- 원본 처리 근거대로 당초금액 복사, 도급액 고정 연결 해제, (감)/(증) 차액 행 - 작성/해제 토글과 원가 화면 당초금액·정산금액 열, 재계산마다 차액 갱신 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -457,6 +457,9 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
||||
- [x] 원가계산서 사용자 양식(원본 TFRMWONGABASE popupSaveAs·popupFormats·popupLoadFrom, TFRMFORMATFILELIST 등록·삭제·열기·파일) — 서버 보관함 `data/forms.json`(`forms-store.cjs`, 종류 wonga·document), 등록·목록·열기·삭제·파일 내려받기/불러오기. 금액 제외, 열 때 도급액 고정 해제(저장 금액을 쓰는 옵션이라 금액 없는 양식에서 0이 됨을 확인).
|
||||
- 검증완료: `test_est_forms_library.py` 2개(등록·같은 이름 교체·목록·삭제·잘못된 종류/이름/내용 거부, 금액 제외·잘못된 행 거부·다른 동작 무시) 통과. ORCA 브라우저에서 등록 → 목록 31행 → 원가 표를 3행으로 줄인 뒤 열기 → 31행 복구·도급액 79,603,324(고정 해제) → 삭제·보관함 비움, 샘플 복구.
|
||||
- 남음: 기본양식으로 저장(새 공사 기본값), 갑지·표지 문서 양식.
|
||||
- [x] 정산원가계산서(원본 popJunSangCalc 0xd0eb28→0xd0f6fc, popJungSangShow 0xd0ea24, 재계산 0xca31c8 — 디스어셈블 근거) — 당초금액 초기화(SC := Cost, 55 → 0, JungSangInit), 작성/해제 토글(JungSangShow), 차액 행(LinkIndex 66, (감)/(증)) 재계산마다 갱신, 원가 화면 당초금액·정산금액 열. 엔진은 66 행 수식을 계산하지 않음.
|
||||
- 검증완료: `test_est_jungsang.py` 2개(차액 글자·빈 행 재사용, 당초 복사·55 해제·재계산 차액·해제 시 66 비움·SC 유지·열 순서), 대조·업무 시험 포함 통과. ORCA 브라우저에서 작성 → 열 「당초금액, 정산금액」·차액 (증)324(도급액 고정 해제분)·오류 0, 해제 → 「원가금액」 복귀, 샘플 복구.
|
||||
- 남음: 당초분·정산분 색상 옵션.
|
||||
- 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133.
|
||||
- [ ] 남은 핵심(실무 빈도순):
|
||||
1. 통합검색 — 저장된 다른 작업에서 단가·호표 찾기.
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
내역서·일위대가 화면의 `편집 → 단가 고르기`는 자재·노임·기계경비·일위대가·단가산출·견적·실적 자료를 검색어로 찾아 선택 행 아래에 넣거나, 선택 행 하나의 연결을 바꿉니다. 여러 개를 골라 한 번에 넣을 수 있고, 바꿀 때는 변경 전·후를 확인한 뒤 같은 코드를 쓰는 이 표의 다른 행도 함께 바꿀지 고릅니다. 넣거나 바꾼 행만 원천 단가를 다시 읽습니다. 단가산출 산출근거 행은 대상이 아닙니다.
|
||||
|
||||
`원가계산서 → 정산원가계산서 작성/해제`는 원본처럼 원가 표에 당초금액 열을 금액 앞에 보이고 금액 열을 정산금액으로 바꿉니다. 처음 켤 때(또는 `당초금액 초기화`) 원가 행마다 현재 금액을 당초금액으로 옮기고 도급액 고정 연결(55)을 풀며, 맨 아래 금액 행의 차이를 `(감)`·`(증)` 금액 행으로 적습니다. 켜져 있는 동안 재계산마다 차액 행을 다시 씁니다. 끄면 차액 행만 비우고 당초금액은 남깁니다.
|
||||
|
||||
`원가계산서 → 원가계산서 양식 목록·등록`은 현재 원가계산서의 항목·수식·요율을 이름 붙여 이 앱 `data/forms.json`에 등록하고, 목록에서 열기·삭제하며, 파일로 내려받거나 불러옵니다. 금액은 남기지 않고 열 때 다시 계산하므로 도급액 고정은 풀립니다.
|
||||
|
||||
예정공정표의 `눈금`은 원본처럼 1일·5일·10일·15일·20일·1주·1개월 단위로 기간 칸 위에 날짜를 적고 막대 뒤에 세로 눈금선을 그립니다. 눈금이 60개를 넘으면 앞 60개만 그립니다.
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<script defer src="native-text.js"></script>
|
||||
<script defer src="native-quantity.cjs"></script>
|
||||
<script defer src="native-wonga.js"></script>
|
||||
<script defer src="native-jungsang.js"></script>
|
||||
<script defer src="native-engine.js"></script>
|
||||
<script defer src="native-row-match.js"></script>
|
||||
<script defer src="native-escalation.js"></script>
|
||||
@@ -152,6 +153,8 @@
|
||||
><button data-action="settings">자릿수·도급액 고정</button
|
||||
><button data-action="reset-wonga">원가계산서 초기화</button>
|
||||
<button data-action="wonga-forms">원가계산서 양식 목록·등록</button>
|
||||
<button data-action="jungsang-toggle">정산원가계산서 작성/해제</button
|
||||
><button data-action="jungsang-init">당초금액 초기화</button>
|
||||
</div>
|
||||
</details>
|
||||
<details>
|
||||
|
||||
@@ -606,7 +606,8 @@ const EstBusinessActions = (() => {
|
||||
(typeof EstExcelMap !== "undefined" && EstExcelMap.handle(name, state)) ||
|
||||
(typeof EstTransfer !== "undefined" && EstTransfer.handle(name, state)) ||
|
||||
(typeof EstBid !== "undefined" && EstBid.handle(name, state)) ||
|
||||
(typeof EstFormsLibrary !== "undefined" && EstFormsLibrary.handle(name, state, node))
|
||||
(typeof EstFormsLibrary !== "undefined" && EstFormsLibrary.handle(name, state, node)) ||
|
||||
(typeof EstJungSang !== "undefined" && EstJungSang.handle(name, state))
|
||||
) {
|
||||
EstApp.render();
|
||||
} else if (name === "load-contract" || name === "load-bid") {
|
||||
|
||||
@@ -339,6 +339,8 @@ const EstNative = (() => {
|
||||
직접공사비: num(direct.TC),
|
||||
});
|
||||
tables.Wonga = wonga.rows;
|
||||
if (options.JungSangShow && typeof EstJungSang !== "undefined")
|
||||
EstJungSang.difference(tables.Wonga);
|
||||
errors.push(...wonga.errors);
|
||||
const wongaNames = new Map(
|
||||
wonga.rows
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// 원본 정산원가계산서(TfrmWongaBase popJunSangCalc 0xd0eb28 → 0xd0f6fc, popJungSangShow 0xd0ea24, 재계산 0xca31c8).
|
||||
// 당초금액 초기화: 원가 행마다 SC(당초금액) := Cost, LinkIndex 55 → 0, 초기화 표지(JungSangInit), 차액 행 기록.
|
||||
// 정산원가계산서 작성/해제: JungSangShow 토글 — 켤 때 초기화 전이면 확인 없이 초기화, 끌 때 차액 행(66)만 비움(SC 유지).
|
||||
// 켜져 있는 동안 재계산마다 차액 행을 다시 쓴다. 차액 d = 마지막 금액 행의 SC − Cost, d>0 「(감)」·d<0 「(증)」.
|
||||
const EstJungSang = (() => {
|
||||
const num = (value) => Number(value) || 0;
|
||||
const blank = (row) => !String(row.Name ?? "").trim() && !num(row.Cost) && !row.VSusic;
|
||||
|
||||
function difference(rows) {
|
||||
let last = -1;
|
||||
rows.forEach((row, i) => {
|
||||
if (num(row.LinkIndex) !== 66 && String(row.Name ?? "").trim() && num(row.Cost)) last = i;
|
||||
});
|
||||
const d = last < 0 ? 0 : num(rows[last].SC) - num(rows[last].Cost);
|
||||
const text = d > 0 ? `(감)${Math.abs(d)}` : d < 0 ? `(증)${Math.abs(d)}` : "0";
|
||||
let at = rows.findIndex((row) => num(row.LinkIndex) === 66);
|
||||
if (at < 0) {
|
||||
for (let i = rows.length - 1; i >= 0 && blank(rows[i]); i--) at = i;
|
||||
}
|
||||
if (at < 0) {
|
||||
rows.push({});
|
||||
at = rows.length - 1;
|
||||
}
|
||||
Object.assign(rows[at], {
|
||||
EndLineIndex: 1,
|
||||
Name: ".",
|
||||
VGungu: text,
|
||||
VSusic: text,
|
||||
LinkIndex: 66,
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
function init(project) {
|
||||
const rows = project.tables.Wonga || [];
|
||||
for (const row of rows) {
|
||||
row.SC = num(row.Cost);
|
||||
if (num(row.LinkIndex) === 55) row.LinkIndex = 0;
|
||||
}
|
||||
project.options.JungSangInit = 1;
|
||||
return difference(rows);
|
||||
}
|
||||
|
||||
function clear(project) {
|
||||
const row = (project.tables.Wonga || []).find((r) => num(r.LinkIndex) === 66);
|
||||
if (row) Object.assign(row, { Name: "", VGungu: "", VSusic: "", LinkIndex: 0 });
|
||||
}
|
||||
|
||||
function toggle(project) {
|
||||
project.options.JungSangShow = !project.options.JungSangShow;
|
||||
if (project.options.JungSangShow) {
|
||||
if (!num(project.options.JungSangInit)) init(project);
|
||||
} else clear(project);
|
||||
return project.options.JungSangShow;
|
||||
}
|
||||
|
||||
// 화면: 정산 모드에서는 원가 표에 당초금액 열을 금액 앞에 보이고 금액 열 이름을 정산금액으로 바꾼다.
|
||||
function columns(project, base) {
|
||||
if (!project.options?.JungSangShow) return base;
|
||||
const out = base.filter((key) => key !== "SC");
|
||||
out.splice(out.indexOf("Cost"), 0, "SC");
|
||||
return out;
|
||||
}
|
||||
function label(project, key) {
|
||||
if (!project.options?.JungSangShow) return null;
|
||||
return { SC: "당초금액", Cost: "정산금액" }[key] || null;
|
||||
}
|
||||
|
||||
function handle(name, state) {
|
||||
if (name === "jungsang-init") {
|
||||
if (!confirm("당초금액이 정산금액으로 대체됩니다.")) return true;
|
||||
EstApp.change((project) => init(project));
|
||||
} else if (name === "jungsang-toggle") {
|
||||
let on;
|
||||
EstApp.change((project) => (on = toggle(project)));
|
||||
state.notice = on ? "정산원가계산서를 작성합니다." : "정산원가계산서를 해제했습니다.";
|
||||
} else return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return { difference, init, clear, toggle, columns, label, handle };
|
||||
})();
|
||||
@@ -76,7 +76,7 @@ const EstViews = (() => {
|
||||
}
|
||||
function grid(table, list, columns) {
|
||||
const { esc, number, state } = EstApp;
|
||||
return `<table class="sheet" data-grid="${table}"><thead><tr><th class="row-number">선택</th>${columns.map((key) => `<th class="col-${key}">${esc(EstModel.labels[key] || key)}</th>`).join("")}</tr></thead><tbody>${list.map(({ row, index }) => `<tr data-index="${index}" class="${state.selection.has(index) ? "selected " : ""}${Number(row.Index) > 0 ? "heading " : ""}${Number(row.Level) > 0 ? "level " : ""}${row.Error ? "error-row" : ""}"><th class="row-number"><label><input type="checkbox" data-select="${index}" ${state.selection.has(index) ? "checked" : ""}>${index + 1}</label></th>${columns.map((key) => `<td class="col-${key}">${cell(table, index, key, row[key])}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
|
||||
return `<table class="sheet" data-grid="${table}"><thead><tr><th class="row-number">선택</th>${columns.map((key) => `<th class="col-${key}">${esc((table === "Wonga" && typeof EstJungSang !== "undefined" && EstJungSang.label(state.project, key)) || EstModel.labels[key] || key)}</th>`).join("")}</tr></thead><tbody>${list.map(({ row, index }) => `<tr data-index="${index}" class="${state.selection.has(index) ? "selected " : ""}${Number(row.Index) > 0 ? "heading " : ""}${Number(row.Level) > 0 ? "level " : ""}${row.Error ? "error-row" : ""}"><th class="row-number"><label><input type="checkbox" data-select="${index}" ${state.selection.has(index) ? "checked" : ""}>${index + 1}</label></th>${columns.map((key) => `<td class="col-${key}">${cell(table, index, key, row[key])}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
|
||||
}
|
||||
function render(state) {
|
||||
const { project, view } = state,
|
||||
@@ -121,7 +121,10 @@ const EstViews = (() => {
|
||||
const all = EstApp.visibleRows(),
|
||||
start = state.page * 100,
|
||||
list = all.slice(start, start + 100),
|
||||
columns = EstModel.columns(view);
|
||||
columns =
|
||||
view === "Wonga" && typeof EstJungSang !== "undefined"
|
||||
? EstJungSang.columns(project, EstModel.columns(view))
|
||||
: EstModel.columns(view);
|
||||
byId("work-content").innerHTML = grid(view, list, columns);
|
||||
byId("paging").hidden = false;
|
||||
byId("page-info").textContent =
|
||||
|
||||
@@ -75,6 +75,7 @@ const EstWonga = (() => {
|
||||
row.Cost = defaultBases[row.BaseField];
|
||||
} else if (
|
||||
row.VSusic &&
|
||||
num(row.LinkIndex) !== 66 &&
|
||||
!/^원금액\s*:/.test(row.VSusic) &&
|
||||
![1, 3].includes(num(row.CalcIndex))
|
||||
) {
|
||||
|
||||
@@ -22,6 +22,7 @@ const scripts = [
|
||||
"native-text.js",
|
||||
"native-quantity.cjs",
|
||||
"native-wonga.js",
|
||||
"native-jungsang.js",
|
||||
"native-engine.js",
|
||||
"native-row-match.js",
|
||||
"native-escalation.js",
|
||||
|
||||
Reference in New Issue
Block a user