Files
Aislo/resources/tester/test_793_2_sangeun_imported.py
eomsangdonandClaude Opus 5.5 2a2822d650 feat(793-2): 가져온 식 793-2a 내보내기 꼴 받음 — 자원 덧칸 · 글줄 이름 → cid · 자리들 합 · 단계 · 중간 검산 · 빈 산근 받음
실무 여덟 파일 455 블록 검산 같음 · 상수 자리 찾기 「(11)」 겹침 고침(M01 식엔 그 꼴 없음 = 옛 상수덮개 차례 그대로)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:51:33 +09:00

392 lines
19 KiB
Python
Raw Permalink 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 줄 자리) · 단가 = 엑셀 목록표 값 → 셈 = 엑셀 금액 · 줄마다 검산
- 덮개 · 상수 덮개 · 식 칸 · 변수 줄이 가져온 줄에도 먹음
- 셈 답 `원래` = 엑셀 금액 · `가져온` 원본 글
- [우리 단가로] · [엑셀 단가로] · 이음 고름(덮개 열쇠 옮김) · [우리 기초로 바꾸기]
- 만들기(임시 층) · 같은 내용 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_우리_기초로_바꾸기():
doc = _doc(덮개={"labor:형틀목공#0": {"qty": 0.2}})
got = imp.to_base(doc, FORM, F211)
new = got["문서"]
assert new["바탕"] == {"로직": FORM} and not imp.is_imported(new) and new["덮개"] == {}
assert float(got["후"]["계"]) == 51419 and float(got["전"]["계"]) > 45652
assert got["차이"]["계"] == got["후"]["계"] - got["전"]["계"]
meta = new[imp.KEY]
assert meta["원본"]["파일"] == "봉화.xlsx" and meta["바꿈"]["로직"]["이름"].startswith("합판")
assert _total(new) == 51419 and same.finger("sangeun", new) == same.finger(
"sangeun", {k: v for k, v in new.items() if k != imp.KEY}
)
with pytest.raises(sd.DocError):
imp.to_base(doc, "GC999999", {})
# ── 꼴 검사 ───────────────────────────────────────────────────────────
@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)
imp.to_base(doc, FORM, F211)
assert doc == keep # 받은 문서 안 고침(화면 캐시 = 답 문서로 바꿈)
assert refs.kind_of("SK-000001") == "sangeun" and MINE["층"] == "personal"
assert flat.flat_doc("sangeun", doc) == doc # 참조 없음 = 그대로
# ── 실무 블록(793-2a 내보내기 → 가져온 식 SK) ─────────────────────────────
REAL = [("봉화", "D00102"), ("동부청영월", "D00003"), ("울진소광", "D01489"), ("북부청임도", 18)]
RULE = {"봉화": "비목버림", "동부청영월": "합버림_큰나머지", "울진소광": "비목버림"}
def _emitted(tag, code):
import helper_m02_hopyo_golden as golden
from M02_MasterTemplete import M02_San_Book as book
from M02_MasterTemplete import M02_San_Emit as em
from M02_MasterTemplete import M02_San_Est as est
from M02_MasterTemplete import M02_San_Stmate as stm
if not golden.FILES[tag].exists():
pytest.skip(f"실무 엑셀 없음 {tag}")
data = book.load(golden.FILES[tag])
if tag in RULE:
blk = next(b for b in data["블록"] if b["코드"] == code)
return em.stmate(blk, stm.parse_block(blk["몸"]), data["단가"], RULE[tag], True, tag)
blk = next(b for b in data["블록"] if b["번"] == code)
want = data["총괄"][blk["번"]]
return em.est(blk, est.parse_block(blk["글"], data["단가"]), want, True, tag)
@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차 줄 판정 이음