fix(M01): 몫이 모두 0 인 장비 줄이 읽는 식에서 사라지던 것 — 0 원 한 줄과 까닭으로 남김

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
2026-09-21 19:58:04 +09:00
co-authored by Claude Opus 5
parent 9a938c0ab0
commit ce0529ff2c
3 changed files with 27 additions and 5 deletions
@@ -47,6 +47,10 @@
- `GC001091`·`GC001096` 진공흡입 준설차 13톤 · 물탱크(살수차) 5,500ℓ · `GC001092` 트럭탑재형 크레인 5톤 · `GC001099` 진공흡입 준설차 13톤
- `GF000137` 트럭 10.5ton · `GF000241` 굴착기(무한궤도) 0.6㎥
### 고친 뒤 (2026-09-21)
`master_text.py` 에 몫 나누기를 `_shares()` 한 자리로 모아, 몫이 전부 0 이면 첫 비목에 0 원 한 줄을 남기고 까닭(「수량 0 — 이 조건에서는 안 쓰는 줄」)을 붙임 — 같은 대조를 다시 돌려 **③ 어긋남 83줄 → 0** · 나머지(①②④·터짐)도 그대로 0(1,291개 전부).
## 딸린 사실
- 비목이 나뉜 하위 로직 줄은 비목마다 한 줄씩 같은 이름으로 나옴(계약대로 · 어긋남 아님).
+12 -5
View File
@@ -86,10 +86,10 @@ def _env_of(master, row: dict, given: dict, depth: int = 0) -> dict:
def _why(item: dict, line: dict) -> str:
"""단가를 못 구했거나 0 인 줄의 까닭 한 마디."""
if line["단가"] not in (0, None):
return ""
if line["수량"] == 0:
return "수량 0 — 이 조건에서는 안 쓰는 줄"
if line["단가"] not in (0, None):
return ""
where = item.get("요소")
where = "재료 고르기 조건" if isinstance(where, dict) else f"{where}"
return f"단가 0 — {where} 값이 아직 없음"
@@ -101,6 +101,15 @@ def _label(item: dict, line: dict) -> str:
return f"{name}({spec})" if spec else name
def _shares(line: dict) -> list[tuple[str, Decimal]]:
"""글로 적을 (비목, 몫) — 비목이 나뉜 로직 줄은 몫이 없는 비목을 건너뜀.
몫이 전부 0 이면 호표 줄이 사라지지 않게 첫 비목에 0 원 한 줄을 남김."""
parts = list(line["비목"].items())
if len(parts) <= 1:
return parts
return [(part, money) for part, money in parts if money != 0] or parts[:1]
def _line_text(item: dict, line: dict, env: dict, master, part: str | None) -> str:
"""「단가 * 수량식 = 금액」 — 비목이 나뉜 로직 줄은 그 비목 몫만."""
qty = render(mf.parse(item["수량"]), env, master)
@@ -134,9 +143,7 @@ def lines(files: dict, key: str, given: dict) -> dict:
env[""] = {r["이름"]: r["금액"] for r in shown if r["이름"]}
items = row.get("호표") or []
for item, line in zip(items, shown):
for part, money in line["비목"].items():
if money == 0 and len(line["비목"]) > 1:
continue # 비목이 나뉜 로직 줄 — 몫이 없는 비목엔 안 적음
for part, money in _shares(line):
split = part if len(line["비목"]) > 1 else None
one = {
"이름": _label(item, line),
+11
View File
@@ -25,6 +25,7 @@ REAL = store.FOLDER
STACK = "GF000219" # 13-4-1 메쌓기(인력) — 증가율이 걸린 줄
FORM = "GF000160" # 12-4 합판거푸집 — 재료 여럿 · 수량 0 줄
PUMP = "GF000182" # 12-17-1 펌프카 타설 — 하위 로직이 세 비목에 걸리고 덧줄이 있음
IDLE = "GC000999" # 일반전정 — 세 비목에 걸린 장비 줄이 수량 0 이라 몫이 모두 0
@pytest.fixture
@@ -95,6 +96,16 @@ def test_펌프카는_하위로직이_비목마다_한_줄씩이고_덧줄이_
assert extra[""].endswith(f"= {mt.fmt(Decimal(str(extra['금액'])))}") and "%" in extra[""]
def test_몫이_모두_0인_장비_줄도_글에_남는다(client: TestClient) -> None:
got, calc = _both(client, IDLE)
idle = [one for one in _all_lines(got) if one["이름"] == "고소작업차"]
assert len(idle) == 1 # 비목 셋 몫이 다 0 이라도 줄이 사라지지 않음
assert Decimal(str(idle[0]["금액"])) == 0 and "수량 0" in idle[0]["까닭"]
assert idle[0][""].endswith(" * 0 = 0")
for group in got["groups"]:
assert Decimal(str(group["소계"])) == Decimal(str(calc["sums"][group["비목"]]))
def test_멈추는_로직은_까닭_한_줄(client: TestClient) -> None:
got = client.post("/api/m01/text", json={"key": STACK, "inputs": {}}).json()
assert got["ok"] is False and "입력" in got["reason"]