fix(est): allocate annual component amounts

This commit is contained in:
2026-11-05 13:22:24 +09:00
parent 216e2ae2a7
commit a007d6358a
5 changed files with 68 additions and 9 deletions
+44
View File
@@ -648,3 +648,47 @@ def test_text_total_without_result_marker_evaluates_its_expression():
assert result["rows"][0]["TC"] == 20
assert result["rows"][1]["TC"] == 20
assert result["rows"][2]["TC"] == 0
def test_annual_formulas_split_component_amounts_and_preserve_remainder():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.options.SourceRefresh=false;
p.tables.EstMainDat=[{PkID:1,Code:'R',Name:'item',GN:1,NU:5,GU:3},{LinkIndex:54}];
p.annual=[{name:'1차',formula:'[기준]*0.5'},{name:'2차',formula:'[기준]-[1차]'}];
const q=EstNative.calculate(p),before=JSON.stringify(q);
return {output:EstBusiness.annual(q),preserved:before===JSON.stringify(q)};
})()""",
)
assert result["preserved"]
output = result["output"]
assert output["errors"] == []
first, second = [item["project"] for item in output["results"]]
assert first["tables"]["EstMainDat"][0]["GN"] == 0.5
assert second["tables"]["EstMainDat"][0]["GN"] == 0.5
assert [item["calculation"]["direct"]["TC"] for item in [first, second]] == [3, 5]
assert first["tables"]["EstMainDat"][0]["NC"] == 2
assert second["tables"]["EstMainDat"][0]["NC"] == 3
assert second["tables"]["EstMainDat"][0]["GC"] == 2
assert all(first["tables"]["EstMainDat"][0][key] == 0 for key in ["TU", "NU", "GU", "JU"])
def test_annual_fixed_amount_with_zero_quantity_and_duplicate_names():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();p.options.SourceRefresh=false;
p.tables.EstMainDat=[{PkID:1,Code:'R',GN:0,NC:3,GC:7,TC:10,CalcIndex:1},{LinkIndex:54}];
p.annual=[{name:'1차',formula:'[기준]*0.5'},{name:'2차',formula:'[기준]-[1차]'}];
const output=EstBusiness.annual(EstNative.calculate(p));
p.annual[1].name='1차';
return {output,invalid:EstBusiness.annual(p)};
})()""",
)
assert result["output"]["errors"] == []
assert [
item["project"]["calculation"]["direct"]["TC"] for item in result["output"]["results"]
] == [4, 6]
assert result["invalid"]["results"] == []
assert len(result["invalid"]["errors"]) == 1
+2
View File
@@ -24,6 +24,8 @@
단가 변경은 산근 소계(61), 비목 참조 비율(58), 선택 구간 합계(62)를 거쳐 내역으로 전달됩니다. 산출근거 편집기의 `구간지정`·`구간합계` 표지로 A~Z 묶음을 제어합니다. 소계 표지 1,433개를 추가한 대용량 원본 대조는 115,145개 일치·해석 오류 0이며, 원본 산근 낙찰 행(52)의 성분 계산과 연결 편집도 회귀 시험합니다.
연차공사의 `[기준]*0.5` 수식은 수량과 각 성분 금액에 각각 적용됩니다. 금액은 정수 절사하고, 다음 차수의 `[기준]-[1차]`로 절사 후 잔액을 배분합니다. 차수 사본은 단가를 비운 고정 금액으로 저장하여 다시 열어도 금액을 유지합니다. `[계약수량]`은 이전 작업 파일과의 호환 표기로 `[기준]`과 같은 역할을 합니다.
**전체 원본 동등성은 아직 확보하지 못했습니다.** 기성·물가변동·연차 등은 분석 분기와 합성 시나리오를 시험했으며 원본 업무 샘플 대조는 없습니다. 원본 화면 실행 비교는 못했습니다. 원본 페이지·도면 배치, CAD/VEC0650 편집, 원본 인쇄 서식, Excel/XML/JDL 변환, ESTX 수정 저장은 미구현입니다. 원본 대체품의 완료 판정으로 보지 않습니다.
검사:
+1 -2
View File
@@ -393,8 +393,7 @@ const EstActions = (() => {
}, false);
} else if (name === "add-annual")
EstApp.change(
() =>
project.annual.push({ name: `${project.annual.length + 1}차`, formula: "[계약수량]" }),
() => project.annual.push({ name: `${project.annual.length + 1}차`, formula: "[기준]" }),
false,
);
else if (name === "open-annual") {
+19 -5
View File
@@ -336,11 +336,16 @@ const EstBusiness = (() => {
function annual(project) {
const errors = [],
results = [];
const names = (project.annual || []).map((year) => String(year.name || ""));
if (names.some((name) => !name.trim()) || new Set(names).size !== names.length)
return { results, errors: ["차수 이름은 비어 있거나 중복될 수 없습니다."] };
for (const year of project.annual || []) {
const next = copy(project);
next.name = `${project.name} · ${year.name}`;
next.progress = [];
next.annual = [];
next.options.TotalFix = false;
next.options.SourceRefresh = false;
for (let index = 0; index < next.tables.EstMainDat.length; index++) {
const row = next.tables.EstMainDat[index];
if (num(row.LinkIndex) === 57) {
@@ -351,11 +356,20 @@ const EstBusiness = (() => {
}
if (num(row.Level) > 0 || [54, 55, 58, 59, 62, 67].includes(num(row.LinkIndex))) continue;
try {
row.GN = EstExpression.evaluate(year.formula || "[계약수량]", (name) => {
if (name === "계약수량") return num(project.tables.EstMainDat[index].GN);
const previous = results.find((r) => r.name === name);
return previous ? num(previous.project.tables.EstMainDat[index]?.GN) : undefined;
});
// f0f9b8/f1098c resolve the same column formula against each current field.
const value = (field) =>
EstExpression.evaluate(year.formula || "[기준]", (name) => {
if (["기준", "계약", "계약수량"].includes(name))
return num(project.tables.EstMainDat[index][field]);
const previous = results.find((result) => result.name === name);
return previous ? num(previous.project.tables.EstMainDat[index]?.[field]) : undefined;
});
row.GN = value("GN");
components.forEach((key) => (row[key] = Math.trunc(value(key))));
row.TC = sum(row);
units.forEach((key) => (row[key] = 0));
row.TU = 0;
row.CalcIndex = num(row.CalcIndex) >= 2 ? 3 : 1;
} catch (error) {
errors.push(`${year.name}: ${error.message}`);
}
+2 -2
View File
@@ -222,7 +222,7 @@ const EstExtra = (() => {
"연차공사",
button("add-annual", "차수 추가"),
table(
["차수", "수량 배분 수식", "순공사비", "도급액", "작업", ""],
["차수", "수량·금액 배분 수식", "순공사비", "도급액", "작업", ""],
project.annual.map((year, index) => [
field("annual", index, "name", year.name, "text"),
field("annual", index, "formula", year.formula, "text"),
@@ -232,7 +232,7 @@ const EstExtra = (() => {
button("delete-extra", "삭제", `data-section="annual" data-row="${index}"`),
]),
),
`수식 예: [계약수량]*0.5, [계약수량]-[1차]. ${esc(output.errors.join(" / "))}`,
`수식 예: [기준]*0.5, [기준]-[1차]. 같은 수식을 수량과 각 성분 금액에 따로 적용하고 금액은 원 미만 절사합니다. ${esc(output.errors.join(" / "))}`,
);
} else if (view === "resources") {
const output = EstResources.calculate(project);