From 6c0211af1fee5e50d6bb19cd240577c28aa1079c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 20:17:06 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(744-1):=20=EB=91=98=EC=A7=B8=20?= =?UTF-8?q?=EA=B8=B0=EC=B4=88=20=EC=84=B8=EB=B6=80=20=EC=9B=90=EB=AC=B8=20?= =?UTF-8?q?=ED=92=88=20=C2=B7=20=EB=90=98=EB=8F=8C=EB=A6=AC=EA=B8=B0=20?= =?UTF-8?q?=EA=B8=B0=EC=96=B5(=EC=B2=98=EC=9D=8C=EC=88=98=EB=9F=89=20?= =?UTF-8?q?=C2=B7=20=EB=BA=80=EB=8D=94=ED=95=9C=EC=A4=84)=20=E2=80=94=20?= =?UTF-8?q?=EC=84=9C=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" From 6893599fa9801c4252892a10291fbc77d0cd2aaa Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 20:17:17 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(740-1):=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80=20=EC=95=88=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20?= =?UTF-8?q?=3D=20=EC=82=AC=EB=B3=B8=20=EB=B8=94=EB=A1=9D(=EC=9B=90?= =?UTF-8?q?=EB=B3=B8=20=EC=97=B0=EA=B2=B0=20=EB=81=8A=EC=9D=8C=20=C2=B7=20?= =?UTF-8?q?=ED=95=98=EC=9C=84=20=ED=98=B8=ED=91=9C=20=EB=81=9D=EC=88=98=20?= =?UTF-8?q?=EA=B7=B8=EB=8C=80=EB=A1=9C)=20=C2=B7=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94=20=EC=9D=B4=EC=9D=8C=20=EA=B8=B0=EA=B3=84=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EB=B0=9B=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SC 안 SC = 안쪽 SC 몸 통째를 그 줄 참조 꼴 {층: 사본, 주인: 몸 지문, 키: 본 SC, 몸} 에 담음(Labor_Ref.copy_ref · read_ref 가 몸을 돌려줌 → 셈 · 알약 · B09 하위 대가 블록 · 지문 = 옛 참조 줄과 같은 길) · 엑셀 대조 거창 같음 14 그대로 · 같은 기초 겹침도 블록 안(400 없음) - 저장 · 읽기 · 옮기기 · 찾아 넣기 모두 사본 블록 · 자기 자신을 담은 줄 = 안 펼침(저장 422 그대로) - 단가산출에 SC 넣기 = 사본 블록 줄도 더한줄로 펼침(바깥 × 안 수량) - 초기화 = 사본 몸 안까지 · 이음 기계 고정 줄(736-3) 단가 = M01 기계 한 시간 값 다시 받음 - 옮기기 = 쓰는 곳 검사 걷음(같은 자리 한 줄이라 안 줄 알약 차례 그대로) · 실 storage 보기만 = 1/3 SC-000002 사본 블록 · 전후 1,446,078 같음 - 낡은 시험 고침(까닭 = 하위 호표 참조가 사본 블록으로): m02_labor_ref(순환 대신 사본 · 복사 딸림 없음) · 733-2(딸림 재사용 = 구조물 조합 복사) · 735-1(쓰는 곳 = 구조물 조합 둘) · hopyo_build · 707-7(하위 호표 줄 = 사본 블록 · 본뜬) Co-Authored-By: Claude Opus 5.5 --- M02_MasterTemplete/M02_Labor_Ref.py | 42 ++++- M02_MasterTemplete/M02_Library_Flat.py | 178 ++++++++---------- resources/tester/test_707_7_whole_import.py | 4 +- resources/tester/test_733_2_library_ask.py | 20 +- resources/tester/test_735_1_library_delete.py | 11 +- resources/tester/test_740_1_library_flat.py | 84 +++++---- resources/tester/test_m02_hopyo_build.py | 4 +- resources/tester/test_m02_labor_ref.py | 27 +-- 8 files changed, 206 insertions(+), 164 deletions(-) diff --git a/M02_MasterTemplete/M02_Labor_Ref.py b/M02_MasterTemplete/M02_Labor_Ref.py index 6a3e39c10..27d54f149 100644 --- a/M02_MasterTemplete/M02_Labor_Ref.py +++ b/M02_MasterTemplete/M02_Labor_Ref.py @@ -42,6 +42,7 @@ _OWNER = {"company": re.compile(r"^\d+$"), "personal": re.compile(r"^\d+/\d+$")} Ref = dict[str, Any] Here = tuple[str, str | None] +COPY = "사본" # 740-1 하위 호표 사본 블록 — 참조 꼴 {층: 사본, 주인: 몸 지문, 키: 본 SC, 몸} def _bad(text: str, status: int = 400) -> store.StoreError: @@ -103,7 +104,28 @@ def ref_path(ref: Ref) -> Path: return folder_of(ref["층"], ref.get("주인")) / kind_of(ref["키"]) / f"{ref['키']}.json" +def is_copy(ref: Any) -> bool: + """사본 블록(740-1) — 일위대가 안 일위대가 몸을 줄 안에 담음(원본 연결 없음 · 키 = 출처).""" + return isinstance(ref, dict) and ref.get("층") == COPY and isinstance(ref.get("몸"), dict) + + +def copy_ref(body: dict[str, Any], key: str) -> Ref: + """사본 블록 참조 — 몸(종류 · 이름 · 단위 · 담은로직 · 자재 · 본뜬) · 주인 = 몸 지문.""" + import hashlib + + keep = {k: body[k] for k in ("종류", "이름", "단위", "담은로직", "자재", "본뜬") if body.get(k)} + raw = json.dumps(keep, ensure_ascii=False, sort_keys=True, default=str) + return { + "층": COPY, + "주인": hashlib.sha256(raw.encode()).hexdigest()[:12], + "키": key, + "몸": keep, + } + + def read_ref(ref: Ref) -> dict[str, Any] | None: + if is_copy(ref): + return ref["몸"] try: got = json.loads(ref_path(ref).read_bytes()) except (OSError, ValueError): @@ -114,6 +136,8 @@ def read_ref(ref: Ref) -> dict[str, Any] | None: def readable(at: Here, ref: Ref) -> bool: """`at` 층이 `ref` 를 가리켜도 되나 — 위 층만(프로젝트는 아무것도 · 굳힌 사본만 씀).""" layer, owner = at + if ref.get("층") == COPY: # 사본 블록 = 줄 안 몸(어느 층도 읽음) + return True if layer == "project": return False if ref["층"] == "system": @@ -127,6 +151,8 @@ def check_ref(raw: Any, at: str, here_: Here) -> Ref: """참조 꼴 · 위 층만 · 있는 문서 — 아니면 400.""" if not isinstance(raw, dict): raise _bad(f"{at} · 참조는 {{층, 주인, 키}}") + if raw.get("층") == COPY: + return _check_copy(raw, at) layer, owner, key = raw.get("층"), raw.get("주인"), str(raw.get("키") or "") if layer == "system": owner = None @@ -142,6 +168,20 @@ def check_ref(raw: Any, at: str, here_: Here) -> Ref: return ref +def _check_copy(raw: Ref, at: str) -> Ref: + """사본 블록 검사 — 키 = SC · 몸 줄 · 자재 = 일위대가 검사 그대로 · 지문 다시 셈.""" + from M02_MasterTemplete import M02_Labor_Doc as labor + + body, key = raw.get("몸"), str(raw.get("키") or "") + if not isinstance(body, dict) or not _KEY.match(key) or kind_of(key) != "precombo": + raise _bad(f"{at} · 사본 블록은 {{층: 사본, 키: SC-…, 몸}}") + try: + labor.clean({"담은로직": body.get("담은로직") or [], "자재": body.get("자재") or []}) + except store.StoreError as e: + raise _bad(f"{at} · 사본 블록 · {e.detail}") from e + return copy_ref(body, key) + + # ── 서고 문서 정리 ───────────────────────────────────────────────────── def check_key(kind: str, name: str) -> None: found = _KEY.match(name) @@ -158,7 +198,7 @@ def clean_doc(kind: str, doc: Any) -> dict[str, Any]: if here()[0] != "project": # 740-1 — M02 참조 줄 = M01 키 줄로 펼침(못 펼친 줄은 그대로) from M02_MasterTemplete import M02_Library_Flat as flat - doc = flat.flat_doc(kind, doc) + doc = flat.flat_doc(kind, doc, me={"층": here()[0], "주인": here()[1], "키": key}) name, unit = str(doc.get("이름") or "").strip(), str(doc.get("단위") or "").strip() if kind == "sangeun": from M02_MasterTemplete import M02_Sangeun_Calc as calc diff --git a/M02_MasterTemplete/M02_Library_Flat.py b/M02_MasterTemplete/M02_Library_Flat.py index f8afd86a2..69ccc2b45 100644 --- a/M02_MasterTemplete/M02_Library_Flat.py +++ b/M02_MasterTemplete/M02_Library_Flat.py @@ -10,15 +10,17 @@ 수량 = 넣은 수 × 줄 수량(저장 `수량.손` › 실무수량 › 1 · 742-1) ÷ 배(물량 알약 배) SC 에 SK 로직 줄 하나 + 패치(= SK 몸) · 별칭 = SK 이름 · 같은 자리(안 줄 알약 키 그대로) · 바탕 없는 SK · 단위 환산 SK = 못 넣음(값 단위가 달라짐) - SC 에 SC 찾아 넣기 길만 — 그 자리에 안 줄 · 자재를 펴 넣음 · 줄 수량 · 본뜬 실무수량 = - 바깥 × 안 · 같은 기초 · 고정 · 자재가 겹치면 못 넣음(일위대가 줄 짝 = 로직 키) · - 저장 · 읽기 · 옮기기는 하위 호표 참조 그대로(엑셀 하위 호표 끝수 · B09 하위 대가) + SC 에 SC 사본 블록 — 안쪽 SC 몸 통째(M01 키 줄 + 덮개 · 실무수량)를 그 줄 참조 꼴 + `{층: 사본, 주인: 몸 지문, 키: 본 SC, 몸}` 에 담음(`M02_Labor_Ref.copy_ref` · 원본 + 연결 없음 · 하위 호표 끝수 단계 · B09 하위 대가 블록 그대로 · 같은 기초 겹침도 + 블록 안) 바탕 SK 참조 · 패치 있는 SK 참조 줄 = 위 덮개를 아래 덮개에 겹쳐 한 벌(`compose`) 못 펼친 줄 = 저장 · 읽기 · 옮기기에선 옛 참조 그대로(값 그대로) · 넣기 길(`strict`)에선 400 까닭. -초기화(`reset`) = 덮개 · 뺀줄 · 더한줄 단가 걷음(안쪽 넣은 몸까지) · 항목 · 수량 · 입력값 · 고름 · -자재표시 · 켜쌓기율 · 고정 줄 · 기본정보 남김 → 값 = 연결된 M01 키 값. +초기화(`reset`) = 덮개 · 뺀줄 · 더한줄 단가 걷음(안쪽 넣은 몸 · 사본 블록까지) · 이음 기계 고정 줄 +단가 = M01 기계 한 시간 값 다시 받음(736-3) · 항목 · 수량 · 입력값 · 고름 · 자재표시 · 켜쌓기율 · +기본정보 남김 → 값 = 연결된 M01 키 값. 옮기기(`migrate`) = 층 문서 펼쳐 다시 씀 — 전후 1 단위 금액 같을 때만(안 줄 수량 모두 1 로 대조) · -쓰는 구조물 조합이 있는 SC 펼쳐 넣기(차례 바뀜)는 안 옮김 · 공유 storage 는 브레인이 돌림. +공유 storage 는 브레인이 돌림. """ from __future__ import annotations @@ -193,6 +195,9 @@ def sc_adds(z: dict, q: Decimal, whole: Any, tag: dict) -> list[dict]: "이름": one["고정"].get("이름") or "", "수량": sd.plain(n), } + elif refs.is_copy(one.get("참조")): # 하위 호표 사본 블록 = 그 줄들도 더한줄로 + out += sc_adds(one["참조"]["몸"], n, whole, tag) + continue else: raise sd.DocError("비율 고정 줄 · 참조 줄은 단가산출에 못 담음") out.append({**entry, "본뜬": tag}) @@ -346,19 +351,13 @@ def _twice(doc: dict) -> str | None: def flat_sc(doc: dict, read: Read, whole: Any, trail: tuple = (), strict: bool = True) -> dict: - """SC 문서 → M01 키만 — SK 참조 = 로직 줄 · SC 참조 = 안 줄 펴 넣음(`strict` 만 · 넣기 길) · - `strict` False(저장 · 읽기 · 옮기기) = SC 참조 그대로(하위 호표 · 엑셀 끝수 단계) · 못 펼친 - 줄 그대로.""" - hands = (doc.get("본뜬") or {}).get("실무수량") - old_q = dict((hands or {}).get("줄") or {}) - new_q, mat_q = {}, dict((hands or {}).get("자재") or {}) - lines, mats = [], list(doc.get("자재") or []) + """SC 문서 → M01 키만 — SK 참조 = 로직 줄 + 패치(같은 자리) · SC 참조 = 사본 블록(같은 자리) · + `strict` False = 못 펼친 줄 그대로(겹침 · 바탕 없음 · 환산 · 없는 참조).""" + lines = [] for at, one in enumerate(doc.get("담은로직") or []): ref = one.get("참조") if isinstance(one, dict) else None kind = refs.kind_of(ref.get("키")) if isinstance(ref, dict) else None - here_q = old_q.get(str(at)) - if kind != "sangeun" and not (strict and kind == "precombo"): # 하위 호표 = 참조 그대로 - new_q |= {str(len(lines)): here_q} if here_q is not None else {} + if kind not in ("sangeun", "precombo") or refs.is_copy(ref): lines.append(one) continue try: @@ -366,80 +365,39 @@ def flat_sc(doc: dict, read: Read, whole: Any, trail: tuple = (), strict: bool = tag = mark(ref, src) if kind == "sangeun": y = flat_sk(sd.clean(src, None, whole), read, whole, path) - put, put_m = [sk_line(y, one, whole, tag)], [] - put_q = {"0": here_q} if here_q is not None else {} + put = sk_line(y, one, whole, tag) else: - z = flat_sc(src, read, whole, path) - put, put_q, put_m = _spread(z, one, here_q, tag) + body = flat_sc(src, read, whole, path) + put = {**one, "참조": refs.copy_ref(body, ref["키"]), "본뜬": tag} rest = (doc.get("담은로직") or [])[at + 1 :] - trial = {"담은로직": [*lines, *put, *rest], "자재": [*mats, *put_m]} + trial = {"담은로직": [*lines, put, *rest], "자재": doc.get("자재") or []} if twice := _twice(trial): raise sd.DocError(f"같은 줄이 겹침 「{twice}」 — 펼쳐 넣지 못함") except FAIL as e: if strict: raise sd.DocError(f"「{ref.get('키')}」 · {e}") from e - new_q |= {str(len(lines)): here_q} if here_q is not None else {} - lines.append(one) - continue - new_q |= {str(len(lines) + int(n)): v for n, v in put_q.items()} - mat_q |= {m["코드"]: m.pop("_q") for m in put_m if "_q" in m} - lines, mats = [*lines, *put], [*mats, *put_m] - out = {**doc, "담은로직": lines, "자재": mats} - if hands is not None: - out["본뜬"] = {**doc["본뜬"], "실무수량": {**hands, "줄": new_q, "자재": mat_q}} - return out - - -def _spread(z: dict, one: dict, here_q: Any, tag: dict) -> tuple[list, dict, list]: - """안 SC 줄 · 자재 펴기 — 실무수량 = 바깥 × 안(둘 다 있을 때만) · 비율 고정 줄 있으면 못 폄 - (밑 = 그 SC 합 → 바깥 합으로 바뀜).""" - if any(refs.is_rate(x) for x in z.get("담은로직") or []): - raise sd.DocError("비율 고정 줄이 있는 일위대가는 펴 넣지 못함(밑 합이 바뀜)") - hands = (z.get("본뜬") or {}).get("실무수량") or {} - inner, inner_m = hands.get("줄") or {}, hands.get("자재") or {} - outer = (one.get("수량") or {}).get("손") if isinstance(one.get("수량"), dict) else None - - def times( - x: dict, - ) -> Any: # 줄 수량(742-1) = 바깥 × 안 · 둘 중 하나라도 없으면 쓰는 쪽 몫(null) - inner = (x.get("수량") or {}).get("손") if isinstance(x.get("수량"), dict) else None - both = outer not in EMPTY and inner not in EMPTY - return {"손": sd.plain(_d(outer) * _d(inner))} if both else None - - lines = [{**x, "수량": times(x), "본뜬": tag} for x in z.get("담은로직") or []] - if len(lines) == 1 and one.get("별칭"): - lines[0]["별칭"] = one["별칭"] - both = here_q is not None - qty = { - str(n): sd.plain(_d(here_q) * _d(inner[str(n)])) - for n in range(len(lines)) - if both and inner.get(str(n)) is not None - } - mats = [] - for m in z.get("자재") or []: - extra = ( - {"_q": sd.plain(_d(here_q) * _d(inner_m[m["코드"]]))} - if both and m.get("코드") in inner_m - else {} - ) - mats.append({**m, "본뜬": tag, **extra}) - return lines, qty, mats + put = one + lines.append(put) + return {**doc, "담은로직": lines} # ── 문 ──────────────────────────────────────────────────────────────── -def flat_doc(kind: str, doc: Any, read: Read = refs.read_ref, strict: bool = False) -> Any: - """층 문서 하나 펼침(저장 · 읽기 · 복사 · 옮기기) — 참조 없으면 받은 그대로.""" +def flat_doc( + kind: str, doc: Any, read: Read = refs.read_ref, strict: bool = False, me: Any = None +) -> Any: + """층 문서 하나 펼침(저장 · 읽기 · 복사 · 옮기기) — 참조 없으면 받은 그대로 · `me` = 이 문서 + 참조(자기 자신을 담은 줄 = 안 펼침 → 저장 검사가 422).""" if not isinstance(doc, dict) or kind not in refs.PREFIX or not refs.refs_in(doc): return doc - whole = _whole() + whole, trail = _whole(), (refs.ref_id(me),) if isinstance(me, dict) else () if kind == "sangeun": - return flat_sk(doc, read, whole, (), strict) - return flat_sc(doc, read, whole, (), strict) + return flat_sk(doc, read, whole, trail, strict) + return flat_sc(doc, read, whole, trail, strict) def flat_lines(kind: str, ref: Any, qty: Any = 1, doc: Any = None) -> dict[str, Any]: """찾아 넣기(`-flat` 길) — `kind` 문서에 `ref` 를 넣을 줄 · 못 펼침 = StoreError 400 · - SC = 지금 화면 문서 `doc` 과 겹치면 400.""" + SC = 로직 줄(SK) · 사본 블록 줄(SC) 하나 · 지금 화면 문서 `doc` 과 겹치면 400.""" from M02_MasterTemplete import M02_MasterTemplete_Store as store at = refs.here() @@ -450,15 +408,27 @@ def flat_lines(kind: str, ref: Any, qty: Any = 1, doc: Any = None) -> dict[str, return {"더한줄": into_sk(ref, qty if qty not in EMPTY else 1, refs.read_ref, whole)} got = flat_sc({"담은로직": [{"참조": ref, "수량": None}]}, refs.read_ref, whole) host = doc if isinstance(doc, dict) else {} - trial = {k: [*(host.get(k) or []), *got[k]] for k in ("담은로직", "자재")} - if twice := _twice(trial): + trial = {"담은로직": [*(host.get("담은로직") or []), *got["담은로직"]]} + if twice := _twice({**trial, "자재": host.get("자재") or []}): raise sd.DocError(f"이미 담은 줄과 겹침 「{twice}」") - return {"담은로직": got["담은로직"], "자재": got["자재"]} + return {"담은로직": got["담은로직"], "자재": []} except FAIL as e: raise store.StoreError(getattr(e, "status", 400), str(e)) from e # ── 초기화 ──────────────────────────────────────────────────────────── +def _reprice(fx: dict) -> tuple[dict, int]: + """이음 기계 고정 줄(736-3) 단가 = M01 기계 한 시간 값 다시 받음 — (줄, 바뀐 수).""" + from M02_MasterTemplete import M02_Machine_Hourly as machine + + key = (fx.get("이음") or {}).get("기계") + got = machine.hourly(str(key)) if key else {} + if not got.get("ok"): + return fx, 0 + price = {k: got[k] for k in sd.PARTS.values()} + return {**fx, "단가": price}, int(price != fx.get("단가")) + + def _reset_adds(adds: list) -> tuple[list, int]: out, n = [], 0 for one in adds or []: @@ -474,31 +444,50 @@ def _reset_adds(adds: list) -> tuple[list, int]: if one.get("더한줄"): one["더한줄"], k = _reset_adds(one["더한줄"]) n += k + if isinstance(one.get("고정"), dict): + one["고정"], k = _reprice(one["고정"]) + n += k out.append(one) return out, n -def reset(kind: str, doc: dict) -> tuple[dict, int]: - """[초기화] — (걷은 문서, 걷힌 고침 수) · 먼저 펼침(옛 참조 줄도 M01 키로).""" +def _reset_sc(doc: dict) -> tuple[dict, int]: + """일위대가 초기화 — 줄 패치 · 사본 블록 몸 · 이음 기계 고정 줄.""" from M02_MasterTemplete import M02_Labor_Patch as patch + lines, n = [], 0 + for one in doc.get("담은로직") or []: + if not isinstance(one, dict): + lines.append(one) + continue + ref, body = one.get("참조"), one.get(patch.FIELD) + if refs.is_copy(ref): + inner, k = _reset_sc(ref["몸"]) + n += k + one = {**one, "참조": refs.copy_ref(inner, ref["키"])} + elif isinstance(one.get("고정"), dict): + fx, k = _reprice(one["고정"]) + n += k + one = {**one, "고정": fx} + elif body: + adds, k = _reset_adds(body.get("더한줄") or []) + n += k + len(body.get("덮개") or {}) + len(body.get("뺀줄") or []) + keep = {x: v for x, v in body.items() if x not in ("덮개", "뺀줄", "더한줄")} + rest = {**keep, **({"더한줄": adds} if adds else {})} + one = {x: v for x, v in one.items() if x != patch.FIELD} + one = {**one, **({patch.FIELD: rest} if rest else {})} + lines.append(one) + return {**doc, "담은로직": lines}, n + + +def reset(kind: str, doc: dict) -> tuple[dict, int]: + """[초기화] — (걷은 문서, 걷힌 고침 수) · 먼저 펼침(옛 참조 줄도 M01 키로).""" doc = flat_doc(kind, doc) if kind == "sangeun": adds, n = _reset_adds(doc.get("더한줄") or []) n += len(doc.get("덮개") or {}) + len(doc.get("뺀줄") or []) return {**doc, "덮개": {}, "뺀줄": [], "더한줄": adds}, n - lines, n = [], 0 - for one in doc.get("담은로직") or []: - body = one.get(patch.FIELD) if isinstance(one, dict) else None - if not body: - lines.append(one) - continue - adds, k = _reset_adds(body.get("더한줄") or []) - n += k + len(body.get("덮개") or {}) + len(body.get("뺀줄") or []) - rest = {**({"더한줄": adds} if adds else {})} - line = {x: v for x, v in one.items() if x != patch.FIELD} - lines.append({**line, **({patch.FIELD: rest} if rest else {})}) - return {**doc, "담은로직": lines}, n + return _reset_sc(doc) def reset_doc(kind: str, key: str | None, doc: Any, look: bool, version: str | None) -> dict: @@ -550,7 +539,9 @@ def migrate(apply: bool = False) -> list[dict[str, Any]]: ref = {"층": layer, "주인": owner, "키": path.stem} got = flat_doc(kind, doc) left = [ - r.get("키") for r in refs.refs_in(got) if refs.kind_of(r.get("키")) != "promote" + r.get("키") + for r in refs.refs_in(got) + if refs.kind_of(r.get("키")) != "promote" and not refs.is_copy(r) ] row = { "어디": where_, @@ -565,10 +556,7 @@ def migrate(apply: bool = False) -> list[dict[str, Any]]: row["전"], row["후"] = _value(ref, refs.read_ref, whole), _value(ref, mine, whole) row["같음"] = row["전"] == row["후"] - used = kind == "precombo" and len(got["담은로직"]) != len(doc.get("담은로직") or []) - if used and refs.users(ref): - row["까닭"] = "쓰는 곳 있는 일위대가 — 안 줄 차례가 바뀌어 안 옮김" - elif not row["같음"]: + if not row["같음"]: row["까닭"] = "전후 금액 다름 — 안 옮김" elif apply and got != doc: with store.at(path.parent.parent): diff --git a/resources/tester/test_707_7_whole_import.py b/resources/tester/test_707_7_whole_import.py index 5c6ee1442..e6a988a0c 100644 --- a/resources/tester/test_707_7_whole_import.py +++ b/resources/tester/test_707_7_whole_import.py @@ -181,8 +181,8 @@ def test_내_자료집에_저장_하위_호표_같이_한_번(world, monkeypatch saved = {p.stem: json.loads(p.read_text("utf-8")) for p in (folder / "precombo").glob("*.json")} for key in keys: for one in saved[key]["담은로직"]: - if "참조" in one: - assert one["참조"]["층"] == "personal" and one["참조"]["키"] in saved + if "참조" in one: # 740-1 하위 호표 = 사본 블록(본뜬 = 같이 만든 개인 SC) + assert one["참조"]["층"] == "사본" and one["본뜬"]["키"] in saved copied = [d["본뜬"]["복사원본"] for d in saved.values() if "복사원본" in (d.get("본뜬") or {})] assert len(copied) == len({json.dumps(x, sort_keys=True) for x in copied}) # 같은 하위 한 번 for p in (folder / "precombo").glob("*.json"): # 다음 시험에 층 쓰기 안 남김 diff --git a/resources/tester/test_733_2_library_ask.py b/resources/tester/test_733_2_library_ask.py index 1b891f2d0..4d6e3b03e 100644 --- a/resources/tester/test_733_2_library_ask.py +++ b/resources/tester/test_733_2_library_ask.py @@ -93,23 +93,23 @@ def test_공유_승인_회사_같은_내용_409_씀_새로(world): def test_복사_딸림_같은_원본_재사용(world): + """구조물 조합 복사 딸림(733-2) = 같은 원본 사본 재사용 · 740-1 — 일위대가 안 일위대가는 + 넣을 때 사본 블록(딸림 없음).""" client = _client(world) _as(world) - mine = layers.personal_dir(7, 42) url = LIB.format("personal", "precombo") inner = client.post(url, json=_doc("안")).json()["이름"] ref = {"층": "personal", "주인": "7/42", "키": inner} outer = {"문서": {"이름": "밖", "단위": "㎡", "담은로직": [{"참조": ref}]}} top = client.post(url, json=outer).json()["이름"] - with store.at(mine): - doc = store.read("precombo", top)["문서"] - src = {"층": "personal", "주인": "7/42", "키": top} + with store.at(layers.personal_dir(7, 42)): + assert store.read("precombo", top)["문서"]["담은로직"][0]["참조"]["층"] == "사본" company = layers.company_dir(7) + combo = {"종류": "일위대가", "열": "rv", "담은로직": [{"참조": ref}], "자재": []} with store.at(company): - one = refs.create("precombo", refs.carry(refs.origin_mark(doc, src), ("personal", "7/42"))) - assert _count(company) == 2 # 밖 + 딸린 안 - two = refs.create("precombo", refs.carry(refs.origin_mark(doc, src), ("personal", "7/42"))) - assert _count(company) == 3 # 다시 복사 = 밖만 새로 · 딸린 안 = 앞 사본 재사용 - with store.at(company): - got = [store.read("precombo", m["이름"])["문서"]["담은로직"][0]["참조"] for m in (one, two)] + one = refs.carry(combo, ("personal", "7/42")) + assert _count(company) == 1 # 딸린 안 = 회사 새 키 + two = refs.carry(combo, ("personal", "7/42")) + assert _count(company) == 1 # 다시 복사 = 앞 사본 재사용 + got = [x["담은로직"][0]["참조"] for x in (one, two)] assert got[0] == got[1] and got[0]["층"] == "company" diff --git a/resources/tester/test_735_1_library_delete.py b/resources/tester/test_735_1_library_delete.py index b18e95d9f..583d2ffcb 100644 --- a/resources/tester/test_735_1_library_delete.py +++ b/resources/tester/test_735_1_library_delete.py @@ -136,7 +136,9 @@ def test_폴더_통째_한_기록_되돌림_자리까지_그새_고친_문서는 a = _sc(me, "가", [FIX]) b = _sc(me, "나", [FIX]) _labor(me, [{"참조": {**MINE, "키": a}, "수량": {"손": 1}}, FIX]) - other = _sc(me, "다른", [{"참조": {**MINE, "키": b}}, FIX]) + with store.at(me): # 쓰는 곳 = 구조물 조합(740-1 — 일위대가 안 일위대가는 사본 블록) + other = [{"참조": {**MINE, "키": b}, "수량": {"손": 1}}, FIX] + store.write("labor", "x2", "", {"열": "x2", "담은로직": other, "자재": []}) assert _tree(api, op="add-folder", 경로=[], 이름="묶음").status_code == 200 for key in (a, b): assert _tree(api, op="move", 키=key, 새경로=["묶음"]).status_code == 200 @@ -147,9 +149,10 @@ def test_폴더_통째_한_기록_되돌림_자리까지_그새_고친_문서는 assert res.status_code == 200, res.text name = res.json()["알림"]["기록"] assert [g["키"] for g in api.get(SAME).json()["합친것"]] == [name] - with store.at(me): # 그새 「다른」 을 고침 → 그 문서는 앞판으로 안 돌림 - have = store.read("precombo", other) - store.write("precombo", other, have["판"], {**have["문서"], "이름": "다른 고침"}) + with store.at(me): # 그새 x2 를 고침 → 그 문서는 앞판으로 안 돌림 + have = store.read("labor", "x2") + lines = [{**x, "메모": "고침"} for x in have["문서"]["담은로직"]] + store.write("labor", "x2", have["판"], {**have["문서"], "담은로직": lines}) back = api.post(SAME + "/undo", json={"키": name}).json() assert back["되돌린곳수"] == 1 and "그새 고침" in back["못한곳"][0] assert (me / "precombo" / f"{a}.json").is_file() and (me / "precombo" / f"{b}.json").is_file() diff --git a/resources/tester/test_740_1_library_flat.py b/resources/tester/test_740_1_library_flat.py index 4eb4bb1ea..e7b9ceb9c 100644 --- a/resources/tester/test_740_1_library_flat.py +++ b/resources/tester/test_740_1_library_flat.py @@ -140,7 +140,8 @@ def test_SC_에_SK_패치_겹침_E1_원본_고쳐도_그대로(world): doc = _read(me, "precombo", host) assert doc["담은로직"][0]["로직"] == PCT and doc["담은로직"][0]["본뜬"]["키"] == sk assert doc["담은로직"][0]["패치"]["덮개"] == {"labor:LB000036#0": {"qty": 0.5}} - assert doc["담은로직"][1]["참조"]["키"] == inner # 하위 호표(SC 안 SC) = 참조 그대로 + assert doc["담은로직"][1]["참조"]["층"] == "사본" # 하위 호표(SC 안 SC) = 사본 블록 + assert doc["담은로직"][1]["본뜬"]["키"] == inner assert _sc_value(me, host) == pytest.approx(before, rel=1e-9) with store.at(me): # E1 — 원본 SK 의 덮지 않은 줄 품 2 now = store.read("sangeun", sk) @@ -151,33 +152,28 @@ def test_SC_에_SK_패치_겹침_E1_원본_고쳐도_그대로(world): assert _sc_value(me, host) == pytest.approx(before, rel=1e-9) # 펼친 것 = 그대로 -def test_SC_에_SC_펴넣기_실무수량_겹침_넣기_400(world): +def test_SC_에_SC_사본_블록_실무수량_겹침_원본_지워도(world): + """하위 호표(SC 안 SC) = 사본 블록 — 원본 연결 없음 · 금액 같음 · 같은 기초 겹침도 블록 안.""" me = world["personal"] hands = {"층": "실무자료", "실무수량": {"줄": {"0": 3}, "자재": {}}} inner = _make(me, "precombo", 이름="안", 담은로직=[{"로직": PCT}], 본뜬=hands) - outer_hands = {"층": "실무자료", "실무수량": {"줄": {"0": 2, "1": 5}, "자재": {}}} - lines = [{"로직": GF}, {"참조": {**MINE, "키": inner}, "별칭": "안쪽"}] - old = _raw(me, "precombo", 이름="옛", 담은로직=lines, 본뜬=outer_hands) - assert flat.flat_doc("precombo", _read(me, "precombo", old))["담은로직"][1][ - "참조" - ] # 저장 · 읽기 - got = flat.flat_sc(_read(me, "precombo", old), refs.read_ref, _whole()) # 넣기 길 = 펴 넣음 - assert [x["로직"] for x in got["담은로직"]] == [GF, PCT] and got["담은로직"][1][ - "별칭" - ] == "안쪽" - assert got["본뜬"]["실무수량"]["줄"] == {"0": 2, "1": 15} # 바깥 5 × 안 3 - - def mine(r): - return got if r["키"] == old else refs.read_ref(r) - - assert _sc_value(me, old, mine) == pytest.approx(_sc_value(me, old), rel=1e-9) - # 같은 기초가 겹침 — 넣기 길 = 400 - dup = [{"로직": PCT}, {"참조": {**MINE, "키": inner}}] - kept = _make(me, "precombo", 이름="겹침", 담은로직=dup) - assert _read(me, "precombo", kept)["담은로직"][1]["참조"]["키"] == inner - with store.at(me), pytest.raises(store.StoreError) as e: - flat.flat_lines("precombo", {**MINE, "키": inner}, None, {"담은로직": [{"로직": PCT}]}) - assert e.value.status == 400 and "겹침" in str(e.value.detail) + lines = [{"로직": PCT}, {"참조": {**MINE, "키": inner}, "별칭": "안쪽"}] # 같은 기초 겹침 + old = _raw(me, "precombo", 이름="옛", 담은로직=lines) + before = _sc_value(me, old) + host = _make(me, "precombo", 이름="밖", 담은로직=lines) + line = _read(me, "precombo", host)["담은로직"][1] + block = line["참조"] + assert block["층"] == "사본" and block["키"] == inner and line["본뜬"]["키"] == inner + assert block["몸"]["담은로직"][0]["로직"] == PCT and line["별칭"] == "안쪽" + assert block["몸"]["본뜬"]["실무수량"]["줄"] == {"0": 3} # 엑셀 대조 · 하위 호표 끝수 + assert _sc_value(me, host) == pytest.approx(before, rel=1e-9) + with store.at(me): + got = flat.flat_lines("precombo", {**MINE, "키": old}, None, {"담은로직": [{"로직": GF}]}) + (me / "precombo" / f"{old}.json").unlink() # 옛 꼴 문서(참조) 치움 + assert refs.users({**MINE, "키": inner}) == [] + store.delete("precombo", inner) + assert _sc_value(me, host) == pytest.approx(before, rel=1e-9) # 원본 지워도 그대로 + assert got["담은로직"][0]["참조"]["층"] == "사본" # 넣기 길도 블록(겹침 없음) def test_SK_에_SC_줄마다_더한줄(world): @@ -192,10 +188,9 @@ def test_SK_에_SC_줄마다_더한줄(world): hand = flat.sc_adds({"담은로직": [{"로직": GF, "수량": {"손": 3}}]}, Decimal(2), _whole(), {}) assert hand[0]["수량"] == 6 - lines, _, _ = flat._spread( - {"담은로직": [{"로직": GF, "수량": {"손": 3}}]}, {"수량": {"손": 2}}, None, {} - ) - assert lines[0]["수량"] == {"손": 6} + block = {"참조": refs.copy_ref({"담은로직": [{"로직": GF, "수량": {"손": 3}}]}, "SC-000001")} + deep = flat.sc_adds({"담은로직": [{**block, "수량": {"손": 2}}]}, Decimal(1), _whole(), {}) + assert deep[0]["로직"] == GF and deep[0]["수량"] == 6 # 사본 블록 = 바깥 × 안 doc = {"키": "SK-000099", "이름": "x", "단위": "㎥", "바탕": None, "더한줄": got} one = float(labor.run_line(_whole(), GF, {})["기준"]["계"]) assert _sk_total(me, doc) == pytest.approx(2 * one, rel=1e-9) @@ -267,7 +262,20 @@ def test_초기화_SK_SC_M01_값_옵션_남음(world): with store.at(me): got = flat.reset_doc("precombo", sc, None, False, None) line = _read(me, "precombo", sc)["담은로직"][0] - assert got["걷힘"] == 2 and line["패치"] == {"더한줄": [{**ADD, "수량": 0.5}]} + assert got["걷힘"] == 2 and line["패치"] == {"더한줄": [{**ADD, "수량": 0.5, "처음수량": 0.5}]} + # 이음 기계 고정 줄(736-3) = M01 기계 한 시간 값 다시 받음 + from M02_MasterTemplete import M02_Machine_Hourly as machine + + hour = machine.hourly("EQ000019") + fx = {"이름": "굴착기", "단위": "시간", "단가": {"노무비": 1, "재료비": 1, "경비": 1}} + fx["이음"] = {"기계": "EQ000019"} + mc = _make(me, "precombo", 이름="기계", 담은로직=[{"고정": fx}]) + with store.at(me): + assert flat.reset_doc("precombo", mc, None, False, None)["걷힘"] == 1 + price = _read(me, "precombo", mc)["담은로직"][0]["고정"]["단가"] + assert price["노무비"] == pytest.approx(hour["노무비"]) and price["경비"] == pytest.approx( + hour["경비"] + ) def test_길_넣기_초기화_권한(world): @@ -289,7 +297,7 @@ def test_길_넣기_초기화_권한(world): assert firm.status_code == 403 # 일반 사용자 회사 층 쓰기 막힘 -def test_옮기기_함수_전후_같음_겹침_남김(world): +def test_옮기기_함수_전후_같음_사본_블록(world): me = world["personal"] y = _y(me) inner = _make(me, "precombo", 이름="안", 담은로직=[{"로직": GF}]) @@ -314,18 +322,18 @@ def test_옮기기_함수_전후_같음_겹침_남김(world): sk: _sk_total(me, _read(me, "sangeun", sk)) } look = {r["키"]: r for r in flat.migrate(False)} - assert look[sk]["같음"] and look[sc]["같음"] and not look[sc]["씀"] - assert look[sc]["남은참조"] == look[dup]["남은참조"] == [inner] # 하위 호표 = 그대로 + assert all(look[k]["같음"] and look[k]["펼침"] and not look[k]["씀"] for k in (sk, sc, dup)) rows = {r["키"]: r for r in flat.migrate(True)} - assert rows[sk]["씀"] and rows[sc]["씀"] and not rows[dup]["씀"] + assert all(rows[k]["씀"] for k in (sk, sc, dup)) assert not refs.refs_in(json.loads((me / "sangeun" / f"{sk}.json").read_bytes())) - left = refs.refs_in(json.loads((me / "precombo" / f"{sc}.json").read_bytes())) - assert [r["키"] for r in left] == [inner] + for key in (sc, dup): # 하위 호표 = 사본 블록만 남음(원본 연결 없음) + left = refs.refs_in(json.loads((me / "precombo" / f"{key}.json").read_bytes())) + assert [(r["층"], r["키"]) for r in left] == [("사본", inner)] assert _sk_total(me, _read(me, "sangeun", sk)) == pytest.approx(before[sk], rel=1e-9) assert _sc_value(me, sc) == pytest.approx(before[sc], rel=1e-9) assert _sc_value(me, dup) == pytest.approx(before[dup], rel=1e-9) - again = flat.migrate(False) # 두 번째 = 하위 호표 참조만 남음(바꿀 것 없음) - assert [r["키"] for r in again] == [sc, dup] and all(r["남은참조"] == [inner] for r in again) + again = flat.migrate(True) # 두 번째 = 바꿀 것 없음 + assert all(r["펼침"] and not r["씀"] for r in again) def test_733_지문_옛_참조_펼친_것_같음(world): diff --git a/resources/tester/test_m02_hopyo_build.py b/resources/tester/test_m02_hopyo_build.py index d3f4f6293..807dbaf59 100644 --- a/resources/tester/test_m02_hopyo_build.py +++ b/resources/tester/test_m02_hopyo_build.py @@ -250,7 +250,9 @@ def test_하위_호표_먼저_확인_퍼센트_빠짐_통째(client): mine = layers.personal_dir(7, 42) made = _doc(mine, got["참조"]["키"]) line = made["담은로직"][0] - assert line["참조"] == child["참조"] and line["별칭"] == "고화매트재료비" + # 740-1 하위 호표 = 사본 블록(원본 연결 없음 · 키 = 출처) + assert line["참조"]["층"] == "사본" and line["참조"]["키"] == child["참조"]["키"] + assert line["본뜬"]["키"] == child["참조"]["키"] and line["별칭"] == "고화매트재료비" assert line["수량"] is None and made["본뜬"]["실무수량"]["줄"]["0"] == 2.3 # 723-1 assert "고화매트재료비||m2" not in _book(mine) # 호표 참조 줄 = 장부 안 씀 whole = _build(client, 3, 통째={"대상": [{"로직": "GF000101", "배": 1}]}) diff --git a/resources/tester/test_m02_labor_ref.py b/resources/tester/test_m02_labor_ref.py index b3534e2ef..45c5e0dce 100644 --- a/resources/tester/test_m02_labor_ref.py +++ b/resources/tester/test_m02_labor_ref.py @@ -179,18 +179,18 @@ def test_사전조합_틀린_줄_400_순환_422(world): with pytest.raises(store.StoreError) as e: _combo(world["system"], lines) assert e.value.status == 400 + # 740-1 — 사전조합에 사전조합 = 사본 블록(원본 연결 없음) · 되돌려 담아도 순환 없음 a = _combo(world["system"], [one], "가") b = _combo(world["system"], [{"참조": {**SYS, "키": a}}], "나") with store.at(world["system"]): + line = store.read("precombo", b)["문서"]["담은로직"][0] + assert line["참조"]["층"] == "사본" and line["본뜬"]["키"] == a + assert line["참조"]["몸"]["담은로직"][0]["참조"] == one["참조"] have = store.read("precombo", a) - with pytest.raises(store.StoreError) as e: - store.write( - "precombo", - a, - have["판"], - {**have["문서"], "담은로직": [{"참조": {**SYS, "키": b}}]}, - ) - assert e.value.status == 422 and f"{a} → {b} → {a}" in e.value.detail + store.write( + "precombo", a, have["판"], {**have["문서"], "담은로직": [{"참조": {**SYS, "키": b}}]} + ) + assert store.read("precombo", a)["문서"]["담은로직"][0]["참조"]["층"] == "사본" def test_층_규칙은_위_층만(world): @@ -329,11 +329,12 @@ def test_복사는_못_읽는_참조를_같이_복사_본뜬(world): ) got = _read(world["company"], "precombo", made["이름"]) assert got["본뜬"] == {**MINE, "키": sc} - assert [one["참조"] for one in got["담은로직"]] == [ - {**COMPANY, "키": "SC-000001"}, # 회사가 못 읽는 개인 사전조합 → 회사 층 새 키 - {**SYS, "키": sys_sp}, # 마스터는 그대로(옛 승격도) - ] - assert _read(world["company"], "precombo", "SC-000001")["본뜬"] == {**MINE, "키": inner} + # 740-1 — 개인 사전조합은 넣을 때 사본 블록(본뜬) → 회사로 따로 복사 안 함 · + # 마스터 옛 승격 그대로 + block = got["담은로직"][0] + assert block["참조"]["층"] == "사본" and block["본뜬"]["키"] == inner + assert got["담은로직"][1]["참조"] == {**SYS, "키": sys_sp} + assert made["이름"] == "SC-000001" # 딸림 복사 없음 # 못 읽는 옛 개인 승격은 따라 복사 안 함(134-2 · 승격 서고 없음) mine_sp = _promote(world["personal"]) with store.at(world["company"]), pytest.raises(store.StoreError) as e: