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

# Conflicts:
#	M01_MasterData/M01_MasterData_Router.py
This commit is contained in:
2026-11-01 16:05:01 +09:00
17 changed files with 582 additions and 54 deletions
+64
View File
@@ -0,0 +1,64 @@
"""712-1 지킴 — 인력 · 재료 · 경비 묶음 셈 때 파생 · 기계경비 판별 한 곳(`master_bundle`)."""
from __future__ import annotations
import sys
from decimal import Decimal
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "resources/master_data/scripts"))
sys.path.insert(0, str(ROOT))
import check_master as cm # noqa: E402
import master_auto as mau # noqa: E402
import master_bundle as mb # noqa: E402
import master_formula as mf # noqa: E402
FILES = cm.load()
MASTER = mf.Master(FILES)
LOGICS = {r["키"]: r for d in FILES.values() if d.get("그룹") == "로직" for r in d.get("줄") or []}
def test_every_line_bundled():
"""호표 · 덧줄 전수 — 하위(못 가름) ≤ 31 · 나머지는 인력 · 재료 · 경비 · 기계경비 줄 = 경비."""
count = dict.fromkeys(mb.BUNDLES, 0)
machine = 0
for row in LOGICS.values():
for item in [*row.get("호표", []), *row.get("덧줄", [])]:
name, is_mach = mb.bundle(MASTER, item)
count[name] += 1
machine += is_mach
if is_mach:
assert name == "경비"
assert sum(count.values()) > 5_000
assert count["하위"] <= 31, count
assert machine >= 1_000, machine
def test_machine_rule_one_place():
"""기계경비 = 결과단위 원/시간 돈 로직 — 작업량(㎥/시간) · 이름 「시간당」 은 아님."""
machines = {k for k, r in LOGICS.items() if mb.is_machine(r)}
assert {"GC000267", "GC000268", "GF000145"} <= machines
assert not machines & {"GF000102", "GC000422", "GC000260"} # 작업량 · ㎡/시간 · 원/㎡
sys.path.insert(0, str(ROOT))
from M02_MasterTemplete import M02_Hopyo_Ours as ours
assert all(ours.is_mach_logic(r) == mb.is_machine(r) for r in LOGICS.values())
def test_run_lines_tagged():
"""엔진 `run` 줄마다 `묶음` · 기계경비 줄은 `장비` + 비목 노 · 재 · 경 세 몫."""
seen = set()
for key in ("GF000169", "GF000103"):
row = LOGICS[key]
given = {k: v for k, v in mau.values(MASTER, row).items() if v is not None}
got = mf.run(MASTER, key, given)
for line in got["줄"]:
assert line["묶음"] in mb.BUNDLES
seen.add(line["묶음"])
if line.get("장비"):
assert set(line["비목"]) == set(mf.COST_ITEMS)
total = sum((line["비목"].get(k) or Decimal(0)) for line in got["줄"] for k in line["비목"])
assert abs(total - got["계"]) < 1
assert {"인력", "재료", "하위"} <= seen
+59
View File
@@ -0,0 +1,59 @@
"""712-2 지킴 — 단위 한 벌(`master_unit`) · 다섯 자리가 같은 모듈을 씀 · 알약 배 전수."""
from __future__ import annotations
import sys
from decimal import Decimal
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "resources/master_data/scripts"))
sys.path.insert(0, str(ROOT))
import check_master as cm # noqa: E402
import master_basis as mbs # noqa: E402
import master_formula as mf # noqa: E402
import master_material as mm # noqa: E402
import master_pick as mp # noqa: E402
import master_unit as mu # noqa: E402
def test_alias_and_count_group():
assert mu.plain("EA") == mu.plain("ea") == "개"
assert mu.plain("Ton") == mu.plain("t") == mu.plain("톤") == "톤"
assert mu.plain("㎥") == "m3" and mu.plain("리터") == mu.plain("ℓ") == "ℓ"
assert mu.same_kind("본", "개") and mu.same_kind("매", "EA")
assert not mu.same_kind("조", "개") # 한 조 = 여럿 — 묶음 밖
assert mu.note("본", "개") == "본 = 개(개수 1:1)"
assert mm.plain_unit("EA") == mu.plain("EA")
assert mp.convert is mu.convert and mp.UNITS is mu.UNITS
def test_same_kind_convert():
assert mu.convert(Decimal(1000), "톤", "kg") == Decimal(1) # 톤 당 1000 원 = kg 당 1 원
assert mu.convert(Decimal(5), "kg", "톤") == Decimal(5000)
assert mu.convert(Decimal(7), "m", "km") == Decimal(7000)
assert mu.factor("kg", "ton") == Decimal("0.001") # B08 「/1000」(kg → ton) 손 식과 같은 값
assert mu.note("kg", "톤") == "kg → 톤 ×1000"
assert mu.convert(Decimal(3), "kg", "ℓ") == Decimal(3) # 밀도 = 경고만 · 그대로
assert "성격 다름" in mu.note("ℓ", "㎾h")
def test_volume_weight_only_when_picked():
"""부피 ↔ 무게 = 단위중량 표 고른 줄이 있을 때만 · 기본 줄 없음."""
master = mf.Master(cm.load())
assert mu.factor("㎥", "톤") is None
assert "단위중량(CC000337)" in mu.note("㎥", "톤")
rho = mu.unit_weight(master, "모래", "건조")
assert rho is not None and rho > 1000
assert mu.factor("㎥", "톤", rho) == rho / 1000 # 1 ㎥ = ρ/1000 톤
assert mu.convert(Decimal(100), "㎥", "톤", rho) == Decimal(100) * 1000 / rho
assert mu.unit_weight(master, "없는 종별", "건조") is None
def test_result_per_one_parser():
assert mu.per("원/10㎡") == [(Decimal(10), "㎡")]
assert mu.per("원/1,000본") == [(Decimal(1000), "본")]
assert mu.per("원/㎡ · 원/㎥(단위 따름)") == [(Decimal(1), "㎡"), (Decimal(1), "㎥")]
assert mu.per("㎥/시간") == []
assert mbs.result_per("원/ton") == [(Decimal(1), "ton")]
+62
View File
@@ -0,0 +1,62 @@
"""714-1 지킴 — 계산서 줄 덧칸(쓰는 입력 · 묶음 · 기준단위 · 환산 근거) · M01 `/text` 와
M02 `/logic-lib/sheet` 가 같은 줄 꼴(`master_sheet_line`)."""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from M01_MasterData import M01_MasterData_Store as store # noqa: E402
from M01_MasterData import M01_MasterData_Store_Sheet as sheet # noqa: E402
from M02_MasterTemplete import M02_Logic_Standard as standard # noqa: E402
msl = sheet.msl
def _text(key: str) -> dict:
auto = {k: v for k, v in store.auto(key)["값"].items() if v is not None}
return sheet.text(key, auto)
def _lines(got: dict, groups: str = "groups") -> list[dict]:
return [line for g in got[groups] for line in g["줄"]]
def test_line_keys_and_bundle():
got = _text("GF000103") # 구조물터파기 — 인력 + 굴착기 기계경비 줄
assert got["ok"] and got["기준단위"] == "㎥"
lines = _lines(got)
assert all({"쓰는입력", "묶음", "기준단위", "환산근거"} <= set(line) for line in lines)
machine = [line for line in lines if line.get("장비")]
assert machine and all(line["묶음"] == "경비" for line in machine)
assert {"지역", "보정작업"} <= set(machine[0]["쓰는입력"])
assert any(line["묶음"] == "인력" for line in lines)
def test_uses_follow_middle_values():
"""중간값을 거슬러 입력까지 — 돌쌓기 석공 품은 높이 · 쌓기 · 돌 입력을 씀."""
lines = _lines(_text("GF000219"))
assert {"높이", "쌓기", "돌"} <= set(lines[0]["쓰는입력"])
def test_basis_note_tenfold():
"""표 10㎡당 → 결과 원/㎡ 「÷10」 · 결과도 원/10㎡ 면 근거 없음."""
one = _lines(_text("GC000037"))
assert one[0]["기준단위"] == "10㎡" and one[0]["환산근거"] == "10㎡당 → 1㎡당 ÷10"
ten = _text("GF000124")
assert ten["기준단위"] == "10㎡" and all(not x["환산근거"] for x in _lines(ten))
def test_two_routes_same_shape():
"""M01 `/text` 와 M02 계산서 — 줄 차례 · 덧칸 값이 같음(금액 끝수만 다를 수 있음)."""
for key in ("GF000169", "GF000103", "GF000219"):
m01 = _lines(_text(key))
m02 = _lines(standard.sheet(key, {}), "묶음")
assert len(m01) == len(m02)
for a, b in zip(m01, m02):
assert set(b) == {"이름", "글", "금액", *msl.KEYS}
assert a["이름"] == b["이름"]
assert [a.get(k) for k in msl.KEYS] == [b.get(k) for k in msl.KEYS]