fix(est): propagate assigned prices into final totals
This commit is contained in:
@@ -69,3 +69,69 @@ def test_parenthesized_volume_and_count_units_recalculate_from_variables():
|
||||
assert result["rows"][2]["TC"] == 0.8
|
||||
assert result["rows"][3]["TC"] == 557971.2
|
||||
assert result["rows"][4]["TC"] == 76.8
|
||||
|
||||
|
||||
def test_quoted_source_conversion_declaration_propagates_changed_value():
|
||||
result = run(
|
||||
"math",
|
||||
r"""[2,3].map(quantity=>EstText.calculate([
|
||||
{Index:1,Code:'quote',LineData:'quote'},
|
||||
{LineData:` * 중량환산 : 사석기준 'Q1 = ${quantity}.0 톤/㎥'`},
|
||||
{LineData:' 노무비 : \x03235,204\x03 ×1.04인×Q1 = 235,204 ×1.04×2 = \x12489,224.3\x12',SCheck:-1}
|
||||
]))""",
|
||||
)
|
||||
assert all(item["errors"] == [] for item in result)
|
||||
assert [item["rows"][2]["TC"] for item in result] == [489224.3, 733836.4]
|
||||
|
||||
|
||||
def test_group_sum_accepts_assigned_price_and_refreshes_from_source():
|
||||
result = run(
|
||||
"math",
|
||||
r"""[550,600].map(price=>{
|
||||
const p=EstNative.empty();p.options.SourceRefresh=true;
|
||||
p.tables.StuffcDat=[{Code:'M',ValueIndex:4,InputTC:price}];
|
||||
p.tables.CostMainDat=[
|
||||
{Index:1,Code:'C',LineData:'C'},
|
||||
{LinkIndex:5,Code:'M',LineData:' \x0ebrick\x0e \x06A\x06\x03550\x03 : 별산'},
|
||||
{LinkIndex:62,SCheck:-1,LineData:' \x07A\x07\x1a550\x1a × 5% = \x1227.5\x12'},
|
||||
{SCheck:-1,LineData:' \x1a노무비:27\x1a'},
|
||||
{SCheck:-5,LineData:' \x1a계:\x1a \x1227\x12'}];
|
||||
p.tables.EstMainDat=[{Code:'C',LinkIndex:104,GN:10},{LinkIndex:54}];
|
||||
return EstNative.calculate(p);
|
||||
})""",
|
||||
)
|
||||
assert all(item["calculation"]["errors"] == [] for item in result)
|
||||
assert [item["tables"]["CostMainDat"][2]["TC"] for item in result] == [27.5, 30]
|
||||
assert [item["tables"]["EstMainDat"][0]["TC"] for item in result] == [270, 300]
|
||||
|
||||
|
||||
def test_subtotal_includes_price_only_rows_and_updates_truncated_final_amount():
|
||||
result = run(
|
||||
"math",
|
||||
r"""[1094,2000].map(price=>{
|
||||
const p=EstNative.empty();
|
||||
p.tables.StuffcDat=[{Code:'T',ValueIndex:4,InputTC:110754},{Code:'H',ValueIndex:4,InputTC:price}];
|
||||
p.tables.CostMainDat=[
|
||||
{Index:1,Code:'C',LineData:'transport'},
|
||||
{Level:2,LineData:'1. 운반 및 하역'},
|
||||
{LinkIndex:5,Code:'T',LineData:' \x0etruck\x0e (\x03110,754\x03 ÷12) ×1.5 = \x1213,844.2\x12'},
|
||||
{LinkIndex:5,Code:'H',LineData:' \x0eunloading\x0e \x031,094\x03'},
|
||||
{LinkIndex:61,SCheck:-3,TC:14938.2,LineData:' 경비소계 : \x0c14,938.2\x0c'},
|
||||
{Level:2,LineData:'2. 합계'},
|
||||
{Level:-2,SCheck:-3,LineData:' \x1a경비:14,938\x1a'},
|
||||
{Level:-2,SCheck:-5,LineData:' \x1a계:\x1a \x1214,938\x12'}];
|
||||
p.tables.EstMainDat=[{Code:'C',LinkIndex:104,GN:10},{LinkIndex:54}];
|
||||
const first=EstNative.calculate(p),again=EstNative.calculate(first);
|
||||
return {first,again};
|
||||
})""",
|
||||
)
|
||||
assert all(item["first"]["calculation"]["errors"] == [] for item in result)
|
||||
assert [item["first"]["tables"]["CostMainDat"][4]["TC"] for item in result] == [
|
||||
14938.2,
|
||||
15844.2,
|
||||
]
|
||||
for key in ["first", "again"]:
|
||||
assert [item[key]["tables"]["EstMainDat"][0]["TC"] for item in result] == [
|
||||
149380,
|
||||
158440,
|
||||
]
|
||||
|
||||
@@ -36,7 +36,11 @@ Ctrl+S는 입력 중인 셀도 확정한 뒤 저장합니다. 연속 저장 요
|
||||
|
||||
설계변경 비교는 웹에서 확정한 당초 사본이 없을 때 원본 파일의 당초 수량·금액을 사용합니다. 별도 당초 확정 사본이 있으면 그 사본이 우선합니다. 원본 당초 전체 자료와 원가 서식의 활성 선택을 자동 추정하지는 않습니다.
|
||||
|
||||
원본 산근의 `배`·`번`·`줄`·`set`·`kg/set`·`㎜`, 괄호 뒤 면적·체적 단위와 별표로 감싼 중량 선언도 계산합니다. 측량설계 저장값 대조는 80,658/80,682 일치·해석 오류 10건, 가시설은 13,978/13,979 일치·해석 오류 4건입니다. 범위 조건과 누락된 참조 등은 임의 값으로 대체하지 않습니다.
|
||||
원본 산근의 `배`·`번`·`줄`·`set`·`kg/set`·`㎜`, 괄호 뒤 면적·체적 단위와 별표·따옴표로 감싼 변수 선언도 계산합니다. 측량설계 저장값 대조는 80,659/80,682 일치·해석 오류 5건, 가시설은 13,978/13,979 일치·해석 오류 4건입니다. 범위 조건과 누락된 참조 등은 임의 값으로 대체하지 않습니다.
|
||||
|
||||
구간 합계와 소계는 결과 금액뿐 아니라 단가만 적힌 지정 행도 포함합니다. 구간 합계의 표지와 최종 합계 표지를 구분하며, 이미 정수로 절사된 최종 합계도 앞 성분 금액과의 합산 관계가 확인되면 변경액을 다시 집계합니다. 단가 550→600 변경이 구간 비율 27.5→30, 최종 합계 27→30, 내역 270→300으로 전달되는 것을 저장·취소까지 확인했습니다.
|
||||
|
||||
이 보완 후 해양 자료는 소계 표지까지 포함해 134,133/134,208 저장값 일치·해석 오류 99건입니다. 계산·이력 전용 시험 64개가 통과했으며, 남은 불일치와 원본 실행 미확인은 별개로 유지합니다.
|
||||
|
||||
**전체 원본 동등성은 아직 확보하지 못했습니다.** 기성·물가변동·연차 등은 분석 분기와 합성 시나리오를 시험했으며 원본 업무 샘플 대조는 없습니다. 원본 화면 실행 비교는 못했습니다. 원본 페이지·도면 배치, CAD/VEC0650 편집, 원본 인쇄 서식, Excel/XML/JDL 변환, ESTX 수정 저장은 미구현입니다. 원본 대체품의 완료 판정으로 보지 않습니다.
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ const EstText = (() => {
|
||||
const numeric = (s) => Number(String(s).replaceAll(",", ""));
|
||||
const sumCurrency = (values) =>
|
||||
values.reduce((n, value) => n + EstMath.even(num(value) * 10000), 0) / 10000;
|
||||
const autoTotal = (row) =>
|
||||
num(row.LinkIndex) !== 62 && String(row.LineData || "").includes("\x1a");
|
||||
const stripLabels = (text) =>
|
||||
String(text)
|
||||
.replace(/\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\([가-힣][^()]*\)/g, "")
|
||||
@@ -87,7 +89,7 @@ const EstText = (() => {
|
||||
const levels = [],
|
||||
componentRows = [];
|
||||
for (let j = index + 1; j < rows.length && num(rows[j].Index) <= 0; j++)
|
||||
if (String(rows[j].LineData || "").includes("\x1a")) levels.push(num(rows[j].Level));
|
||||
if (autoTotal(rows[j])) levels.push(num(rows[j].Level));
|
||||
else if (component[num(rows[j].SCheck)] && marker.test(String(rows[j].LineData || "")))
|
||||
componentRows.push(j);
|
||||
finalLevel = levels.length ? Math.min(...levels) : -1;
|
||||
@@ -153,7 +155,7 @@ const EstText = (() => {
|
||||
const values = [];
|
||||
for (let j = index - 1; j >= 0 && !(num(rows[j].Index) > 0); j--)
|
||||
for (const match of String(rows[j].LineData || "").matchAll(
|
||||
/\x06([A-Z])\x06\s*([\x12\x0c])([-\d.,]+)\2/g,
|
||||
/\x06([A-Z])\x06\s*([\x03\x12\x0c])([-\d.,]+)\2/g,
|
||||
))
|
||||
if (match[1] === group) values.unshift(numeric(match[3]));
|
||||
if (!values.length) selectionError = `합계 구간 ${group}에 지정된 결과가 없습니다.`;
|
||||
@@ -166,7 +168,7 @@ const EstText = (() => {
|
||||
const previous = rows[j];
|
||||
if (num(previous.Index) !== -1) {
|
||||
const results = [
|
||||
...String(previous.LineData || "").matchAll(/([\x12\x0c])([-\d.,]+)\1/g),
|
||||
...String(previous.LineData || "").matchAll(/([\x03\x12\x0c])([-\d.,]+)\1/g),
|
||||
];
|
||||
if (results.length) values.push(numeric(results.at(-1)[2]));
|
||||
}
|
||||
@@ -219,6 +221,7 @@ const EstText = (() => {
|
||||
);
|
||||
else if (currentSource && sc === 0 && /\x0e/.test(original))
|
||||
text = text.replace(/\x03([-\d.,]+)\x03/g, () => `\x03${num(currentSource.TC)}\x03`);
|
||||
row.LineData = text;
|
||||
text = text.replace(
|
||||
/\x11([-\d.,]+)\x11/g,
|
||||
(whole, value) => `\x11${changed.get(numeric(value)) ?? numeric(value)}\x11`,
|
||||
@@ -273,8 +276,7 @@ const EstText = (() => {
|
||||
for (let j = index - 1; j >= 0; j--) {
|
||||
const previous = rows[j];
|
||||
if (!previous.LineData) continue;
|
||||
if (!String(previous.LineData).includes("\x1a") || !component[num(previous.SCheck)])
|
||||
break;
|
||||
if (!autoTotal(previous) || !component[num(previous.SCheck)]) break;
|
||||
section.push(num(previous.TC));
|
||||
}
|
||||
calculated = sumCurrency(section);
|
||||
@@ -316,7 +318,7 @@ const EstText = (() => {
|
||||
? []
|
||||
: [
|
||||
...assignmentText.matchAll(
|
||||
/(?:^\s*| {2,}|[,,]\s*|(?<=[가-힣])\s+|[::]\s*)([A-Za-z][A-Za-z0-9]*)\s*=/g,
|
||||
/(?:^\s*| {2,}|[,,]\s*|(?<=[가-힣])\s+|[::]\s*|['"]\s*)([A-Za-z][A-Za-z0-9]*)\s*=/g,
|
||||
),
|
||||
];
|
||||
for (let j = 0; j < assignments.length; j++) {
|
||||
@@ -325,6 +327,8 @@ const EstText = (() => {
|
||||
let body = text.slice(assignment.index + assignment[0].length, end);
|
||||
if (!matched && /^\s*\*.*\*\s*$/.test(text)) body = body.replace(/\*\s*$/, "");
|
||||
body = body.replace(/\{[^}]*\}/g, "");
|
||||
if (/['"]\s*[A-Za-z][A-Za-z0-9]*\s*=/.test(assignment[0]))
|
||||
body = body.replace(/['"]\s*$/, "");
|
||||
if (!matched) body = body.split("{")[0];
|
||||
body = body.split(/\s+[::]\s*/)[0].replace(/[,,]\s*$/, "");
|
||||
if (body.includes("─") && calculated !== null) {
|
||||
@@ -394,7 +398,7 @@ const EstText = (() => {
|
||||
row.Precision ??
|
||||
explicit?.digits ??
|
||||
(sc < 0 || num(row.LinkIndex) !== 0
|
||||
? (num(row.Level) === finalLevel && text.includes("\x1a")) ||
|
||||
? (num(row.Level) === finalLevel && autoTotal(row)) ||
|
||||
index === singleComponent ||
|
||||
options.CostValueTrunc
|
||||
? 0
|
||||
@@ -475,7 +479,7 @@ const EstText = (() => {
|
||||
row.LineData = rendered;
|
||||
changed.set(oldValue, firstValue);
|
||||
checks.push({ slot: index, stored: oldValue, calculated: firstValue });
|
||||
if (text.includes("\x1a") && component[sc]) {
|
||||
if (autoTotal(row) && component[sc]) {
|
||||
if (num(row.Level) === finalLevel) totals[component[sc]] = calculated;
|
||||
}
|
||||
} else if (!matched && component[sc] && text.includes("\x03") && !text.includes("\x1a")) {
|
||||
@@ -485,13 +489,36 @@ const EstText = (() => {
|
||||
1,
|
||||
);
|
||||
row.LineData = text;
|
||||
} else if (!matched && text.includes("\x1a") && component[sc]) {
|
||||
const value = clean(text).split(":").slice(1).join(":").trim();
|
||||
} else if (!matched && autoTotal(row) && component[sc]) {
|
||||
let value = clean(text).split(":").slice(1).join(":").trim();
|
||||
// Bare final totals can contain an already truncated amount rather than an equation.
|
||||
// Rebuild only when the original component rows establish that subtotal relationship.
|
||||
const savedValue = clean(original).split(":").slice(1).join(":").trim();
|
||||
if (num(row.Level) === finalLevel && /^[-\d.,]+$/.test(savedValue)) {
|
||||
const before = [],
|
||||
after = [];
|
||||
for (let j = index - 1; j >= 0 && num(rows[j].Index) <= 0; j--) {
|
||||
if (num(rows[j].SCheck) !== sc) continue;
|
||||
if (autoTotal(rows[j])) break;
|
||||
const results = [...String(input[j].LineData || "").matchAll(/\x12([-\d.,]+)\x12/g)];
|
||||
if (!results.length && input[j].TC == null) continue;
|
||||
before.push(results.length ? numeric(results.at(-1)[1]) : num(input[j].TC));
|
||||
after.push(num(rows[j].TC));
|
||||
if (num(rows[j].LinkIndex) === 61) break;
|
||||
}
|
||||
if (before.length && floating(sumCurrency(before), 0, 2) === numeric(savedValue))
|
||||
value = String(sumCurrency(after));
|
||||
}
|
||||
const calculated = floating(
|
||||
EstExpression.evaluate(expression(value || "0"), (key) => variables[key]),
|
||||
num(row.Level) === finalLevel ? 0 : 1,
|
||||
);
|
||||
row.TC = calculated;
|
||||
if (value !== clean(text).split(":").slice(1).join(":").trim()) {
|
||||
const start = text.indexOf(":") + 1,
|
||||
end = text.lastIndexOf("\x1a");
|
||||
row.LineData = text.slice(0, start) + calculated + text.slice(end);
|
||||
}
|
||||
if (num(row.Level) === finalLevel) totals[component[sc]] = calculated;
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user