diff --git a/B09_Estimation/B09_Estimation_Calc_Base.py b/B09_Estimation/B09_Estimation_Calc_Base.py index 88218def0..775553d86 100644 --- a/B09_Estimation/B09_Estimation_Calc_Base.py +++ b/B09_Estimation/B09_Estimation_Calc_Base.py @@ -11,7 +11,8 @@ 자재단가대비표 = 출처 단가 중 최저(0 · 빈칸 뺌 · 견적 포함 = 엑셀 MIN) 자동 · 적용 차례 = 고른 출처(pick · 견적이 높아도) › 사용자 적용 단가 › 최저 · 견적 = 사용자 손입력(quote · quote_page · 견적서 파일은 `B09_Estimation_Router_Quotes`) - · 관급 · 별도계상 = 사용자 표시 → 재료비목록표 단가 = 적용 단가 + · 관급 · 별도계상 = 사용자 표시(없으면 입력 줄 기본 · B08 파형강관 = 관급) + → 재료비목록표 단가 = 적용 단가 재료비목록표 = 수량(B08 총괄자재수량집계표 합계 열) × 적용 단가 = 총액 ROUND(·, 0) (엑셀 재료비수량금액집계표 G 열 자리 · 수량 · 단가 하나라도 빈칸이면 빈칸) 중기사용료 = 손료(경비 · 기준가 천원 × 손료계수 ÷ 10,000) + 운전사(노무 · 인 × 시간 노임) @@ -157,7 +158,7 @@ def compare_sheet( applied, applied_from = low, low_from if applied is None: gaps.append(gap("compare", m["key"], m["name"], "적용 단가 없음(출처 없음)")) - supply = mine.get("supply") if mine.get("supply") in SUPPLY else None + supply = next((v for v in (mine.get("supply"), m.get("supply")) if v in SUPPLY), None) rows.append( { **_head(m, no), diff --git a/B09_Estimation/B09_Estimation_Calc_Base.ts b/B09_Estimation/B09_Estimation_Calc_Base.ts index 5d4789743..c5223530c 100644 --- a/B09_Estimation/B09_Estimation_Calc_Base.ts +++ b/B09_Estimation/B09_Estimation_Calc_Base.ts @@ -138,7 +138,8 @@ function compareSheet(materials: Row[], user: Row, gaps: Gap[]): Row[] { else if ("applied" in mine) [applied, appliedFrom] = [num(mine.applied), "사용자"]; else [applied, appliedFrom] = [low, lowFrom]; if (applied === null) gaps.push(gap("compare", m.key, m.name, "적용 단가 없음(출처 없음)")); - const supply = (SUPPLY as readonly string[]).includes(mine.supply) ? mine.supply : null; + const supply = + [mine.supply, m.supply].find((v) => (SUPPLY as readonly string[]).includes(v)) ?? null; return { ...head(m, i + 1), unit: m.unit || "", diff --git a/B09_Estimation/B09_Estimation_Prices.py b/B09_Estimation/B09_Estimation_Prices.py index 198287b39..87badd217 100644 --- a/B09_Estimation/B09_Estimation_Prices.py +++ b/B09_Estimation/B09_Estimation_Prices.py @@ -7,7 +7,8 @@ 연결된 사본이 부른 자원 키(`B09_Estimation_Linked` — 143-1 「연결된 사본」 원천 · 없으면 빈 표) + 기계가 부르는 운전사(조종원직종 → 인력 원문번호) · 연료(주연료종류 → 유가 「종류:전국평균」) + B08 총괄자재수량집계표 자재(M01 짝 없음 → 출처 빈칸 · 적용 단가는 사용자 입력) - · 수량 = 합계 열 · 채집석 공제 줄 거름 · 키가 MT 코드(맞춤 없는 사본 자재)면 사본 자원 줄에 수량만 얹음 + · 수량 = 합계 열 · 채집석 공제 줄 거름 · 키가 MT 코드(맞춤 없는 사본 자재)면 + 사본 자원 줄에 수량만 얹음 · 파형강관 = 관급 기본 운전사 시간 계수 = M01 CC000336(시간환산 · 상여계수 · 휴지계수 분자 · 분모 곱). 연료 지역 = 전국평균(M01 기계경비 로직 지역 첫 값) — 다른 지역 값은 자재 적용 단가를 고쳐 씀. """ @@ -227,6 +228,8 @@ def b08_material_rows(b08_sheets: dict[str, Any] | None) -> list[dict[str, Any]] "pages": {}, "source": "B08 총괄자재수량집계표", "qty": row.get("amount"), + # 파형강관 = 관급(B08 할증 0 과 같은 뜻) — 사용자가 관급 · 별도 칸으로 바꿈 + **({"supply": "관급"} if str(row["key"]).startswith("pipe_") else {}), } ) return out diff --git a/B09_Estimation/B09_Estimation_Router.py b/B09_Estimation/B09_Estimation_Router.py index 23da75085..ffa23ccb6 100644 --- a/B09_Estimation/B09_Estimation_Router.py +++ b/B09_Estimation/B09_Estimation_Router.py @@ -56,6 +56,8 @@ async def _b08(project_id: UUID, notes: list[str]) -> dict[str, Any]: out[key] = None # 구조물 집계표 = 자재 집계표가 이미 부른 값(열 · 계 · 조합) — 두 번 안 부름 out["summary"] = (out["materials"] or {}).get("source") + # 총괄 · 콘크리트 표 = B08 답의 `sheets` 한 층 아래(재료비목록표 · 운반 수량이 읽음 · 167-2) + out["materials"] = (out["materials"] or {}).get("sheets") return out diff --git a/resources/tester/test_144_b09_skeleton.py b/resources/tester/test_144_b09_skeleton.py index c34c59776..5c652cd11 100644 --- a/resources/tester/test_144_b09_skeleton.py +++ b/resources/tester/test_144_b09_skeleton.py @@ -98,7 +98,7 @@ def test_단가_굳히기_운전사_연료_따라옴(): def test_연결_없으면_빈_표와_연결_없음(tmp_path, monkeypatch): async def fake_materials(_project_id): - return B08_SHEETS + return {"source": None, "sheets": B08_SHEETS} # B08 길 답 꼴(167-2) monkeypatch.setattr(router_mod, "material_sheets", fake_materials) body = asyncio.run(router_mod.payload("p", tmp_path)) diff --git a/resources/tester/test_145_5_b09_summary.py b/resources/tester/test_145_5_b09_summary.py index 2a6375f82..bea11b629 100644 --- a/resources/tester/test_145_5_b09_summary.py +++ b/resources/tester/test_145_5_b09_summary.py @@ -89,8 +89,8 @@ def test_호표_구성_없으면_자재_줄만(): def test_GET_응답에_집계표(tmp_path, monkeypatch): async def fake_materials(_project_id): - return {"material": {"rows": [{"key": "sand", "group": "골재", "spec": "모래", - "unit": "㎥", "amount": 16}]}} # fmt: skip + return {"sheets": {"material": {"rows": [{"key": "sand", "group": "골재", "spec": "모래", + "unit": "㎥", "amount": 16}]}}} # fmt: skip monkeypatch.setattr(router_mod, "material_sheets", fake_materials) body = asyncio.run(router_mod.payload("p", tmp_path)) diff --git a/resources/tester/test_166_3_b09_material_qty.py b/resources/tester/test_166_3_b09_material_qty.py index 9c674c0b7..13ea9c4a9 100644 --- a/resources/tester/test_166_3_b09_material_qty.py +++ b/resources/tester/test_166_3_b09_material_qty.py @@ -1,4 +1,4 @@ -"""166-3 재료비목록표 = B08 총괄 합계 수량 × 적용 단가 = 총액 ROUND 0 · 공제 줄 거름 · MT 합침 · 거울.""" +"""166-3 재료비목록표 = B08 총괄 합계 × 적용 단가 = 총액 ROUND 0 · 공제 거름 · MT 합침 · 거울.""" import sys from pathlib import Path @@ -65,3 +65,52 @@ def test_공제_거름_MT_합침(monkeypatch): assert [r["key"] for r in got] == ["MT1", "MO1", "b08:cement", "b08:MT9"] assert got[0]["qty"] == 30.1 and "qty" not in book["rows"]["MT1"] # 책은 안 바꿈 assert got[2]["qty"] == 12.5 + + +def test_167_2_B08_답_한_층_아래_총괄_줄(monkeypatch): + """B08 길 답 = {source, sheets: {material: {rows}}} — sheets 를 읽어야 총괄 줄이 들어옴.""" + import asyncio + + from B09_Estimation import B09_Estimation_Router as router_mod + + rows = [ + {"key": "pipe_1200", "group": "파형강관", "spec": "Φ1200mm", "unit": "m", "amount": 7}, + { + "key": "coupling_1200", + "group": "커플링밴드", + "spec": "Φ1200mm", + "unit": "개", + "amount": 0, + }, + {"key": "합판(준내수)|㎡", "group": "합판(준내수)", "unit": "㎡", "amount": 7}, + {"key": "picked_deduct", "group": "채집석 공제", "amount": None}, + ] + + async def materials(_pid): + return {"source": {"columns": []}, "notes": [], "sheets": {"material": {"rows": rows}}} + + async def earthwork(_pid): + return None + + monkeypatch.setattr(router_mod, "material_sheets", materials) + monkeypatch.setattr(router_mod, "earthwork_table", earthwork) + b08 = asyncio.run(router_mod._b08(None, [])) + assert b08["summary"] == {"columns": []} + got = prices.b08_material_rows(b08["materials"]) + assert [(r["key"], r["qty"], r.get("supply")) for r in got] == [ + ("b08:pipe_1200", 7, "관급"), + ("b08:coupling_1200", 0, None), + ("b08:합판(준내수)|㎡", 7, None), + ] + s = compute_base({"labor": [], "machines": [], "materials": got}, USER_PIPE) + rows_out = {r["key"]: r for r in s["material"]} + assert rows_out["b08:pipe_1200"]["supply"] == "별도" # 사용자 표시가 앞섬 + plain = compute_base({"labor": [], "machines": [], "materials": got}, {}) + assert plain["material"][0]["supply"] == "관급" # 파형강관 기본 = 관급 + assert rows_out["b08:합판(준내수)|㎡"]["amount"] == 7 * 12000 + assert rows_out["b08:coupling_1200"]["price"] is None # 단가 없음 = 빈칸(견적 칸으로) + assert _run_ts({"labor": [], "machines": [], "materials": got}, USER_PIPE) == s + + +USER_PIPE = {"materials": {"b08:합판(준내수)|㎡": {"quote": 12000}, + "b08:pipe_1200": {"supply": "별도"}}} # fmt: skip