코드 - 고정 줄 단가 = M02_Labor_Fixed.line(사본 묶음 · 이은 노임 · 자재 = 묶음 값) — B09 단가 책으로 다시 안 덮음 - 대가 · 산근 고친 칸 = 품만 실제로(단가 칸 덮기 줄 남던 흠 · 품 고친 뒤 줄 금액 다시 셈 흠 py · ts 짝) - 산근 글 = 엔진 단가 셈 그대로 · 별도계상 재료 줄 끝 「· 별도계상 = 0.0」 · 계 줄 = 블록 합 - 노무비 · 중기목록 이름에서 규격 겹침 뗌 · TS 바탕 셈 이름 PARTS → MACHINE_PARTS(짝 시험이 TS 를 이어 붙일 때 겹침) 시험(고친 까닭 = 단가 원천이 B09 책 → M02 엔진 · 노임 · 기계 덮개 · 단가 고친 칸 · 중기사용료 걷음) - 144 바탕 · 뼈대: 운전사 · 중기사용료 · 경비 · 덮개 시험 걷음 → 노무 · 중기 나열 · 사급 표시 · 자재만 책 - 145-1 · 145-2 · 155 · 157-1 · 166-2 · 166-6 · 719-3 · 719-4 · 723-4 · 778-4 · 792-3: 줄 단가 = own(엔진) · 산근 = Solver() · collect 새 부름 꼴 - 702 · 705-1 · 719-2: 단가 칸 덮개 = 안 읽음 · 고정 줄 = M02 셈(노임 새 판 따라가기 시험 걷음) - 203-1 · 296 · 703-3 · 795-4: 단가 책 = 자재 줄로 - 707 · 707-6 · 707-7: 단가표 불러오기 = 자재만 · 거창 이은 안내판 줄 = M02 사본 셈(엑셀 −50,000) - 201-1: 환율 · 중기사용료 표 걷음 · b09 저장 속도: 산근 앞 풀이 그대로(단가 표 같음 견줌 걷음) - 대상 113 파일 764 통과 · 25 실패 = 원래 실패(formatMoney · formatPrice 없음 · 단위 글 ㎜ · sec · 719-1 · 796-2e · 717-4 · 721 · 311 · 243 · 246) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
269 lines
13 KiB
Python
269 lines
13 KiB
Python
"""705-1 고정 줄 갈래 — 품 · 수량 · 단가 숫자를 그대로 적어 둔 일위대가 줄(요구서 1장 · 4장 1 · 2 · 3).
|
|
|
|
- M02 일위대가 줄 `{고정: {이름 · 규격 · 단위 · 단가 · 이음 · 출처}}` — 정리 · 셈 · 거울(`settle` 파이썬 ↔ TS)
|
|
- B09 대가 줄 `fixed` — 이음 있으면 이 공사 단가 책 값 · 없으면 줄 단가 · 덮개 · 거울(Calc_Labor 파이썬 ↔ TS)
|
|
- 노임 새 판 = 이은 비목만 따라감 · 품(수량) 그대로 · 원가계산서 빈 줄 0 · 집계표 이음 · 빠짐 수
|
|
견본 = `test_145_2_b09_labor_calc`(엑셀 바탕 표 · 사본 꼴) · `test_m02_labor_calc_mirror`(TS 헬퍼).
|
|
"""
|
|
|
|
import importlib.util
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from B09_Estimation import B09_Estimation_Prices as prices # noqa: E402
|
|
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor, daega_price # noqa: E402
|
|
from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs # noqa: E402
|
|
from B09_Estimation.B09_Estimation_Linked import linked_resource_keys # noqa: E402
|
|
from B09_Estimation.B09_Estimation_Summary import compute_summary, hopyo_lines # noqa: E402
|
|
from M02_MasterTemplete import M02_Item_Prices as item_prices # noqa: E402
|
|
from M02_MasterTemplete import M02_Item_Snapshot as snapshots # noqa: E402
|
|
from M02_MasterTemplete import M02_Labor_Doc as labor # noqa: E402
|
|
from M02_MasterTemplete import M02_Labor_Fixed as fixed # noqa: E402
|
|
from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402
|
|
|
|
|
|
def _load(name):
|
|
spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(f"{name}.py"))
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
b145 = _load("test_145_2_b09_labor_calc")
|
|
m02 = _load("test_m02_labor_calc_mirror")
|
|
|
|
LB = "LB000033" # 메쌓기 인력 줄 직종(견본 사본 · M01 에 있음)
|
|
|
|
|
|
def _fx(name, price, link=None, unit="인"):
|
|
return {"이름": name, "규격": "", "단위": unit, "단가": price, "이음": link or {}}
|
|
|
|
|
|
# ── M02 일위대가 줄 ────────────────────────────────────────────────────────
|
|
def test_M02_정리_꼴과_막힘():
|
|
doc = labor.clean(
|
|
{
|
|
"담은로직": [
|
|
{"로직": m02.TEN, "수량": None},
|
|
{
|
|
"고정": {
|
|
**_fx("보통인부", {"노무비": "1,000"}, {"노임": LB}),
|
|
"출처": {"파일": "a.xlsx", "호표": 3, "몰래": 1},
|
|
},
|
|
"수량": {"손": 0.5},
|
|
},
|
|
]
|
|
}
|
|
)
|
|
line = doc["담은로직"][1]
|
|
assert line["고정"] == {
|
|
"이름": "보통인부", "규격": "", "단위": "인",
|
|
"단가": {"노무비": 1000, "재료비": 0, "경비": 0},
|
|
"이음": {"노임": LB, "자재": None, "기계": None},
|
|
"출처": {"파일": "a.xlsx", "호표": 3},
|
|
} # fmt: skip
|
|
assert line["수량"] == {"손": 0.5} and labor.line_id(line) == "고정|보통인부||인"
|
|
bad = [
|
|
[{"고정": _fx("", {})}], # 이름 없음
|
|
[{"고정": _fx("a", {"노무비": "글"})}], # 단가가 수 아님
|
|
[{"고정": _fx("a", {}, {"노임": "MT000008"})}], # 이음 머리 틀림
|
|
[{"고정": _fx("a", {}, {"자재": "MT999999"})}], # M01 에 없음
|
|
[{"고정": _fx("a", {})}, {"고정": _fx("a", {"재료비": 1})}], # 같은 줄 두 번
|
|
]
|
|
for lines in bad:
|
|
with pytest.raises(store.StoreError) as e:
|
|
labor.clean({"담은로직": lines})
|
|
assert e.value.status == 400
|
|
|
|
|
|
def _mixed_doc():
|
|
"""섞인 호표 — 로직 줄 셋 + 고정 줄(이은 노임 · 이은 자재 · 손 단가) + 자재."""
|
|
return labor.clean(
|
|
{
|
|
"열": "rv",
|
|
"담은로직": [
|
|
{"로직": m02.TEN, "수량": None},
|
|
{"고정": _fx("석공", {"노무비": 1}, {"노임": LB}), "수량": {"손": 0.3}},
|
|
{"로직": m02.THOUSAND, "수량": {"손": 4}},
|
|
{
|
|
"고정": _fx("자갈", {"재료비": 1}, {"자재": m02.MAT}, unit=""),
|
|
"수량": {"손": 2},
|
|
},
|
|
{"고정": _fx("잡재료", {"재료비": 1234.5}, unit="식"), "수량": None},
|
|
{"로직": m02.PLAIN, "수량": None},
|
|
],
|
|
"자재": [{"코드": m02.MAT, "수량": None}],
|
|
}
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"values",
|
|
[{}, {"사석 수상고르기|㎡": 2.5, f"자재|{m02.MAT}": "3"}, {"나무식재|본": "1,200"}],
|
|
)
|
|
def test_M02_섞인_호표_파이썬과_TS_가_같은_답(values):
|
|
doc = _mixed_doc()
|
|
answer = labor.calc(doc, {})
|
|
py = labor.settle(answer, doc, values)
|
|
m02._same(py, m02._ts(answer, doc, values))
|
|
assert py == labor.calc(doc, values)
|
|
stone, gravel, misc = (ln for ln in py["줄"] if "고정" in ln)
|
|
whole = labor.m01.loaded()[1]
|
|
assert stone["기준"]["노무비"] == whole.get(LB)["값"] and stone["이은"] == ["노무비"]
|
|
assert stone["반영"]["노무비"] == pytest.approx(stone["기준"]["노무비"] * 0.3)
|
|
assert gravel["이은"] == ["재료비"] and gravel["기준"]["재료비"] != 1
|
|
assert misc["기준"] == {"노무비": 0, "재료비": 1234.5, "경비": 0, "계": 1234.5}
|
|
assert (
|
|
misc["이은"] == [] and misc["수량"] is None and misc["반영"] is None
|
|
) # 손 단가 · 수량 없음
|
|
|
|
|
|
def test_M02_사본_셈_이음_단가_묶음(tmp_path):
|
|
"""사본 `단가.json` 에 이은 키가 실림(price_bundle) · 사본 셈 = 층 셈."""
|
|
from M02_MasterTemplete.M02_Item_Source import price_bundle
|
|
|
|
doc = _mixed_doc()
|
|
bundle = price_bundle({"로직": [], "줄": doc}, tmp_path)
|
|
got = item_prices.load_prices(bundle)
|
|
assert got.get(LB)["값"] == labor.m01.loaded()[1].get(LB)["값"]
|
|
snap = item_prices.calc({"줄": doc}, bundle, {}, {})
|
|
fixed_lines = [ln for ln in snap["줄"] if "고정" in ln]
|
|
assert [ln["이은"] for ln in fixed_lines] == [["노무비"], ["재료비"], []]
|
|
|
|
|
|
# ── B09 대가 셈(바탕 표 = 엑셀 견본) ──────────────────────────────────────────
|
|
def _fixed_blocks():
|
|
"""조합 블록 — 노무 줄 + 고정 줄 넷(이은 노임 · 이은 관급 자재 · 이은 중기 · 손 단가)."""
|
|
own = {"labor": 100.0, "material": 200.0, "expense": 300.0}
|
|
lines = [
|
|
{"kind": "labor", "key": "L00017", "name": "보통인부", "qty": 0.1, "own": {"labor": 165545}},
|
|
{"kind": "fixed", "name": "석공", "qty": 0.2, "own": own,
|
|
"link": {"labor": "L00005", "material": None, "machine": None}},
|
|
{"kind": "fixed", "name": "파형강관", "qty": 1, "own": own,
|
|
"link": {"labor": None, "material": "M00222", "machine": None}},
|
|
{"kind": "fixed", "name": "굴착기", "qty": 0.5, "own": own,
|
|
"link": {"labor": None, "material": None, "machine": "X00044"}},
|
|
{"kind": "fixed", "name": "잡재료", "qty": 2, "own": {"material": 1234.5},
|
|
"link": {"labor": "L99999", "material": None, "machine": None}}, # 책에 없음 = 손 단가
|
|
] # fmt: skip
|
|
for n, ln in enumerate(lines):
|
|
ln["cid"] = f"{ln['kind']}:{ln.get('key') or ln['name']}#0"
|
|
ln["n"] = n
|
|
block = {"id": "DGF", "cover": "S:f", "kind": "조합", "name": "섞인", "spec": "", "unit": "m",
|
|
"cite": "", "origin": {}, "lines": lines} # fmt: skip
|
|
return {"blocks": [block], "owners": {"pipe@100.0": "DGF"}}
|
|
|
|
|
|
def _line(out, name):
|
|
return next(ln for ln in out["daega_blocks"][0]["lines"] if ln["name"] == name)
|
|
|
|
|
|
def test_B09_고정_줄_단가_M02_셈_그대로():
|
|
"""841-1 고정 줄 단가 = M02 엔진 고정 줄 셈(`own` · 이은 노임 · 자재 = 사본 묶음 값) — B09 단가 책
|
|
값으로 다시 안 덮음 · 이은 자재가 관급 · 별도계상이면 대가 안 0원."""
|
|
out = compute_labor(_fixed_blocks(), b145.SHEETS, {})
|
|
assert _line(out, "석공")["prices"] == {"labor": 100, "material": 200, "expense": 300}
|
|
assert _line(out, "파형강관")["prices"]["material"] == 0 # 관급 = 대가 안 0원
|
|
assert _line(out, "굴착기")["prices"] == {"labor": 100, "material": 200, "expense": 300}
|
|
misc = _line(out, "잡재료")
|
|
assert misc["prices"] == {"labor": 0, "material": 1234.5, "expense": 0}
|
|
assert misc["amounts"]["material"] == 2469.0
|
|
block = out["daega_blocks"][0]
|
|
assert block["blank"] == 0 and out["gaps"] == [] # 빈칸 규칙에 안 걸림
|
|
assert daega_price(out, "pipe@100.0")["sum"] == block["sum"] is not None
|
|
|
|
|
|
def test_B09_덮개는_품만_고정_줄에_먹음():
|
|
cover = {"S:f": {"fixed:잡재료#0": {"qty": 3, "material": 1000}}} # 단가 칸 = 841-1 안 읽음
|
|
out = compute_labor(_fixed_blocks(), b145.SHEETS, {}, cover)
|
|
misc = _line(out, "잡재료")
|
|
assert misc["qty"] == 3 and misc["prices"]["material"] == 1234.5
|
|
assert misc["base"] == {"qty": 2, "labor": 0, "material": 1234.5, "expense": 0}
|
|
assert misc["covered"] == ["qty"] and out["daega_orphans"] == []
|
|
|
|
|
|
@pytest.mark.parametrize("cover", [None, {"S:f": {"fixed:석공#0": {"labor": 1}}}])
|
|
def test_B09_거울_파이썬_TS(cover):
|
|
daega = _fixed_blocks()
|
|
py = json.loads(json.dumps(compute_labor(daega, b145.SHEETS, {}, cover), ensure_ascii=False))
|
|
ts = b145._ts(daega, b145.SHEETS, {}, cover)
|
|
assert b145._same(py, ts["out"])
|
|
assert b145._same(daega_price(py, "pipe@100.0"), ts["price"])
|
|
|
|
|
|
# ── 사본 → 대가 블록 → 내역 · 원가 · 집계표 한 바퀴 ─────────────────────────────
|
|
def _snap(root, owner, key, lines):
|
|
folder = snapshots.structures_dir(root) / snapshots.folder_name(owner)
|
|
folder.mkdir(parents=True)
|
|
|
|
def write(name, data):
|
|
(folder / name).write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
|
|
|
doc = labor.clean({"단위": "m", "담은로직": lines})
|
|
codes = [one["로직"] for one in lines if "로직" in one]
|
|
write(snapshots.MANIFEST_NAME, {"원본": {**b145._item(key), "경로": ["기슭막이", key]}})
|
|
write("단가.json", item_prices.to_json(item_prices.collect(codes, fixed.link_keys(doc))))
|
|
write("일위대가.json", {"줄": doc})
|
|
write("입력값.json", b145._values(b145.STONE))
|
|
|
|
|
|
def test_한_바퀴_사본_고정_줄_원가_빈칸_0_집계표(tmp_path):
|
|
from B09_Estimation.B09_Estimation_Router import compute_sheets
|
|
|
|
root = b145._project(tmp_path / "loop", [(100, "SS000901")])
|
|
lines = [
|
|
{"로직": "GF000219", "승격": True, "수량": {"손": 1}}, # 품셈 줄(메쌓기)
|
|
{"고정": _fx("석공", {"노무비": 1}, {"노임": LB}), "수량": {"손": 0.5}}, # 이은 노임
|
|
{"고정": _fx("잡재료", {"재료비": 1000}, unit="식"), "수량": {"손": 1}}, # 손 단가
|
|
]
|
|
_snap(root, "pipe@100.0", "SS000901", lines)
|
|
keys = linked_resource_keys(root)
|
|
assert LB in keys # 단가 책 수요에 이음 키
|
|
inputs = prices.build_inputs(None, keys)
|
|
inputs["daega"] = daega_inputs(root)
|
|
inputs["hopyo"] = {}
|
|
inputs["bill"] = {
|
|
"items": [
|
|
{"key": "st:revet", "section": "2.1", "name": "돌기슭막이", "spec": "", "unit": "m",
|
|
"qty": 10, "ref": {"tab": "dg_list", "key": "revet"}, "note": "",
|
|
"owners": {"pipe@100.0": 10}},
|
|
]
|
|
} # fmt: skip
|
|
sheets = compute_sheets(root, inputs, {})
|
|
combo = next(b for b in sheets["daega_blocks"] if b["kind"] == "조합")
|
|
kinds = [ln["kind"] for ln in combo["lines"]]
|
|
assert kinds == ["daega", "fixed", "fixed"] and combo["blank"] == 0
|
|
stone = combo["lines"][1]
|
|
bundle = next(snapshots.structures_dir(root).rglob("단가.json")).read_text(encoding="utf-8")
|
|
whole = item_prices.load_prices(json.loads(bundle))
|
|
assert stone["prices"]["labor"] == float(whole.get(LB)["값"]) # M02 고정 줄 셈 = 묶음 노임
|
|
assert sheets["bill"]["totals"]["direct"]["blank"] == 0 # 원가계산서 빈 줄 0
|
|
assert sheets["bill"]["sums"]["material"] > 0
|
|
summary = compute_summary(sheets, hopyo_lines(sheets))
|
|
labor_qty = {r["key"]: r["qty"] for r in summary["sum_labor"]}
|
|
assert labor_qty[LB] >= 10 * 0.5 # 이은 고정 줄 = 노임 자원으로 셈
|
|
assert summary["fixed"] == 1 # 손 단가 줄 = 집계에서 빠짐 1
|
|
|
|
|
|
def test_사본_다시_읽어도_고정_줄_그대로(tmp_path):
|
|
"""[재생성] 꼴 = 사본 다시 읽음 — 고정 줄 품 · 손 단가 · 이은 단가(M02 셈) 그대로."""
|
|
root = b145._project(tmp_path / "upd", [(100, "SS000901")])
|
|
lines = [
|
|
{"고정": _fx("석공", {"노무비": 1}, {"노임": LB}), "수량": {"손": 0.5}},
|
|
{"고정": _fx("잡재료", {"재료비": 1000}, unit="식"), "수량": {"손": 1}},
|
|
]
|
|
_snap(root, "pipe@100.0", "SS000901", lines)
|
|
first = compute_labor(daega_inputs(root), b145.SHEETS, {})
|
|
second = compute_labor(daega_inputs(root), b145.SHEETS, {})
|
|
a, b = first["daega_blocks"][0]["lines"], second["daega_blocks"][0]["lines"]
|
|
assert [ln["qty"] for ln in a] == [ln["qty"] for ln in b] == [0.5, 1]
|
|
assert a[0]["prices"] == b[0]["prices"] and a[0]["prices"]["labor"] > 1 # 묶음 노임
|
|
assert a[1]["prices"] == b[1]["prices"] == {"labor": 0, "material": 1000, "expense": 0}
|