Files
Aislo/resources/tester/test_793_2_sangeun_imported.py
T
eomsangdonandClaude Opus 5.5 1f9f8a1467 fix(827-6): 호표 자재 이음 되살림(어닐링철선 결속선) · 가져온 식 검산 묶은 줄 합 맞댐 · 계약 블록
- M02_Hopyo_Material.spec_fit: 827-2 「자재 수 한 번씩」 을 좁힘 — 줄 수와 그대로 같은 자재 수만 그 줄 수 몫 · 배율로만 맞는 수는 같이 씀(글 붙은 「#200.9mm200.4m/kg」 = #20 0.9mm 200.4m/kg → MT000209 되살림 · 블레이드 d=320~400 t=3.2 → 4"×3.2 는 그대로 풀림 · 21-17-17 의 17 둘 각각)
- 호표 길 전수(실무 엑셀 8 · 일위대가 줄 1564): 827-2 앞 대비 바뀜 = 블레이드 1 줄(틀린 이음 풀림)뿐 · 어닐링철선 5 줄 되살림 · 단산 길 전수 827-2 전후표 수치 그대로(새 이음 · 풀림 · 키 바뀜 0)
- M02_Sangeun_Imported 검산: 첫 자리가 같은 글줄 여럿(814-1 기계 노 · 재 · 경 · 노 줄만 딸린 기계까지) = 값 합으로도 맞댐(check · 화면 more 둘 다) · 계약(낙찰률) 블록 = 비목 × 낙찰률 원 버림과도 맞댐(머리 금액만 있는 STmate)
- 세 원천 3241 블록 검산 다름 287 → 4(EST main 1308 · 북부청임도 2 · 4 · 48 = 엑셀 글 소계가 줄 합과 다름 — 진짜 다름 · 금액은 같음)
- 시험: 827-5 xfail 셋 뗌(test_778_5 자재 표 · test_793_2 동부청영월 · 울진소광) · 지킴 둘 더함(글 붙은 규격 수 · 묶은 줄 합 · 계약 셈)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 15:48:04 +09:00

391 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""793-2 가져온 식 단가산출(SK 바탕 갈래) — 경쟁사 산근 블록 = 엔진 로직 줄을 SK 가 품고 다시 셈.
- 견본 둘 = 791-1 손 옮김 견본(봉화 D00102 합판거푸집 STmate 번지 · 율 / 북부청임도 18 문양거푸집
EST 줄 자리) · 단가 = 엑셀 목록표 값 → 셈 = 엑셀 금액 · 줄마다 검산
- 덮개 · 상수 덮개 · 식 칸 · 변수 줄이 가져온 줄에도 먹음
- 셈 답 `원래` = 엑셀 금액 · `가져온` 원본 글
- [우리 단가로] · [엑셀 단가로] · 이음 고름(덮개 열쇠 옮김) · [우리 기초로 바꾸기] 없음(809-1)
- 만들기(임시 층) · 같은 내용 409 · 옛 SK 지문 그대로 · 펼침 막음 · B09 굳힘 · 단가 전파
"""
from __future__ import annotations
import copy
import json
from decimal import Decimal
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Item_Sangeun as sk
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Library_Flat as flat
from M02_MasterTemplete import M02_Library_Same as same
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from M02_MasterTemplete import M02_Sangeun_Const as const
from M02_MasterTemplete import M02_Sangeun_Doc as sd
from M02_MasterTemplete import M02_Sangeun_Imported as imp
from resources.tester.test_m02_labor_ref import MINE, world # noqa: F401 — 같은 임시 층
D = Decimal
MAN, CARP = "LB000002", "LB000007" # M01 보통인부 · 형틀목공
FORM = "GC000211" # 합판거푸집 기초
F211 = {"자재지역": "포항", "유형": "보통", "사용횟수": "6회", "높이감률": 0, "지붕슬래브": "아님"}
F211 |= {"폼타이": "아님", "폼타이규격": "12.7㎜", "측압": "3t/㎡", "구멍땜": 0}
def _whole():
return m01.loaded()[1]
def _man(qty, name, price, **more):
line = {"종류": "인력", "이름": name, "단위": "인", "수량": qty, "비목": "노무비"}
return {**line, "단가": price, **more}
def _mat(qty, name, unit, price, **more):
line = {"종류": "재료", "이름": name, "단위": unit, "수량": qty, "비목": "재료비"}
return {**line, "단가": price, **more}
def _bonghwa() -> tuple[dict, dict]:
"""봉화 단가산출근거 2909~2943 행(791-1 견본) — 보통인부만 M01 에 이음."""
row = {
"이름": "합판거푸집(봉화 D00102)",
"결과단위": "원/㎡",
"호표": [
_mat("1.03 * 0.327", "합판(내수)", "㎡", 11468, 번지="A01"),
_mat("0.038 * 0.327", "각재", "㎥", 500661, 번지="A01"),
_man("5 / 50", "형틀목공", 274978, 번지="A02"),
_man("2 / 50", "보통인부", 165545, 번지="A02", 이음=MAN),
],
"덧줄": [
{"이름": "소모자재(박리재 등)", "식": "번지.A01.재료비 * 11 / 100", "비목": "재료비"},
{"이름": "공구손료", "식": "번지.A02.노무비 * 1 / 100", "비목": "경비"},
],
"단계": [
{"이름": "자재비 소계", "단": 1, "줄": ["합판(내수)", "각재", "소모자재(박리재 등)"]},
{"이름": "설치 소계", "단": 1, "줄": ["형틀목공", "보통인부", "공구손료"]},
{"이름": "계", "단": 2},
],
}
src = {
"파일": "봉화.xlsx",
"시트": "단가산출근거",
"행": [2909, 2943],
"갈래": "STmate",
"엑셀금액": {"노무비": 34119, "재료비": 11192, "경비": 341, "계": 45652},
"글줄": [
{"행": 2909, "글": "합판거푸집 ㎡ 당"},
{"행": 2910, "글": "합판(내수) &M00081& * 1.03 * 0.327", "자리": PLY, "값": 3862.5},
{"행": 2912, "글": "형틀목공 5 / 50", "자리": "labor:형틀목공#0", "값": 27497.8},
{"행": 2913, "글": "보통인부 2 / 50", "자리": f"labor:{MAN}#0", "값": 6621.8},
{"행": 2915, "글": "소모자재 &PA01M:M& * 11'%'", "자리": BURN, "값": 1109.2},
],
}
return row, src
PLY, BURN = "material:합판(내수)#0", "pct:소모자재(박리재 등)#0"
def _bukbu() -> tuple[dict, dict]:
"""북부청임도 단가산출 18 번(791-1 견본) — 잡재료 줄만 반올림 0 · 이음 없음."""
row = {
"이름": "문양거푸집(북부청 18)",
"결과단위": "원/㎡",
"호표": [
_mat("1 * (1 / 0.72)", "문양거푸집(ABS코팅폼)", "㎡", 4166),
_man("0.07", "형틀목공", 275790),
_man("0.03", "보통인부", 172068),
],
"덧줄": [
{
"이름": "잡재료비",
"식": "재료비 * 2 / 100",
"비목": "재료비",
"자리": {"방법": "반올림", "자리": 0},
}
],
"단계": [{"이름": "인부소계", "단": 1, "줄": ["형틀목공", "보통인부"]}],
}
src = {"파일": "북부청임도.xlsx", "시트": "단가산출", "행": [410, 420], "갈래": "EST"}
src |= {"엑셀금액": {"노무비": 24467, "재료비": 5902, "경비": 0, "계": 30369}}
src |= {"글줄": [{"행": 415, "글": "5,786.1 × 2% = 116", "자리": "pct:잡재료비#0", "값": 116}]}
return row, src
def _doc(sample=_bonghwa, **more):
row, src = sample()
meta = {"로직": row, "단가기준": "엑셀", "원본": src}
return {"키": "SK-000001", "이름": "가져온", "단위": "㎡", "바탕": None, imp.KEY: meta, **more}
def _run(doc, whole=None):
whole = whole or _whole()
return calc.run(sd.clean(doc, None, whole), whole)
def _sum(doc, whole=None) -> dict:
return {k: float(v) for k, v in _run(doc, whole)["1단위"].items()}
def _total(doc, whole=None) -> float:
return _sum(doc, whole)["계"]
# ── 셈 = 엑셀 ─────────────────────────────────────────────────────────
def test_봉화_셈_엑셀_검산():
assert _sum(_doc()) == {"노무비": 34119, "재료비": 11192, "경비": 341, "계": 45652}
got = imp.check(sd.clean(_doc(), None, _whole()))
assert got["같음"] is True and len(got["줄"]) == 4 and all(x["같음"] for x in got["줄"])
bad = _doc()
bad[imp.KEY]["원본"]["엑셀금액"]["계"] = 45653
bad[imp.KEY]["원본"]["글줄"][1]["값"] = 3870
got = imp.check(sd.clean(bad, None, _whole()))
assert got["같음"] is False and got["까닭"] == "금액 다름"
assert got["줄"][0]["까닭"] == "셈 3862.5 · 엑셀 3870"
def test_북부청_18_줄_자리_셈_엑셀():
assert _sum(_doc(_bukbu)) == {"노무비": 24467, "재료비": 5902, "경비": 0, "계": 30369}
assert imp.check(sd.clean(_doc(_bukbu), None, _whole()))["같음"] is True
# ── 덮개 · 식 칸 · 셈 답 ─────────────────────────────────────────────
def test_덮개_상수_식칸_가져온_줄에_먹음():
base = _total(_doc())
covered = _doc(덮개={"labor:형틀목공#0": {"qty": 0.2}})
assert _total(covered) > base # 형틀목공 5/50 → 0.2 인
assert (
_total(_doc(상수덮개={BURN: {"0": 12}})) == 45652 + 101
) # 소모자재 11 → 12 %(A01 재료 10,083.7 × 1 %)
assert _total(_doc(식덮개={"labor:형틀목공#0": "6 / 50"})) > base
var = [{"이름": "할증", "식": "1.2", "자리": ""}]
assert _total(_doc(변수줄=var, 식덮개={"labor:형틀목공#0": "5 / 50 * 할증"})) > base
assert _total(_doc(뺀줄=[PLY])) < base
adds = [{"종류": "인력", "요소": MAN, "수량": 0.01}]
assert _total(_doc(더한줄=adds)) > base
def test_셈_답_원래_엑셀_원본_글(world):
doc = _doc(덮개={"labor:형틀목공#0": {"qty": 0.2}}, 까닭={"labor:형틀목공#0": "현장 품"})
with store.at(world["personal"]):
got = calc.calc_doc(doc)
assert got["원래"] == {"노무비": 34119, "재료비": 11192, "경비": 341, "계": 45652}
assert got["기준"]["계"] > 45652
spot = got["시트"]["자리"]["labor:형틀목공#0"]
assert spot["덮개"] == ["품"] and spot["까닭"] == "현장 품"
view = got[imp.KEY]
assert view["단가기준"] == "엑셀" and view["파일"] == "봉화.xlsx" and view["행"] == [2909, 2943]
lines = view["글줄"]
assert lines[0] == {"행": 2909, "글": "합판거푸집 ㎡ 당", "자리": None}
assert lines[1]["같음"] is True and lines[2]["같음"] is False # 덮은 줄 = 엑셀과 다름
assert [x["이름"] for x in view["이음없음"]] == ["합판(내수)", "각재", "형틀목공"]
# ── 단가 기준 · 이음 ──────────────────────────────────────────────────
def test_우리_단가로_엑셀_단가로():
got = imp.to_price(_doc(), "우리")
assert got["문서"][imp.KEY]["단가기준"] == "우리" and float(got["전"]["계"]) == 45652
price = float(_whole().get(MAN)["값"])
assert float(got["후"]["노무비"]) == pytest.approx(34119 + (price - 165545) * 2 / 50, abs=1)
back = imp.to_price(got["문서"], "엑셀")
assert float(back["후"]["계"]) == 45652 and back["전"] == got["후"]
assert len(got["이음없음"]) == 3
with pytest.raises(sd.DocError):
imp.to_price(_doc(), "남의")
with pytest.raises(sd.DocError):
imp.to_price({"키": "SK-000001", "바탕": {"로직": FORM}}, "우리")
def test_이음_고름_덮개_열쇠_옮김():
doc = _doc(덮개={"labor:형틀목공#0": {"qty": 0.2}}, 까닭={"labor:형틀목공#0": "현장"})
doc["글줄"] = [{"자리": "labor:형틀목공#0", "글": "※ 현장 조정"}]
got = imp.link(doc, "labor:형틀목공#0", CARP)["문서"]
new = f"labor:{CARP}#0"
assert got["덮개"] == {new: {"qty": 0.2}} and got["까닭"] == {new: "현장"}
assert got["글줄"][0]["자리"] == new
assert got[imp.KEY]["원본"]["글줄"][2]["자리"] == new
assert _total(got) == _total(doc) # 엑셀 단가 그대로 = 금액 그대로
ours = imp.to_price(got, "우리")
assert len(ours["이음없음"]) == 2 and ours["후"]["계"] != ours["전"]["계"]
with pytest.raises(sd.DocError):
imp.link(doc, "labor:형틀목공#0", "LB999999")
with pytest.raises(sd.DocError):
imp.link(doc, "labor:없는줄#0", CARP)
def test_우리_기초로_바꾸기_없음():
"""809-1 · 요구 703 — 경쟁사 로직 인정 · 가져온 식 → 우리 기초 바꾸기 길 없음."""
assert not hasattr(imp, "to_base")
# ── 꼴 검사 ───────────────────────────────────────────────────────────
@pytest.mark.parametrize(
("change", "why"),
[
(lambda r: r.update(결과단위="㎥/시간"), "결과단위"),
(lambda r: r["호표"][0].update(수량="1.03 * (0.327"), "수량"),
(lambda r: r["호표"][0].update(비목="잡비"), "비목"),
(lambda r: r["호표"][0].pop("단가"), "둘 다 없음"),
(lambda r: r["호표"][0].update(이음="MT999999"), "없는 이음"),
(lambda r: r["덧줄"][0].update(식="번지.A01.재료비 * "), "덧줄"),
(lambda r: r.update(호표="줄"), "목록"),
],
)
def test_틀린_가져온_로직(change, why):
doc = _doc()
change(doc[imp.KEY]["로직"])
with pytest.raises(sd.DocError, match=why):
sd.clean(doc, None, _whole())
def test_바탕과_같이_못_둠_펼침_막음():
with pytest.raises(sd.DocError, match="바탕 없음"):
sd.clean(_doc(바탕={"로직": FORM}), None, _whole())
with pytest.raises(sd.DocError, match="펼치지 않음"):
const.no_flat(_doc())
# ── 만들기 · 저장 · 같은 내용 ────────────────────────────────────────
def test_만들기_저장_같은_내용(world):
mine = world["personal"]
row, src = _bonghwa()
with store.at(mine):
made = imp.create(row, src, "합판거푸집 가져옴")
doc = store.read("sangeun", made["이름"])["문서"]
with pytest.raises(store.StoreError) as e: # 같은 내용 = 409
imp.create(row, {**src, "파일": "다른.xlsx"}, "또")
assert e.value.status == 409
again = imp.create(row, src, "또", new=True)["이름"]
ours = imp.create(row, src, "우리", new=True)["이름"]
assert made["검산"]["같음"] is True and made["단가기준"] == "엑셀"
meta = doc[imp.KEY]
assert doc["바탕"] is None and doc["단위"] == "㎡" and meta["원본"] == src
assert meta["검산"]["같음"] is True and meta["로직"]["호표"][3]["이음"] == MAN
assert again != made["이름"] != ours
plain = {"키": "SK-000009", "이름": "x", "단위": "식", "바탕": {"로직": FORM}, "입력값": F211}
one = same.finger("sangeun", doc)
assert one and one != same.finger("sangeun", imp.to_price(doc, "우리")["문서"])
assert same.finger("sangeun", {**doc, imp.KEY: {**doc[imp.KEY], "원본": {}}}) == one
assert same.finger("sangeun", {k: v for k, v in plain.items()}) == same.finger(
"sangeun", {**plain, imp.KEY: {}}
)
with store.at(mine): # 저장 = 서버가 검산 다시(브라우저 값 안 받음)
lie = {**doc, imp.KEY: {**doc[imp.KEY], "검산": {"같음": False}}}
store.write("sangeun", made["이름"], store.read("sangeun", made["이름"])["판"], lie)
assert store.read("sangeun", made["이름"])["문서"][imp.KEY]["검산"]["같음"] is True
def test_옛_SK_그대로():
plain = {"키": "SK-000001", "이름": "x", "단위": "식", "바탕": {"로직": FORM}, "입력값": F211}
got = sd.clean(plain, None, _whole())
assert imp.KEY not in got and _total(plain) == 51419
empty = {"키": "SK-000001", "이름": "x", "단위": "식", "바탕": None}
assert sd.empty_row(empty)["호표"] == [] and same.finger("sangeun", empty) is None
# ── B09 굳힘 · 단가 전파 ───────────────────────────────────────────────
class _Priced:
"""M01 색인 위 노임 하나만 바꿈(단가 전파 흉내)."""
def __init__(self, whole, key, value):
self._whole, self._key, self._value = whole, key, value
def value(self, ref):
got = self._whole.value(ref)
return (D(self._value), got[1]) if ref == self._key else got
def __getattr__(self, name):
return getattr(self._whole, name)
def _frozen_total(doc, whole):
rid = "personal|7/42|SK-000001"
frozen = sk.freeze({rid: sd.clean(doc, None, _whole())})[rid]
key, row = frozen["바탕"]["로직"], json.loads(json.dumps(frozen[sk.ROW]))
assert key.startswith("GX9") and frozen["원본"][imp.KEY]["로직"]
over = calc._Over(whole, key, row)
return float(calc.run(sd.clean(frozen, None, over), over)["1단위"]["계"])
def test_B09_굳힘_단가_전파():
whole = _whole()
excel, ours = _doc(), imp.to_price(_doc(), "우리")["문서"]
assert _frozen_total(excel, whole) == 45652
live = _total(ours)
assert _frozen_total(ours, whole) == live
up = _Priced(whole, MAN, float(whole.get(MAN)["값"]) + 50000)
assert _frozen_total(excel, up) == 45652 # 엑셀 단가 그대로 = 안 바뀜
assert _frozen_total(ours, up) == pytest.approx(live + 50000 * 2 / 50 * 1.01, abs=2)
covered = _doc(덮개={"labor:형틀목공#0": {"qty": 0.2}})
assert _frozen_total(covered, whole) == _total(covered)
def test_원본_보존_사본():
doc = _doc()
keep = copy.deepcopy(doc)
imp.to_price(doc, "우리")
imp.link(doc, "labor:형틀목공#0", CARP)
assert doc == keep # 받은 문서 안 고침(화면 캐시 = 답 문서로 바꿈)
assert refs.kind_of("SK-000001") == "sangeun" and MINE["층"] == "personal"
assert flat.flat_doc("sangeun", doc) == doc # 참조 없음 = 그대로
# ── 실무 블록(793-2a 내보내기 → 가져온 식 SK) ─────────────────────────────
# 동부청영월 · 울진소광 = 814-1 기계 노 · 재 · 경 세 글줄이 묶은 한 줄을 가리킴(827-6 묶음 합 맞댐)
REAL = [("봉화", "D00102"), ("동부청영월", "D00003"), ("울진소광", "D01489"), ("북부청임도", 18)]
def test_묶은_줄_글줄_셋_합_맞댐():
"""827-6 — 첫 자리가 같은 글줄 셋(노 · 재 · 경 · 노 줄만 딸린 기계까지) = 값 합으로 맞댐 ·
하나뿐이면 제 값 · 계약(낙찰률) 블록 = 비목 × 낙찰률 원 버림과도 맞댐."""
gc, br = "logic:GC1#0", "logic:브레이커#0"
texts = [{"행": 1, "자리": gc, "자리들": [gc, br], "값": 956.8}]
texts += [{"행": r, "자리": gc, "값": v} for r, v in ((2, 309.4), (3, 397.3))]
group = imp._groups(texts + [{"행": 4, "자리": "logic:M#0", "값": 5}])
assert group == {gc: Decimal("1663.5")}
now = {gc: 1663.5, br: 0, "logic:M#0": 5}
assert all(imp._compare(t, now, group=group)["같음"] for t in texts)
assert imp._compare({"행": 4, "자리": "logic:M#0", "값": 5}, now, group=group)["같음"]
assert not imp._compare(texts[1], {gc: 1600}, group=group)["같음"]
mine = {"노무비": 484.3, "재료비": 134, "경비": 168, "계": 786.3}
assert imp._contract(mine, 87.745) == {"노무비": 424, "재료비": 117, "경비": 147, "계": 688}
def _emitted(tag, code):
"""실무 블록 → 812-1 엑셀 읽기기 → 원파일 길(`M02_Doc_FromNative.emit`)."""
import helper_m02_hopyo_golden as golden
from M02_MasterTemplete import M02_Doc_FromNative as fn
from M02_MasterTemplete import M02_San_Xlsx as sx
path = golden.FILES[tag]
if not path.exists():
pytest.skip(f"실무 엑셀 없음 {tag}")
saved = fn.pack(sx.read(path.name, path.read_bytes()), path.name)
return fn.emit(saved, int(str(code).lstrip("D")))
@pytest.mark.parametrize(("tag", "code"), REAL)
def test_실무_블록_가져온_식(tag, code, world):
out = _emitted(tag, code)
with store.at(world["personal"]):
made = imp.create(out["로직"], out["원본"], f"{tag} {code}")
doc = store.read("sangeun", made["이름"])["문서"]
ans = calc.calc_doc(doc)
check = made["검산"]
excel = {k: round(v) for k, v in out["원본"]["엑셀금액"].items()}
assert check["같음"] is True and check["셈"] == excel and len(check["줄"]) >= 3
assert all(":" in x["자리"] for x in check["줄"]) # 줄 이름 → cid · mid: · 단계:
assert ans["원래"] == out["원본"]["엑셀금액"] and round(ans["기준"]["계"]) == excel["계"]
shown = [x for x in ans[imp.KEY]["글줄"] if "같음" in x]
assert shown and all(x["같음"] for x in shown)
assert _frozen_total(doc, _whole()) == excel["계"]
ours = imp.to_price(doc, "우리")
assert {k: round(float(v)) for k, v in ours["전"].items()} == excel
assert len(ours["이음없음"]) < len(doc[imp.KEY]["로직"]["호표"]) # 1차 줄 판정 이음