fix(est): guard invalid contract review totals
This commit is contained in:
@@ -1,6 +1,23 @@
|
||||
from test_est_rea_parity import run
|
||||
|
||||
|
||||
def test_review_rejects_unallocated_fixed_amounts_and_ambiguous_targets():
|
||||
result = run(
|
||||
"math",
|
||||
"""(()=>{
|
||||
const p=EstNative.empty();p.tables.EstMainDat=[
|
||||
{Name:'fixed',PkID:1,GN:1,TC:100,CalcIndex:1},
|
||||
{Name:'first',Code:'A',GN:1,GU:10},{Name:'second',Code:'A',GN:1,GU:20}];
|
||||
p.contractReview={edits:{'row:A':{GU:999}}};
|
||||
const out=EstReview.calculate(p);let rejected=false;
|
||||
try{EstReview.copy(p)}catch{rejected=true}
|
||||
return {errors:out.errors,rejected,units:out.reviewed.tables.EstMainDat.slice(1).map(row=>row.GU)};
|
||||
})()""",
|
||||
)
|
||||
assert len(result["errors"]) == 2 and result["rejected"]
|
||||
assert result["units"] == [10, 20]
|
||||
|
||||
|
||||
def test_review_recalculates_user_inputs_and_opens_independent_copy():
|
||||
result = run(
|
||||
"math",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
- **기성 진도 반영**: 예정공정표에서 최근 확정 회차의 누계 수량을 해당 회차 계약 수량으로 나누어 연결된 공정의 진도를 갱신할 수 있습니다. 수량이 0이거나 기성 내역에 연결되지 않은 공정은 기존 진도를 유지합니다. 0~100%를 벗어나는 결과는 갱신을 중단합니다.
|
||||
- **공종별 동원 출력**: 한 번의 원천 계산에서 공종별 인원·자재·장비·실적·견적을 나누며 전체 합계와 금액/수량을 대조합니다. 문서 본문과 인쇄 설정도 저장·복원·취소 대상입니다.
|
||||
- **계약심사**: 작업모드의 계약심사 화면에서 심사 수량과 비목별 단가·심사 사유를 직접 입력하고 기존액·심사액·차액을 비교합니다. 고정금액 행은 비목별 금액을 입력하며, 원가계산서도 다시 계산합니다. 항목별 입력을 초기화하거나 실행 취소할 수 있습니다. 심사 입력은 JSON에 저장하고, 확인한 결과는 원래 작업과 별도인 심사 내역 사본으로 열 수 있습니다. 심사결과요약서에는 내역별 비교·사유와 도급액 차액을 출력합니다. 심사율이나 법적 기준은 자동으로 정하지 않습니다.
|
||||
- 고정금액과 비목 합계가 다른 내역은 심사 합계를 정확히 산정할 수 없어 오류를 표시합니다. 내역서에서 비목 금액을 확인해 입력한 뒤 심사합니다.
|
||||
|
||||
Ctrl+S는 입력 중인 셀도 확정한 뒤 저장합니다. 연속 저장 요청은 순서대로 처리하며, 저장 대기 중의 추가 입력이나 새로 연 문서를 이전 저장 응답으로 덮어쓰지 않습니다. 실행 취소는 현재 작업 파일의 연결을 유지합니다.
|
||||
|
||||
|
||||
@@ -48,13 +48,26 @@ const EstReview = (() => {
|
||||
working.tables.EstMainDat = structuredClone(project.tables.EstMainDat);
|
||||
working.tables.Wonga = structuredClone(project.tables.Wonga);
|
||||
const baseline = EstNative.calculate(working);
|
||||
const counts = new Map();
|
||||
working.tables.EstMainDat.forEach((row, index) => {
|
||||
if (!editable(row)) return;
|
||||
const key = rowKey(row, index);
|
||||
if (seen.has(key)) errors.push(`심사 내역 식별자 중복: ${key}`);
|
||||
counts.set(key, (counts.get(key) || 0) + 1);
|
||||
});
|
||||
working.tables.EstMainDat.forEach((row, index) => {
|
||||
if (!editable(row)) return;
|
||||
const key = rowKey(row, index);
|
||||
if (counts.get(key) > 1) errors.push(`심사 내역 식별자 중복: ${key}`);
|
||||
seen.add(key);
|
||||
if (
|
||||
fixed(row) &&
|
||||
EstMath.even(EstMath.num(row.TC) * 10000) !== EstMath.even(EstMath.sum(row) * 10000)
|
||||
)
|
||||
errors.push(
|
||||
`${row.Name || key}: 고정 금액과 비목 합계가 달라 심사 합계를 계산할 수 없습니다.`,
|
||||
);
|
||||
const edit = edits[key];
|
||||
if (!edit) return;
|
||||
if (!edit || counts.get(key) > 1) return;
|
||||
for (const field of Object.keys(edit)) {
|
||||
if (field === "Reason") continue;
|
||||
if (!allowed(row).includes(field))
|
||||
@@ -127,6 +140,14 @@ const EstReview = (() => {
|
||||
const field = node.dataset.review,
|
||||
value = field === "Reason" ? node.value : Number(node.value),
|
||||
key = node.dataset.key;
|
||||
const matches = EstApp.state.project.tables.EstMainDat.filter(
|
||||
(row, index) => editable(row) && rowKey(row, index) === key,
|
||||
);
|
||||
if (matches.length !== 1) {
|
||||
EstApp.state.notice = "심사 내역 식별자가 없거나 중복됩니다.";
|
||||
EstApp.render();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
field === "Reason"
|
||||
? value.length > 10000
|
||||
|
||||
Reference in New Issue
Block a user