- 지움(git rm) 10 파일 3,374 줄: M02_Hopyo_Finger · Ours · Option · Fit · Combine · Verdict · Line · SkLink · SkPatch · Basis
- Match: 흐름 몫(candidates · name_cands · fix_typo · clause · b_order · _logic_specs · _line_mod · units_of · _state · view · guess · picks · chain) 지움 · 이름 도구 몫 · line_key 남김
- Words: conds · cond_score · close · cosine · jacc 지움 / Shares: sections 지움(trade · eq_key 남김)
- 옮김: Finger.kind_of → Fixed.kind_of · SkLink(sk_line · kept · left · LEFT · WHY) → Sangeun · SkPatch 기계 몫(MACH_IN · MACHINE · SOLO · _mach_logics) → Units.mach_logics
- Sangeun: 옛 /sangeun 몸(build · _sure · Maker.basis · judge · plan · make) 지움 · Kids: first · proposal · order 지움 · Build: build() · unlinked() 지움(모두 고정 Builder · build_all · _order · _origins 남김)
- Router: 옛 길 지움(GET hopyo/{키}/{번} · guess · hopyo-picks · hopyo-build · hopyo-unlinked · hopyo/{키}/sangeun) · GET hopyo/{키} = Judge.view(1차 줄 판정 줄상태 · 만든 · 블록 풀이 셈)
- Marks: 칠 = Judge.judged 만 · Forget: 맞춤 캐시 지움 줄 걷음 · Ledger: Ours.version 판 칸 걷음 · Item: Kids.first 걷음
- Row(덧): 단산 줄 = 장부 짝이 있어도 늘 「2차 필요」(장부 = 1차 후보 힌트만)
시험 정리(까닭 = 지운 길 · 모듈만 시험함):
- 지움 11 파일: test_m02_hopyo_finger · fit · golden · option · ours · basis · seam · flow · draft_calc · test_635 · test_628(지문 · 순위 · 옵션 거꾸로 · 맡은 절 · 옛 확인 흐름)
- 함수 지움: /sangeun · /hopyo-build · /hopyo-unlinked · hopyo-picks · 옛 줄 맞춤 보기를 부르던 것(572 · 718_1 · 729 · 739_1 · 777_1 · 778_5 · 780_1 · labor_national · 705_3 · m02_hopyo_match · 746_1)
- test_621 S2(지문 해시) 지움 · S1 남김 · test_m02_hopyo_build = 장부 길 시험만(PUT 으로 심음)
- 새 길로: 779_2(줄상태 = 1차 판정 · 칠 셈 전 None) · grid · stmate(hopyo-judge 줄 판) · 778_1 · 778_2(Units.mach_logics) · 642_6(Marks 원문) · 642_3 장부 짝 단산 줄 2차 지킴
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
223 lines
12 KiB
Python
223 lines
12 KiB
Python
"""705-3 실무자료 불러오기 빠짐 0 · 시트 통째 한 번 · [품셈으로 바꾸기](요구서 1장 · 4장 2 · 3 · 7).
|
||
|
||
- 확인 안 한 줄 · 노임 · % · 「우리에 없음」 = 고정 줄(엑셀 숫자 그대로) · 체크한 줄만 품셈 · 하위 호표 = 참조
|
||
- % 줄 = 비율 고정 줄 — M02 settle 거울(파이썬 ↔ TS) · 사전조합 셈 · B09 `pct` 줄
|
||
- 모두 고정(`build_all` 막지 않음 = 붙여넣기 길) — 북부청 · 실무 xlsx 넷: 빠짐 0 · 엑셀 합계 = 우리 합계 ·
|
||
줄 꼬리표 수 · 확인된 호표 그대로 · 이음 = 단가까지 같은 줄만(노임 이름 · 기계 EQ = 불러오기 길만) · 같은 이름 줄 차례
|
||
가름 · 몸에 숫자 칸 없음(422) · [모두 고정으로 들여오기] 길 = 남은 줄 있는 호표 실패(729-2 · 718-1 시험)
|
||
- [품셈으로 바꾸기] 전후 — 체크 한 줄 = 품셈 · 나머지 고정(「우리에 없음」 고름) · 되돌리기 = 모두 고정
|
||
층은 임시 폴더(공용 storage 안 씀).
|
||
"""
|
||
|
||
import importlib.util
|
||
import json
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor
|
||
from B09_Estimation.B09_Estimation_Labor_Source import fixed_line
|
||
from M02_MasterTemplete import M02_Hopyo_Build as hb
|
||
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
|
||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||
|
||
# 올리기 길 시험의 `client` 마당을 같이 씀
|
||
# ruff: noqa: F811
|
||
from resources.tester.test_m02_hopyo_excel import NORTH, _post, client, master_hash # noqa: F401
|
||
|
||
|
||
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
|
||
|
||
|
||
m02 = _load("test_m02_labor_calc_mirror")
|
||
ALL = "/api/m02/layers/{}/hopyo-build-all"
|
||
BUILD = "/api/m02/layers/{}/hopyo-build"
|
||
DOCS = Path(__file__).resolve().parents[2] / "resources" / "knowledge" / "original" / "실무문서"
|
||
FILES = { # 실무 xlsx 넷 — EST 꼴 거창 · STmate 꼴 영월 · 울진 대흥 · 울진 소광
|
||
"거창": "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx",
|
||
"영월": "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx",
|
||
"대흥": "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/"
|
||
"2024년 간선임도사업(기번3).xlsx",
|
||
"소광": "25년 산불진화임도(기번8)(울진소광)/"
|
||
"2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx",
|
||
}
|
||
# (호표, 줄, 고정, 하위 참조, 이음, 엑셀 = 우리) — 영월 제22호표 = STmate 하위 호표 비목 반올림 1 원 끝수
|
||
EXPECT = {
|
||
"거창": (14, 68, 56, 12, 11, 14),
|
||
"영월": (23, 138, 135, 3, 0, 23), # 751-3 끝수 뒤 엑셀 금액과 같음 하나 늚(23 개 전부)
|
||
"대흥": (34, 171, 143, 28, 0, 34),
|
||
"소광": (25, 117, 98, 19, 0, 25),
|
||
}
|
||
|
||
|
||
def _fx(name, price, rate=None, unit="식"):
|
||
out = {"이름": name, "규격": "", "단위": unit, "단가": price, "이음": {}}
|
||
return {**out, "비율": rate} if rate else out
|
||
|
||
|
||
def _all(client, key="HP-000001"):
|
||
"""모두 고정(막지 않음 = 붙여넣기 길 `build_all`) — 길(729-2)은 남은 줄 있는 호표 = 실패."""
|
||
folder = layers.personal_dir(7, 42)
|
||
with store.at(folder):
|
||
return hb.build_all(folder, folder, key)
|
||
|
||
|
||
def _decided(client, body):
|
||
"""남은 줄(729-2)을 「우리에 없음」으로 고른 몸."""
|
||
left = client.post(BUILD.format("personal").replace("build", "unlinked"), json=body).json()
|
||
return {
|
||
**body,
|
||
"줄": [*body["줄"], *({"차례": x["차례"], "뺌": "우리에 없음"} for x in left["남은줄"])],
|
||
}
|
||
|
||
|
||
def _docs():
|
||
folder = layers.personal_dir(7, 42) / "precombo"
|
||
return [json.loads(p.read_bytes()) for p in sorted(folder.glob("SC-*.json"))]
|
||
|
||
|
||
# ── 비율 고정 줄 셈 ──────────────────────────────────────────────────────────
|
||
def _ratio_doc():
|
||
return labor.clean(
|
||
{
|
||
"열": "rv",
|
||
"담은로직": [
|
||
{"고정": _fx("고화매트", {"재료비": 37000}, unit="㎡"), "수량": {"손": 1}},
|
||
{"고정": _fx("고정핀", {"재료비": 200}, unit="EA"), "수량": {"손": 4.95}},
|
||
{"고정": _fx("인부", {"노무비": 1000}, unit="인"), "수량": None},
|
||
{"로직": m02.PLAIN, "수량": None},
|
||
{
|
||
"고정": _fx("잡재료비", {}, {"기준": "재료비", "비목": "재료비"}),
|
||
"수량": {"손": 3},
|
||
},
|
||
{
|
||
"고정": _fx("공구손료", {}, {"기준": "노무비", "비목": "경비"}),
|
||
"수량": {"손": 2},
|
||
},
|
||
],
|
||
"자재": [{"코드": m02.MAT, "수량": None}],
|
||
}
|
||
)
|
||
|
||
|
||
@pytest.mark.parametrize("values", [{}, {f"자재|{m02.MAT}": "3"}, {"사석 수상고르기|㎡": 2}])
|
||
def test_비율_고정_줄_파이썬과_TS_가_같은_답(values):
|
||
doc = _ratio_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)
|
||
misc, tool = [ln for ln in py["줄"] if (ln.get("고정") or {}).get("비율")]
|
||
others = [ln for ln in py["줄"] if ln not in (misc, tool)]
|
||
under = sum((ln["반영"] or {}).get("재료비", 0) for ln in others) # 자재 = 별도계상(합 밖)
|
||
assert misc["반영"]["재료비"] == pytest.approx(under * 3 / 100, abs=0.1) and misc["배"] == 100
|
||
assert misc["반영"]["노무비"] == misc["반영"]["경비"] == 0
|
||
assert tool["반영"]["경비"] == 0 # 인부 수량 없음 = 노무비 밑 0
|
||
gap = sum((ln["반영"] or {}).get("계", 0) for ln in py["줄"]) - py["반영합"]["계"]
|
||
assert -1e-6 <= gap < 3 # 751-3 반영합 = 비목마다 1 원 버림
|
||
|
||
|
||
def test_비율_정리_막힘():
|
||
bad = _fx("잡", {}, {"기준": "합계", "비목": "재료비"})
|
||
with pytest.raises(Exception, match="비율"):
|
||
labor.clean({"담은로직": [{"고정": bad, "수량": {"손": 1}}]})
|
||
|
||
|
||
def test_B09_비율_고정_줄_pct():
|
||
fx = _fx("잡재료비", {}, {"기준": "재료비", "비목": "재료비"})
|
||
one = fixed_line(fx, 3)
|
||
assert {k: one[k] for k in ("kind", "part", "base", "qty", "unit")} == {
|
||
"kind": "pct", "part": "material", "base": "part:material", "qty": 3.0, "unit": "%"
|
||
} # fmt: skip
|
||
lines = [
|
||
{"kind": "fixed", "name": "고화매트", "qty": 1, "own": {"material": 37000.0},
|
||
"link": {"labor": None, "material": None, "machine": None}},
|
||
{"kind": "fixed", "name": "고정핀", "qty": 4.95, "own": {"material": 200.0},
|
||
"link": {"labor": None, "material": None, "machine": None}},
|
||
one,
|
||
] # fmt: skip
|
||
for n, ln in enumerate(lines):
|
||
ln["cid"], ln["n"] = f"{ln['kind']}:{ln['name']}#0", n
|
||
block = {"id": "DGF", "cover": "S:f", "kind": "조합", "name": "고화매트재료비", "spec": "",
|
||
"unit": "㎡", "cite": "", "origin": {}, "lines": lines} # fmt: skip
|
||
out = compute_labor({"blocks": [block], "owners": {}}, {}, {})
|
||
got = out["daega_blocks"][0]
|
||
pct = next(ln for ln in got["lines"] if ln["kind"] == "pct")
|
||
assert pct["amounts"]["material"] == 1139.7 # (37000 + 990) × 3 %
|
||
assert got["blank"] == 0 and out["gaps"] == []
|
||
|
||
|
||
# ── 줄 하나 → 고정 줄 ────────────────────────────────────────────────────────
|
||
def test_같은_이름_차례_가름_합계만_재료비_금액만():
|
||
linker, taken = hfix.Linker(), set()
|
||
line = {"차례": 0, "이름": "잡석", "규격": "", "단위": "㎥", "수량": 2, "갈래": "자재",
|
||
"단가": {"계": 5000, "노무비": None, "재료비": None, "경비": None}} # fmt: skip
|
||
a = hfix.line_of(line, [], {"파일": "x", "호표": 1}, taken, linker)
|
||
b = hfix.line_of({**line, "차례": 1}, [], {"파일": "x", "호표": 1}, taken, linker)
|
||
assert (a["고정"]["이름"], b["고정"]["이름"]) == ("잡석", "잡석 (2)")
|
||
assert a["고정"]["단가"] == {"노무비": 0, "재료비": 5000, "경비": 0} # 합계만 = 재료비
|
||
assert b["고정"]["출처"] == {"파일": "x", "호표": 1, "줄": 1} and a["수량"] == {"손": 2}
|
||
doc = labor.clean({"담은로직": [a, b]}) # 같은 줄 가름 400 안 남
|
||
assert len(doc["담은로직"]) == 2
|
||
only = {**line, "차례": 2, "이름": "운반", "수량": None,
|
||
"금액": {"계": 700, "노무비": 700, "재료비": None, "경비": None}} # fmt: skip
|
||
c = hfix.line_of(only, [], {}, taken, linker)
|
||
assert c["수량"] == {"손": 1} and c["고정"]["단가"]["노무비"] == 700 # 금액만
|
||
|
||
|
||
def test_퍼센트_밑수_다르면_금액_그대로():
|
||
pct = {"차례": 2, "이름": "소모자재", "규격": "주재료비의", "단위": "%", "수량": 11, "갈래": "%",
|
||
"단가": {"계": 7937.2}, "금액": {"계": 873, "노무비": 0, "재료비": 873, "경비": 0}} # fmt: skip
|
||
got = hfix.line_of(pct, [], {}, set(), hfix.Linker())
|
||
assert "비율" not in got["고정"] and got["고정"]["단위"] == "식"
|
||
assert got["수량"] == {"손": 1} and got["고정"]["단가"]["재료비"] == 873
|
||
|
||
|
||
# ── 시트 통째 ────────────────────────────────────────────────────────────────
|
||
def test_북부청_모두_고정_빠짐_0_합계_같음_그대로(client):
|
||
before = master_hash()
|
||
assert _post(client).status_code == 200
|
||
got = _all(client)
|
||
assert {k: got[k] for k in ("호표", "줄", "품셈", "고정", "호표참조", "빠짐", "같음")} == {
|
||
"호표": 68, "줄": 415, "품셈": 0, "고정": 414, "호표참조": 1, "빠짐": 0, "같음": 68
|
||
} # fmt: skip
|
||
assert got["실패"] == [] and got["다름"] == [] and got["그대로"] == 0
|
||
assert got["이음"] == 13 and got["이음후보"] == 31
|
||
docs = [d for d in _docs() if d["본뜬"]["키"].count(" ") == 1] # 호표 문서(감싼 줄 빼고)
|
||
lines = [x for d in docs for x in d["담은로직"]]
|
||
assert len(docs) == 68 and all(d["본뜬"]["품셈"] == [] for d in docs)
|
||
assert sum("고정" in x for x in lines) == 414 and sum("참조" in x for x in lines) == 1
|
||
assert sum(bool(x.get("고정", {}).get("비율")) for x in lines) == 1 # 잡재료비 3 %
|
||
again = _all(client) # 다시 = 확인된 호표 덮지 않음
|
||
assert again["그대로"] == 68 and again["고정"] == 0 and again["같음"] == 68
|
||
assert master_hash() == before
|
||
|
||
|
||
@pytest.mark.parametrize("tag", list(FILES))
|
||
def test_실무_xlsx_넷_모두_고정(client, tag):
|
||
path = DOCS / FILES[tag]
|
||
res = _post(client, data=path.read_bytes(), name=path.name)
|
||
assert res.status_code == 200, res.text
|
||
got = _all(client)
|
||
no, rows, fixed_n, kids, links, same = EXPECT[tag]
|
||
assert (got["호표"], got["줄"], got["고정"], got["호표참조"]) == (no, rows, fixed_n, kids)
|
||
assert got["빠짐"] == 0 and got["품셈"] == 0 and got["실패"] == []
|
||
assert got["고정"] + got["호표참조"] == got["줄"] # 줄 하나도 안 빠짐
|
||
assert got["이음"] == links and got["같음"] == same
|
||
assert all(abs(x["차이"]) <= 1 for x in got["다름"])
|
||
|
||
|
||
def test_몸에_숫자_칸_없음(client):
|
||
assert _post(client).status_code == 200
|
||
bad = {"층": "personal", "키": "HP-000001", "금액": 1}
|
||
assert client.post(ALL.format("personal"), json=bad).status_code == 422
|
||
|
||
|
||
# ── [품셈으로 바꾸기] 전후 ────────────────────────────────────────────────────
|