From 6c0211af1fee5e50d6bb19cd240577c28aa1079c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 20:17:06 +0900 Subject: [PATCH] =?UTF-8?q?feat(744-1):=20=EB=91=98=EC=A7=B8=20=EA=B8=B0?= =?UTF-8?q?=EC=B4=88=20=EC=84=B8=EB=B6=80=20=EC=9B=90=EB=AC=B8=20=ED=92=88?= =?UTF-8?q?=20=C2=B7=20=EB=90=98=EB=8F=8C=EB=A6=AC=EA=B8=B0=20=EA=B8=B0?= =?UTF-8?q?=EC=96=B5(=EC=B2=98=EC=9D=8C=EC=88=98=EB=9F=89=20=C2=B7=20?= =?UTF-8?q?=EB=BA=80=EB=8D=94=ED=95=9C=EC=A4=84)=20=E2=80=94=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 둘째 기초 펼침 줄 덧칸 원문 {수량, 금액, 비목} = 그 기초 1 기준단위 몫(M01 원문 품 · 머리 수량 안 곱함) · 줄 수량 · 금액 = 머리 수량 곱한 몫(상자 합) 그대로 - 더한줄 항목 처음수량 = 넣을 때 수량(없으면 저장 때 지금 수량) · SK · 일위대가 줄 패치 모두 - 뺀더한줄 [{차례, 줄}] = [기초 빼기] · 더한 항목 빼기 기억(셈 안 함 · 못 읽는 줄 버림) · 일위대가 줄 패치도 - 733 지문에서 처음수량 · 뺀더한줄 뺌 · Labor_Patch.as_doc = 셈 칸(CALC)만(화면 짝 꼴 그대로) - 이음 = PLAN 744 아래 줄(화면 = sub2) Co-Authored-By: Claude Opus 5.5 --- M02_MasterTemplete/M02_Labor_Patch.py | 9 +- M02_MasterTemplete/M02_Library_Same.py | 2 +- M02_MasterTemplete/M02_Sangeun_Calc.py | 6 +- M02_MasterTemplete/M02_Sangeun_Doc.py | 18 ++++ resources/tester/test_744_1_sk_detail_undo.py | 89 +++++++++++++++++++ 5 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 resources/tester/test_744_1_sk_detail_undo.py diff --git a/M02_MasterTemplete/M02_Labor_Patch.py b/M02_MasterTemplete/M02_Labor_Patch.py index aa0b3db09..87cd6a61a 100644 --- a/M02_MasterTemplete/M02_Labor_Patch.py +++ b/M02_MasterTemplete/M02_Labor_Patch.py @@ -15,7 +15,8 @@ from typing import Any from M02_MasterTemplete import M02_Sangeun_Doc as sd FIELD = "패치" -KEEP = ("덮개", "뺀줄", "더한줄") +CALC = ("덮개", "뺀줄", "더한줄") # 셈에 드는 칸(`as_doc` · 화면 짝 꼴) +KEEP = (*CALC, "뺀더한줄") # 뺀더한줄 = [되살림] 기억(744-1) TEMP = "SK-000000" # 이름 없는 줄 단가산출 — 저장 안 함 @@ -41,7 +42,9 @@ def clean(one: dict, where: str, whole: Any, row: dict | None, opened: list | No ) except sd.DocError as e: raise _bad(f"{where} · {e}") from e - out = {k: got[k] for k in KEEP if got[k]} + out = {k: got[k] for k in KEEP if got.get(k)} + for add in out.get("더한줄") or []: # 처음수량(744-1) = 넣을 때 수량 · 없으면 지금 수량 + add.setdefault("처음수량", add["수량"]) return {FIELD: out} if out else {} @@ -71,7 +74,7 @@ def _keyed(value: Any, ids: list[str]) -> dict: def as_doc(one: dict, whole: Any, name: str = "줄", unit: str = "식") -> dict[str, Any]: """일위대가 줄 → SK 문서(바탕 = 그 로직 · 그 SK) — 고름 · 자재표시는 cid 로.""" - body = {k: (one.get(FIELD) or {}).get(k) or ([] if k != "덮개" else {}) for k in KEEP} + body = {k: (one.get(FIELD) or {}).get(k) or ([] if k != "덮개" else {}) for k in CALC} head = {"키": TEMP, "이름": name, "단위": unit, **body} if "참조" in one: return {**head, "바탕": {"참조": one["참조"]}} diff --git a/M02_MasterTemplete/M02_Library_Same.py b/M02_MasterTemplete/M02_Library_Same.py index 744ea5814..42f4f795c 100644 --- a/M02_MasterTemplete/M02_Library_Same.py +++ b/M02_MasterTemplete/M02_Library_Same.py @@ -24,7 +24,7 @@ from common_util.common_util_derived_store import derived_key from M02_MasterTemplete import M02_Labor_Ref as refs DROP = {"키", "이름", "구분", "상세구분", "본뜬", "메모", "별칭", "바탕판", "원값", "계산값", "열"} -DROP |= {"일위대가번", "종류", "수정일", "출처"} +DROP |= {"일위대가번", "종류", "수정일", "출처", "처음수량", "뺀더한줄"} # 744-1 되돌림 기억 UNIT = {"m3": "㎥", "m2": "㎡", "m³": "㎥", "m²": "㎡"} EMPTY = (None, "", [], {}) diff --git a/M02_MasterTemplete/M02_Sangeun_Calc.py b/M02_MasterTemplete/M02_Sangeun_Calc.py index 4bd2ae5fd..e94909b07 100644 --- a/M02_MasterTemplete/M02_Sangeun_Calc.py +++ b/M02_MasterTemplete/M02_Sangeun_Calc.py @@ -249,7 +249,8 @@ def tag_of(whole: Any, code: str) -> str: def _detail(one: dict, at: int, whole: Any) -> dict[str, Any]: - """둘째 기초 세부 줄(화면) — 수량 · 금액 · 비목 = 세부 × 더한줄 수량 · cid `add:#k/세부 cid` · + """둘째 기초 세부 줄(화면) — 수량 · 금액 · 비목 = 세부 × 더한줄 수량(상자 합 몫) · `원문` = + 그 기초 1 기준단위 몫(M01 원문 품 · 744-1 화면 품 칸) · cid `add:#k/세부 cid` · `바탕값` = 덮기 전 1 기준단위 품 · 단가 · `뺀세부` = 뺀 줄 이름.""" sub = sub_run(one, whole) plain = sub_run(one, whole, False) if sd.sub_patched(one) else sub @@ -272,6 +273,7 @@ def _detail(one: dict, at: int, whole: Any) -> dict[str, Any]: **scaled, "비목": parts, "cid": f"add:#{at}/{cid}", + "원문": {k: line.get(k) for k in ("수량", "금액", "비목")}, "바탕값": _was(was[cid]) if cid in was else None, **({"덮개": [k for k in sd.COVER_KEYS if k in mine]} if mine else {}), **({"원값": mine["원값"]} if "원값" in mine else {}), @@ -439,6 +441,8 @@ def clean_doc(doc: Any, here_: refs.Here) -> dict[str, Any]: base = _base_of(rough, whole, refs.read_ref, ()) out = sd.clean(doc, base["줄"], whole) out["바탕"], out["더한줄"] = rough["바탕"], rough["더한줄"] + for one in out["더한줄"]: # 처음수량(744-1) = 넣을 때 수량 · 없으면 지금 수량 + one.setdefault("처음수량", one["수량"]) if out["덮개"]: bare = run({**out, "덮개": {}}, whole) # 덮기 전 값 = 원값(그때 M01) was = {x["cid"]: x for x in bare.get("줄") or []} diff --git a/M02_MasterTemplete/M02_Sangeun_Doc.py b/M02_MasterTemplete/M02_Sangeun_Doc.py index 5cdf81fbc..890daff6b 100644 --- a/M02_MasterTemplete/M02_Sangeun_Doc.py +++ b/M02_MasterTemplete/M02_Sangeun_Doc.py @@ -196,9 +196,25 @@ def _added(raw: Any, where: str, whole: Any) -> dict: else: raise DocError(f"{where} · 종류는 인력 · 재료 · 기계 · 로직 · 참조 · 고정") mark = {"본뜬": raw["본뜬"]} if isinstance(raw.get("본뜬"), dict) else {} # 출처(740-1) + if raw.get("처음수량") is not None: # 넣을 때 수량(744-1 [되돌림] 값) + mark["처음수량"] = plain(_num(raw["처음수량"], f"{where} 처음수량")) return {**head, "이름": name, "단위": unit, "수량": qty, **mark} +def _gone_adds(raw: Any, whole: Any) -> list[dict]: + """뺀 더한줄 기억(744-1 · [되살림]) `[{차례, 줄}]` — 셈 안 함 · 못 읽는 줄은 버림.""" + out = [] + for one in raw if isinstance(raw, list) else []: + at = one.get("차례") if isinstance(one, dict) else None + if not isinstance(at, int) or isinstance(at, bool) or at < 0: + continue + try: + out.append({"차례": at, "줄": _added(one.get("줄"), "뺀 더한줄", whole)}) + except DocError: + continue + return out + + def _sub_patch(raw: dict, where: str) -> dict: """둘째 기초 세부 고침(727-2) — `덮개` {세부 cid: 덮개} · `뺀줄` [세부 cid] · 빈 칸은 안 둠 · `원값` · `바탕판` = 서버 몫(버림).""" @@ -327,6 +343,8 @@ def clean(doc: Any, base_row: dict | None = None, whole: Any = None) -> dict[str if len(set(drop)) != len(drop): raise DocError("뺀줄에 같은 줄 두 번") out["뺀줄"] = drop + if gone := _gone_adds(doc.get("뺀더한줄"), whole): + out["뺀더한줄"] = gone return out diff --git a/resources/tester/test_744_1_sk_detail_undo.py b/resources/tester/test_744_1_sk_detail_undo.py new file mode 100644 index 000000000..9da898dbf --- /dev/null +++ b/resources/tester/test_744_1_sk_detail_undo.py @@ -0,0 +1,89 @@ +# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀 +"""744-1 서버 — 둘째 기초 세부 품 원문 · 되돌리기 기억(임시 storage · M01 읽기만). + +- 둘째 기초 `펼침` 줄 `원문` = 그 기초 1 기준단위 몫(GF000316 품 그대로) · 줄 수량 · 금액 = 머리 + 수량 곱한 몫(상자 합) 그대로 · 머리 수량 = 더한줄 수량 +- 더한줄 `처음수량` = 넣을 때 수량(없으면 저장 때 지금 수량) · 셈 · 지문 안 듦 +- `뺀더한줄 [{차례, 줄}]` = [기초 빼기] · 더한 항목 빼기 기억 · 셈 · 지문 안 듦 · 못 읽는 줄 버림 · + 일위대가 줄 패치도 같음 +""" + +import pytest + +from M01_MasterData import M01_MasterData_Store as m01 +from M02_MasterTemplete import M02_Labor_Doc as labor +from M02_MasterTemplete import M02_Labor_Ref as refs +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 resources.tester.test_m02_labor_ref import world # noqa: F401 + +GF = "GF000099" # 원/㎥ +BIG = "GF000316" # 기준단위 100㎥ — ㎥ 바탕에서 처음 수량 0.01 +PCT = "GC001109" +MAN = {"종류": "인력", "요소": "LB000002", "수량": 1} + + +def _sk(folder, **doc): + with store.at(folder): + made = refs.create("sangeun", {"이름": "744", "단위": "㎥", "더한줄": [], **doc}) + return store.read("sangeun", made["이름"])["문서"] + + +def _calc(folder, doc): + with store.at(folder): + return calc.calc_doc(doc) + + +def test_둘째_기초_세부_원문_품_머리_수량(world): + doc = _sk( + world["personal"], 바탕={"로직": GF}, 더한줄=[{"종류": "로직", "로직": BIG, "수량": 0.01}] + ) + got = _calc(world["personal"], doc) + head = next(x for x in got["줄"] if x["cid"] == "add:#0") + m01_lines = labor.run_line(m01.loaded()[1], BIG, {})["줄"] + assert head["수량"] == pytest.approx(0.01) + for row, line in zip(head["펼침"], m01_lines): + assert row["원문"]["수량"] == pytest.approx(float(line["수량"]), rel=1e-9) # 원문 그대로 + assert row["수량"] == pytest.approx(float(line["수량"]) * 0.01, rel=1e-9) # 상자 몫 + assert sum(x["금액"] for x in head["펼침"]) == pytest.approx(head["금액"], rel=1e-9) + assert sum(x["원문"]["금액"] for x in head["펼침"]) * 0.01 == pytest.approx(head["금액"]) + + +def test_처음수량_넣을때_값_셈_지문_안_듦(world): + me = world["personal"] + plain = _sk(me, 바탕={"로직": PCT}, 단위="㎡", 더한줄=[{**MAN, "수량": 3}]) + assert plain["더한줄"][0]["처음수량"] == 3 # 없으면 저장 때 지금 수량 + kept = _sk(me, 바탕={"로직": PCT}, 단위="㎡", 더한줄=[{**MAN, "수량": 3, "처음수량": 1}]) + assert kept["더한줄"][0]["처음수량"] == 1 + assert _calc(me, kept)["기준"]["계"] == _calc(me, plain)["기준"]["계"] + assert same.finger("sangeun", kept) == same.finger("sangeun", plain) + + +def test_뺀더한줄_기억_셈_지문_안_듦_못읽는_줄_버림(world): + me = world["personal"] + gone = [ + {"차례": 0, "줄": {"종류": "로직", "로직": BIG, "수량": 0.01, "덮개": {}}}, + {"차례": 1, "줄": MAN}, + {"차례": -1, "줄": MAN}, # 차례 틀림 + {"차례": 2, "줄": {"종류": "로직", "로직": "GX999999", "수량": 1}}, # 없는 기초 + {"차례": 3}, # 줄 없음 + ] + doc = _sk(me, 바탕={"로직": GF}, 뺀더한줄=gone) + assert [x["차례"] for x in doc["뺀더한줄"]] == [0, 1] + assert doc["뺀더한줄"][1]["줄"]["요소"] == "LB000002" + bare = _sk(me, 바탕={"로직": GF}) + assert _calc(me, doc)["기준"]["계"] == _calc(me, bare)["기준"]["계"] + assert same.finger("sangeun", doc) == same.finger("sangeun", bare) + + +def test_일위대가_줄_패치_기억_처음수량(world): + me = world["personal"] + patch = {"더한줄": [{**MAN, "수량": 2}], "뺀더한줄": [{"차례": 0, "줄": MAN}]} + with store.at(me): + sc = refs.create( + "precombo", {"이름": "744", "단위": "㎡", "담은로직": [{"로직": PCT, "패치": patch}]} + ) + line = store.read("precombo", sc["이름"])["문서"]["담은로직"][0] + assert line["패치"]["더한줄"][0]["처음수량"] == 2 + assert line["패치"]["뺀더한줄"][0]["줄"]["요소"] == "LB000002"