diff --git a/B09_Estimation/B09_Estimation_Calc_Labor.py b/B09_Estimation/B09_Estimation_Calc_Labor.py index d666d4b4d..126549c6c 100644 --- a/B09_Estimation/B09_Estimation_Calc_Labor.py +++ b/B09_Estimation/B09_Estimation_Calc_Labor.py @@ -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( diff --git a/B09_Estimation/B09_Estimation_Calc_Labor.ts b/B09_Estimation/B09_Estimation_Calc_Labor.ts index ba238cbbc..d58bb4be4 100644 --- a/B09_Estimation/B09_Estimation_Calc_Labor.ts +++ b/B09_Estimation/B09_Estimation_Calc_Labor.ts @@ -121,14 +121,18 @@ function linePrices(line: Row, books: Row, sums: Record): 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, 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 }); diff --git a/B09_Estimation/B09_Estimation_Summary.py b/B09_Estimation/B09_Estimation_Summary.py index a03cc0307..f9dd0978d 100644 --- a/B09_Estimation/B09_Estimation_Summary.py +++ b/B09_Estimation/B09_Estimation_Summary.py @@ -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] diff --git a/resources/tester/test_707_7_whole_import.py b/resources/tester/test_707_7_whole_import.py index fe19a7352..f053ed7d1 100644 --- a/resources/tester/test_707_7_whole_import.py +++ b/resources/tester/test_707_7_whole_import.py @@ -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) diff --git a/resources/tester/test_842_1_b09_fixed_aside.py b/resources/tester/test_842_1_b09_fixed_aside.py new file mode 100644 index 000000000..c0eb49f8a --- /dev/null +++ b/resources/tester/test_842_1_b09_fixed_aside.py @@ -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"])