From e9106ae4d68a44af45888a885e660341d2da80a9 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 21:47:26 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(720-2):=20=EB=8B=A8=EA=B0=80=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=C2=B7=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20?= =?UTF-8?q?=EC=B8=B5=20=EC=A4=84=20=E3=80=8C+=E3=80=8D=20=3D=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=AC=BC=20=EC=A1=B0=ED=95=A9=EA=B3=BC=20=EA=B0=99?= =?UTF-8?q?=EC=9D=80=20=EC=9E=90=EB=A6=AC(=EC=A4=84=20=EC=98=A4=EB=A5=B8?= =?UTF-8?q?=EC=AA=BD=20=EB=81=9D=20m02-tree=5F=5Ftools)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- M02_MasterTemplete/M02_MasterTemplete_UI_Side_Library.ts | 4 +++- resources/tester/test_720_m02_folder_layer.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Library.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Library.ts index 5bdbad680..7bb16a460 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Library.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side_Library.ts @@ -143,6 +143,8 @@ export function buildLibrarySection(opt: LibraryOptions): LibraryHandle { add.classList.add("m02-tree__add"); add.title = L("M02_TreeAddTop"); add.setAttribute("aria-label", L("M02_TreeAddTop")); + // 구조물 조합 줄과 같은 자리(줄 오른쪽 끝 · `m02-tree__tools`) + const addTools = el("span", { className: "ui-toolbar m02-tree__tools", children: [add] }); const leaves = new Map(); const toggled = new Map(); // 손으로 펴고 접은 폴더 — 다시 그려도 그대로 const targets = new Map(); @@ -470,7 +472,7 @@ export function buildLibrarySection(opt: LibraryOptions): LibraryHandle { const mine = ++seq; const at = opt.scope(); add.hidden = !at.canWrite; - barHost.replaceChildren(opt.bar(add)); + barHost.replaceChildren(opt.bar(addTools)); if (!keep) treeHost.replaceChildren(createEmpty({ text: tc("Loading"), small: true })); const [got, tree] = await Promise.all([ // 못 받으면 빈 나무(목록표에서 까닭) · 폴더를 못 받으면 문서는 모두 뿌리 diff --git a/resources/tester/test_720_m02_folder_layer.py b/resources/tester/test_720_m02_folder_layer.py index d22012be0..511e97ab0 100644 --- a/resources/tester/test_720_m02_folder_layer.py +++ b/resources/tester/test_720_m02_folder_layer.py @@ -64,7 +64,7 @@ def test_화면_층_단추_최상위_폴더_이름_바꾸기(): # 720-2 — 층 줄 「+」 = 서고 칸 맨 위 메뉴 · 메뉴 줄 = 같은 addFolder([]) assert page.count(" add,\n") == 3 # 구조물 조합 · 일위대가 · 단가산출 assert "onClick: (event) => openTreeMenu(event, menuAt([]))" in lib - assert "add.hidden = !at.canWrite;" in lib and "opt.bar(add)" in lib + assert "add.hidden = !at.canWrite;" in lib and "opt.bar(addTools)" in lib assert lib.count('L("M02_TreeAddTopFolder"), () => void addFolder([])') == 1 # 폴더 줄 assert '{ label: L("M02_TreeAddTopFolder"), run: () => void addFolder([])' in lib # 문서 줄 tree = _text("M02_MasterTemplete_TreePanel.ts") From 7d007e4feb65371134d047d31468c3cfa4415415 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 21:51:27 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(723-1):=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80(=EC=82=AC=EC=A0=84=EC=A1=B0=ED=95=A9)=20=EC=95=88=20?= =?UTF-8?q?=EC=A4=84=20=EC=88=98=EB=9F=89=20=EC=A0=80=EC=9E=A5=20=EC=95=88?= =?UTF-8?q?=20=ED=95=A8=20=E2=80=94=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=95=A9=20=EC=95=88=20=EC=A4=84=20=EC=95=8C=EC=95=BD?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20=C2=B7=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20=EC=88=98=EB=9F=89=20=3D=20=EB=B3=B8=EB=9C=AC=20?= =?UTF-8?q?=EC=B6=9C=EC=B2=98=20=EA=B8=B0=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 저장(`M02_Labor_Ref.no_qty`) = 안 줄 · 자재 수량 늘 null(비율 고정 줄 율만) · 기본 {손: 1} 걷음 · 굳힌 사본도 걷음 - 펼침(`Expand`) = 안 줄 수량 = 쓰는 쪽 given 알약 「참조|층|주인|키#차례」 · 「…#자재|코드」 · 층 원본 옛 손 안 씀 · 굳힌 사본(프로젝트) 손 · 비율 줄 율만 · 없으면 「수량 없음」(1 아님 · 그 일위대가 기준 없음) - 구조물 조합 알약(`pills`) = 참조 줄 + 안 줄 알약(`inner_pills`) — 면적산출 칸에 이음 - 불러오기(`Hopyo_Build`) = 수량 안 들임 · 엑셀 수량 = `본뜬.실무수량`(셈 안 씀 · 엑셀 대조만 · 하위 호표까지) - B08 `_ref_mats` = 안 줄 수량 = 굳힌 손 · 아니면 사본 면적 알약 값 - 낡은 시험 고침(사유 = 수량 저장 안 함 · 이은 값으로 셈): test_m02_labor_ref · test_175_precombo_mats · test_715_3_labor_patch · test_m02_hopyo_build · test_m02_hopyo_flow · test_160_3(굳힌 옛 사본 꼴 손 채움) - 남김: B09 시험 5(test_204_2 · test_703 · test_719_1 · test_719_2) = sub8 723-4 가 B09 코드와 함께 Co-Authored-By: Claude Opus 5.5 --- B08_Quantity/B08_Quantity_Router_Material.py | 31 +++++-- M02_MasterTemplete/M02_Hopyo_Build.py | 27 +++++- M02_MasterTemplete/M02_Labor_Doc.py | 11 +-- M02_MasterTemplete/M02_Labor_Expand.py | 91 ++++++++++++++++--- M02_MasterTemplete/M02_Labor_Ref.py | 30 +++++- .../tester/test_160_3_b08_unmatched_total.py | 3 +- resources/tester/test_175_precombo_mats.py | 63 +++++++++---- resources/tester/test_715_3_labor_patch.py | 6 +- resources/tester/test_m02_hopyo_build.py | 31 ++++--- resources/tester/test_m02_hopyo_flow.py | 6 +- resources/tester/test_m02_labor_ref.py | 49 ++++++---- 11 files changed, 261 insertions(+), 87 deletions(-) diff --git a/B08_Quantity/B08_Quantity_Router_Material.py b/B08_Quantity/B08_Quantity_Router_Material.py index bed6b5f55..483076076 100644 --- a/B08_Quantity/B08_Quantity_Router_Material.py +++ b/B08_Quantity/B08_Quantity_Router_Material.py @@ -58,11 +58,14 @@ def _qty(value: Any) -> float | None: return _num(float(value) if isinstance(value, Decimal) else value) -def _ref_mats(whole: Any, read: Any, ref: dict, trail: tuple[str, ...] = ()) -> list[tuple]: +def _ref_mats( + whole: Any, read: Any, ref: dict, trail: tuple[str, ...] = (), given: dict | None = None +) -> list[tuple]: """참조 문서 1 단위 재료 `[(재료 줄, 수량)]` — 층 셈 펼침(`M02_Labor_Ref.Expand`)과 같은 길: - 승격 = 원 로직 재료 ÷ 배 · 사전조합 = 안 줄 재료 × 손 수량(빈칸 1) · 로직 줄은 알약 배로 나눔 · - 사전조합 자재(175-2) = 손 수량(빈칸 = 안 셈 · 품목 키 = 코드 · `자재` 표시 = 연료 거름 안 함) · - 순환 · 깊이 넘음 · 없는 문서 = 빈 것.""" + 승격 = 원 로직 재료 ÷ 배 · 사전조합 = 안 줄 재료 × 안 줄 수량(굳힌 손 · 아니면 면적 알약 + 「참조|…#차례」 값 `given` · 723-1 · 없으면 안 셈) · 로직 줄은 알약 배로 나눔 · + 사전조합 자재(175-2) = 손 · 알약 「…#자재|코드」(없음 = 안 셈 · 품목 키 = 코드 · `자재` 표시 = + 연료 거름 안 함) · 순환 · 깊이 넘음 · 없는 문서 = 빈 것.""" from M01_MasterData import M01_MasterData_Store_Combo as combo_store from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete import M02_Labor_Ref as refs @@ -90,17 +93,23 @@ def _ref_mats(whole: Any, read: Any, ref: dict, trail: tuple[str, ...] = ()) -> pills = kept if isinstance(kept, list) else combo_store.combo_pills({"담은로직": lines})["물량"] by_logic = {code: pill for pill in pills for code in pill.get("로직") or []} out: list[tuple] = [] - for one in lines: + given = given or {} + + def qty_of(one: dict, key: str) -> float | None: # 723-1 — 굳힌 손 · 아니면 이은 칸 값 + hand = _qty((one.get("수량") or {}).get("손")) + return _qty(given.get(f"{rid}#{key}")) if hand is None else hand + + for at, one in enumerate(lines): if isinstance(one.get("참조"), dict): - sub = _ref_mats(whole, read, one["참조"], (*trail, rid)) + sub = _ref_mats(whole, read, one["참조"], (*trail, rid), given) elif one.get("로직"): sub = run(one, (by_logic.get(one["로직"]) or {}).get("배")) else: continue - hand = _qty((one.get("수량") or {}).get("손")) - out += [(m, q * (1.0 if hand is None else hand)) for m, q in sub] + if (hand := qty_of(one, str(at))) is not None: + out += [(m, q * hand) for m, q in sub] for mat in doc.get("자재") or []: - if hand := _qty((mat.get("수량") or {}).get("손")): + if hand := qty_of(mat, f"자재|{mat.get('코드')}"): line = {k: mat.get(k) for k in ("이름", "단위")} out.append(({**line, "품목": {"키": mat.get("코드")}, "자재": True}, hand)) return out @@ -111,6 +120,7 @@ def _folder_units(root: str | Path, folder: str, meta: dict[str, dict]) -> dict[ (키 = 고른 품목 MT 코드 · 없으면 이름|단위 · 재료 수량 × 줄 수량 ÷ 배) + 참조 줄 안 재료 (160-3) · 줄 수량 빈칸 · 0 · 파형강관 · 기계 연료는 안 담음 · 셈 못 하면 빈 것.""" from M02_MasterTemplete import M02_Item_Copy as copies + from M02_MasterTemplete import M02_Labor_Doc as labor_doc from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete.M02_Item_Prices import load_prices from M02_MasterTemplete.M02_Item_Source import LABOR_FILE, PRICE_FILE, read_json @@ -149,6 +159,7 @@ def _folder_units(root: str | Path, folder: str, meta: dict[str, dict]) -> dict[ frozen = (labor.get("참조사본") if isinstance(labor, dict) else None) or {} whole = None # 사본 단가 — 참조 줄이 있을 때만 읽음 + given = labor_doc.area_values(read_json(copies.folder_of(root, folder) / copies.KINDS["면적"])) def read(ref: dict) -> Any: return frozen.get(refs.ref_id(ref)) @@ -159,7 +170,7 @@ def _folder_units(root: str | Path, folder: str, meta: dict[str, dict]) -> dict[ continue if isinstance(line.get("참조"), dict): # 참조 줄 = 배 1 whole = load_prices(bundle) if whole is None else whole - for mat, per in _ref_mats(whole, read, line["참조"]): + for mat, per in _ref_mats(whole, read, line["참조"], given=given): logic_mat(mat, per * qty) continue for mat in line.get("재료") or []: diff --git a/M02_MasterTemplete/M02_Hopyo_Build.py b/M02_MasterTemplete/M02_Hopyo_Build.py index 38aaf6f1a..a34230d62 100644 --- a/M02_MasterTemplete/M02_Hopyo_Build.py +++ b/M02_MasterTemplete/M02_Hopyo_Build.py @@ -25,6 +25,8 @@ 통째 대신 · 로직 자재 겹침 · 전국만. `본뜬.품셈` = 품셈으로 넣은 엑셀 줄 차례([품셈으로 바꾸기] 첫 체크) · `줄수 {품셈, 고정, 호표, 빠짐, 이음, 이음후보}` · 만든 값 ↔ 엑셀 호표 단가(`비교` · 같음 = 비목마다 1 원 안). 엔진 · 일위대가 계산은 그대로(`M02_Labor_Ref.Expand`). +수량(723-1 · 내역 17-1) = 저장 때 걷음(`refs.no_qty`) · 엑셀 수량은 `본뜬.실무수량 {줄: {차례: 수}, 자재: {코드: 수}}` +(출처 기록 · 셈 안 씀 · 엑셀 대조 `compare` 만 — 하위 호표 것까지 `excel_given`). """ from __future__ import annotations @@ -90,7 +92,12 @@ def confirmed(folder: str | Path, source: refs.Here | None = None) -> dict[str, def _save(doc: dict[str, Any]) -> tuple[dict[str, Any], bool]: - """(참조, 다시씀) — 같은 본뜬(키 · 원본)이 있으면 그 키에 덮음 · 없으면 새 키.""" + """(참조, 다시씀) — 같은 본뜬(키 · 원본)이 있으면 그 키에 덮음 · 없으면 새 키 · 수량 걷음(723-1).""" + hands = { + "줄": {str(i): x["수량"]["손"] for i, x in enumerate(doc["담은로직"]) if x.get("수량")}, + "자재": {m["코드"]: m["수량"]["손"] for m in doc.get("자재") or [] if m.get("수량")}, + } + doc = {**refs.no_qty(doc), "본뜬": {**doc["본뜬"], "실무수량": hands}} at = refs.here() want = doc["본뜬"] have = next( @@ -411,10 +418,24 @@ def _excel_round(got: dict[str, Any], how: str, by_line: bool) -> Decimal | None return sum(v.to_integral_value(how) for v in parts.values()) +def excel_given(ref: dict[str, Any], depth: int = 1) -> dict[str, Any]: + """`본뜬.실무수량` → 안 줄 알약 값(723-1 · 하위 사전조합까지) — 엑셀 대조 전용.""" + doc = refs.read_ref(ref) if depth <= refs.DEPTH else None + if not isinstance(doc, dict): + return {} + rid, hands = refs.ref_id(ref), (doc.get("본뜬") or {}).get("실무수량") or {} + out = {f"{rid}#{i}": v for i, v in (hands.get("줄") or {}).items()} + out |= {f"{rid}#자재|{code}": v for code, v in (hands.get("자재") or {}).items()} + for one in refs.refs_in(doc): + out |= excel_given(one, depth + 1) + return out + + def compare(ref: dict[str, Any], hopyo: dict[str, Any]) -> dict[str, Any]: """만든 사전조합 1 단위 값 ↔ 엑셀 호표 단가(참고 · 판정 아님) — 우리 = 엑셀 끝수 규칙(내림 · - 반올림 중 엑셀과 같은 쪽 · `_excel_round`).""" - got = refs.Expand(m01.loaded()[1])({k: ref[k] for k in ("층", "주인", "키")}) + 반올림 중 엑셀과 같은 쪽 · `_excel_round`) · 수량 = 엑셀 수량(`excel_given` · 723-1).""" + head = {k: ref[k] for k in ("층", "주인", "키")} + got = refs.Expand(m01.loaded()[1], given=excel_given(head))(head) base = got.get("기준") or {} excel = (hopyo.get("금액") or {}).get("계") cuts = [_excel_round(got, *one) for one in CUTS] if base else [] diff --git a/M02_MasterTemplete/M02_Labor_Doc.py b/M02_MasterTemplete/M02_Labor_Doc.py index 71d496dd0..e975e76aa 100644 --- a/M02_MasterTemplete/M02_Labor_Doc.py +++ b/M02_MasterTemplete/M02_Labor_Doc.py @@ -12,8 +12,8 @@ 기준단가가 된 칸 이름(`기준칸`)도 실음(43-3). 켜쌓기율(85-2) = 표 주문 「골쌓기 기준」 로직만 · % · 없음 = 100 · 그 로직 세부항목 줄 전부 수량 × 율 (`layered` · `run_line` 한 곳 → 문서 · 프로젝트 사본 · 서고 같은 함수). -참조 줄(단가승격 · 사전조합 · 50-3) = 기준이 가리킨 문서 1 단위 값(펼침 `M02_Labor_Ref.Expand`) · -분모 = 그 문서 단위 · 배 1 · 알약 키 = `ref_id` · 같은 참조 두 번 금지 · 위 층만. +참조 줄(단가승격 · 사전조합 · 50-3) = 기준이 가리킨 문서 1 단위 값(펼침 `M02_Labor_Ref.Expand` · 안 줄 +수량 = 같은 given 의 안 줄 알약 · 723-1) · 분모 = 그 문서 단위 · 배 1 · 알약 키 = `ref_id` · 같은 참조 두 번 금지. 짝(CLAUDE.md 5장 ①): 수량 · 반영 · 합 `settle` ↔ `M02_MasterTemplete_Labor_Calc.ts` — 거울 시험 `resources/tester/test_m02_labor_calc_mirror.py`. 규격 열(704-4) = 문서 `규격?` 칸 · 열마다 줄 수량(`M02_Labor_Spec` · settle `col`) · 계산값 `규격합`. @@ -354,7 +354,7 @@ def pills(doc: dict) -> list[dict[str, Any]]: """면적산출 알약 — 로직 물량 알약(`combo_pills` 그대로) + 참조 알약(`refs.pill`) + 자재 알약 「자재|MT키」.""" logic = combo_store.combo_pills({"담은로직": doc.get("담은로직") or []})["물량"] - logic += [refs.pill(one["참조"]) for one in doc.get("담은로직") or [] if _is_ref(one)] + logic += [p for one in doc.get("담은로직") or [] if _is_ref(one) for p in refs.pills_of(one["참조"])] # fmt: skip mats = [ { "키": material_key(m["코드"]), @@ -507,8 +507,8 @@ def calc( 프로젝트 사본 셈(142-1)도 이 한 벌 — `whole` = 사본 `단가.json`(`M02_Item_Prices.load_prices`) · `read` = 참조 → 굳힌 `참조사본` · `pills` = 사본 `알약`(가져온 때 것). 없으면 지금 M01 · 층.""" - whole = m01.loaded()[1] if whole is None else whole - expand = refs.Expand(whole) if read is None else refs.Expand(whole, read) + whole, have = m01.loaded()[1] if whole is None else whole, _area_of(doc.get("열")) if given is None else given # fmt: skip + expand = refs.Expand(whole, read or refs.read_ref, have) # 사전조합 안 줄 수량 = 알약 값(723-1) if pills is None: pills = combo_store.combo_pills({"담은로직": doc.get("담은로직") or []})["물량"] by_logic = {code: pill for pill in pills for code in pill.get("로직") or []} @@ -541,7 +541,6 @@ def calc( } for one in doc.get("자재") or [] ] - have = _area_of(doc.get("열")) if given is None else given return settle(_json({"줄": lines, "자재": mats}), doc, have) diff --git a/M02_MasterTemplete/M02_Labor_Expand.py b/M02_MasterTemplete/M02_Labor_Expand.py index 10054f5c7..0f18a8798 100644 --- a/M02_MasterTemplete/M02_Labor_Expand.py +++ b/M02_MasterTemplete/M02_Labor_Expand.py @@ -2,6 +2,9 @@ 참조 → `{이름, 단위, 기준(1 단위), 까닭, 안?}` — 단가승격(옛 SP) · 사전조합(SC) · 단가산출(SK · `M02_Sangeun_Calc.unit`). 이름은 `M02_Labor_Ref.Expand` 로 그대로 부름. +사전조합 안 줄 수량(723-1 · 내역 17-1) = 쓰는 쪽 `given` 의 알약 「참조|층|주인|키#차례」(자재 = +「…#자재|코드」 · `inner_pills`) · 문서 손은 굳힌 사본(`hands` · 프로젝트 · 불러오기 대조)과 비율 줄(율)만 · +없으면 「수량 없음」(1 아님 · 그 사전조합 기준 없음). """ from __future__ import annotations @@ -16,14 +19,35 @@ from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_Sangeun_Calc as calc Ref = dict[str, Any] +NO_QTY = "수량 없음" +MAT = "자재|" class Expand: """참조 → `{이름, 단위, 기준(1 단위), 까닭, 안?}` — 한 번 셈 안에서 키별 캐시 · 깊이 상한 · 순환은 까닭(통째로 멈추지 않음). `read` = 참조 → 문서(프로젝트는 굳힌 사본).""" - def __init__(self, whole: Any, read: Callable[[Ref], dict | None] = refs.read_ref) -> None: + def __init__( + self, + whole: Any, + read: Callable[[Ref], dict | None] = refs.read_ref, + given: dict[str, Any] | None = None, + hands: bool | None = None, + ) -> None: self.whole, self.read, self.memo = whole, read, {} + self.given = given or {} + self.hands = ( + read is not refs.read_ref if hands is None else hands + ) # 층 원본 손 = 옛 값(안 씀) + + def _qty(self, one: dict, key: str) -> Decimal | None: + """안 줄 수량 — 손(굳힌 사본 · 비율 줄) · 알약 값(given) · 없음.""" + from M02_MasterTemplete import M02_Labor_Doc as labor + + hand = labor._number((one.get("수량") or {}).get("손")) + if hand is not None and (self.hands or refs.is_rate(one)): + return hand + return labor._number(self.given.get(key)) def __call__(self, ref: Ref, trail: tuple[str, ...] = ()) -> dict[str, Any]: rid = refs.ref_id(ref) @@ -110,8 +134,8 @@ class Expand: for pill in pills or combo_store.combo_pills({"담은로직": lines})["물량"] for code in pill["로직"] } - inner, rows, why = [], [], None - for one in lines: + inner, rows, why, rid = [], [], None, trail[-1] if trail else "" + for at, one in enumerate(lines): ref = one.get("참조") if isinstance(ref, dict): # 줄 덮개(715-3) 있는 SK 참조 = 그 줄만 다시 셈 head = {"참조": ref} @@ -122,18 +146,21 @@ class Expand: head, sub = {"고정": one["고정"]}, fixed.line(self.whole, one["고정"]) else: continue - qty = labor._number((one.get("수량") or {}).get("손")) - qty = Decimal(1) if qty is None else qty + qty = self._qty(one, f"{rid}#{at}") keep = ("이름", "단위", "기준", "까닭") inner.append({**head, "수량": qty, **{k: sub.get(k) for k in keep}}) if sub.get("기준") is None: why = why or f"「{sub.get('이름')}」 {sub.get('까닭')}" continue - rows.append({k: v * qty for k, v in sub["기준"].items()}) - for mat in doc.get("자재") or []: # 사용자 자재(175-2) = 기준단가 × 손 · 빈칸 = 안 셈 - qty = labor._number((mat.get("수량") or {}).get("손")) - if qty is None or labor.apart(mat): # 별도계상 = 안 셈 · 안 줄에도 없음(222-1) + if qty is None: # 723-1 — 이은 값 없음 = 금액 빠짐(1 로 안 셈) + inner[-1]["까닭"] = NO_QTY + why = why or f"「{sub.get('이름')}」 {NO_QTY}" continue + rows.append({k: v * qty for k, v in sub["기준"].items()}) + for mat in doc.get("자재") or []: # 사용자 자재(175-2) = 기준단가 × 수량(이은 값) + if labor.apart(mat): # 별도계상 = 안 셈 · 안 줄에도 없음(222-1) + continue + qty = self._qty(mat, f"{rid}#{MAT}{mat['코드']}") got = labor.material_price(self.whole, mat["코드"], mat.get("단위")) price = labor._number(got["기준단가"]) zero = dict.fromkeys(labor.COSTS, Decimal(0)) @@ -149,8 +176,9 @@ class Expand: "까닭": got["까닭"], } ) - if base is None: - why = why or f"「{name}」 {got['까닭']}" + if qty is None or base is None: + inner[-1]["까닭"] = got["까닭"] or NO_QTY # 단가 까닭 먼저 + why = why or f"「{name}」 {inner[-1]['까닭']}" continue rows.append({k: v * qty for k, v in base.items()}) under = labor._sum(rows) @@ -172,3 +200,44 @@ def head_of(doc: dict) -> str | None: """상세 머리 「1 ㎡ 당」(715-3 · 일위대가 = 기준단위 1 당) — 단위 빈 옛 문서는 None.""" unit = str(doc.get("단위") or "").strip() return f"1 {unit} 당" if unit else None + + +def inner_pills( + ref: Ref, read: Callable[[Ref], dict | None] = refs.read_ref, depth: int = 1 +) -> list[dict[str, Any]]: + """사전조합 안 줄 알약(723-1) — 키 「참조|…#차례」 · 「참조|…#자재|코드」 · 이름 = 「일위대가 · 줄」 · + 단위 = 그 줄 분모 · 비율 줄 뺌 · 안의 사전조합도 한 번씩(깊이 상한) · 사전조합 아니면 빈 것.""" + from M01_MasterData import M01_MasterData_Store_Combo as combo_store + + doc = read(ref) if refs.kind_of(ref["키"]) == "precombo" and depth <= refs.DEPTH else None + if not isinstance(doc, dict): + return [] + rid, top = refs.ref_id(ref), doc.get("이름") or ref["키"] + lines = [one for one in doc.get("담은로직") or [] if isinstance(one, dict)] + by_logic = { + code: pill + for pill in combo_store.combo_pills({"담은로직": lines})["물량"] + for code in pill["로직"] + } + out: list[dict[str, Any]] = [] + for at, one in enumerate(lines): + if refs.is_rate(one): + continue + sub = one.get("참조") + if isinstance(sub, dict): + got = read(sub) or {} + name, unit = got.get("이름") or sub["키"], got.get("단위") + out += inner_pills(sub, read, depth + 1) + elif fixed.is_fixed(one): + name, unit = one["고정"].get("이름"), one["고정"].get("단위") + else: + pill = by_logic.get(one.get("로직")) or {} + name, unit = pill.get("이름") or one.get("로직"), pill.get("단위") + name = one.get("별칭") or name + out.append({"키": f"{rid}#{at}", "이름": f"{top} · {name}", "단위": unit or "", "로직": [], "참조": ref}) # fmt: skip + for mat in doc.get("자재") or []: + name = mat.get("이름") or mat.get("코드") + key = f"{rid}#{MAT}{mat.get('코드')}" + out.append({"키": key, "이름": f"{top} · {name}", "단위": mat.get("단위") or "", "로직": [], "참조": ref}) # fmt: skip + seen: set[str] = set() + return [p for p in out if not (p["키"] in seen or seen.add(p["키"]))] diff --git a/M02_MasterTemplete/M02_Labor_Ref.py b/M02_MasterTemplete/M02_Labor_Ref.py index b474e7b66..ced3611f8 100644 --- a/M02_MasterTemplete/M02_Labor_Ref.py +++ b/M02_MasterTemplete/M02_Labor_Ref.py @@ -7,9 +7,9 @@ precombo `{층}/precombo/SC-######.json` 사전조합 `{종류:"사전조합", 키, 이름, 단위, 본뜬, 담은로직:[{참조, 수량:{손}, 메모} | {로직, 입력값, 메모, 수량:{손}, 고름?, 자재표시?}], 자재:[일위대가 자재 꼴]}` — - 승격 · 사전조합 · 단가산출 로직(51-11) · 수량은 손(기본 1) · - 자재 수량은 손 · 빈칸 = 안 셈(175) · - 값 = Σ 안 줄 1 단위 값 × 수량(로직 줄 = 기준 ÷ 배) + Σ 자재 기준단가 × 손 + 승격 · 사전조합 · 단가산출 로직(51-11) · 안 줄 · 자재 수량 = 저장 안 함(늘 null · + 723-1 · 내역 17-1 · `no_qty` · 비율 줄 율만 남김) · 쓰는 쪽 알약으로 이음(`Expand` given) · + 값 = Σ 안 줄 1 단위 값 × 이은 수량(로직 줄 = 기준 ÷ 배) + Σ 자재 기준단가 × 이은 수량 sangeun `{층}/sangeun/SK-######.json` 단가산출(715-2) — 꼴 `M02_Sangeun_Doc` · 셈 `M02_Sangeun_Calc` · 참조 자리 = `바탕.참조` · `더한줄[].참조`(`refs_in`) 참조 = `{층, 주인, 키}`(마스터 주인 null · 회사 `C` · 개인 `C/U`) — 종류는 키 머리(SP · SC · SK) · @@ -170,7 +170,19 @@ def clean_doc(kind: str, doc: Any) -> dict[str, Any]: out["자재"] = _combo_mats(doc, out["담은로직"]) out.update({k: str(doc.get(k) or "").strip() for k in ("구분", "상세구분")}) out["본뜬"] = doc["본뜬"] if isinstance(doc.get("본뜬"), dict) else None - return out + return no_qty(out) + + +def is_rate(one: Any) -> bool: + """비율 고정 줄(705-3) — 수량 = 율(%) · 기준단위 당 수량 아님.""" + return isinstance(one, dict) and bool((one.get("고정") or {}).get("비율")) + + +def no_qty(doc: dict[str, Any]) -> dict[str, Any]: + """사전조합 안 줄 · 자재 수량 걷음(723-1 · 내역 17-1 = M02 에 저장 안 함) · 비율 줄 율만 남김.""" + lines = [one if is_rate(one) else {**one, "수량": None} for one in doc.get("담은로직") or []] + mats = [{**one, "수량": None} for one in doc.get("자재") or []] + return {**doc, "담은로직": lines, "자재": mats} _OLD_CLASS = re.compile(r"품셈") # 134-5 — 옛 품셈 분류(건설품셈 공통 · 산림품셈 · 07장 …) @@ -234,7 +246,7 @@ def _combo_lines(doc: dict, key: str) -> list[dict[str, Any]]: head.update(로직=one["로직"], 입력값=one["입력값"]) head.update({k: one[k] for k in ("켜쌓기율", "고름", "자재표시") if k in one}) head.update({"별칭": one["별칭"]} if one["별칭"] else {}) # 실무 품명(324-3) - lines.append({**head, "수량": one["수량"] or {"손": 1}, "메모": one["메모"]}) + lines.append({**head, "수량": one["수량"], "메모": one["메모"]}) at_ = here() me = {"층": at_[0], "주인": at_[1], "키": key} for one in lines: @@ -458,9 +470,17 @@ def with_pills(doc: dict[str, Any]) -> dict[str, Any]: from M01_MasterData import M01_MasterData_Store_Combo as combo_store lines = [one for one in doc.get("담은로직") or [] if isinstance(one, dict)] + doc = no_qty({**doc, "담은로직": lines}) # 723-1 — 옛 손도 굳히지 않음(쓰는 쪽 알약으로) return {**doc, "알약": combo_store.combo_pills({"담은로직": lines})["물량"]} +def pills_of(ref: Ref) -> list[dict[str, Any]]: + """참조 줄 알약 + 사전조합 안 줄 알약(723-1 · `inner_pills`).""" + from M02_MasterTemplete.M02_Labor_Expand import inner_pills + + return [pill(ref), *inner_pills(ref)] + + def frozen_materials(copies: dict[str, Any]) -> list[str]: """굳힌 사전조합의 자재 · 고른 로직 자재 코드(175-2) — 프로젝트 단가 묶음에 넣음.""" out = [] diff --git a/resources/tester/test_160_3_b08_unmatched_total.py b/resources/tester/test_160_3_b08_unmatched_total.py index eacc1d617..647ba7a4d 100644 --- a/resources/tester/test_160_3_b08_unmatched_total.py +++ b/resources/tester/test_160_3_b08_unmatched_total.py @@ -96,7 +96,8 @@ def test_참조_줄_재료_연료_뺌_고른_품목(tmp_path, monkeypatch): price = {"묶음": {"줄": [{"키": "MT9", "이름": "레디믹스트 레미콘", "규격": "25-18-80"}]}} frozen = { ref_id(SC): { - "담은로직": [{"로직": "L1", "수량": {"손": 2}}, {"참조": SP}], + # 굳힌 옛 사본 = 안 줄 손 다 있음(723-1 앞 · 빈 수량 = 이은 칸 값 · 없으면 안 셈) + "담은로직": [{"로직": "L1", "수량": {"손": 2}}, {"참조": SP, "수량": {"손": 1}}], "알약": [{"로직": ["L1"], "배": 10}], }, ref_id(SP): {"담은로직": [{"로직": "L2"}], "배": 4}, diff --git a/resources/tester/test_175_precombo_mats.py b/resources/tester/test_175_precombo_mats.py index 3688f2103..2713c1f34 100644 --- a/resources/tester/test_175_precombo_mats.py +++ b/resources/tester/test_175_precombo_mats.py @@ -2,7 +2,7 @@ 175-1 화면 · 저장 — 서고 모드 자재목록 · 로직 자재 세 칸 막힘 풂 · 저장 `_combo_lines` 가 고름 · 자재표시 남김 · 문서 `자재`(일위대가 자재 검사 · 로직 자재와 같은 품목 400). -175-2 셈 — 담은 자재(손 · 빈칸 = 안 셈)가 사전조합 1 단위 값(`Expand._combo` · 안 줄) · B08 참조 줄 +175-2 셈 — 담은 자재(수량 = 이은 알약 값 · 723-1 · 손은 저장 안 함)가 사전조합 1 단위 값(`Expand._combo` · 안 줄) · B08 참조 줄 재료(`_ref_mats`) · B09 겹 대가 자재 줄 · 프로젝트 사본 단가 묶음(`frozen_materials`)에 들어감. """ @@ -16,7 +16,7 @@ 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_MasterTemplete_Store as store -from resources.tester.test_m02_labor_ref import world # noqa: F401 — 임시 층 fixture +from resources.tester.test_m02_labor_ref import _area, world # noqa: F401 — 임시 층 fixture HERE = os.path.dirname(os.path.abspath(__file__)) SRC = os.path.join(HERE, "..", "..", "M02_MasterTemplete") @@ -51,7 +51,7 @@ def test_저장_왕복_고름_자재표시_자재_남음(world): # noqa: F811 _, got = _save(world["system"], {"담은로직": [line], "자재": mats}) one = got["담은로직"][0] assert one["고름"] == {"4": NET_ITEM} and one["자재표시"] == {"3": {"자재구분": "관급"}} - assert [(m["코드"], m["수량"]) for m in got["자재"]] == [(MAT, {"손": 2}), (WIRE, None)] + assert [(m["코드"], m["수량"]) for m in got["자재"]] == [(MAT, None), (WIRE, None)] # 723-1 assert got["자재"][0]["운반비"] == "미포함" and got["자재"][0]["이름"] != "틀린 이름" # 자재 없음 = 빈 목록(옛 문서 꼴 그대로 읽힘) _, empty = _save(world["system"], {"담은로직": [{"로직": NET}]}) @@ -73,8 +73,23 @@ def _price(code): return Decimal(str(labor.material_price(whole, code, None)["기준단가"])) +def _given(key): + """안 줄 알약 값(723-1) — STONE 1 · NET 0 · MAT 2 · WIRE 0.""" + rid = refs.ref_id({**SYS, "키": key}) + return {f"{rid}#0": 1, f"{rid}#1": 0, f"{rid}#자재|{MAT}": 2, f"{rid}#자재|{WIRE}": 0} + + +def _legacy(world, key): # noqa: F811 + """옛 손 수량 문서(723-1 앞 · 굳힌 사본 꼴) — B08 · B09 셈 길 시험용 · 파일은 안 바꿈.""" + doc = json.loads((world["system"] / "precombo" / f"{key}.json").read_bytes()) + hands = ({"손": 1}, {"손": 0}) + lines = [{**one, "수량": q} for one, q in zip(doc["담은로직"], hands)] + mats = [{**m, "수량": {"손": 2} if m["코드"] == MAT else None} for m in doc["자재"]] + return {**doc, "담은로직": lines, "자재": mats} + + def _world_combo(world): # noqa: F811 - """STONE 1 + MAT 손 2 + WIRE 빈칸(안 셈) · NET 고름 4 = NET_ITEM — (키, 손셈 1 단위 값).""" + """STONE 1 + MAT 2 + WIRE 0 · NET 고름 4 = NET_ITEM — (키, 1 단위 값) · 수량은 `_given`.""" lines = [ {"로직": STONE, "수량": {"손": 1}}, {"로직": NET, "수량": {"손": 0}, "고름": {"4": NET_ITEM}}, @@ -95,34 +110,42 @@ def test_사전조합_값에_자재_금액_빈칸은_안_셈_안_줄에_자재(w doc = labor.clean( {"열": "rv", "담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 3}}]} ) - one = labor.calc(doc, {})["줄"][0] + bare = labor.calc(doc, {})["줄"][0] + one = labor.calc(doc, _given(key))["줄"][0] + assert bare["기준"] is None and "수량 없음" in bare["까닭"] # 723-1 — 안 이으면 값 없음 assert one["기준"]["계"] == pytest.approx(float(want)) assert one["반영"]["계"] == pytest.approx(float(want * 3)) mats = [x for x in one["안"] if x.get("자재")] - assert [(x["자재"], x["수량"]) for x in mats] == [(MAT, 2)] # WIRE 빈칸 = 안 셈 + assert [(x["자재"], x["수량"]) for x in mats] == [(MAT, 2)] # WIRE = 기본 별도계상(안 셈) assert mats[0]["기준"]["재료비"] == pytest.approx(float(_price(MAT))) - # 목록 기준금액(51-4)도 같은 펼침 + # 목록 기준금액(51-4) — 이은 값 없음 = 빈 값(723-1) rows = [{"종류": "precombo", "이름": key}] refs.summaries(world["system"], rows) - assert rows[0]["기준금액"] == pytest.approx(float(want)) + assert rows[0]["기준금액"] is None def test_B08_참조_줄_재료에_사전조합_자재_손_곱(world): # noqa: F811 from B08_Quantity.B08_Quantity_Router_Material import _ref_mats key, _ = _world_combo(world) - with store.at(world["system"]): - outer = refs.create( - "precombo", - { - "이름": "겹", - "단위": "㎡", - "담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 3}}], - }, - )["이름"] - got = _ref_mats(m01.loaded()[1], refs.read_ref, {**SYS, "키": outer}) + outer = { + "이름": "겹", + "단위": "㎡", + "담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 3}}], + } + docs = {"SC-겹": outer, key: _legacy(world, key)} # 옛 손 문서(723-1 앞 꼴 · B08 길만 잼) + got = _ref_mats(m01.loaded()[1], lambda r: docs.get(r["키"]), {**SYS, "키": "SC-겹"}) mats = [(m["품목"]["키"], q) for m, q in got if m.get("자재")] assert mats == [(MAT, pytest.approx(6.0))] # 2 × 겹 손 3 · WIRE 빈칸 없음 + # 723-1 — 수량 없는 새 문서 = 면적 알약 값(given)으로 · 안 이으면 안 셈 + live = {"SC-겹": outer, key: refs.read_ref({**SYS, "키": key})} + bare = _ref_mats(m01.loaded()[1], lambda r: live.get(r["키"]), {**SYS, "키": "SC-겹"}) + assert [m for m, _ in bare if m.get("자재")] == [] + got = _ref_mats( + m01.loaded()[1], lambda r: live.get(r["키"]), {**SYS, "키": "SC-겹"}, given=_given(key) + ) + mats = [(m["품목"]["키"], q) for m, q in got if m.get("자재")] + assert mats == [(MAT, pytest.approx(6.0))] def test_B09_겹_대가에_자재_줄(world): # noqa: F811 @@ -131,7 +154,7 @@ def test_B09_겹_대가에_자재_줄(world): # noqa: F811 key, _ = _world_combo(world) ref = {**SYS, "키": key} build = Builder() - bid = build.precombo(m01.loaded()[1], ref, refs.read_ref(ref), {}) + bid = build.precombo(m01.loaded()[1], ref, _legacy(world, key), {}) # 옛 손 문서(723-1 앞) mats = [x for x in build.blocks[bid]["lines"] if x["kind"] == "material"] assert [(x["key"], x["qty"]) for x in mats] == [(MAT, 2.0), (WIRE, None)] assert mats[0]["name"] and mats[0]["unit"] @@ -150,5 +173,5 @@ def test_사본_단가_묶음에_사전조합_자재_고름_사본_값_같음(wo assert "refs.frozen_materials(" in open(source.__file__, encoding="utf-8").read() bundle = prices.to_json(prices.collect(refs.frozen_logics(rows["참조사본"]), mats)) (world["system"] / "precombo" / f"{key}.json").unlink() # 원본이 사라져도 굳힌 사본 - got = prices.calc(rows, bundle, {}, {}, None)["줄"][0] + got = prices.calc(rows, bundle, {}, {}, _area(_given(key)))["줄"][0] # 사본 면적 칸 = 안 줄 값 assert got["기준"]["계"] == pytest.approx(float(want), rel=1e-9) diff --git a/resources/tester/test_715_3_labor_patch.py b/resources/tester/test_715_3_labor_patch.py index b6b4de67a..1ac5bdf78 100644 --- a/resources/tester/test_715_3_labor_patch.py +++ b/resources/tester/test_715_3_labor_patch.py @@ -32,8 +32,12 @@ def _sc(folder, lines, unit="㎡"): def _value(folder, key): + """안 줄 수량 = 모두 1(723-1 · 저장 안 함 → 쓰는 쪽 알약 값).""" + ref = {**MINE, "키": key} with store.at(folder): - return refs.Expand(_whole())({**MINE, "키": key}) + lines = refs.read_ref(ref)["담은로직"] + given = {f"{refs.ref_id(ref)}#{i}": 1 for i in range(len(lines))} + return refs.Expand(_whole(), given=given)(ref) def test_logic_line_cover_in_precombo(world): diff --git a/resources/tester/test_m02_hopyo_build.py b/resources/tester/test_m02_hopyo_build.py index 881a5fb7e..d6e0bd709 100644 --- a/resources/tester/test_m02_hopyo_build.py +++ b/resources/tester/test_m02_hopyo_build.py @@ -61,22 +61,24 @@ def test_확인한_줄만_별칭_수량_빠진_줄_장부(client): assert origin["확인"]["누가"] == {"id": 42, "이름": "갑"} and origin["확인"]["때"] stone, *made, soil = doc["담은로직"] # 705-3 — 확인 안 한 줄 · 「우리에 없음」 = 고정 줄 assert stone["고정"]["단가"] == {"노무비": 0, "재료비": 1400000, "경비": 0} - assert stone["고정"]["출처"]["줄"] == 0 and stone["수량"] == {"손": 1} - assert soil["고정"]["이름"] == "잔토처리(인력100%)" and soil["수량"] == {"손": 0.09} + hands = origin["실무수량"]["줄"] # 723-1 — 수량 = 저장 안 함 · 엑셀 수량은 출처 기록만 + assert stone["고정"]["출처"]["줄"] == 0 and stone["수량"] is None and hands["0"] == 1 + assert soil["고정"]["이름"] == "잔토처리(인력100%)" and soil["수량"] is None + assert hands[str(len(doc["담은로직"]) - 1)] == 0.09 assert origin["품셈"] == [1, 2] and got["줄수"]["고정"] == 2 and got["빠진줄"] == [] assert made == [ { "로직": "GF000101", "입력값": {"지역": "서울"}, "별칭": "구조물터파기(인력100%)", - "수량": {"손": 0.14}, + "수량": None, # 723-1 — 엑셀 0.14 = 본뜬.실무수량 "메모": "실무 0.14㎥ × 1 · 토사", }, { "로직": "GF000113", "입력값": {}, "별칭": "구조물되메우기(인력100%)", - "수량": {"손": 0.1}, # 엑셀 0.05 × 배 2 + "수량": None, # 엑셀 0.05 × 배 2 = 본뜬.실무수량(723-1) "메모": "실무 0.05㎥ × 2", }, ] @@ -108,14 +110,20 @@ def test_로직_여럿_같은_별칭_감쌈_자재_몫_덮음(client): first, second, fx, wrapped = doc["담은로직"] assert fx["고정"]["출처"]["줄"] == 2 # 확인 안 한 줄 = 고정(705-3) assert first["별칭"] == second["별칭"] == "구조물터파기(인력100%)" # 한 줄 로직 둘 = 같은 별칭 - assert second["수량"] == {"손": 0.014} and second["메모"] == "실무 0.14㎥ × 0.1(몫 0.1) · 토사" - assert wrapped["별칭"] == "잔토처리(인력100%)" and wrapped["수량"] == {"손": 0.09} + hands = doc["본뜬"]["실무수량"]["줄"] # 723-1 — 저장 수량 없음 · 엑셀 수량 = 출처 기록 + assert second["수량"] is None and hands["1"] == 0.014 + assert second["메모"] == "실무 0.14㎥ × 0.1(몫 0.1) · 토사" + assert ( + wrapped["별칭"] == "잔토처리(인력100%)" and wrapped["수량"] is None and hands["3"] == 0.09 + ) inner = _doc(mine, wrapped["참조"]["키"]) assert inner["본뜬"]["키"] == "HP-000001 제3호표 4째 줄 GF000101" - assert inner["담은로직"][0]["로직"] == "GF000101" and inner["담은로직"][0]["수량"] == {"손": 1} + assert inner["담은로직"][0]["로직"] == "GF000101" and inner["담은로직"][0]["수량"] is None + assert inner["본뜬"]["실무수량"]["줄"] == {"0": 1} assert [(m["코드"], m["수량"], m["별도계상"]) for m in doc["자재"]] == [ - ("MT004258", {"손": 1}, "직접계산") + ("MT004258", None, "직접계산") ] + assert doc["본뜬"]["실무수량"]["자재"] == {"MT004258": 1} stone = next(v for k, v in _book(mine).items() if k.startswith("임도표지석(받침")) assert stone["짝"][0]["자재"] == "MT004258" files = sorted(p.name for p in (mine / "precombo").glob("*.json")) @@ -232,14 +240,15 @@ def test_하위_호표_먼저_확인_퍼센트_빠짐_통째(client): assert rate["비율"] == {"기준": "재료비", "비목": "재료비"} got = _build(client, 45, ref) mine = layers.personal_dir(7, 42) - line = _doc(mine, got["참조"]["키"])["담은로직"][0] + made = _doc(mine, got["참조"]["키"]) + line = made["담은로직"][0] assert line["참조"] == child["참조"] and line["별칭"] == "고화매트재료비" - assert line["수량"] == {"손": 2.3} + assert line["수량"] is None and made["본뜬"]["실무수량"]["줄"]["0"] == 2.3 # 723-1 assert "고화매트재료비||m2" not in _book(mine) # 호표 참조 줄 = 장부 안 씀 whole = _build(client, 3, 통째={"대상": [{"로직": "GF000101", "배": 1}]}) doc = _doc(mine, whole["참조"]["키"]) assert [x["로직"] for x in doc["담은로직"]] == ["GF000101"] - assert doc["담은로직"][0]["수량"] == {"손": 1} + assert doc["담은로직"][0]["수량"] is None and doc["본뜬"]["실무수량"]["줄"] == {"0": 1} assert {x["까닭"] for x in whole["빠진줄"]} == {"호표 통째로 대신"} head = next(v for k, v in _book(mine).items() if k.startswith("호표|임도표지석|")) assert head["짝"][0]["로직"] == "GF000101" and head["원래"]["품명"] == "임도표지석" diff --git a/resources/tester/test_m02_hopyo_flow.py b/resources/tester/test_m02_hopyo_flow.py index 3e79e1655..cdb995510 100644 --- a/resources/tester/test_m02_hopyo_flow.py +++ b/resources/tester/test_m02_hopyo_flow.py @@ -128,8 +128,10 @@ def test_섞인_줄_몫_배(client): assert screen["lines"][0]["checkable"] and not screen["lines"][0]["checked"] one = {k: part[k] for k in ("로직", "입력값", "배", "몫")} made = _build(client, "HP-000001", no, [{"차례": line["차례"], "대상": [one]}]) - row = next(x for x in _doc(made["참조"])["담은로직"] if x.get("로직") == "GF000211") - assert row["수량"]["손"] == round(line["수량"] * 0.1, 6) + doc = _doc(made["참조"]) + at = next(i for i, x in enumerate(doc["담은로직"]) if x.get("로직") == "GF000211") + assert doc["담은로직"][at]["수량"] is None # 723-1 — 수량 저장 안 함 · 엑셀 수량 = 출처 기록 + assert doc["본뜬"]["실무수량"]["줄"][str(at)] == round(line["수량"] * 0.1, 6) def test_막는_길(client): diff --git a/resources/tester/test_m02_labor_ref.py b/resources/tester/test_m02_labor_ref.py index 144178dde..b3534e2ef 100644 --- a/resources/tester/test_m02_labor_ref.py +++ b/resources/tester/test_m02_labor_ref.py @@ -73,6 +73,13 @@ def _combo(folder, lines, name="조합"): return refs.create("precombo", {"이름": name, "단위": "㎡", "담은로직": lines})["이름"] +def _area(given): + """면적산출 사본 꼴 — 알약 키마다 칸 하나(723-1 · 안 줄 수량 = 이은 칸 값).""" + cells = {f"A{i}": {"값": v} for i, v in enumerate(given.values(), 1)} + links = [{"칸": f"A{i}", "키": k} for i, k in enumerate(given, 1)] + return {"시트": [{"id": "s", "칸": cells, "연결": links}]} + + def _read(folder, kind, key): return json.loads((folder / kind / f"{key}.json").read_bytes()) @@ -221,7 +228,9 @@ def test_calc_는_참조를_펼침_승격은_기준_나누기_배_사전조합 ref = {**SYS, "키": sc} with store.at(world["system"]): doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 5}}]}) - got = labor.calc(doc, {}) + bare = labor.calc(doc, {})["줄"][0] # 723-1 — 안 줄 손은 저장 안 함 · 안 이으면 수량 없음 + got = labor.calc(doc, {f"{refs.ref_id(ref)}#0": 2}) # 안 줄 알약 = 구조물 조합 칸 값 + assert bare["기준"] is None and "수량 없음" in bare["까닭"] and bare["안"][0]["수량"] is None one = got["줄"][0] want = Decimal(str(_base(TEN)["계"])) / 10 * 2 assert one["기준"]["계"] == pytest.approx(float(want)) @@ -233,7 +242,9 @@ def test_calc_는_참조를_펼침_승격은_기준_나누기_배_사전조합 assert one["반영"]["계"] == pytest.approx(float(want * 5)) assert one["안"][0]["수량"] == 2 and one["안"][0]["참조"]["키"] == sp pills = labor.pills(doc) - assert pills == [{"키": refs.ref_id(ref), "이름": "둘", "단위": "㎡", "로직": [], "참조": ref}] + assert pills[0] == {"키": refs.ref_id(ref), "이름": "둘", "단위": "㎡", "로직": [], "참조": ref} + assert [p["키"] for p in pills[1:]] == [f"{refs.ref_id(ref)}#0"] # 안 줄 알약(723-1) + assert pills[1]["이름"].startswith("둘 · ") def test_승격_줄_상세는_원_로직_키_표_사전조합은_안_줄만(world): @@ -386,9 +397,10 @@ def test_프로젝트는_참조사본으로_굳힘_원본을_지워도_같은_ sp = _promote(world["system"]) sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}, "수량": {"손": 2}}]) ref = {**SYS, "키": sc} + given = {f"{refs.ref_id(ref)}#0": 2} # 723-1 — 안 줄 수량 = 이은 칸 값 with store.at(world["system"]): doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 1}}]}) - live = labor.calc(doc, {})["줄"][0]["기준"]["계"] + live = labor.calc(doc, given)["줄"][0]["기준"]["계"] rows = source.labor_doc_rows(doc) assert set(rows["참조사본"]) == {refs.ref_id(ref), refs.ref_id({**SYS, "키": sp})} assert refs.frozen_logics(rows["참조사본"]) == [TEN] @@ -396,7 +408,7 @@ def test_프로젝트는_참조사본으로_굳힘_원본을_지워도_같은_ for key in (sc, sp): # 원본이 사라져도 프로젝트는 사본만 봄 kind = refs.kind_of(key) (world["system"] / kind / f"{key}.json").unlink() - got = prices.calc(rows, bundle, {}, {}, None) + got = prices.calc(rows, bundle, {}, {}, _area(given)) line = got["줄"][0] assert not line["까닭"] and line["참조"] == ref # 층 셈 꼴(142-1) assert line["기준"]["계"] == pytest.approx(live, rel=1e-9) @@ -438,8 +450,8 @@ def test_층_목록_서고_줄은_이름_단위_기준금액을_실음(world): sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}, "수량": {"손": 3}}], "셋") rows = [{"종류": "precombo", "이름": sc}, {"종류": "labor", "이름": "rv"}] refs.summaries(world["system"], rows) - one = float(Decimal(str(_base(TEN)["계"])) / 10) - assert (rows[0]["제목"], rows[0]["기준금액"]) == ("셋", pytest.approx(one * 3)) + # 723-1 — 안 줄 수량 저장 안 함 → 목록 기준금액 없음(쓰는 쪽 알약으로만 값) + assert (rows[0]["제목"], rows[0]["기준금액"]) == ("셋", None) assert "기준금액" not in rows[1] @@ -456,16 +468,17 @@ def test_사전조합_로직_줄_섞임_합은_손셈_지우기_409_굳힘(world ] sc = _combo(world["system"], lines, "섞음") saved = _read(world["system"], "precombo", sc)["담은로직"] - assert saved[1:] == [ - {"로직": STONE, "입력값": {}, "수량": {"손": 3}, "메모": "돌"}, - {"로직": TEN, "입력값": {}, "수량": {"손": 1}, "메모": None}, + assert saved[1:] == [ # 723-1 — 안 줄 수량 저장 안 함 + {"로직": STONE, "입력값": {}, "수량": None, "메모": "돌"}, + {"로직": TEN, "입력값": {}, "수량": None, "메모": None}, ] ten, stone = (Decimal(str(_base(c)["계"])) for c in (TEN, STONE)) - want = ten / 10 * 2 + stone * 3 + ten / 10 * 1 # 손셈 — 승격 · TEN 은 원/10㎡(배 10) + want = ten / 10 * 2 + stone * 3 + ten / 10 * 1 # 이은 수량 — 승격 · TEN 은 원/10㎡(배 10) ref = {**SYS, "키": sc} + given = {f"{refs.ref_id(ref)}#{i}": q for i, q in enumerate((2, 3, 1))} with store.at(world["system"]): doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 5}}]}) - one = labor.calc(doc, {})["줄"][0] + one = labor.calc(doc, given)["줄"][0] assert one["기준"]["계"] == pytest.approx(float(want)) assert one["반영"]["계"] == pytest.approx(float(want * 5)) assert [x.get("로직") or x["참조"]["키"] for x in one["안"]] == [sp, STONE, TEN] @@ -482,16 +495,16 @@ def test_사전조합_로직_줄_섞임_합은_손셈_지우기_409_굳힘(world bundle = prices.to_json(prices.collect([TEN, STONE])) for key in (sc, sp): # 원본이 사라져도 굳힌 사본으로 같은 값 (world["system"] / refs.kind_of(key) / f"{key}.json").unlink() - got = prices.calc(rows, bundle, {}, {}, None)["줄"][0] + got = prices.calc(rows, bundle, {}, {}, _area(given))["줄"][0] assert got["기준"]["계"] == pytest.approx(float(want), rel=1e-9) -def test_사전조합이_자기_자신을_담으면_422_수량_기본_1(world): +def test_사전조합이_자기_자신을_담으면_422_수량_없음(world): sp = _promote(world["system"]) a = _combo(world["system"], [{"참조": {**SYS, "키": sp}}], "가") with store.at(world["system"]): have = store.read("precombo", a) - assert have["문서"]["담은로직"][0]["수량"] == {"손": 1} + assert have["문서"]["담은로직"][0]["수량"] is None # 723-1 — 기본 1 걷음 with pytest.raises(store.StoreError) as e: store.write( "precombo", @@ -535,14 +548,16 @@ def test_사전조합_굳힘은_안_로직_알약도_사본_값_142_2(world, mon with store.at(world["system"]): doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 1}}]}) rows = source.labor_doc_rows(doc) + given = {f"{refs.ref_id(ref)}#0": 1} kept = rows["참조사본"][refs.ref_id(ref)]["알약"] assert [(p["로직"], p.get("배")) for p in kept] == [([TEN], 10)] assert "알약" not in _read(world["system"], "precombo", sc) # 층 문서는 그대로 bundle = prices.to_json(prices.collect([TEN])) - before = prices.calc(rows, bundle, {}, {}, None)["줄"][0]["기준"]["계"] + before = prices.calc(rows, bundle, {}, {}, _area(given))["줄"][0]["기준"]["계"] changed = {"물량": [{"키": "x|㎡", "이름": "x", "단위": "㎡", "로직": [TEN]}]} # 배 1 로 바뀜 monkeypatch.setattr(combo_store, "combo_pills", lambda row: changed) - assert prices.calc(rows, bundle, {}, {}, None)["줄"][0]["기준"]["계"] == pytest.approx(before) + again = prices.calc(rows, bundle, {}, {}, _area(given))["줄"][0]["기준"]["계"] + assert again == pytest.approx(before) with store.at(world["system"]): - live = labor.calc(doc, {})["줄"][0]["기준"]["계"] + live = labor.calc(doc, given)["줄"][0]["기준"]["계"] assert live == pytest.approx(before * 10) # 층 셈은 지금 M01(배 1)