fix(842-1): B09 고정 줄 이은 자재 별도 · 관급 = 자재대에 한 번 — 거창 안내판 줄 사라진 50,000 되살림

- 고정 줄 이은 자재가 별도 · 관급이면 대가 안 0 원 + 그 몫 `aside` → 집계 `aside` 줄(자재대 수량)
  (`Calc_Labor` py · ts 짝 `_fixed_apart` · `Summary._fixed_lines`) — 기본 표시는 M02 자재표시 그대로
- 거창 직접비 합 = 엑셀(384,916,227) · 견본 넷 사라진 재료 0 · 겹쳐 셈 0
- 지킴 시험 `test_842_1_b09_fixed_aside`(재료 · 고정 · 관급 · 기계 겸 · 거울) + `test_707_7` 전수 시험
- `test_707_7` 낡은 기대(거창 gap 50000 · 다시 부르기 384866227) 고침

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-15 18:16:21 +09:00
co-authored by Claude Sonnet 5.5
parent 1402d7eb4d
commit 5b65e9470d
5 changed files with 132 additions and 10 deletions
+11 -4
View File
@@ -139,14 +139,18 @@ def _none() -> dict[str, float | None]:
def _fixed(line: dict[str, Any], books: dict[str, Any]) -> dict[str, float | None]:
"""고정 줄 단가(705-1) — 줄 단가 그대로 · 이은 자재가 관급 · 별도계상이면 재료 0(841-1 단가 책
값으로 다시 안 덮음)."""
own, link = line.get("own") or {}, line.get("link") or {}
out = {part: num(own.get(part)) or 0.0 for part in PARTS}
row = books["material"].get(link.get("material")) if link.get("material") else None
if row and row.get("supply") in SUPPLY:
out = {part: num((line.get("own") or {}).get(part)) or 0.0 for part in PARTS}
if _fixed_apart(line, books):
out["material"] = 0.0
return out
def _fixed_apart(line: dict[str, Any], books: dict[str, Any]) -> bool:
"""고정 줄이 이은 자재가 관급 · 별도(842-1) — 대가 안 0 · 그 몫은 자재대(`aside`)로."""
key = (line.get("link") or {}).get("material")
return bool(key) and (books["material"].get(key) or {}).get("supply") in SUPPLY
def _base(spec: str, lines: list[dict[str, Any]], src: list) -> float | None:
"""% 줄 밑수 — `part:비목` 그 비목 줄 금액 합 · `lines:labor` 노무 줄 금액 합 ·
`line:이름` 그 줄 · `bundle:labor|material|machine` 그 종류 줄(+ 그 이음 고정 줄) 합 ·
@@ -212,6 +216,9 @@ def _block(
}
if line["kind"] == "material" and _apart(line, books): # 관급 · 별도계상 = 대가 안 0원
extra["aside"], prices["material"], amounts["material"] = amounts["material"], 0.0, 0.0
if line["kind"] == "fixed" and _fixed_apart(line, books): # 842-1 이은 자재 = 자재대 몫
mat = num((line.get("own") or {}).get("material")) or 0.0
extra["aside"] = None if qty is None else cut(mat * qty, line.get("place"))
ref = {"ref": {"key": line["key"]}} if line.get("key") else {} # 집계표(145-5) 이음
ask = {"why": line["ask"]} if qty is None and line.get("ask") else {} # 723-4
lines.append(
+12 -3
View File
@@ -121,14 +121,18 @@ function linePrices(line: Row, books: Row, sums: Record<string, Row>): Parts {
/** 고정 줄 단가(705-1) — 줄 단가 그대로 · 이은 자재가 관급 · 별도계상이면 재료 0(짝 `_fixed`) */
function fixedPrices(line: Row, books: Row): Parts {
const own: Row = line.own || {};
const link: Row = line.link || {};
const out = zeros();
for (const part of PARTS) out[part] = num(own[part]) ?? 0;
const row: Row | undefined = link.material ? books.material[link.material] : undefined;
if (row && (SUPPLY as readonly string[]).includes(row.supply)) out.material = 0;
if (fixedApart(line, books)) out.material = 0;
return out;
}
/** 고정 줄이 이은 자재가 관급 · 별도(842-1) — 대가 안 0 · 그 몫은 자재대(`aside`)로(짝 `_fixed_apart`) */
function fixedApart(line: Row, books: Row): boolean {
const key: string | undefined = (line.link || {}).material;
return !!key && (SUPPLY as readonly string[]).includes((books.material[key] || {}).supply);
}
/** % 줄 밑수 — part:비목 그 비목 줄 금액 합 · lines:labor 노무 줄 금액 합 · line:이름 그 줄 ·
* bundle:labor|material|machine 그 종류 줄(+ 그 이음 고정 줄) 합 · rows:0,2 블록 그 차례 줄(src cid) 합
* (778-4 · 짝 `.py` `_base`) */
@@ -204,6 +208,11 @@ function block(b: Row, books: Row, sums: Record<string, Row>, gaps: Gap[], own:
prices.material = 0;
amounts.material = 0;
}
if (line.kind === "fixed" && fixedApart(line, books)) {
// 842-1 이은 자재 = 자재대 몫(짝 `_block`)
extra.aside =
qty === null ? null : cut((num((line.own || {}).material) ?? 0) * qty, line.place);
}
const ref = line.key ? { ref: { key: line.key } } : {}; // 집계표(145-5) 이음
const ask: Row = qty === null && line.ask ? { why: line.ask } : {}; // 723-4
lines.push({ ...line, ...ref, ...ask, qty, prices, amounts, ...extra });
+2
View File
@@ -168,6 +168,8 @@ def _fixed_lines(block: dict[str, Any], line: dict[str, Any]) -> list[dict[str,
link = line.get("link") or {}
pairs = [("machine", link.get("machine"))] if link.get("machine") else []
pairs = pairs or [(k, link.get(k)) for k in ("labor", "material") if link.get(k)]
if "aside" in line and link.get("material"): # 842-1 이은 자재 별도 · 관급 = 자재대 몫
pairs = [(k, v) for k, v in pairs if k != "material"] + [("aside", link["material"])]
if not pairs:
pairs = [("fixed", f"{block['key']}|{line.get('cid') or line.get('name')}")]
return [{"kind": k, "key": str(v), "qty": line["qty"]} for k, v in pairs]
+25 -3
View File
@@ -35,8 +35,8 @@ FILES = {
#: 영월 = 내역 줄이 코드로만 가리킴(번 채움) · STmate 하위 호표 반올림 끝수 28 줄
#: (줄당 1 원/단위 · 브레인 정함 = 그래도 이음)
WANT = {
# 841-1 이은 안내판 줄 = M02 사본 셈(엑셀보다 50,000 적음 · 노임 · 기계 덮개 걷음)
"거창": (14, 68, 56, 12, 11, 8, 8, 384916227, 0, 50000),
# 842-1 이은 안내판 줄 표지판지주 = 별도 → 대가 안 0 · 자재대 50,000 (직접비 합 = 엑셀)
"거창": (14, 68, 56, 12, 11, 8, 8, 384916227, 0, 0),
"대흥": (34, 171, 143, 28, 0, 26, 45, 710359136, 0, 0),
"소광": (25, 117, 98, 19, 0, 0, 0, 173311386, 0, 0),
"영월": (23, 138, 135, 3, 0, 21, 34, 767148700, 28, 2951),
@@ -117,6 +117,28 @@ def test_실무_엑셀_통째_빠짐_0_합계_같음_층_쓰기_0(world, name):
assert not [p for p in world["storage"].rglob("*") if p.is_file()] # 층 쓰기 0
@pytest.mark.parametrize("name", ["거창", "대흥", "소광", "영월"])
def test_대가_안_0_원_재료_자재대에_한_번(world, name):
"""842-1 전수 — 별도 · 관급 이은 자재(재료 줄 · 고정 줄)마다 집계 `aside` 줄 · 4 자재대 · 관급
자재대 수량 = 그 모음(사라짐 0) · 재료 줄 아닌 같은 자재 이중 집계 0."""
from B09_Estimation.B09_Estimation_Summary import block_lines
got = world["run"](name)["payload"]
sheets, aside = got["sheets"], got["inputs"].get("aside_qty") or {}
apart = {r["key"] for r in sheets["compare"] if r["supply"] in ("별도", "관급")}
items = {r["ref"]["key"]: r["qty"] for r in sheets["bill"]["rows"]
if r.get("kind") == "item" and str(r["key"]).startswith("mat:")} # fmt: skip
for tab in ("sg", "daega_blocks"):
for block in sheets[tab]:
lines = block_lines([block])[str(block["key"])]["lines"]
for ln in block["lines"]:
key = (ln.get("ref") or {}).get("key") or (ln.get("link") or {}).get("material")
if key in apart and ln["kind"] in ("material", "fixed") and ln.get("qty"):
assert any(x["kind"] == "aside" and x["key"] == key for x in lines), ln["name"]
assert all(x["kind"] != "material" or x["key"] != key for x in lines)
assert items == {k: v for k, v in aside.items() if k in apart}
def test_대흥_총공사비_엑셀과_같음_하위는_참조사본(world):
got = world["run"]("대흥")
assert got["합계"]["우리총"] == got["합계"]["엑셀총"] == 1201879000
@@ -145,7 +167,7 @@ def test_다시_부르기_같은_사본_두_번_셈_0(world):
first = world["run"]("거창")
again = world["run"]("거창", fresh=False)
assert _libs(again["root"]) == _libs(first["root"])
assert again["합계"]["우리직접"] == first["합계"]["우리직접"] == 384866227
assert again["합계"]["우리직접"] == first["합계"]["우리직접"] == 384916227
rows = again["user"]["bill"]["rows"]
assert len({r["key"] for r in rows}) == len(rows)
@@ -0,0 +1,82 @@
"""842-1 B09 — 대가 안 0 원이 된 재료는 자재대(`aside`)에 꼭 한 번 · 겹쳐 세지 않음.
고정 줄이 이은 자재(`link.material`)가 별도 · 관급이면 대가 안 0 원 + 그 몫 = 줄 `aside` →
집계표 `aside` 줄(자재대 수량) · 재료 줄 · 붙인 줄 · 관급도 같은 길 · 직접계산 = `aside` 없음.
거울: `B09_Estimation_Calc_Labor.py` ↔ `.ts` (`fixedApart`).
"""
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor # noqa: E402
from B09_Estimation.B09_Estimation_Summary import block_lines, totals # noqa: E402
from resources.tester.test_145_2_b09_labor_calc import _b, _l, _same, _ts # noqa: E402
SHEETS = {
"labor": [],
"material": [
{"key": "MA", "price": 50000, "supply": "별도"},
{"key": "MB", "price": 800, "supply": "관급"},
{"key": "MC", "price": 300, "supply": None},
],
"machine": [],
"gaps": [],
}
def _fix(name, qty, mat, price, **link):
own = {"labor": 0.0, "material": price, "expense": 0.0}
return {
"kind": "fixed",
"name": name,
"qty": qty,
"own": own,
"link": {"material": mat, **link},
}
DAEGA = {
"blocks": [
_b(
"DG1",
"품셈",
"안내판",
[
_fix("지주", 1, "MA", 50000), # 별도 → 대가 안 0 · 자재대 50000
_fix("관급 줄", 2, "MB", 800), # 관급 → 자재대(관급)
_fix("직접 줄", 3, "MC", 300), # 직접계산 → 대가 안
_fix(
"기계 겸", 1, "MA", 50000, machine="X1"
), # 기계 이음이 먼저여도 자재대 몫은 감
{**_l("material", "MA", 4), "own": {"material": 50000}}, # 재료 줄도 같은 자재
],
),
],
"owners": {"x": "DG1"},
}
def test_고정_줄_자재_대가_안_0_자재대에_한_번():
got = compute_labor(DAEGA, SHEETS)["daega_blocks"][0]
by = {ln["name"]: ln for ln in got["lines"]}
assert by["지주"]["amounts"]["material"] == 0 and by["지주"]["aside"] == 50000
assert by["관급 줄"]["amounts"]["material"] == 0 and by["관급 줄"]["aside"] == 1600
assert by["직접 줄"]["amounts"]["material"] == 900 and "aside" not in by["직접 줄"]
assert got["material"] == 900 # 대가 안 = 직접계산 줄만
lines = block_lines([got])[got["key"]]["lines"]
assert [x for x in lines if x["kind"] == "material"] == [ # 직접 줄만 일반 재료 줄
{"kind": "material", "key": "MC", "qty": 3}
]
aside = totals({"bill": {"rows": [{"kind": "item", "qty": 1, "ref": {"key": got["key"]}}]}},
{got["key"]: {"lines": lines}})[0]["aside"] # fmt: skip
assert aside == {"MA": 1 + 1 + 4, "MB": 2} # 재료 줄 4 + 고정 줄 둘 = 한 번씩 모음
def test_거울_파이썬_TS():
py = json.loads(json.dumps(compute_labor(DAEGA, SHEETS), ensure_ascii=False))
assert _same(py, _ts(DAEGA, SHEETS, {})["out"])