diff --git a/M02_MasterTemplete/M02_Hopyo_Sangeun.py b/M02_MasterTemplete/M02_Hopyo_Sangeun.py index 9634dc0a2..1326cb3f5 100644 --- a/M02_MasterTemplete/M02_Hopyo_Sangeun.py +++ b/M02_MasterTemplete/M02_Hopyo_Sangeun.py @@ -1,24 +1,16 @@ -"""M02 일위대가 불러오기 → 단가산출(PLAN 718-1 · 729-1 · 요구서 내역19 1장 7 · 3장 6) — -산근(단산) 블록마다 매칭 → 비교 → 기초 · SK · 고름 대기. +"""M02 일위대가 불러오기 → 단가산출(PLAN 718-1 · 729-1 · 778-5 D-3 · 요구 621 · 623) — 산근(단산) 블록마다 +줄 판정 모음(`M02_Hopyo_SkUnit`) → 사용자가 확인한 블록만 단위 SK. -블록마다 그 블록을 부르는 첫 엑셀 줄(「단산N참조」)의 맞춤셈(`M02_Hopyo_Fit` 캐시 · 없으면 그 블록만 -셈) → `M02_Hopyo_SkPatch.plan`: - 기초 = SK 바탕 = 1위 기초 · 입력값 · 덮개 없음 · SK = 바탕 기초 + 덮개 · 더한줄 · 뺀줄(엑셀 품 그대로) · - 고름 대기 = SK 안 만듦(바탕 없는 SK 로 몰래 채우지 않음) · 후보 다섯(성격 다른 단위 = `계수` 칸 뜻) → - 사용자가 고른 기초 `바탕 {번: {로직?, 입력값?, 환산?, 잇기?}}` 로 다시 부르면 그 기초에 견줘 SK(이미 있으면 - 그 키에 덮음) · 잇기 `[{id, 노임|기계|자재: 키} | {id, 뺌}]` = 못옮김 → SK 더한줄 · 남은 못옮김 = 응답 - `만듦[].못옮김` · 본뜬 그대로(그 SK 를 쓰는 일위대가 만들기 = 남은 줄 409 · `M02_Hopyo_SkLink`) · - 그대로 SK 의 못옮김 = 응답 `남은못옮김 [{번, 이름, 키, 못옮김}]` · 잇기만 보냈는데 1위 기초가 고름 대기면 - 그 SK 바탕(로직 · 입력값 · 환산)으로 다시 셈(729-5). +모두 줄로만(바탕 기초 안 씀 · 맞춤셈 1위 기초 = 근거 꼬리표만) · 자동 없음 — `확인 [{번, 고름?: {차례: 고름}}]` +(블록마다 [확인] · [옮기기])에 든 블록만 만듦 · 확인 안 한 블록 = 판정만(안 만듦) · 줄 다 이어지지 않은 확인 블록 = +`못만듦` · 하위 단산 줄 = 그 블록 SK 참조(확인 목록에 있으면 먼저 만듦 · 없으면 못만듦). SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 · -본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래, 엑셀품, 못옮김} · 같은 본뜬(키 · 원본)이 -이 층에 있으면 그대로(겹침 없음) · 만드는 층 = 부르는 층. +바탕 없음 · 본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래 「단위」, 근거 {로직, 이름}, 못옮김 []} · +같은 본뜬(키 · 원본)이 이 층에 있으면 그 키에 덮음 · 만드는 층 = 부르는 층. 같은 내용(733-1 · `M02_Library_Same` 지문) SK 가 이 층에 있으면 새로 안 만들고 그것을 씀 — 이름 = 먼저 만든 것 · -뒤 출처 = 그 SK `본뜬.또 [그 블록 본뜬 + 이름]`(`made` · `origin_of` 가 읽음 · 갈래 · 못옮김은 출처마다) · -다른 출처가 쓰는 SK 는 내용을 안 바꿈(고른 기초 · 잇기로 바뀌면 떼고 다시 찾음) · 응답 `같은것씀 [{번, 키}]` · -위 층(회사 · 마스터) 같은 것 = 응답 `위층같음 [{번, 층, 주인, 키}]`(알림만 · 연결 안 함). -별칭 제안 = 기초 · SK 만든 블록의 엑셀 줄 → 이 층 별칭 장부 짝 = 바탕 기초까지만(차이 SK 는 이 층 문서 · -요구서 3장 6) · 장부에 이미 짝 · 갈래가 있는 줄은 그대로 · 승인 길 = M01 별칭 장부(716). +뒤 출처 = 그 SK `본뜬.또 [그 블록 본뜬 + 이름]`(`made` · `origin_of` 가 읽음) · 다른 출처가 쓰는 SK 는 내용을 +안 바꿈(바뀌면 떼고 다시 찾음) · 응답 `같은것씀 [{번, 키}]` · 위 층 같은 것 = `위층같음 [{번, 층, 주인, 키}]`(알림만). +일위대가 만들기(`M02_Hopyo_SkLink`)가 부르는 `Maker.plan(번)`(확인 없음) = 고름 대기 「확인 전」 — 못 이음. """ from __future__ import annotations @@ -32,14 +24,16 @@ from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Hopyo_Fit as fit from M02_MasterTemplete import M02_Hopyo_Ledger as ledger from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch +from M02_MasterTemplete import M02_Hopyo_SkUnit as skunit +from M02_MasterTemplete import M02_Hopyo_Units as units from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store 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_Doc as sd ORIGIN = "실무자료" KIND = "sangeun" +WAIT_WHY = "확인 전 — [단가산출]에서 블록 [확인] 뒤 [옮기기]" def mark(key: str, no: int) -> str: @@ -99,50 +93,26 @@ def _bad(text: str, status: int = 400) -> store.StoreError: return store.StoreError(status, text) -def _picks(raw: Any) -> dict[int, dict[str, Any]]: - """사용자 고름 `{번: {로직?, 입력값?, 환산?, 잇기?}}` 재검사 — 돈 로직만 · 입력 이름 = 그 로직 입력 · - 환산 = SK 꼴(`M02_Sangeun_Doc`) · 잇기 = `[{id, …}]`(id 짝 · 키는 `M02_Hopyo_SkPatch` 가 봄).""" +def _sure(raw: Any) -> dict[int, dict[int, Any]]: + """확인 목록 `[{번, 고름?: {차례: 고름}}]` → `{번: {차례: 고름}}`(고름 꼴은 `M02_Hopyo_SkUnit` 이 봄).""" if not raw: return {} - if not isinstance(raw, dict) or len(raw) > 500: - raise _bad("바탕은 {블록 번: {로직, 입력값}}") - logics = ledger._known()[0] - out = {} - for no, one in raw.items(): - 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(code) if isinstance(given, dict) else {1} - if bad: - raise _bad(f"단산{no} · 모르는 입력 {sorted(map(str, bad))}") - 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 + if not isinstance(raw, list) or len(raw) > 500: + raise _bad("확인은 [{번, 고름?: {차례: 고름}}]") + out: dict[int, dict[int, Any]] = {} + for one in raw: + try: + picks = one.get("고름") or {} + out[int(one["번"])] = {int(k): v for k, v in picks.items()} + except (TypeError, ValueError, KeyError, AttributeError) as e: + raise _bad("확인은 [{번, 고름?: {차례: 고름}}]") from e return out def _doc(line: dict, block: dict, no: int, hp: dict, plan: dict, sure: dict) -> dict[str, Any]: 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 = {k: plan[k] for k in ("갈래", "엑셀품", "못옮김", "근거") if k in plan} keep |= {"놓음": plan["놓음"]} if plan.get("놓음") else {} return { "종류": "단가산출", @@ -160,32 +130,6 @@ def _doc(line: dict, block: dict, no: int, hp: dict, plan: dict, sure: dict) -> } -def _old_base(folder: Path, key: str, mark_: str, source: refs.Here) -> dict[str, Any]: - """이미 있는 SK 의 바탕 고름 `{로직, 입력값, 환산?}` · `본뜬` — 읽기 실패 = 빈 것.""" - try: - doc = json.loads((Path(folder) / KIND / f"{key}.json").read_bytes()) - except (OSError, ValueError): - return {} - base = {"로직": (doc.get("바탕") or {}).get("로직"), "입력값": doc.get("입력값") or {}} - return { - **base, - **({"환산": doc["환산"]} if doc.get("환산") else {}), - "본뜬": origin_of(doc, mark_, source), - } - - -def _alias(line: dict, plan: dict) -> dict[str, Any]: - origin = {"품명": line.get("이름"), "규격": line.get("규격"), "단위": line.get("단위")} - clauses = (line.get("근거") or {}).get("출처") or [] - pair = {"로직": plan["바탕"]["로직"], "입력값": plan["입력값"], "배": 1, "몫": None} - return { - "원래": {k: str(v or "") for k, v in origin.items()}, - "갈래": "짝", - "짝": [pair], - "엑셀조항": clauses if isinstance(clauses, list) else [], - } - - class Maker: """한 번 만들기 — 파일 · 맞춤셈 · 이 층 SK 를 한 번 읽음(`source` = 파일 층 · 쓰기 = `store.root()`).""" @@ -199,16 +143,77 @@ class Maker: self.whole = m01.loaded()[1] self.sure = {"원본": list(self.here), "확인": {"누가": who, "때": utc_now_text()}} self.same, self.reused, self.above = same.Same(), [], [] + self.keys = made(source, key, self.here) # {본뜬 키: SK} — 만든 것 더해 감 + self.judged: dict[int, dict] = {} + self._units: units.Units | None = None - def plan(self, no: int, pick: dict | None = None) -> dict[str, Any]: + def units(self) -> units.Units: + if self._units is None: + books = ledger.books(self.source) + self._units = units.Units(books, ledger.doc_id(self.source, self.key)) + return self._units + + def basis(self, no: int) -> dict[str, Any] | None: + """근거 꼬리표 = 맞춤셈 1위 기초(참고).""" + entry = self.cache.get(fit.unit_key(self.first[no])) or {} + top = (entry.get("차례") or [None])[0] + if not top: + return None + name = (self.whole.logic(top) or {}).get("이름") + return {"로직": top, "이름": name, "판정": (entry.get("판정") or {}).get("상태")} + + def ref_of(self, sub: dict[str, Any]) -> dict[str, Any] | None: + """하위 줄 → 참조 칸 — 단산 = 이 HP 그 블록 SK · 일위 = 확인된 그 호표 SC · 없으면 None.""" + from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build # Build → SkLink → 여기 + + if sub.get("종류") == "단산": + got = self.keys.get(mark(self.key, int(sub["번"]))) + else: + done = hopyo_build.confirmed(store.root(), self.here) + got = done.get(hopyo_build.mark(self.key, int(sub["번"]))) + layer, owner = refs.here() + return {"층": layer, "주인": owner, "키": got} if got else None + + def judge(self, no: int, picks: 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 or {}).get("로직"): - entry = fit.compute(block, line) + unit = str(line.get("단위") or block.get("단위") or "").strip() try: - return skpatch.plan(entry, block, line, self.whole, pick) - except skpatch.LinkError as e: + got = skunit.judge(block, self.units(), self.ref_of, picks, unit) + except skunit.PickError as e: raise _bad(f"단산{no} · {e}") from e + self.judged[no] = got + return got + + def plan(self, no: int, picks: dict | None = None) -> dict[str, Any]: + """확인한 블록 → 단위 SK 꼴 · 확인 없음(`picks` None) · 고를 줄 남음 = 고름 대기.""" + if picks is None: + return {"갈래": skpatch.WAIT, "까닭": WAIT_WHY, "후보": []} + got = self.judge(no, picks) + if got["고를줄"]: + return {"갈래": skpatch.WAIT, "까닭": got["판정"], "후보": []} + return skunit.plan(got, self.basis(no)) + + def make(self, no: int, sure: dict[int, dict], out: dict, path: tuple = ()) -> None: + """확인한 블록 하나 — 확인 목록의 하위 단산 먼저 · 못 만들면 `못만듦`.""" + mark_ = mark(self.key, no) + if no in path or any(x["번"] == no for x in out["만듦"] + out["못만듦"]): + return + for one in self.blocks[no].get("항목") or []: + sub = one.get("하위") or {} + m = int(sub.get("번") or 0) + if sub.get("종류") == "단산" and m in sure and mark(self.key, m) not in self.keys: + self.make(m, sure, out, (*path, no)) + plan = self.plan(no, sure[no]) + got = self.judged[no] + lost = [r for r in got["줄"] if r["상태"] == units.LINKED and r["차례"] not in got["담음"]] + name = self.blocks[no].get("이름") or self.first[no].get("이름") + if plan["갈래"] == skpatch.WAIT or lost: + why = plan.get("까닭") or f"하위 「{lost[0]['이름']}」 — 그 블록 먼저 확인" + out["못만듦"].append({"번": no, "이름": name, "까닭": why}) + return + key = self.write(no, plan, self.keys.get(mark_)) + self.keys[mark_] = key + out["만듦"].append({"번": no, "이름": name, "키": key, "갈래": skunit.UNIT, "못옮김": []}) def write(self, no: int, plan: dict, have: str | None) -> str: """그 블록 SK 를 씀 → 키 — 다른 출처(또)가 쓰는 SK 의 내용은 안 바꿈(바뀌면 떼고 다시 찾음).""" @@ -261,53 +266,29 @@ class Maker: def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[str, Any]: - """[단가산출 만들기] — 산근 블록마다 기초 · SK · 고름 대기 → `{블록, 만듦 [{번, 이름, 키, 갈래, 못옮김, - 환산?}], 그대로, 남은못옮김 [{번, 이름, 키, 못옮김}], 기초, SK, 고름대기 [{번, 이름, 단위, 까닭, 후보}], - 별칭, 같은것씀 [{번, 키}], 위층같음 [{번, 층, 주인, 키}]}` · `picks` = 고름.""" - chosen = _picks(picks) + """[단가산출] — 블록마다 줄 판정 · 확인한 블록만 단위 SK → `{블록, 판정 [{번, 이름, 단위, 판정, 고를줄, + 우리금액, 엑셀금액, 근거, 만든, 줄}], 만듦 [{번, 이름, 키, 갈래, 못옮김}], 못만듦 [{번, 이름, 까닭}], 그대로, + 같은것씀, 위층같음}` · `picks` = 확인 목록.""" + sure = _sure(picks) maker = Maker(folder, key, who) - bad = sorted(set(chosen) - set(maker.first)) + bad = sorted(set(sure) - set(maker.first)) if bad: raise _bad(f"없는 블록 · 부르는 줄 없음 「단산{bad[0]}」", 404) - if not maker.done and set(maker.first) - set(chosen): + if not maker.done: fit.ensure(folder, key) raise _bad("맞춤 셈 중 — 끝나면 다시 누름", 409) - have = made(folder, key, maker.here) - out: dict[str, Any] = {"블록": len(maker.first), "만듦": [], "그대로": 0, "고름대기": []} - out["남은못옮김"] = [] - out.update({skpatch.BASE: 0, skpatch.SK: 0}) - books, alias = ledger.books(folder), {} + out: dict[str, Any] = {"블록": len(maker.first), "만듦": [], "못만듦": []} + for no in sorted(sure): + maker.make(no, sure, out) + rows = [] for no in sorted(maker.first): - old = have.get(mark(key, no)) - line = maker.first[no] - name = maker.blocks[no].get("이름") or line.get("이름") - if old and no not in chosen: - out["그대로"] += 1 - lost = _old_base(folder, old, mark(key, no), maker.here)["본뜬"].get("못옮김") - if lost: - out["남은못옮김"].append({"번": no, "이름": name, "키": old, "못옮김": lost}) - continue - plan = maker.plan(no, chosen.get(no)) - if plan["갈래"] == skpatch.WAIT and old and not chosen[no].get("로직"): - base = _old_base( - folder, old, mark(key, no), maker.here - ) # 고른 기초로 만든 SK 의 잇기 = 그 바탕 그대로 - if base.get("로직"): - pick = {k: v for k, v in base.items() if k != "본뜬"} - plan = maker.plan(no, {**pick, **chosen[no]}) - if plan["갈래"] == skpatch.WAIT: - head = {"번": no, "이름": maker.blocks[no].get("이름") or line.get("이름")} - out["고름대기"].append({**head, "단위": line.get("단위"), **plan}) - continue - got = maker.write(no, plan, old) - out[plan["갈래"]] += 1 - 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) - out["별칭"] = ledger.write( - folder, alias, who, maker.hp.get("파일") or None, ledger.doc_id(folder, key) - ) + line, block = maker.first[no], maker.blocks[no] + got = maker.judged.get(no) or maker.judge(no) + head = {"번": no, "이름": block.get("이름") or line.get("이름"), "단위": line.get("단위")} + keep = ("판정", "고를줄", "우리금액", "엑셀금액", "줄") + head |= {k: got[k] for k in keep} | {"근거": maker.basis(no)} + rows.append({**head, "만든": maker.keys.get(mark(key, no))}) + out["판정"] = rows + out["그대로"] = sum(1 for r in rows if r["만든"] and r["번"] not in sure) out["같은것씀"], out["위층같음"] = maker.reused, maker.above return out diff --git a/M02_MasterTemplete/M02_Hopyo_SkUnit.py b/M02_MasterTemplete/M02_Hopyo_SkUnit.py new file mode 100644 index 000000000..bb85300c2 --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_SkUnit.py @@ -0,0 +1,190 @@ +"""M02 실무 단가산출 블록 = 줄 판정 모음 · 단위 SK(PLAN 778-5 · D-3 · 요구 623 · 624). + +모두 줄로만 — 바탕 기초 안 씀(기초 = 근거 꼬리표만) · 자동 없음(사용자가 확인한 블록만 만듦). +줄 판정 = `M02_Hopyo_Units` 잇기(상태 · 방법 · 단위) + 매개값(품 · 시간 · 수량 · 율) · 사용자 고름 +`{차례: 단위 | {뺌: 까닭}}`(그 줄만 · 꼬리표 「내가 고름」) · 단가 = M01 지금 값으로 다시 셈(`우리금액`) · +엑셀 금액과의 `차` = 참고(엑셀 = 과거 값 · 판정에 안 씀) · 셈 밖 줄 = 상태 「밖」(SK 에 안 들임). +블록 판정 = 줄 모음 — 모두 이어짐 = 「옮길 수 있음」 · 아니면 「고를 줄 n」. +단위 SK 더한줄(바탕 없음 · 778-4 꼴) = 인력 `{요소 LB, 수량 품}` · 기계경비 로직 `{로직 GC, 입력값 {기계}, +수량 시간 × 할증}` · 재료 `{요소 MT, 수량(손료율 든), 별도계상?}`(못 찾은 별도계상 = 0 원 고정 줄) · +율 `{고정 {비율 {기준, 비목}}, 수량 = 율 %}`(줄 목록 = 그 행 줄 `add:#k`) · 하위 `{참조 SK · SC}` · 값 고정 줄. +근거 꼬리표 = 맞춤셈 1위 기초(본뜬 칸 · 참고). 서버 단독 · 쓰기 = `M02_Hopyo_Sangeun`. +""" + +from __future__ import annotations + +from typing import Any + +from M02_MasterTemplete import M02_Hopyo_Fixed as hfix +from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch +from M02_MasterTemplete import M02_Hopyo_UnitLine as unit_line +from M02_MasterTemplete import M02_Hopyo_Units as units +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 + +GOOD = "옮길 수 있음" +OUT, DROP = "밖", "뺌" +UNIT = "단위" # 본뜬 갈래 +MINE = "내가 고름" +WORD = {"L": "품", "X": "시간", "M": "수량", "R": "율", "S": "수량"} +PICKS = {"L": ("노임", "LB"), "X": ("기계", "EQ"), "M": ("자재", "MT")} + + +class PickError(ValueError): + """고름 틀림 — 부르는 쪽이 400.""" + + +def _picked(one: dict[str, Any], pick: Any, us: units.Units) -> dict[str, Any]: + """사용자 고름 → 이어짐 단위 — 노임 LB · 기계 EQ(→ 기계경비 로직) · 자재 MT · 율 기준 · 뺌.""" + if not isinstance(pick, dict): + raise PickError("고름은 {노임|기계|자재: 키} · {기준, 비목?} · {뺌}") + if pick.get("뺌"): + return {"상태": DROP, "단위": None, "글": str(pick["뺌"]), "꼬리표": [MINE]} + kind = one["묶음"] + if kind == "R" and pick.get("기준") is not None: + unit = {"기준": pick["기준"], "비목": pick.get("비목") or units.rate_part(one.get("이름"))} + hfix.fixed.clean({"이름": "율", "비율": unit}, "고름", us.whole) + return {"상태": units.LINKED, "단위": unit, "꼬리표": [MINE]} + field, head = PICKS.get(kind, ("", "?")) + code = str(pick.get(field) or "") + if not field or not code.startswith(head): + raise PickError(f"{units.HOW.get(kind, kind)} 줄 · {field or '뺌'} 키({head}…)로 고름") + try: + row = us.whole.get(code) + except Exception as e: # noqa: BLE001 — 없는 키 + raise PickError(f"없는 키 「{code}」") from e + unit: dict[str, Any] = {field: code} + if kind == "X": + kinds = mtypes(f"{row.get('상세구분')} {row.get('이름')}") + unit["로직"] = skpatch._mach_logics(us.whole, kinds, row)[0] + unit |= {"할증": one["할증"]} if one.get("할증") else {} + if kind == "M" and (one.get("어떻게") == "별도" or pick.get("별도계상")): + unit["별도계상"] = True + return {"상태": units.LINKED, "단위": unit, "꼬리표": [MINE]} + + +def lines(block: dict[str, Any], us: units.Units, picks: dict | None = None) -> list[dict]: + """블록 단위 줄 → 줄 판정 `[{차례, 행, 묶음, 이름, 규격, 매개값, 엑셀금액, 재현, 상태, 방법, 단위, 후보, + 꼬리표, 글}]`(재현 = 엑셀 단가 × 매개값 ≈ 엑셀 금액 · D-6 띠) · `picks` = `{차례: 고름}`.""" + out = [] + for at, one in enumerate(block.get("항목") or []): + q = one.get("품") + row = {"차례": at, "행": one.get("행"), "묶음": one["묶음"], "이름": one.get("이름")} + row |= {"규격": one.get("규격") or "", "엑셀금액": one.get("금액"), "재현": one.get("재현")} + row["매개값"] = {WORD[one["묶음"]]: q} if one["묶음"] in WORD and q is not None else {} + if one.get("밖"): + out.append({**row, "상태": OUT, "방법": "", "단위": None, "후보": [], "꼬리표": []}) + continue + got = us.line(one) + pick = (picks or {}).get(at) + if pick is not None: + got = {**got, "후보": [], "글": "", **_picked(one, pick, us)} + out.append({**row, **got}) + return out + + +def verdict(rows: list[dict]) -> tuple[str, int]: + """블록 판정 — (「옮길 수 있음」 | 「고를 줄 n」, n).""" + n = sum(1 for r in rows if r["상태"] not in (units.LINKED, OUT, DROP)) + return (GOOD if not n else f"고를 줄 {n}"), n + + +def adds(block: dict, rows: list[dict], us: units.Units, ref_of: Any) -> tuple[list, dict]: + """이어진 줄 → 단위 SK 더한줄 · `{차례: add 번}` — `ref_of(하위)` = 참조 칸 | None(빠짐).""" + out: list[dict] = [] + at_of: dict[int, int] = {} + by_row = {r.get("행"): r["차례"] for r in rows} + items = block.get("항목") or [] + for r in rows: + if r["상태"] != units.LINKED: + continue + one, unit = items[r["차례"]], r["단위"] or {} + name = " ".join(str(one.get("이름") or "").split()) or "이름 없음" + q = float(one.get("품") or 0) + kind = one["묶음"] + if kind == "L": + add = {"종류": "인력", "요소": unit["노임"], "이름": name, "단위": "인", "수량": q} + elif kind == "X": + eq = us.whole.get(unit["기계"]) or {} + given = {skpatch.MACH_IN: eq.get("원문번호")} + q *= float(unit.get("할증") or 1) + add = { + "종류": "로직", + "로직": unit["로직"], + "입력값": given, + "이름": name, + "단위": "시간", + } + add["수량"] = q + elif kind == "M" and unit.get("자재"): + add = {"종류": "재료", "요소": unit["자재"], "이름": name, "수량": q} + add |= {"별도계상": True} if unit.get("별도계상") else {} + elif kind == "R": + basis = unit["기준"] + if isinstance(basis, dict): # 줄 목록 = 그 행 줄 + basis = [f"add:#{at_of[k]}" for h in basis.get("행") or [] + if (k := by_row.get(h)) in at_of] # fmt: skip + fx = { + "이름": name, + "단가": {}, + "이음": {}, + "비율": {"기준": basis, "비목": unit["비목"]}, + } + add = {"고정": fx, "이름": name, "단위": "%", "수량": round(q * 100, 9)} + elif kind == "S": + ref = ref_of(unit["하위"]) + if ref is None: + continue + add = {"참조": ref, "이름": name, "수량": q or 1} + else: # 값 · 못 찾은 별도계상 = 고정 금액 줄 + money = unit.get("값", 0) + fx = { + "이름": name, + "단가": {k: float(v) for k, v in hfix._parts({"계": money}).items()}, + } + add = {"고정": {**fx, "이음": {}}, "이름": name, "수량": q if kind == "M" else 1} + at_of[r["차례"]] = len(out) + out.append(add) + return out, at_of + + +def priced(rows: list[dict], made: list[dict], at_of: dict, unit: str, whole: Any) -> Any: + """M01 지금 값으로 다시 셈 → (저장 꼴 더한줄, 블록 1 단위 계 | None) · 줄 `우리금액` · `차`(참고) · + 셈 못 함 = 금액 칸 비움(판정은 그대로).""" + doc = {"키": "SK-000000", "이름": "판정", "단위": unit or "식", "바탕": None, "더한줄": made} + try: + doc = sd.clean(doc, None, whole) # 저장 꼴(같은 내용 SK 지문이 저장본과 같게) + got = calc.run(doc, whole) + except Exception: # noqa: BLE001 + return made, None + money = {x.get("cid"): x.get("금액") for x in got.get("줄") or []} + for r in rows: + k = at_of.get(r["차례"]) + mine = money.get(f"add:#{k}") if k is not None else None + r["우리금액"] = float(mine) if mine is not None else None + theirs = r.get("엑셀금액") + r["차"] = round(r["우리금액"] - float(theirs), 4) if mine is not None and theirs else None + total = (got.get("1단위") or {}).get("계") + return doc["더한줄"], float(total) if total is not None else None + + +def judge( + block: dict, us: units.Units, ref_of: Any, picks: dict | None = None, unit: str = "" +) -> dict[str, Any]: + """블록 하나 → `{판정, 고를줄, 우리금액, 엑셀금액, 줄 [...], 더한줄, 담음}`(더한줄 = 단위 SK 몸 · + 담음 = 더한줄에 든 줄 차례 · 이어졌는데 빠진 줄 = 하위 SK · SC 아직 없음).""" + rows = lines(block, us, picks) + word, n = verdict(rows) + made, at_of = adds(block, rows, us, ref_of) + made, total = priced(rows, made, at_of, unit, us.whole) + head = {"판정": word, "고를줄": n, "우리금액": total} + head["엑셀금액"] = unit_line.money(block.get("금액")) + return {**head, "줄": rows, "더한줄": made, "담음": sorted(at_of)} + + +def plan(got: dict[str, Any], basis: dict | None) -> dict[str, Any]: + """판정 → `Sangeun.Maker.write` 꼴 — 바탕 없음 · 덮개 · 뺀줄 없음 · 근거 꼬리표.""" + keep = {"갈래": UNIT, "바탕": None, "입력값": {}, "덮개": {}, "자재표시": {}, "뺀줄": []} + out = {**keep, "더한줄": got["더한줄"], "못옮김": [], "놓음": []} + return out | ({"근거": basis} if basis else {}) diff --git a/M02_MasterTemplete/M02_Labor_Router.py b/M02_MasterTemplete/M02_Labor_Router.py index c369283f1..3dd51b9ad 100644 --- a/M02_MasterTemplete/M02_Labor_Router.py +++ b/M02_MasterTemplete/M02_Labor_Router.py @@ -489,9 +489,9 @@ def hopyo_delete( class SangeunBody(_Strict): - """`바탕` = 고름 대기 블록에 사용자가 고른 기초 `{블록 번: {로직, 입력값?}}`(729-1).""" + """`확인` = 사용자가 확인한 블록만 단위 SK `[{번, 고름?: {차례: 고름}}]`(778-5 · 자동 없음).""" - 바탕: dict[str, dict[str, Any]] = Field(default_factory=dict) + 확인: list[dict[str, Any]] = Field(default_factory=list, max_length=500) @router.post("/layers/{layer}/hopyo/{key}/sangeun") @@ -502,10 +502,10 @@ def hopyo_make_sangeun( owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: - """불러오기 결과 「단가산출」(718-1 · 729-1) — 산근 블록마다 매칭 → 비교 → 기초 · SK · 고름 대기 - (파일 층 = 이 층) → `{블록, 만듦, 그대로, 기초, SK, 고름대기, 별칭}` · 맞춤 셈 전 409.""" + """불러오기 결과 「단가산출」(718-1 · 778-5) — 블록마다 줄 판정 · 확인한 블록만 단위 SK + (파일 층 = 이 층) → `{블록, 판정, 만듦, 못만듦, 그대로, 같은것씀, 위층같음}` · 맞춤 셈 전 409.""" folder = _root(session, layer, owner, write=True) - picks = body.바탕 if body else {} + picks = body.확인 if body else [] return _call(folder, hopyo_sangeun.build, folder, key, _who(session), picks) diff --git a/resources/tester/test_572_hopyo_same.py b/resources/tester/test_572_hopyo_same.py index 89b643d94..f0b3799c5 100644 --- a/resources/tester/test_572_hopyo_same.py +++ b/resources/tester/test_572_hopyo_same.py @@ -41,7 +41,9 @@ def _make(client, folder, data, name): assert res.status_code == 200, res.text key = res.json()["키"] fit.run_job(folder, key) - got = client.post(f"{URL.format('personal')}/{key}/sangeun") + url = f"{URL.format('personal')}/{key}/sangeun" + rows = client.post(url).json()["판정"] # 778-5 — 확인한 블록만(옮길 수 있음 모두 확인) + got = client.post(url, json={"확인": [{"번": r["번"]} for r in rows if not r["고를줄"]]}) assert got.status_code == 200, got.text return key, got.json() @@ -53,10 +55,8 @@ def test_불러오기_같은내용_SK_씀_바이트만_다른_사본_새_SK_0(cl docs = _sks(folder) fingers = [same.finger("sangeun", d) for d in docs.values()] assert len(fingers) == len(set(fingers)) # 같은 내용 SK 한 벌 - assert one["같은것씀"] and len(docs) + len(one["같은것씀"]) == len(one["만듦"]) + assert one["만듦"] and len(docs) + len(one["같은것씀"]) == len(one["만듦"]) assert one["위층같음"] == [] - shared = docs[one["같은것씀"][0]["키"]] - assert [x["키"].split(" ")[0] for x in shared["본뜬"]["또"]] == [hp1] again = client.post(f"{URL.format('personal')}/{hp1}/sangeun").json() assert again["만듦"] == [] and again["그대로"] == len(one["만듦"]) # 위 층(회사)에 같은 것 = 알림만 @@ -71,7 +71,9 @@ def test_불러오기_같은내용_SK_씀_바이트만_다른_사본_새_SK_0(cl {k: x[k] for k in ("층", "주인", "키")} for x in two["위층같음"] ] also = {x["키"].split(" ")[0] for d in _sks(folder).values() for x in d["본뜬"].get("또", [])} - assert also == {hp1, hp2} + assert also == ( + {hp1, hp2} if one["같은것씀"] else {hp2} + ) # 778-5 단위 SK = 파일 안 겹침 없을 수 있음 again = client.post(f"{URL.format('personal')}/{hp2}/sangeun").json() assert again["만듦"] == [] and again["그대로"] == len(one["만듦"]) diff --git a/resources/tester/test_718_1_hopyo_import.py b/resources/tester/test_718_1_hopyo_import.py index 82cd3701a..48d77359f 100644 --- a/resources/tester/test_718_1_hopyo_import.py +++ b/resources/tester/test_718_1_hopyo_import.py @@ -72,7 +72,8 @@ def test_옛_HP_목록에서만_숨김(client): assert old.is_file() and _keys(client) == ["HP-000008"] -def test_결과_단가산출_산근_블록_기초_SK_고름대기_겹침_없음(client): +def test_결과_단가산출_확인한_블록만_단위_SK_겹침_없음(client): + """778-5 — 확인 없음 = 판정만 · 확인한 블록(옮길 수 있음)만 단위 SK · 같은 내용 한 벌 · 다시 = 그대로.""" folder = layers.personal_dir(7, 42) assert _post(client).json()["키"] == "HP-000001" url = URL.format("personal") + "/HP-000001/sangeun" @@ -81,16 +82,17 @@ def test_결과_단가산출_산근_블록_기초_SK_고름대기_겹침_없음( got = client.post(url) assert got.status_code == 200, got.text got = got.json() - wait = len(got["고름대기"]) - assert got["블록"] > 0 and len(got["만듦"]) + wait == got["블록"] and got["그대로"] == 0 + assert got["블록"] == len(got["판정"]) > 0 and got["만듦"] == [] and got["그대로"] == 0 + good = [r["번"] for r in got["판정"] if not r["고를줄"]] + made = client.post(url, json={"확인": [{"번": n} for n in good]}).json() + assert sorted(x["번"] for x in made["만듦"]) == good and made["못만듦"] == [] docs = [json.loads(p.read_bytes()) for p in sorted((folder / "sangeun").glob("SK-*.json"))] - assert len(got["만듦"]) == got["기초"] + got["SK"] - assert len(docs) + len(got["같은것씀"]) == len(got["만듦"]) # 같은 내용 SK 는 한 벌(733-1) + assert len(docs) + len(made["같은것씀"]) == len(made["만듦"]) # 같은 내용 SK 는 한 벌(733-1) one = docs[0] - assert one["바탕"]["로직"] and one["본뜬"]["키"].startswith("HP-000001 단산") # 바탕 없는 SK 0 - assert one["구분"] == "실무자료" and one["단위"] and one["본뜬"]["갈래"] in ("기초", "SK") + assert one["바탕"] is None and one["본뜬"]["키"].startswith("HP-000001 단산") # 단위 SK + assert one["구분"] == "실무자료" and one["단위"] and one["본뜬"]["갈래"] == "단위" again = client.post(url).json() # 다시 = 그대로(겹침 없음) - assert again["만듦"] == [] and again["그대로"] == len(got["만듦"]) + assert again["만듦"] == [] and again["그대로"] == len(made["만듦"]) res = client.delete(URL.format("personal") + "/HP-000001").json() assert res["만든"] == len(docs) # 같은 내용 SK 한 벌 — 사본 걷은 문서 수 assert len(list((folder / "sangeun").glob("SK-*.json"))) == len(docs) # 남음 diff --git a/resources/tester/test_729_hopyo_sk_link.py b/resources/tester/test_729_hopyo_sk_link.py index 60873446b..450411f72 100644 --- a/resources/tester/test_729_hopyo_sk_link.py +++ b/resources/tester/test_729_hopyo_sk_link.py @@ -1,11 +1,12 @@ -"""실무문서 단가산출 → 기초 · SK · 고름 대기 · 일위대가 이음 · 남은 줄 막음(PLAN 729-1 · 729-2 · 요구서 내역19). +"""실무문서 단가산출 → 줄 판정 · 확인한 블록만 단위 SK · 일위대가 이음 · 남은 줄 막음(PLAN 729-1 · 729-2 · +778-5 D-3 · 요구 623). -북부청 임도(실물 엑셀)를 임시 층에 올려 맞춤셈 → [단가산출 만들기] → -기초잡석채집 · 운반 · 다짐(엑셀만 덤프트럭) · 철근가공조립 Type-Ⅰ · Type-Ⅱ · 구조물되메우기 = SK · -SK 품 = 엑셀 품(차이는 엑셀 값 그대로) · 같은 단가산출 = 기초(덮개 없음) + 별칭 제안(짝 = 기초까지만) · -고름 대기 = SK 안 만듦 → 고른 기초로 SK · 안 이은 줄 남으면 만들기 409(미리 보기 = 같은 줄) · -사용자가 고르면 만듦 · 노임 = 이름 이음 · 기계 = EQ 이음 · master_data 원본 그대로. 층 = 임시 폴더. -729-4 = 단위 성격 다름(계수 · 고름 대기) · 못옮김 잇기 · 남은 못옮김 = 일위대가 막음. +북부청 임도(실물 엑셀)를 임시 층에 올려 맞춤셈 → [단가산출](확인 없음) = 판정만 · SK 0 → +기초잡석채집 · 운반 · 다짐 · 철근가공조립 Type-Ⅰ · Type-Ⅱ · 구조물되메우기 확인 = 바탕 없는 단위 SK(줄 = 엑셀 품 그대로 · +인력 · 기계경비 로직 줄 · 율 줄 = 줄 목록 · 별도계상 철근 = 0 원) · 근거 꼬리표 = 맞춤셈 1위 · +확인 안 한 단가산출 줄 = 일위대가 남은 줄(확인 전) · 확인한 블록 = SK 참조로 펼쳐 담음 · +고를 줄 남은 블록(도장) = 못만듦 → 줄마다 고름 · 뺌 = 만듦 · 노임 = 이름 이음 · 기계 = EQ 이음 · master_data 원본 그대로. +층 = 임시 폴더. """ import json @@ -13,38 +14,24 @@ import json from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Hopyo_Fit as fit from M02_MasterTemplete import M02_Hopyo_Fixed as hfix -from M02_MasterTemplete import M02_Hopyo_Ledger as ledger -from M02_MasterTemplete import M02_Hopyo_Option as option from M02_MasterTemplete import M02_Hopyo_Ours as ours -from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch +from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun +from M02_MasterTemplete import M02_Hopyo_SkUnit as skunit 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 BUILD = "/api/m02/layers/personal/hopyo-build" LEFT = "/api/m02/layers/personal/hopyo-unlinked" -SAMPLES = { # 블록 번 → (이름 앞, 엑셀 규격 글) +SAMPLES = { # 블록 번 → 이름 앞 33: "기초잡석채집,운반,다짐", - 47: "철근가공조립(현장)", # Type-Ⅰ(소량 시공) — 철근공 4.2 · 보통인부 1.4 - 72: "철근가공조립(현장)", # Type-ll — 철근공 2.67 · 보통인부 1.02 + 47: "철근가공조립(현장)", # Type-Ⅰ(소량 시공) — 철근공 0.67 + 3.53 · 보통인부 0.22 + 1.18 + 72: "철근가공조립(현장)", # Type-ll 12: "구조물되메우기(인력100%)", } -def _same(a: float, b: float) -> bool: - return abs(a - b) <= 1e-9 or option.near(a, b, fit.TOL[0], option.ru(a)) - - -def _by_type(rows: list[dict]) -> 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 _sk_docs(folder) -> dict[int, dict]: out = {} for path in sorted((folder / "sangeun").glob("SK-*.json")): @@ -54,177 +41,111 @@ def _sk_docs(folder) -> dict[int, dict]: return out -def test_실무_단가산출_기초_SK_고름대기_일위대가_막음(client): +def _caller(folder, key: str, no: int) -> int: + """그 블록을 부르는 첫 호표 번.""" + return sangeun.calls(hs.read(folder, key))[1][no]["_호표"] + + +def test_실무_단가산출_확인한_블록만_단위_SK_일위대가_막음(client): before = master_hash() folder = layers.personal_dir(7, 42) key = _post(client).json()["키"] url = f"{URL.format('personal')}/{key}/sangeun" assert client.post(url).status_code == 409 # 맞춤 셈 전 fit.run_job(folder, key) - got = client.post(url, json={}) - assert got.status_code == 200, got.text - got = got.json() - assert ( - got["기초"] + got["SK"] + len(got["고름대기"]) - == got["블록"] - == len(got["만듦"]) + len(got["고름대기"]) - ) - assert got["SK"] > 0 and got["기초"] > 0 and got["고름대기"] - docs, whole = _sk_docs(folder), m01.loaded()[1] - assert all(d["바탕"] and d["바탕"]["로직"] for d in docs.values()) # 바탕 없는 SK 0 - assert not set(docs) & {x["번"] for x in got["고름대기"]} - # SK 품 = 엑셀 품 + got = client.post(url, json={}).json() + assert got["만듦"] == [] and not _sk_docs(folder) # 확인 없음 = 판정만 + rows = {r["번"]: r for r in got["판정"]} + assert all(rows[n]["판정"] == skunit.GOOD for n in SAMPLES) + sure = {"확인": [{"번": n} for n in SAMPLES]} + made = client.post(url, json=sure).json() + assert sorted(x["번"] for x in made["만듦"]) == sorted(SAMPLES) and made["못만듦"] == [] + docs = _sk_docs(folder) for no, head in SAMPLES.items(): doc = docs[no] - assert doc["이름"].startswith(head) and doc["본뜬"]["갈래"] == "SK", (no, doc["본뜬"]) - excel = doc["본뜬"]["엑셀품"] - with store.at(folder): - ours_q = skpatch.measure(doc, whole) - assert set(ours_q["인력"]) == set(excel["인력"]), no - assert all(_same(q, ours_q["인력"][t]) for t, q in excel["인력"].items()), no - theirs, mine = _by_type(excel["장비"]), _by_type(ours_q["장비"]) - assert set(theirs) == set(mine), no - assert all(_same(q, mine[t]) for t, q in theirs.items()), no + assert doc["이름"].startswith(head) and doc["바탕"] is None, no + assert doc["본뜬"]["갈래"] == skunit.UNIT and doc["본뜬"]["근거"] == rows[no]["근거"] + mine = [x["수량"] for x in doc["더한줄"] if x.get("종류") == "인력"] # 엑셀 품 그대로 + excel = [r["매개값"]["품"] for r in rows[no]["줄"] if r["묶음"] == "L"] + assert mine == excel, no + assert docs[12]["더한줄"] == [ + { + "종류": "인력", + "요소": "LB000002", + "비목": "노무비", + "이름": "보통인부", + "단위": "인", + "수량": 0.1, + "처음수량": 0.1, + } + ] dump = [x for x in docs[33]["더한줄"] if x.get("로직") == "GC000268"] - assert any("덤프트럭" in ours.eq_of(_eq(x))[0] for x in dump) # 엑셀만 장비 = 더한줄 - assert docs[12]["덮개"]["labor:LB000002#0"]["qty"] == 0.1 # 엑셀 값 그대로(우리 0.01) - assert docs[47]["더한줄"][0]["로직"] == "GC000206" # 조합 둘째 = 더한줄 기초 - # 같은 단가산출 = 기초 + 별칭 제안(짝 = 기초까지만) - base = next(d for d in docs.values() if d["본뜬"]["갈래"] == "기초") - assert base["덮개"] == {} and base["더한줄"] == [] and base["뺀줄"] == [] - book = ledger.read(folder) - hp = hs.read(folder, key) - lines = { - int(x["참조"]["번"]): x - for h in reversed(hp["호표"]) - for x in reversed(h["줄"]) - if (x.get("참조") or {}).get("종류") == "단산" - } - no = int(base["본뜬"]["키"].rsplit("단산", 1)[1]) - pair = book[ledger.key(lines[no])]["짝"] - assert pair == [{"로직": base["바탕"]["로직"], "입력값": base["입력값"], "배": 1, "몫": None}] - assert book[ledger.key(lines[12])]["짝"][0]["로직"] == docs[12]["바탕"]["로직"] - assert got["별칭"] > 0 - # 다시 = 그대로 · 고름 대기 = 고른 기초로 SK - again = client.post(url, json={}).json() - assert again["만듦"] == [] and again["그대로"] == len(docs) - wait = got["고름대기"][0] - code = (wait["후보"] or [{"로직": "GF000113"}])[0]["로직"] - picked = client.post(url, json={"바탕": {str(wait["번"]): {"로직": code}}}).json() - assert [x["번"] for x in picked["만듦"]] == [wait["번"]] - assert _sk_docs(folder)[wait["번"]]["바탕"]["로직"] == code - assert client.post(url, json={"바탕": {"9999": {"로직": code}}}).status_code == 404 - assert client.post(url, json={"바탕": {str(wait["번"]): {"로직": "XX"}}}).status_code == 400 - # 안 이은 줄 남으면 만들기 막힘 · 미리 보기 = 같은 줄 · 고르면 만듦 - body = {"층": "personal", "키": key, "번": 1, "줄": []} + assert any("덤프트럭" in ours.eq_of(_eq(x))[0] for x in dump) # 기계 = 기계경비 로직 줄 + rates = [x for x in docs[47]["더한줄"] if (x.get("고정") or {}).get("비율")] + assert [x["고정"]["비율"]["기준"] for x in rates] == [ + ["add:#1", "add:#2"], + ["add:#5", "add:#6"], + ] + assert ( + docs[47]["더한줄"][0]["이름"] == "철근" and docs[47]["더한줄"][0]["고정"] + ) # 별도계상 0 원 + again = client.post(url, json={}).json() # 다시 = 그대로 + assert again["만듦"] == [] and again["그대로"] == len(SAMPLES) + # 일위대가 — 확인 안 한 단가산출 줄 = 남은 줄(확인 전) · 확인한 블록 = SK 펼쳐 담음 + no = _caller(folder, key, 12) + body = {"층": "personal", "키": key, "번": no, "줄": []} left = client.post(LEFT, json=body).json() res = client.post(BUILD, json=body) assert res.status_code == 409 and left["수"] > 0 assert res.json()["detail"]["남은줄"] == left["남은줄"] + assert not any(x.get("단산") == 12 for x in left["남은줄"]) assert not list((folder / "precombo").glob("SC-*.json")) # 막힘 = 저장 0 picks = [{"차례": x["차례"], "뺌": "우리에 없음"} for x in left["남은줄"]] ok = client.post(BUILD, json={**body, "줄": picks}) assert ok.status_code == 200, ok.text doc = json.loads((folder / "precombo" / f"{ok.json()['참조']['키']}.json").read_bytes()) - refs_sk = [x for x in doc["담은로직"] if str((x.get("본뜬") or {}).get("키")).startswith("SK")] - assert refs_sk # 단가산출 줄 = SK 펼쳐 담음(740-1 · 로직 줄 + 패치 · 본뜬 = 그 SK) + marks = {(x.get("본뜬") or x.get("참조") or {}).get("키") for x in doc["담은로직"]} + assert docs[12]["키"] in marks # 단가산출 줄 = SK(바탕 없는 SK = 참조 그대로 · 740-1) assert client.post(LEFT, json={**body, "줄": picks}).json()["수"] == 0 + wait = _caller(folder, key, 2) # 확인 안 한 블록을 부르는 호표 + rest = client.post(LEFT, json={"층": "personal", "키": key, "번": wait, "줄": []}).json() + assert any(x.get("까닭") == sangeun.WAIT_WHY for x in rest["남은줄"]) 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 test_별칭_체크_줄_못옮김_SK_남은줄_그대로_SK_남은못옮김(client): - """729-5 — 확인한 로직 줄이 못옮김 남은 차이 SK 의 바탕이면 남은 줄(차이 잃지 않음) · 만들기 409 · - 그대로 SK 의 못옮김 = 응답 남은못옮김 · 잇기 뒤 사라짐.""" +def test_고를_줄_남으면_못만듦_줄마다_고름_뺌(client): + """778-5 — 도장(엑셀 자재 넷 = 글자 규격 · 후보) · 확인만 = 못만듦 · 줄마다 고름 · 뺌 = 만듦 · + 틀린 고름 400 · 고른 줄 꼬리표 「내가 고름」.""" before = master_hash() folder = layers.personal_dir(7, 42) key = _post(client).json()["키"] url = f"{URL.format('personal')}/{key}/sangeun" fit.run_job(folder, key) - made = {x["번"]: x for x in client.post(url, json={}).json()["만듦"]} - sk = made[60] # 제27호표 2째 줄 와이어메시바닥깔기 — SK(바탕 GC000665) · 엑셀만 와이어메쉬 - assert sk["갈래"] == "SK" and len(sk["못옮김"]) == 1 - check = {"차례": 2, "대상": [{"로직": "GC000665", "배": 1}]} # 별칭 자동 체크와 같은 꼴 - body = {"층": "personal", "키": key, "번": 27, "줄": [check]} + rows = {r["번"]: r for r in client.post(url, json={}).json()["판정"]} + paint = rows[95] + picks = [r for r in paint["줄"] if r["상태"] != "이어짐"] + assert paint["판정"] == "고를 줄 4" and [r["묶음"] for r in picks] == ["M"] * 4 + only = client.post(url, json={"확인": [{"번": 95}]}).json() + assert only["만듦"] == [] and only["못만듦"] == [ + {"번": 95, "이름": paint["이름"], "까닭": "고를 줄 4"} + ] - def mine(rows: list) -> list: - return [x for x in rows if x["차례"] == 2] + def sure(chosen: dict) -> object: + return client.post(url, json={"확인": [{"번": 95, "고름": chosen}]}) - left = client.post(LEFT, json=body).json()["남은줄"] - assert [(x["단산"], x["SK"], x["못옮김"]) for x in mine(left)] == [(60, sk["키"], sk["못옮김"])] - res = client.post(BUILD, json=body) - assert res.status_code == 409 and mine(res.json()["detail"]["남은줄"]) == mine(left) - # 그대로 SK 의 못옮김 = 응답 남은못옮김(만듦[].못옮김 과 같은 꼴) - again = client.post(url, json={}).json() - rest = {x["번"]: x for x in again["남은못옮김"]} - assert again["만듦"] == [] and set(rest) == {n for n, x in made.items() if x["못옮김"]} - assert rest[60] == {"번": 60, "이름": sk["이름"], "키": sk["키"], "못옮김": sk["못옮김"]} - cut = {"id": sk["못옮김"][0]["id"], "자재": "MT000010"} # 이음 = SK 더한줄(차이 남음) - linked = client.post(url, json={"바탕": {"60": {"잇기": [cut]}}}).json() - assert [(x["번"], x["키"], x["못옮김"]) for x in linked["만듦"]] == [(60, sk["키"], [])] - assert 60 not in {x["번"] for x in client.post(url, json={}).json()["남은못옮김"]} - left = client.post(LEFT, json=body).json()["남은줄"] - assert mine(left) == [] - picks = [{"차례": x["차례"], "뺌": "우리에 없음"} for x in left] - ok = client.post(BUILD, json={**body, "줄": [check, *picks]}) - assert ok.status_code == 200, ok.text - doc = json.loads((folder / "precombo" / f"{ok.json()['참조']['키']}.json").read_bytes()) - # 차이 SK = 펼쳐 담음(740-1 · 본뜬 = 그 SK) - assert any((x.get("본뜬") or {}).get("키") == sk["키"] for x in doc["담은로직"]) + first = picks[0] + assert sure({str(first["차례"]): {"기계": "EQ000001"}}).status_code == 400 + assert sure({str(first["차례"]): {"자재": "MT999999"}}).status_code == 400 + code = first["후보"][0]["자재"] + chosen = {str(r["차례"]): {"뺌": "현장 지급"} for r in picks[1:]} + done = sure({str(first["차례"]): {"자재": code}, **chosen}).json() + assert [x["번"] for x in done["만듦"]] == [95] + doc = _sk_docs(folder)[95] + mat = [x for x in doc["더한줄"] if x.get("종류") == "재료"] + assert [(x["요소"], x["수량"]) for x in mat] == [(code, 0.322)] + mine = next(r for r in done["판정"] if r["번"] == 95)["줄"] + assert all("내가 고름" in r["꼬리표"] for r in mine if r["차례"] in {p["차례"] for p in picks}) + assert [r["상태"] for r in mine if r["차례"] in {p["차례"] for p in picks[1:]}] == ["뺌"] * 3 assert master_hash() == before diff --git a/resources/tester/test_778_5_hopyo_units.py b/resources/tester/test_778_5_hopyo_units.py new file mode 100644 index 000000000..789a245ab --- /dev/null +++ b/resources/tester/test_778_5_hopyo_units.py @@ -0,0 +1,172 @@ +"""778-5 줄 잇기 한 벌 · 블록 = 줄 판정 모음 · 단위 SK(D-2 · D-3 · 요구 622 · 623 · 624). + +- 줄 잇기(`M02_Hopyo_Units`) — 작업반장 = 건설업 · 숫자 규격 치수 차례 · 구분자 무관 · 글자 규격 = 고를 것 · + 손료율 = 자재 줄 방법 · 우리에 없는 율 항목(설치비 · 제경비) = 율 줄 · 여덟 파일 묶음별 수 굳힘 +- 단위 SK(`M02_Hopyo_SkUnit` · `M02_Hopyo_Sangeun`) — 확인 안 한 블록 안 만듦 · 확인 블록 = 바탕 없는 SK + + 근거 꼬리표 · 고를 줄 남으면 못만듦 · 고르면 만듦 · 다시 올려도 같은 SK 재사용 · 일위대가 = 확인 전 못 이음 +층 = 임시 폴더 · M01 · 마스터 읽기만. +""" + +from __future__ import annotations + +import json + +import pytest + +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Hopyo_Material as hmat +from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun +from M02_MasterTemplete import M02_Hopyo_SkUnit as skunit +from M02_MasterTemplete import M02_Hopyo_Store as hs +from M02_MasterTemplete import M02_Hopyo_Units as units +from M02_MasterTemplete import M02_Labor_Doc as labor +from M02_MasterTemplete import M02_Sangeun_Calc as calc +from M02_MasterTemplete import M02_Sangeun_Doc as sd +from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester import helper_m02_hopyo_golden as golden +from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_hash # noqa: F401 + +BUILD = "/api/m02/layers/personal/hopyo-build" +# 여덟 파일 불리는 블록 줄 연결 표(778-5 D-2 굳힘 · 바뀌면 까닭 커밋 글) +TABLE = { + "L": {"이어짐": 366, "고를 것": 0, "못 이음": 0}, + "X": {"이어짐": 180, "고를 것": 10, "못 이음": 0}, + "M": {"이어짐": 71, "고를 것": 55, "못 이음": 4}, + "R": {"이어짐": 150, "고를 것": 2, "못 이음": 0}, + "S": {"이어짐": 3, "고를 것": 0, "못 이음": 0}, + "E": {"이어짐": 1, "고를 것": 0, "못 이음": 0}, +} +ALL_LINKED = 156 + + +@pytest.fixture(scope="module") +def us(): + return units.Units() + + +def test_작업반장_건설업_기본(us): + got = us.line({"묶음": "L", "이름": "작업반장", "직종": "작업반장", "품": 0.1}) + assert got["상태"] == units.LINKED and got["단위"] == {"노임": "LB000001"} # 건설업 + assert got["꼬리표"] == ["기본 — 건설업 작업반장"] and got["후보"][0]["키"] == "LB000261" + one = us.line({"묶음": "L", "이름": "보통인부", "직종": "보통인부", "품": 0.5}) + assert one["단위"] == {"노임": "LB000002"} and not one["꼬리표"] + + +def test_숫자_규격_차례_구분자_무관_글자_규격_후보(us): + assert hmat.dims("600×1200") == hmat.dims("1200x600 mm") == hmat.dims("1200*600㎜") + assert hmat.dims("1종2류(철재)") is None and hmat.lettered("1종2류(철재)") + assert not hmat.lettered("600*1200mm") and not hmat.lettered("#20, 0.9mm") + for spec in ("600×1200", "1200x600", "1200 X 600mm"): + got = us.line({"묶음": "M", "이름": "유로폼패널", "규격": spec, "단위": "매", "품": 1}) + assert got["상태"] == units.LINKED and got["단위"] == {"자재": "MT000467"}, spec + named = us.line({"묶음": "M", "이름": "유로폼(패널)(600×1200)", "단위": None, "품": 1}) + assert named["단위"] == {"자재": "MT000467"} # 「유로폼(패널)」 = 유로폼패널 · 단위 없는 EST 줄 + paint = {"묶음": "M", "이름": "광명단조합페인트", "규격": "1종2류(철재)", "단위": "L", "품": 1} + got = us.line(paint) + assert got["상태"] == units.PICK and got["글"] == units.LETTERS and got["후보"] + + +def test_손료율_자재_줄_율_항목_율_줄(us): + wear = us.line({"묶음": "M", "이름": "유로폼패널", "규격": "600×1200", "품": 0.267, + "손료율": 0.267}) # fmt: skip + assert wear["방법"] == units.WEAR and wear["상태"] == units.LINKED + setup = {"묶음": "R", "이름": "설치비(재료비의 5%)", "품": 0.05, "기준꼴": "자재"} + assert us.line(setup)["단위"] == {"기준": "재료", "비목": "재료비"} # 우리에 없는 율 항목 + rest = {"묶음": "R", "이름": "제경비(노무비의 60%)", "품": 0.6, "기준꼴": "노무비"} + assert us.line(rest)["단위"] == {"기준": "노무비", "비목": "경비"} + lost = us.line({"묶음": "R", "이름": "기구손료(노무비의 9%)", "기준": "노무비", "품": 0.09}) + assert lost["상태"] == units.PICK and lost["후보"] == [{"기준": "노무비", "비목": "경비"}] + # 단위 SK 셈 — 설치비 = 재료 줄 × 5 % · 제경비 = 노무비 × 60 % + block = {"항목": [ + {"묶음": "L", "이름": "보통인부", "직종": "보통인부", "품": 0.5, "행": 1}, + {"묶음": "M", "이름": "유로폼패널", "규격": "600×1200", "단위": "매", "품": 2, "행": 2}, + {**setup, "행": 3}, {**rest, "행": 4}, + ]} # fmt: skip + got = skunit.judge(block, us, lambda sub: None, None, "㎡") + assert got["판정"] == skunit.GOOD and [r["상태"] for r in got["줄"]] == [units.LINKED] * 4 + labor_, mat = (got["줄"][0]["우리금액"], got["줄"][1]["우리금액"]) + assert got["줄"][2]["우리금액"] == pytest.approx(mat * 0.05, abs=0.1) + assert got["줄"][3]["우리금액"] == pytest.approx(labor_ * 0.6, abs=0.1) + doc = {"키": "SK-000001", "이름": "t", "단위": "㎡", "바탕": None, "더한줄": got["더한줄"]} + whole = us.whole + ran = labor._json(calc.run(sd.clean(doc, None, whole), whole)) + assert ran["1단위"]["계"] == pytest.approx(got["우리금액"]) + + +def test_여덟_파일_줄_연결_표(us): + rows, full, blocks = [], 0, 0 + for tag in golden.FILES: + hp = golden.read(tag) + _, first = sangeun.calls(hp) + for b in hp["단산"]: + if int(b["번"]) not in first: + continue + got = [(o["묶음"], us.line(o)["상태"]) for o in b.get("항목") or [] if not o.get("밖")] + rows += got + blocks += 1 + full += all(s == units.LINKED for _, s in got) + assert units.summary(rows) == TABLE and (full, blocks) == (ALL_LINKED, 206) + + +def _sks(folder) -> dict[str, dict]: + return {p.stem: json.loads(p.read_bytes()) for p in (folder / "sangeun").glob("SK-*.json")} + + +def test_확인한_블록만_단위_SK_고름_재사용(client): + before = master_hash() + folder = layers.personal_dir(7, 42) + key = _post(client).json()["키"] + url = f"{URL.format('personal')}/{key}/sangeun" + fit.run_job(folder, key) + got = client.post(url, json={}).json() + assert got["만듦"] == [] and not _sks(folder) # 확인 안 한 블록 = 안 만듦 + assert len(got["판정"]) == got["블록"] == 49 # 불리는 블록 + rows = {r["번"]: r for r in got["판정"]} + good = [n for n, r in rows.items() if r["판정"] == skunit.GOOD and r["근거"]] + pick = next(n for n, r in rows.items() if r["고를줄"]) + assert rows[pick]["판정"] == f"고를 줄 {rows[pick]['고를줄']}" + one = rows[good[0]] + assert all("우리금액" in x and "차" in x for x in one["줄"] if x["상태"] == units.LINKED) + # 확인 = 그 블록만 · 고를 줄 남은 블록 = 못만듦 + made = client.post(url, json={"확인": [{"번": good[0]}, {"번": pick}]}).json() + assert [x["번"] for x in made["만듦"]] == [good[0]] and made["못만듦"][0]["번"] == pick + docs = _sks(folder) + doc = docs[made["만듦"][0]["키"]] + assert doc["바탕"] is None and doc["본뜬"]["갈래"] == skunit.UNIT and doc["더한줄"] + assert doc["본뜬"]["근거"]["로직"] == one["근거"]["로직"] and doc["본뜬"]["못옮김"] == [] + # 고르면 만듦(못 이은 줄마다 첫 후보 · 후보 없으면 뺌) + picks = {} + for x in rows[pick]["줄"]: + if x["상태"] in (units.PICK, units.NONE): + c = (x["후보"] or [{}])[0] + k = c.get("키") or c.get("자재") + field = {"L": "노임", "X": "기계", "M": "자재"}.get(x["묶음"]) + picks[str(x["차례"])] = {field: k} if k and field else c or {"뺌": "현장"} + again = client.post(url, json={"확인": [{"번": pick, "고름": picks}]}).json() + assert [x["번"] for x in again["만듦"]] == [pick] and again["그대로"] == 1 + mine = next(r for r in again["판정"] if r["번"] == pick) + assert all("내가 고름" in x["꼬리표"] for x in mine["줄"] if str(x["차례"]) in picks) + bad = {"확인": [{"번": pick, "고름": {"0": {"노임": "XX"}}}]} + assert client.post(url, json=bad).status_code == 400 + assert client.post(url, json={"확인": [{"번": 9999}]}).status_code == 404 + # 일위대가 만들기 — 확인 안 한 단가산출 줄 = 못 이음(확인 전) + calls = {} + for h in hs.read(folder, key)["호표"]: + for x in h.get("줄") or []: + ref = x.get("참조") or {} + if ref.get("종류") == "단산": + calls.setdefault(int(ref["번"]), h["번"]) + wait = next(n for n in sorted(calls) if n not in (good[0], pick)) + res = client.post(BUILD, json={"층": "personal", "키": key, "번": calls[wait], "줄": []}) + left = [x for x in res.json()["detail"]["남은줄"] if x.get("까닭") == sangeun.WAIT_WHY] + assert res.status_code == 409 and left + # 다시 올림(끝내기 뒤) = 같은 내용 SK 재사용 + n = len(_sks(folder)) + client.delete(f"{URL.format('personal')}/{key}") + key2 = _post(client).json()["키"] + fit.run_job(folder, key2) + url2 = f"{URL.format('personal')}/{key2}/sangeun" + re_ = client.post(url2, json={"확인": [{"번": good[0]}]}).json() + assert re_["같은것씀"] == [{"번": good[0], "키": made["만듦"][0]["키"]}] + assert len(_sks(folder)) == n + assert master_hash() == before diff --git a/resources/tester/test_779_2_hopyo_marks.py b/resources/tester/test_779_2_hopyo_marks.py index 018a93957..7eee6a7b4 100644 --- a/resources/tester/test_779_2_hopyo_marks.py +++ b/resources/tester/test_779_2_hopyo_marks.py @@ -64,8 +64,10 @@ def test_marks_읽기만_판정_캐시_같음_SK(client): assert judged and all(r["판정"] in ("맞음", "다른 점", "후보", "없음") for r in judged) assert {r["판정"] for r in judged} <= {e["판정"]["상태"] for e in cache.values()} assert all(r["SK"] is None and r["갈래"] is None and r["못옮김"] == [] for r in rows) - # [단가산출 만들기] 뒤 = SK · 갈래 · 못옮김 실림 - made = client.post(f"{base}/{KEY}/sangeun") + # [단가산출 만들기] 뒤 = SK · 갈래 · 못옮김 실림(778-5 — 확인한 블록만) + rows = client.post(f"{base}/{KEY}/sangeun").json()["판정"] + sure = [{"번": r["번"]} for r in rows if not r["고를줄"]] + made = client.post(f"{base}/{KEY}/sangeun", json={"확인": sure}) assert made.status_code == 200, made.text after = {r["번"]: r for r in client.get(f"{base}-marks/{KEY}").json()["블록"]} for one in made.json()["만듦"]: diff --git a/resources/tester/test_m02_hopyo_flow.py b/resources/tester/test_m02_hopyo_flow.py index 8c1d4452c..e40f6792a 100644 --- a/resources/tester/test_m02_hopyo_flow.py +++ b/resources/tester/test_m02_hopyo_flow.py @@ -67,6 +67,17 @@ def _build(client, key: str, no: int, lines: list, decide: bool = False, **extra return res.json() +def _sure(client, key: str, hopyo: dict, body: list) -> None: + """778-5 — 체크 안 한 단가산출 줄의 블록을 [단가산출]에서 확인(자동 없음 · 옮길 수 있는 것만).""" + picked = {x["차례"] for x in body} + want = {int(x["참조"]["번"]) for x in hopyo["줄"] + if (x.get("참조") or {}).get("종류") == "단산" and x["차례"] not in picked} # fmt: skip + url = f"{URL.format('personal')}/{key}/sangeun" + rows = client.post(url).json()["판정"] + sure = [{"번": r["번"]} for r in rows if r["번"] in want and not r["고를줄"]] + assert client.post(url, json={"확인": sure}).status_code == 200 + + def _doc(ref: dict) -> dict: path = layers.personal_dir(*FOLDER) / "precombo" / f"{ref['키']}.json" return json.loads(path.read_text(encoding="utf-8")) @@ -95,6 +106,7 @@ def test_끝까지_확인한_줄만_별칭(client): two = [{k: p[k] for k in ("로직", "입력값", "배")} for p in rebar["맞춤"]["대상"]] assert {p["로직"] for p in two} == {"GC000206", "GC000207"} # 다른 점 → 사용자가 체크 body = body + [{"차례": rebar["차례"], "대상": two}] + _sure(client, "HP-000001", hopyo, body) made = _build(client, "HP-000001", no, body) national = [x for x in body if any(t.get("전국") for t in x.get("대상", []))] # 361-1 assert national and all(x["대상"] == [{"전국": True, "배": 1}] for x in national)