fix(b09): 재료비목록표 B08 총괄 줄 안 들어옴 — B08 답 sheets 한 층 아래 읽기 · 파형강관 관급 기본(167-2)
B08 자재 길 답 = {source, sheets: {material}} 인데 B09 가 한 층 위를 읽어 총괄 줄 · 운반 수량이 늘 빈칸 · 시험 가짜 답도 같은 꼴로 바로잡음
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sDZSjFvoiDjdGpgj3muc3
This commit is contained in:
@@ -11,7 +11,8 @@
|
|||||||
자재단가대비표 = 출처 단가 중 최저(0 · 빈칸 뺌 · 견적 포함 = 엑셀 MIN) 자동 ·
|
자재단가대비표 = 출처 단가 중 최저(0 · 빈칸 뺌 · 견적 포함 = 엑셀 MIN) 자동 ·
|
||||||
적용 차례 = 고른 출처(pick · 견적이 높아도) › 사용자 적용 단가 › 최저
|
적용 차례 = 고른 출처(pick · 견적이 높아도) › 사용자 적용 단가 › 최저
|
||||||
· 견적 = 사용자 손입력(quote · quote_page · 견적서 파일은 `B09_Estimation_Router_Quotes`)
|
· 견적 = 사용자 손입력(quote · quote_page · 견적서 파일은 `B09_Estimation_Router_Quotes`)
|
||||||
· 관급 · 별도계상 = 사용자 표시 → 재료비목록표 단가 = 적용 단가
|
· 관급 · 별도계상 = 사용자 표시(없으면 입력 줄 기본 · B08 파형강관 = 관급)
|
||||||
|
→ 재료비목록표 단가 = 적용 단가
|
||||||
재료비목록표 = 수량(B08 총괄자재수량집계표 합계 열) × 적용 단가 = 총액 ROUND(·, 0)
|
재료비목록표 = 수량(B08 총괄자재수량집계표 합계 열) × 적용 단가 = 총액 ROUND(·, 0)
|
||||||
(엑셀 재료비수량금액집계표 G 열 자리 · 수량 · 단가 하나라도 빈칸이면 빈칸)
|
(엑셀 재료비수량금액집계표 G 열 자리 · 수량 · 단가 하나라도 빈칸이면 빈칸)
|
||||||
중기사용료 = 손료(경비 · 기준가 천원 × 손료계수 ÷ 10,000) + 운전사(노무 · 인 × 시간 노임)
|
중기사용료 = 손료(경비 · 기준가 천원 × 손료계수 ÷ 10,000) + 운전사(노무 · 인 × 시간 노임)
|
||||||
@@ -157,7 +158,7 @@ def compare_sheet(
|
|||||||
applied, applied_from = low, low_from
|
applied, applied_from = low, low_from
|
||||||
if applied is None:
|
if applied is None:
|
||||||
gaps.append(gap("compare", m["key"], m["name"], "적용 단가 없음(출처 없음)"))
|
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(
|
rows.append(
|
||||||
{
|
{
|
||||||
**_head(m, no),
|
**_head(m, no),
|
||||||
|
|||||||
@@ -138,7 +138,8 @@ function compareSheet(materials: Row[], user: Row, gaps: Gap[]): Row[] {
|
|||||||
else if ("applied" in mine) [applied, appliedFrom] = [num(mine.applied), "사용자"];
|
else if ("applied" in mine) [applied, appliedFrom] = [num(mine.applied), "사용자"];
|
||||||
else [applied, appliedFrom] = [low, lowFrom];
|
else [applied, appliedFrom] = [low, lowFrom];
|
||||||
if (applied === null) gaps.push(gap("compare", m.key, m.name, "적용 단가 없음(출처 없음)"));
|
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 {
|
return {
|
||||||
...head(m, i + 1),
|
...head(m, i + 1),
|
||||||
unit: m.unit || "",
|
unit: m.unit || "",
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
연결된 사본이 부른 자원 키(`B09_Estimation_Linked` — 143-1 「연결된 사본」 원천 · 없으면 빈 표)
|
연결된 사본이 부른 자원 키(`B09_Estimation_Linked` — 143-1 「연결된 사본」 원천 · 없으면 빈 표)
|
||||||
+ 기계가 부르는 운전사(조종원직종 → 인력 원문번호) · 연료(주연료종류 → 유가 「종류:전국평균」)
|
+ 기계가 부르는 운전사(조종원직종 → 인력 원문번호) · 연료(주연료종류 → 유가 「종류:전국평균」)
|
||||||
+ B08 총괄자재수량집계표 자재(M01 짝 없음 → 출처 빈칸 · 적용 단가는 사용자 입력)
|
+ B08 총괄자재수량집계표 자재(M01 짝 없음 → 출처 빈칸 · 적용 단가는 사용자 입력)
|
||||||
· 수량 = 합계 열 · 채집석 공제 줄 거름 · 키가 MT 코드(맞춤 없는 사본 자재)면 사본 자원 줄에 수량만 얹음
|
· 수량 = 합계 열 · 채집석 공제 줄 거름 · 키가 MT 코드(맞춤 없는 사본 자재)면
|
||||||
|
사본 자원 줄에 수량만 얹음 · 파형강관 = 관급 기본
|
||||||
운전사 시간 계수 = M01 CC000336(시간환산 · 상여계수 · 휴지계수 분자 · 분모 곱).
|
운전사 시간 계수 = M01 CC000336(시간환산 · 상여계수 · 휴지계수 분자 · 분모 곱).
|
||||||
연료 지역 = 전국평균(M01 기계경비 로직 지역 첫 값) — 다른 지역 값은 자재 적용 단가를 고쳐 씀.
|
연료 지역 = 전국평균(M01 기계경비 로직 지역 첫 값) — 다른 지역 값은 자재 적용 단가를 고쳐 씀.
|
||||||
"""
|
"""
|
||||||
@@ -227,6 +228,8 @@ def b08_material_rows(b08_sheets: dict[str, Any] | None) -> list[dict[str, Any]]
|
|||||||
"pages": {},
|
"pages": {},
|
||||||
"source": "B08 총괄자재수량집계표",
|
"source": "B08 총괄자재수량집계표",
|
||||||
"qty": row.get("amount"),
|
"qty": row.get("amount"),
|
||||||
|
# 파형강관 = 관급(B08 할증 0 과 같은 뜻) — 사용자가 관급 · 별도 칸으로 바꿈
|
||||||
|
**({"supply": "관급"} if str(row["key"]).startswith("pipe_") else {}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ async def _b08(project_id: UUID, notes: list[str]) -> dict[str, Any]:
|
|||||||
out[key] = None
|
out[key] = None
|
||||||
# 구조물 집계표 = 자재 집계표가 이미 부른 값(열 · 계 · 조합) — 두 번 안 부름
|
# 구조물 집계표 = 자재 집계표가 이미 부른 값(열 · 계 · 조합) — 두 번 안 부름
|
||||||
out["summary"] = (out["materials"] or {}).get("source")
|
out["summary"] = (out["materials"] or {}).get("source")
|
||||||
|
# 총괄 · 콘크리트 표 = B08 답의 `sheets` 한 층 아래(재료비목록표 · 운반 수량이 읽음 · 167-2)
|
||||||
|
out["materials"] = (out["materials"] or {}).get("sheets")
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ def test_단가_굳히기_운전사_연료_따라옴():
|
|||||||
|
|
||||||
def test_연결_없으면_빈_표와_연결_없음(tmp_path, monkeypatch):
|
def test_연결_없으면_빈_표와_연결_없음(tmp_path, monkeypatch):
|
||||||
async def fake_materials(_project_id):
|
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)
|
monkeypatch.setattr(router_mod, "material_sheets", fake_materials)
|
||||||
body = asyncio.run(router_mod.payload("p", tmp_path))
|
body = asyncio.run(router_mod.payload("p", tmp_path))
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ def test_호표_구성_없으면_자재_줄만():
|
|||||||
|
|
||||||
def test_GET_응답에_집계표(tmp_path, monkeypatch):
|
def test_GET_응답에_집계표(tmp_path, monkeypatch):
|
||||||
async def fake_materials(_project_id):
|
async def fake_materials(_project_id):
|
||||||
return {"material": {"rows": [{"key": "sand", "group": "골재", "spec": "모래",
|
return {"sheets": {"material": {"rows": [{"key": "sand", "group": "골재", "spec": "모래",
|
||||||
"unit": "㎥", "amount": 16}]}} # fmt: skip
|
"unit": "㎥", "amount": 16}]}}} # fmt: skip
|
||||||
|
|
||||||
monkeypatch.setattr(router_mod, "material_sheets", fake_materials)
|
monkeypatch.setattr(router_mod, "material_sheets", fake_materials)
|
||||||
body = asyncio.run(router_mod.payload("p", tmp_path))
|
body = asyncio.run(router_mod.payload("p", tmp_path))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""166-3 재료비목록표 = B08 총괄 합계 수량 × 적용 단가 = 총액 ROUND 0 · 공제 줄 거름 · MT 합침 · 거울."""
|
"""166-3 재료비목록표 = B08 총괄 합계 × 적용 단가 = 총액 ROUND 0 · 공제 거름 · MT 합침 · 거울."""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
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 [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[0]["qty"] == 30.1 and "qty" not in book["rows"]["MT1"] # 책은 안 바꿈
|
||||||
assert got[2]["qty"] == 12.5
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user