From b63d000822c80d4e079d7c969d49349a2fad06f2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 11:33:31 +0900 Subject: [PATCH] =?UTF-8?q?fix(729-4):=20=EB=8B=A8=EA=B0=80=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=EB=A7=8C=EB=93=A4=EA=B8=B0=20=EB=8B=A8=EC=9C=84=20?= =?UTF-8?q?=EC=84=B1=EA=B2=A9=20=EB=8B=A4=EB=A6=84=20500=20=EA=B1=B7?= =?UTF-8?q?=EC=9D=8C=20=C2=B7=20=EB=AA=BB=EC=98=AE=EA=B9=80=20=EC=9E=87?= =?UTF-8?q?=EA=B8=B0=20=EC=84=9C=EB=B2=84=20=EA=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 728-2 단위 환산 뒤 [단가산출 만들기] 500 · 400(100개 ↔ ㎡ · ㎥ ↔ m2 · 본 ↔ M · 개소 ↔ m) 걷음 — 견줌 SK 배 = ratio_of(728 규칙) · 성격 다름 = 환산 계수 = 우리 품 ÷ 엑셀 품(인력 · 장비 둘 이상 같은 비일 때만) · 못 구함 = 고름 대기(까닭 「단위 성격 다름 · 계수 필요」 · 후보[].계수 칸) - 맞음 블록도 단위 견줌(전엔 셈 없이 기초로 써서 뒤 셈에서 막힘) - 못옮김 = [{id, 갈래 노임|기계|자재, 이름, 규격, 단위, 수량, 글, 후보}] · 응답 만듦[].못옮김 · 환산 - 몸 바탕[번] = {로직?, 입력값?, 환산?, 잇기?} · 잇기 [{id, 노임|기계|자재: 키} | {id, 뺌}] → SK 더한줄 인력 · 기계 · 재료(엑셀 수량 × 배) · 뺌 = 본뜬 놓음 · 틀린 id · 키 400 - 못옮김 남은 SK 를 쓰는 일위대가 = 남은줄(단산 · SK · 못옮김) · hopyo-build · build-all 409 - 못옮김 남은 블록 = SK(같지 않음) - 시험 test_729 에 단위 · 잇기 · 409 시험 하나 더함 Co-Authored-By: Claude Opus 5.5 --- M02_MasterTemplete/M02_Hopyo_Sangeun.py | 55 +++++-- M02_MasterTemplete/M02_Hopyo_SkLink.py | 15 +- M02_MasterTemplete/M02_Hopyo_SkPatch.py | 158 ++++++++++++++++++--- resources/tester/test_729_hopyo_sk_link.py | 57 ++++++++ 4 files changed, 246 insertions(+), 39 deletions(-) diff --git a/M02_MasterTemplete/M02_Hopyo_Sangeun.py b/M02_MasterTemplete/M02_Hopyo_Sangeun.py index 9d071aecf..42455f336 100644 --- a/M02_MasterTemplete/M02_Hopyo_Sangeun.py +++ b/M02_MasterTemplete/M02_Hopyo_Sangeun.py @@ -4,8 +4,10 @@ 블록마다 그 블록을 부르는 첫 엑셀 줄(「단산N참조」)의 맞춤셈(`M02_Hopyo_Fit` 캐시 · 없으면 그 블록만 셈) → `M02_Hopyo_SkPatch.plan`: 기초 = SK 바탕 = 1위 기초 · 입력값 · 덮개 없음 · SK = 바탕 기초 + 덮개 · 더한줄 · 뺀줄(엑셀 품 그대로) · - 고름 대기 = SK 안 만듦(바탕 없는 SK 로 몰래 채우지 않음) · 후보 다섯 → 사용자가 고른 기초 `바탕 {번: {로직, - 입력값?}}` 로 다시 부르면 그 기초에 견줘 SK(이미 있으면 그 키에 덮음). + 고름 대기 = SK 안 만듦(바탕 없는 SK 로 몰래 채우지 않음) · 후보 다섯(성격 다른 단위 = `계수` 칸 뜻) → + 사용자가 고른 기초 `바탕 {번: {로직?, 입력값?, 환산?, 잇기?}}` 로 다시 부르면 그 기초에 견줘 SK(이미 있으면 + 그 키에 덮음) · 잇기 `[{id, 노임|기계|자재: 키} | {id, 뺌}]` = 못옮김 → SK 더한줄 · 남은 못옮김 = 응답 + `만듦[].못옮김` · 본뜬 그대로(그 SK 를 쓰는 일위대가 만들기 = 남은 줄 409 · `M02_Hopyo_SkLink`). SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 · 본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래, 엑셀품, 못옮김} · 같은 본뜬(키 · 원본)이 이 층에 있으면 그대로(겹침 없음) · 만드는 층 = 부르는 층. @@ -27,6 +29,7 @@ from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_MasterTemplete_Store as store +from M02_MasterTemplete import M02_Sangeun_Doc as sd ORIGIN = "실무자료" KIND = "sangeun" @@ -73,7 +76,8 @@ def _bad(text: str, status: int = 400) -> store.StoreError: def _picks(raw: Any) -> dict[int, dict[str, Any]]: - """사용자가 고른 기초 `{번: {로직, 입력값?}}` 재검사 — 돈 로직만 · 입력 이름 = 그 로직 입력.""" + """사용자 고름 `{번: {로직?, 입력값?, 환산?, 잇기?}}` 재검사 — 돈 로직만 · 입력 이름 = 그 로직 입력 · + 환산 = SK 꼴(`M02_Sangeun_Doc`) · 잇기 = `[{id, …}]`(id 짝 · 키는 `M02_Hopyo_SkPatch` 가 봄).""" if not raw: return {} if not isinstance(raw, dict) or len(raw) > 500: @@ -81,14 +85,33 @@ def _picks(raw: Any) -> dict[int, dict[str, Any]]: logics = ledger._known()[0] out = {} for no, one in raw.items(): - if not isinstance(one, dict) or str(one.get("로직") or "") not in logics: - raise _bad(f"단산{no} · 돈 로직이 아님 「{(one or {}).get('로직')}」") - given = one.get("입력값") + code = str((one or {}).get("로직") or "") if isinstance(one, dict) else "?" + if code and code not in logics: + raise _bad(f"단산{no} · 돈 로직이 아님 「{code}」") + if not code and not (one.get("환산") or one.get("잇기")): + raise _bad(f"단산{no} · 로직 · 환산 · 잇기 중 하나") + pick: dict[str, Any] = {} + given = one.get("입력값") if code else None + if code: + pick["로직"] = code if given is not None: - bad = set(given) - ledger.input_names(one["로직"]) if isinstance(given, dict) else {1} + bad = set(given) - ledger.input_names(code) if isinstance(given, dict) else {1} if bad: raise _bad(f"단산{no} · 모르는 입력 {sorted(map(str, bad))}") - out[int(no)] = {"로직": one["로직"], **({"입력값": given} if given is not None else {})} + pick["입력값"] = given + if one.get("환산"): + try: + pick["환산"] = sd._swap_how(one["환산"]) + except sd.DocError as e: + raise _bad(f"단산{no} · {e}") from e + links = one.get("잇기") + if links: + if not isinstance(links, list) or not all( + isinstance(x, dict) and x.get("id") for x in links + ): + raise _bad(f"단산{no} · 잇기는 [{{id, 노임|기계|자재: 키}}]") + pick["잇기"] = links + out[int(no)] = pick return out @@ -96,6 +119,7 @@ def _doc(line: dict, block: dict, no: int, hp: dict, plan: dict, sure: dict) -> name = str(block.get("이름") or line.get("이름") or "").strip() or f"단산{no}" unit = str(line.get("단위") or block.get("단위") or "").strip() or "식" keep = {k: plan[k] for k in ("갈래", "엑셀품", "못옮김")} + keep |= {"놓음": plan["놓음"]} if plan.get("놓음") else {} return { "종류": "단가산출", "이름": name, @@ -105,6 +129,7 @@ def _doc(line: dict, block: dict, no: int, hp: dict, plan: dict, sure: dict) -> "본뜬": {"층": ORIGIN, "키": mark(hp["키"], no), "파일": hp.get("파일"), **sure, **keep}, "바탕": plan["바탕"], "입력값": plan["입력값"], + **({"환산": plan["환산"]} if plan.get("환산") else {}), "열린입력": [], "고름": {}, **{k: plan[k] for k in ("자재표시", "덮개", "더한줄", "뺀줄")}, @@ -139,9 +164,12 @@ class Maker: def plan(self, no: int, pick: dict | None = None) -> dict[str, Any]: line, block = self.first[no], self.blocks[no] entry = self.cache.get(fit.unit_key(line)) - if entry is None and not pick: + if entry is None and not (pick or {}).get("로직"): entry = fit.compute(block, line) - return skpatch.plan(entry, block, line, self.whole, pick) + try: + return skpatch.plan(entry, block, line, self.whole, pick) + except skpatch.LinkError as e: + raise _bad(f"단산{no} · {e}") from e def write(self, no: int, plan: dict, have: str | None) -> str: doc = _doc(self.first[no], self.blocks[no], no, self.hp, plan, self.sure) @@ -153,8 +181,8 @@ class Maker: def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[str, Any]: - """[단가산출 만들기] — 산근 블록마다 기초 · SK · 고름 대기 → `{블록, 만듦 [{번, 이름, 키, 갈래}], - 그대로, 기초, SK, 고름대기 [{번, 이름, 단위, 까닭, 후보}], 별칭}` · `picks` = 고른 기초.""" + """[단가산출 만들기] — 산근 블록마다 기초 · SK · 고름 대기 → `{블록, 만듦 [{번, 이름, 키, 갈래, 못옮김, + 환산?}], 그대로, 기초, SK, 고름대기 [{번, 이름, 단위, 까닭, 후보}], 별칭}` · `picks` = 고름.""" chosen = _picks(picks) maker = Maker(folder, key, who) bad = sorted(set(chosen) - set(maker.first)) @@ -181,7 +209,8 @@ def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[st got = maker.write(no, plan, old) out[plan["갈래"]] += 1 name = maker.blocks[no].get("이름") or line.get("이름") - out["만듦"].append({"번": no, "이름": name, "키": got, "갈래": plan["갈래"]}) + made_ = {"번": no, "이름": name, "키": got, "갈래": plan["갈래"], "못옮김": plan["못옮김"]} + out["만듦"].append({**made_, **({"환산": plan["환산"]} if plan.get("환산") else {})}) found = ledger.recall(line, folder, books) if no in chosen or found is None or found.get("규격다름"): alias[ledger.key(line)] = _alias(line, plan) diff --git a/M02_MasterTemplete/M02_Hopyo_SkLink.py b/M02_MasterTemplete/M02_Hopyo_SkLink.py index e2f990493..8977178de 100644 --- a/M02_MasterTemplete/M02_Hopyo_SkLink.py +++ b/M02_MasterTemplete/M02_Hopyo_SkLink.py @@ -4,7 +4,8 @@ 확인 안 한 단가산출 줄 = 그 블록 SK 참조(이 층 · 같은 원본 · 없으면 `M02_Hopyo_Sangeun.Maker` 로 그 블록만 만듦 · 미리 보기 = 키만 · 맞춤 셈 전 · 고름 대기 · 단위 다름 = 못 이음) · 확인한 로직 줄이 차이 SK 의 바탕 = 그 SK 참조(엑셀 차이 보존 · 별칭 짝 = 기초까지만) · -남은 줄 = 엑셀 줄 · 갈래 · 까닭 · 후보(단가산출 고름 대기). +남은 줄 = 엑셀 줄 · 갈래 · 까닭 · 후보(단가산출 고름 대기) · 못옮김 남은 SK = 못 이음(`단산` 번 · `못옮김` · +잇기 = [단가산출 만들기] `바탕[번].잇기` · 729-4). """ from __future__ import annotations @@ -64,6 +65,7 @@ def sk_line(b: Any, line: dict[str, Any], create: bool) -> tuple[dict | None, di return None, None no = int(ref.get("번") or 0) key = sks(b).get(sangeun.mark(b.key, no)) + lost = None if key is None and create: if b._maker is None: b._maker = sangeun.Maker(b.src, b.key, b.who) @@ -74,11 +76,17 @@ def sk_line(b: Any, line: dict[str, Any], create: bool) -> tuple[dict | None, di plan = b._maker.plan(no) if plan["갈래"] == sangeun.skpatch.WAIT: return None, {"까닭": plan["까닭"], "후보": plan["후보"]} + lost = plan["못옮김"] key = f"SK-미리{no}" if b.dry else b._maker.write(no, plan, None) sks(b)[sangeun.mark(b.key, no)] = key if key is None: return None, None - unit = sk_doc(b, key).get("단위") or line.get("단위") + doc = sk_doc(b, key) + lost = (doc.get("본뜬") or {}).get("못옮김") if lost is None else lost + if lost: # 연결 안 되면 못 만듦(내역 20-4) + why = f"단가산출 못옮김 {len(lost)} — [단가산출 만들기]에서 이음" + return None, {"까닭": why, "단산": no, "SK": key, "못옮김": lost} + unit = doc.get("단위") or line.get("단위") if mm_unit(unit) != mm_unit(line.get("단위")): return None, {"까닭": f"단가산출 단위 「{unit}」 다름"} qty = Decimal(str(line.get("수량") or 0)) @@ -105,4 +113,5 @@ def left(line: dict[str, Any], why: dict | None) -> dict[str, Any]: keep = ("차례", "이름", "규격", "단위", "수량") out = {**{k: line.get(k) for k in keep}, "갈래": kind} out["까닭"] = (why or {}).get("까닭") or WHY.get(kind, "이을 것 없음") - return {**out, "후보": (why or {}).get("후보") or []} + more = {k: v for k, v in (why or {}).items() if k not in ("까닭", "후보")} + return {**out, "후보": (why or {}).get("후보") or [], **more} diff --git a/M02_MasterTemplete/M02_Hopyo_SkPatch.py b/M02_MasterTemplete/M02_Hopyo_SkPatch.py index 04be3e43f..f4073176f 100644 --- a/M02_MasterTemplete/M02_Hopyo_SkPatch.py +++ b/M02_MasterTemplete/M02_Hopyo_SkPatch.py @@ -8,6 +8,10 @@ 장비 = 시간 같은 같은 갈래 줄은 그대로 · 같은 갈래 남은 줄 = 덮개 qty · 엑셀만 = 더한줄 기계경비 로직(굴착기 GC000267 · 그 밖 GC000268 · 기계 = 옵션 거꾸로 찾기) · 남은 우리 = 뺀줄 자재 = 같은 이름 우리 줄 수량 덮개 · 옮기지 못한 차이 = `못옮김`(버리지 않고 본뜬에 적음) + 못옮김 = `[{id, 갈래 노임|기계|자재, 이름, 규격, 단위, 수량(엑셀 1 단위 품), 글, 후보}]` · 사용자 `잇기` + `[{id, 노임|기계|자재: 키}]` = SK 더한줄 인력 · 기계 · 재료(엑셀 수량 그대로) · `{id, 뺌: 까닭}` = 놓음. + SK 단위(728-2) = 엑셀 줄 단위 · 바탕 기준단위와 성격 다름 = 환산 계수 = 우리 품 ÷ 엑셀 품(인력 · 장비 + 둘 이상이 같은 비일 때만 · `환산` {계수}) · 못 구함 = 고름 대기(까닭 「단위 성격 다름 · 계수 필요」). 고름 대기 = 후보 · 숫자 없음 — SK 안 만듦 · 후보 다섯(사용자가 고른 기초 `pick` = 견줘 SK · 기초). cid = `kind:키#n`(첫 기초 바탕 줄만 · `M02_Sangeun_Doc.line_ids`) · 더한줄 · 덮개 qty = 바탕 행 몫 (1 단위 품 × 바탕 결과단위 앞 수 · 셈이 ÷ 배) · 더한줄 로직 수량 = 몫 × 바탕 배 ÷ 그 기초 배. @@ -28,12 +32,23 @@ from M02_MasterTemplete import M02_Hopyo_Fit as fit from M02_MasterTemplete import M02_Hopyo_Option as option from M02_MasterTemplete import M02_Hopyo_Ours as ours from M02_MasterTemplete import M02_Sangeun_Calc as calc +from M02_MasterTemplete import M02_Sangeun_Doc as sd from M02_MasterTemplete.M02_Hopyo_Words import mtypes, specnums BASE, SK, WAIT = "기초", "SK", "고름 대기" KEYS = ("덮개", "자재표시", "더한줄", "뺀줄", "못옮김") TINY = 1e-9 _ADD = re.compile(r"^add:#(\d+)$") +UNIT_WHY = "단위 성격 다름 · 계수 필요" +LINK = { + "노임": ("인력", "LB"), + "기계": ("기계", "EQ"), + "자재": ("재료", "MT"), +} # 잇기 칸 → 더한줄 · 키 머리 + + +class LinkError(ValueError): + """잇기 틀림(없는 id · 갈래 다름 · 없는 키) — 부르는 쪽이 400.""" def _near(a: float, b: float) -> bool: @@ -119,9 +134,9 @@ def measure(doc: dict, whole: Any) -> dict[str, Any]: class _Plan: def __init__(self, doc: dict, whole: Any) -> None: self.doc, self.whole = doc, whole - self.tb = float(calc.times_of(whole.logic(doc["바탕"]["로직"]))) + self.tb = float(calc.ratio_of(doc, whole.logic(doc["바탕"]["로직"]), whole)) self.lines = parts(doc, whole) - self.lost: list[str] = [] + self.lost: list[dict[str, Any]] = [] self.lb = {name: key for key, name in ours._labor_names().items()} def cover(self, cid: str, **one: float) -> None: @@ -134,6 +149,38 @@ class _Plan: def add(self, one: dict[str, Any]) -> None: self.doc["더한줄"].append(one) + def miss(self, kind: str, name: str, qty: float, text: str, **more: Any) -> None: + """못옮김 한 줄 — id = 「갈래:이름」(같은 것 = #n) · 수량 = 엑셀 1 단위 품.""" + base = f"{kind}:{name}" + n = sum(1 for x in self.lost if (x["갈래"], x["이름"]) == (kind, name)) + one = {"id": base if not n else f"{base}#{n}", "갈래": kind, "이름": name, "규격": "", + "단위": "", "수량": _num(float(f"{qty:.10g}")), "글": text, "후보": []} # fmt: skip + self.lost.append({**one, **more}) + + def link(self, picks: list[dict[str, Any]]) -> None: + """사용자 잇기 — 이은 줄 = 더한줄(엑셀 수량 × 배) · 뺌 = 놓음 · 나머지 못옮김 그대로.""" + for raw in picks: + got = next((x for x in self.lost if x["id"] == str(raw.get("id"))), None) + if got is None: + raise LinkError(f"없는 못옮김 「{raw.get('id')}」") + if raw.get("뺌"): + self.lost.remove(got) + self.doc.setdefault("놓음", []).append({**got, "뺌": str(raw["뺌"])}) + continue + kind, head = LINK.get(got["갈래"], ("", "")) + key = str(raw.get(got["갈래"]) or "") + if not kind or not key.startswith(head): + raise LinkError(f"「{got['id']}」 · {got['갈래']} 키({head}…)로 이음") + try: + elem = self.whole.get(key) or {} + except Exception as e: # noqa: BLE001 — 없는 키 + raise LinkError(f"「{got['id']}」 · 없는 키 「{key}」") from e + self.lost.remove(got) + name = str(elem.get("이름") or got["이름"]) + unit = got["단위"] or ("인" if kind == "인력" else str(elem.get("단위") or "")) + self.add({"종류": kind, "요소": key, "이름": name, "단위": unit, + "수량": _num(float(got["수량"]) * self.tb)}) # fmt: skip + def labor(self, excel: dict[str, float]) -> None: mine = [x for x in self.lines if x["갈래"] == "인력" and not x["cid"].startswith("add:")] mine = [{**x, "품": x["품"] or 0.0} for x in mine] @@ -151,14 +198,14 @@ class _Plan: for x in rows: self.drop(x["cid"]) if want < -TINY: - self.lost.append(f"{t} 엑셀 {a:.6g} · 더한 기초만 {f:.6g}") + self.miss("노임", t, a, f"엑셀 {a:.6g} · 더한 기초만 {f:.6g}", 단위="인") elif not rows: if t in self.lb: unit = "인" self.add({"종류": "인력", "요소": self.lb[t], "이름": t, "단위": unit, "수량": _num(want * self.tb)}) # fmt: skip else: - self.lost.append(f"{t} 엑셀 {a:.6g} — 우리 인력에 없음") + self.miss("노임", t, want, f"엑셀 {a:.6g} — 우리 인력에 없음", 단위="인") elif len(rows) == 1 or not b: self.cover(rows[0]["cid"], qty=want * self.tb) for x in rows[1:]: @@ -195,7 +242,8 @@ class _Plan: for x in mine: if x["cid"] not in used: self.drop(x["cid"]) - self.lost += [f"우리만 장비 {'·'.join(sorted(a))} — 더한 기초 안" for a, _ in fixed] + for a, q in fixed: + self.miss("기계", "·".join(sorted(a)), q, "우리만 장비 — 더한 기초 안", 단위="시간") def _add_machine(self, m: dict[str, Any], line: dict[str, Any]) -> None: """엑셀만 장비 → 기계경비 로직 줄(기계 = 그 갈래 · 규격 옵션 거꾸로 찾기).""" @@ -210,7 +258,9 @@ class _Plan: self.add({"종류": "로직", "로직": code, "입력값": got["입력"], "이름": name, "단위": "시간", "수량": _num(m["시간"] * self.tb)}) # fmt: skip return - self.lost.append(f"엑셀만 장비 {'·'.join(m['갈래'])} {m['시간']:.6g} 시간 — 기계 못 찾음") + spec = " ".join(f"{x:g}" for x in m.get("규격") or []) + name = "·".join(m["갈래"]) + self.miss("기계", name, m["시간"], "엑셀만 장비 — 기계 못 찾음", 규격=spec, 단위="시간") def materials(self, excel: list[dict[str, Any]]) -> None: mine = [x for x in self.lines if x["갈래"] == "자재" and not x["cid"].startswith("add:")] @@ -229,7 +279,7 @@ class _Plan: for x in rows[1:]: self.drop(x["cid"]) else: - self.lost.append(f"엑셀만 자재 {m['이름']} {m['품']:.6g}") + self.miss("자재", m["이름"], m["품"], "엑셀만 자재") def _extras(entry: dict, code: str, whole: Any, tb: float) -> list[dict[str, Any]]: @@ -245,11 +295,49 @@ def _extras(entry: dict, code: str, whole: Any, tb: float) -> list[dict[str, Any return out -def _wait(entry: dict) -> dict[str, Any]: +def _need(code: str, unit: str, whole: Any) -> dict[str, Any] | None: + """후보 기초 기준단위 ↔ 엑셀 단위가 성격 다름 = 계수 칸 `{가, 나, 갈래 사용자|단위중량}` · 아니면 None.""" + from M01_MasterData import M01_MasterData_Store_Combo as combo_store + + try: + got = calc.base_unit(whole.logic(code)) + except Exception: # noqa: BLE001 — 없는 로직 + return None + if not got or str(unit or "").strip() in ("", "식"): + return None + swap = combo_store.mu.swap(got[0], unit) + return None if swap["배"] is not None else {"가": got[0], "나": unit, "갈래": swap["계수"]} + + +def _wait(entry: dict, whole: Any, unit: str, why: str | None = None) -> dict[str, Any]: keep = ("로직", "이름", "단위", "판정", "글") cands = [{k: c.get(k) for k in keep} for c in (entry.get("후보") or [])[: fit.TOP_OPT]] - why = (entry.get("판정") or {}).get("글") or "후보 없음" - return {"갈래": WAIT, "까닭": f"맞는 기초 없음 — {why}", "후보": cands} + for c in cands: + c["계수"] = _need(str(c.get("로직") or ""), unit, whole) + if why is None: + why = "맞는 기초 없음 — " + ((entry.get("판정") or {}).get("글") or "후보 없음") + return {"갈래": WAIT, "까닭": why, "후보": cands} + + +def _by_type(rows: list[dict[str, Any]]) -> dict[str, float]: + out: dict[str, float] = {} + for x in rows: + k = "+".join(sorted(x["갈래"])) + out[k] = out.get(k, 0.0) + x["시간"] + return out + + +def _coef(doc: dict, whole: Any, excel: dict[str, Any]) -> float | None: + """성격 다른 단위 계수(바탕 앞 수 뗀 1 단위 = SK 몇 단위) = 우리 품 ÷ 엑셀 품 — 인력 직종 · 장비 갈래 + 둘 이상이 같은 비(맞춤 허용 폭)일 때만 · 아니면 None(고름 대기).""" + mine = measure({**doc, "단위": ""}, whole) # 「」 = 앞 수 뗀 바탕 단위(옛 셈) + pairs = [(mine["인력"].get(t, 0.0), q) for t, q in excel["인력"].items()] + ours_m = _by_type(mine["장비"]) + pairs += [(ours_m.get(k, 0.0), q) for k, q in _by_type(excel["장비"]).items()] + rates = [a / b for a, b in pairs if a > TINY and b > TINY] + if len(rates) < 2 or max(rates) > min(rates) * (1 + fit.TOL[0]): + return None + return float(f"{sum(rates) / len(rates):.6g}") def plan( @@ -259,13 +347,14 @@ def plan( whole: Any, pick: dict | None = None, ) -> dict[str, Any]: - """블록 한 칸 → `{갈래 기초|SK|고름 대기, 바탕, 입력값, 덮개, 자재표시, 더한줄, 뺀줄, 못옮김, 엑셀품, - 후보?}` · - `pick` = 사용자가 고른 기초 `{로직, 입력값?}`(옵션 없으면 거꾸로 찾음 · 늘 견줌).""" - entry = entry or {} + """블록 한 칸 → `{갈래 기초|SK|고름 대기, 바탕, 입력값, 환산?, 덮개, 자재표시, 더한줄, 뺀줄, 못옮김, + 놓음, 엑셀품, 후보?}` · `pick` = 사용자 `{로직?, 입력값?, 환산?, 잇기?}` — 로직 = 고른 기초(옵션 없으면 + 거꾸로 찾음 · 늘 견줌) · 환산 = SK `환산` 꼴 · 잇기 = 못옮김 잇기(`_Plan.link` · 틀림 `LinkError`).""" + entry, pick = entry or {}, pick or {} state = (entry.get("판정") or {}).get("상태") efp = finger.excel(block, line) - if pick: + unit = str(line.get("단위") or "").strip() or "식" + if pick.get("로직"): code = str(pick["로직"]) given = pick.get("입력값") if given is None: @@ -274,21 +363,44 @@ def plan( code = entry["차례"][0] given = ((entry.get("옵션") or {}).get(code) or {}).get("입력") or {} else: - return _wait(entry) - doc = {"키": "SK-000000", "이름": "견줌", "단위": line.get("단위") or "식", - "바탕": {"로직": code}, "입력값": dict(given), "덮개": {}, "뺀줄": [], "자재표시": {}} # fmt: skip - tb = float(calc.times_of(whole.logic(code))) - doc["더한줄"] = [] if pick else _extras(entry, code, whole, tb) + return _wait(entry, whole, unit) + doc = {"키": "SK-000000", "이름": "견줌", "단위": unit, "바탕": {"로직": code}, + "입력값": dict(given), "덮개": {}, "뺀줄": [], "자재표시": {}} # fmt: skip + if pick.get("환산"): + doc["환산"] = dict(pick["환산"]) from M02_MasterTemplete import M02_Hopyo_Verdict as verdict # 판정이 Fit 을 부름 — 늦게 읽음 excel = verdict.excel_qty(efp) + row = whole.logic(code) + + def extras(tb: float) -> list[dict[str, Any]]: + return [] if pick.get("로직") else _extras(entry, code, whole, tb) + + try: + tb = float(calc.ratio_of(doc, row, whole)) + except sd.DocError as e: + old = {**doc, "더한줄": extras(float(calc.times_of(row)))} + coef = None if pick.get("환산") else _coef(old, whole, excel) + if coef is None: + cands = entry.get("후보") or [] + if code not in [c.get("로직") for c in cands]: + cands = [{"로직": code, "이름": row.get("이름")}, *cands] + why = f"{UNIT_WHY} — {str(e).split(' · ', 1)[-1]}" + return _wait({"후보": cands}, whole, unit, why) + doc["환산"] = {"계수": coef} + tb = float(calc.ratio_of(doc, row, whole)) + doc["더한줄"] = extras(tb) head = {"바탕": doc["바탕"], "입력값": doc["입력값"], "엑셀품": excel} + head |= {"환산": doc["환산"]} if doc.get("환산") else {} if state == "맞음" and not pick and not doc["더한줄"]: - return {**head, "갈래": BASE, **dict.fromkeys(KEYS[:2], {}), **dict.fromkeys(KEYS[2:], [])} + base = {**dict.fromkeys(KEYS[:2], {}), **dict.fromkeys(KEYS[2:], []), "놓음": []} + return {**head, "갈래": BASE, **base} work = _Plan(doc, whole) work.labor({t: x["품"] for t, x in efp["인력"].items() if x.get("품")}) work.machines([m for m in efp["장비"] if m.get("시간")], line) work.materials([m for m in efp["자재"] if m.get("품")]) + work.link(pick.get("잇기") or []) out = {k: doc[k] for k in KEYS[:4]} - changed = any(out.values()) - return {**head, **out, "갈래": SK if changed else BASE, "못옮김": work.lost} + changed = any(out.values()) or bool(work.lost) + kept = {"못옮김": work.lost, "놓음": doc.get("놓음") or []} + return {**head, **out, "갈래": SK if changed else BASE, **kept} diff --git a/resources/tester/test_729_hopyo_sk_link.py b/resources/tester/test_729_hopyo_sk_link.py index ba8a8087d..2bd40edc1 100644 --- a/resources/tester/test_729_hopyo_sk_link.py +++ b/resources/tester/test_729_hopyo_sk_link.py @@ -5,6 +5,7 @@ SK 품 = 엑셀 품(차이는 엑셀 값 그대로) · 같은 단가산출 = 기초(덮개 없음) + 별칭 제안(짝 = 기초까지만) · 고름 대기 = SK 안 만듦 → 고른 기초로 SK · 안 이은 줄 남으면 만들기 409(미리 보기 = 같은 줄) · 사용자가 고르면 만듦 · 노임 = 이름 이음 · 기계 = EQ 이음 · master_data 원본 그대로. 층 = 임시 폴더. +729-4 = 단위 성격 다름(계수 · 고름 대기) · 못옮김 잇기 · 남은 못옮김 = 일위대가 막음. """ import json @@ -18,6 +19,7 @@ from M02_MasterTemplete import M02_Hopyo_Ours as ours from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch from M02_MasterTemplete import M02_Hopyo_Store as hs from M02_MasterTemplete import M02_MasterTemplete_Store as store +from M02_MasterTemplete import M02_Sangeun_Calc as calc from M02_MasterTemplete import M02_Template_Layers as layers from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_hash # noqa: F401 @@ -129,6 +131,61 @@ def test_실무_단가산출_기초_SK_고름대기_일위대가_막음(client): assert master_hash() == before +def test_단위_성격_다름_계수_못옮김_잇기_남으면_일위대가_막음(client): + """729-4 — 728-2 단위 환산 뒤 500 0 · 성격 다른 단위 = 계수 거꾸로(자신 있을 때) · 아니면 고름 대기 + (후보 계수 칸) · 못옮김 잇기 = SK 더한줄 · 뺌 = 놓음 · 남으면 그 SK 를 쓰는 일위대가 막음.""" + before = master_hash() + folder, whole = layers.personal_dir(7, 42), m01.loaded()[1] + key = _post(client).json()["키"] + url = f"{URL.format('personal')}/{key}/sangeun" + fit.run_job(folder, key) + got = client.post(url, json={}) + assert got.status_code == 200, got.text + got = got.json() + wait = {x["번"]: x for x in got["고름대기"]} + assert wait[30]["까닭"].startswith(skpatch.UNIT_WHY) # 큰돌채집운반 ㎡ ↔ 100개 + assert wait[30]["후보"][0]["계수"] == {"가": "100개", "나": "㎡", "갈래": "사용자"} + docs = _sk_docs(folder) + coef = [d for d in docs.values() if d.get("환산")] + assert coef # 계수 거꾸로 = 셈 됨 · 엑셀 품 그대로 + for d in coef: + with store.at(folder): + mine = skpatch.measure(d, whole) + assert all(_same(q, mine["인력"][t]) for t, q in d["본뜬"]["엑셀품"]["인력"].items()) + lost = {x["번"]: x["못옮김"] for x in got["만듦"] if x["못옮김"]} + a, b = lost[95] # 도장 — 엑셀만 자재 둘 + assert a["갈래"] == "자재" and a["id"] != b["id"] and a["수량"] == 0.322 + hp = hs.read(folder, key) + no = next(h["번"] for h in hp["호표"] for x in h["줄"] + if (x.get("참조") or {}).get("종류") == "단산" and int(x["참조"]["번"]) == 95) # fmt: skip + body = {"층": "personal", "키": key, "번": no, "줄": []} + + def left() -> list: + return [x for x in client.post(LEFT, json=body).json()["남은줄"] if x.get("단산") == 95] + + assert [len(x["못옮김"]) for x in left()] == [2] + assert client.post(BUILD, json=body).status_code == 409 + + def link(*rows: dict): + return client.post(url, json={"바탕": {"95": {"잇기": list(rows)}}}) + + assert link({"id": "자재:없음", "자재": "MT000010"}).status_code == 400 + assert link({"id": a["id"], "기계": "EQ000001"}).status_code == 400 + assert link({"id": a["id"], "자재": "MT999999"}).status_code == 400 + one = link({"id": a["id"], "자재": "MT000010"}).json() + assert [x["못옮김"] for x in one["만듦"]] == [[b]] + doc = _sk_docs(folder)[95] + tb = float(calc.ratio_of(doc, whole.logic(doc["바탕"]["로직"]), whole)) + add = doc["더한줄"][-1] + assert add["종류"] == "재료" and add["요소"] == "MT000010" and _same(add["수량"], 0.322 * tb) + assert [len(x["못옮김"]) for x in left()] == [1] + two = link({"id": a["id"], "자재": "MT000010"}, {"id": b["id"], "뺌": "현장 지급"}).json() + assert two["만듦"][0]["못옮김"] == [] + assert _sk_docs(folder)[95]["본뜬"]["놓음"][0]["id"] == b["id"] + assert left() == [] + assert master_hash() == before + + def _eq(one: dict) -> str: return str((one.get("입력값") or {}).get("기계") or "")