Merge remote-tracking branch 'origin/sub_laptop_5' into main_laptop_1

This commit is contained in:
2026-10-10 00:29:45 +09:00
7 changed files with 64 additions and 8 deletions
+3 -2
View File
@@ -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),
+2 -1
View File
@@ -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 || "",
+4 -1
View File
@@ -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
+2
View File
@@ -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
+1 -1
View File
@@ -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))
+2 -2
View File
@@ -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))
@@ -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