diff --git a/M02_MasterTemplete/M02_Doc_FromNative.py b/M02_MasterTemplete/M02_Doc_FromNative.py index 125ac650b..3180dc8ce 100644 --- a/M02_MasterTemplete/M02_Doc_FromNative.py +++ b/M02_MasterTemplete/M02_Doc_FromNative.py @@ -50,6 +50,9 @@ def pack(got: dict, name: str) -> dict[str, Any]: """파서 답 → 둘 꼴(셈 · 변환에 쓰는 칸만 · JSON).""" out: dict[str, Any] = {"꼴": got["꼴"], "파일": name, "표지": got.get("표지") or []} out |= {"엑셀": True} if got.get("엑셀") else {} # 812-1 엑셀 읽기기(`M02_San_Xlsx`) — 같은 꼴 + out |= ( + {"기계구성": got["기계구성"]} if got.get("기계구성") else {} + ) # 821-4 EST 원파일 기계 구성 head = ("코드", "번", "이름", "규격", "단위", "저장") if got.get( "엑셀" @@ -104,7 +107,15 @@ def numbers(saved: dict) -> dict[int, dict]: def emit(saved: dict, no: int, keys: bool = True) -> dict[str, Any]: - """원파일 산근 블록 하나 → `{로직, 원본}`(원본 `원파일` True · 코드 · 확정 수) · 없는 번 = KeyError.""" + """원파일 산근 블록 하나 → `{로직, 원본}`(원본 `원파일` True · 코드 · 확정 수) · 없는 번 = KeyError · + 기계 찾기 = M01 만(821-4 — 못 이은 기계 → 내 기계는 `M02_San_MyMachine.attach`).""" + from M02_MasterTemplete.M02_Machine_Find import m01_only + + with m01_only(): + return _emit(saved, no, keys) + + +def _emit(saved: dict, no: int, keys: bool) -> dict[str, Any]: blk = numbers(saved)[int(no)] if saved["꼴"] == "EST": names = {b["코드"]: n for n, b in numbers(saved).items()} diff --git a/M02_MasterTemplete/M02_Doc_Import.py b/M02_MasterTemplete/M02_Doc_Import.py index a019b1b44..0e4d53ca5 100644 --- a/M02_MasterTemplete/M02_Doc_Import.py +++ b/M02_MasterTemplete/M02_Doc_Import.py @@ -325,6 +325,8 @@ def _attach(doc: dict, row: dict, ids: list[str], src: dict[str, dict]) -> None: if not item.get("참조") and var["연결"].get("값") != excel: continue # SK 덮개 단가 = 사용자 값 link, note = _link_of(item) + if mine := (item.get("자원") or {}).get("내기계"): # 821-4 꼬리표 「내 기계」 + note = f"내 기계 {mine}" + ("" if link else " · 시간당 값 직접 — 엑셀 단가 그대로") if note: var["설명"] = note if link is None: diff --git a/M02_MasterTemplete/M02_Machine_Find.py b/M02_MasterTemplete/M02_Machine_Find.py index b0de98932..fe6fb7ef0 100644 --- a/M02_MasterTemplete/M02_Machine_Find.py +++ b/M02_MasterTemplete/M02_Machine_Find.py @@ -19,6 +19,9 @@ import math import re import time import unicodedata +from collections.abc import Iterator +from contextlib import contextmanager +from contextvars import ContextVar from functools import lru_cache from typing import Any @@ -150,10 +153,26 @@ def _table(whole: Any) -> tuple[list[Row], frozenset[str], dict[str, Row]]: _SEEN: list = [0.0, None, None] # (때, 표, 층) — M01 · 내 기계 판 확인은 층마다 1 초에 한 번 +_M01_ONLY: ContextVar[bool] = ContextVar("m01_only", default=False) + + +@contextmanager +def m01_only() -> Iterator[None]: + """이 안의 찾기 = M01 기계만(821-4 불러오기 — 내 기계 잇기는 `M02_San_MyMachine.attach` 한 곳).""" + token = _M01_ONLY.set(True) + try: + yield + finally: + _M01_ONLY.reset(token) + + def table() -> tuple[list[Row], frozenset[str], dict[str, Row]]: + from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_MyMachine as mym + if _M01_ONLY.get(): + return _table(m01.loaded()[1]) now, here = time.monotonic(), store.root() if _SEEN[1] is None or now - _SEEN[0] > 1.0 or _SEEN[2] != here: _SEEN[:] = [now, _table(mym.whole()), here] diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ImportedPanel.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ImportedPanel.ts index d7b67663e..2290bcb87 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ImportedPanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_ImportedPanel.ts @@ -5,6 +5,7 @@ * 바꾸기 전후 금액 · [원본 글 보기] * · 줄 2 = 이음 없는 자원 = 표지 + [이음 고르기] * · 줄 2-2 = 소요량 · 계수 이음(797-2 · 후보 집계 + [이음 보기] → `Hopyo_QtyLinks` 모달) + * · 줄 2-3 = 못 이은 기계 → 내 기계(821-4) — 「내 기계 생성 n」 · 줄마다 「내 기계」 꼬리표 · 개인 층 밖 = 「만들어질 내 기계 n」 * · 경쟁사 로직 그대로 — 기초로 바꾸기 · 근거 기초 제안 없음(809-1 · 요구 703) * 부품 = 단추 · 배지 · 글 · 입력 · 띠 · 표 · 모달(날 단추 · 입력 없음) · 서버 길은 부르는 쪽. * ========================================================================== */ @@ -47,6 +48,12 @@ const TEXT = { Gap: ["바꾸기 전 {a} → 후 {b} ({d})", "Before {a} → after {b} ({d})"], QtyLinks: ["소요량 · 계수 이음 {n}", "Quantity / coefficient links {n}"], QtyOpen: ["이음 보기", "Show links"], + MyMade: ["내 기계 생성 {n}", "My machines created {n}"], + MyUsed: ["내 기계 이음 {n}", "Linked to my machines {n}"], + MyPlan: ["만들어질 내 기계 {n}", "My machines to create {n}"], + MyTag: ["내 기계", "My machine"], + MyNew: ["이번에 만듦 · 값 확인 필요", "Created now · check values"], + MyOld: ["있던 내 기계에 이음", "Linked to an existing one"], Close: ["닫기", "Close"], } as const satisfies Record; export const tx = (k: keyof typeof TEXT): string => @@ -116,6 +123,34 @@ function checkBadge(doc: ImportedDoc): HTMLElement { return b; } +/** 못 이은 기계 → 내 기계(821-4) — 생성 수 · 이은 수 · 줄마다 꼬리표 · 예정 수 */ +function myMachines(src: NonNullable["원본"]): HTMLElement | null { + const got = src?.내기계 ?? []; + const plan = src?.내기계예정 ?? []; + if (!got.length && !plan.length) return null; + const made = got.filter((m) => m.새로).length; + const items: HTMLElement[] = []; + if (made) items.push(createBadge(tx("MyMade").replace("{n}", String(made)), "info")); + if (got.length > made) items.push(createBadge(tx("MyUsed").replace("{n}", String(got.length - made)), "neutral")); // prettier-ignore + for (const m of got) { + const b = createBadge( + `${tx("MyTag")} · ${[m.이름, m.규격].filter(Boolean).join(" ")}`, + "accent", + ); + b.title = `${m.키} · ${m.새로 ? tx("MyNew") : tx("MyOld")}`; + b.dataset.importedMine = m.키; + items.push(b); + } + if (plan.length) { + const b = createBadge(tx("MyPlan").replace("{n}", String(plan.length)), "warning"); + b.title = plan.map((m) => [m.이름, m.규격].filter(Boolean).join(" ")).join("\n"); + items.push(b); + } + const bar = createToolbar({ items }); + bar.dataset.importedMineBar = String(made); + return bar; +} + /** 머리 판 */ export function importedPanel(s: PanelState, a: PanelActs): HTMLElement { const meta = s.doc.가져온 ?? {}; @@ -163,6 +198,8 @@ export function importedPanel(s: PanelState, a: PanelActs): HTMLElement { ]; kids.push(createToolbar({ items: [...tags, ...items] })); } + const mine = myMachines(src); + if (here && mine) kids.push(mine); if (here && s.qty?.자리) { const open = createButton({ label: tx("QtyOpen"), size: 24, variant: "ghost", onClick: a.qty }); open.dataset.importedQty = String(s.qty.자리); diff --git a/M02_MasterTemplete/M02_San_Book.py b/M02_MasterTemplete/M02_San_Book.py index 151f4c99b..148f659f2 100644 --- a/M02_MasterTemplete/M02_San_Book.py +++ b/M02_MasterTemplete/M02_San_Book.py @@ -21,6 +21,7 @@ STM_SHEETS = ( "재료비목록표", "경비목록표", "중기목록표", + "중기사용료", # 821-4 기계 구성(못 이은 기계 → 내 기계 값) ) EST_SHEETS = ( "단가산출총괄표", diff --git a/M02_MasterTemplete/M02_San_Estx.py b/M02_MasterTemplete/M02_San_Estx.py index 098f665b8..353208dea 100644 --- a/M02_MasterTemplete/M02_San_Estx.py +++ b/M02_MasterTemplete/M02_San_Estx.py @@ -444,5 +444,8 @@ def read(data: bytes) -> dict[str, Any]: blk["줄합"] = head_sum(lines) out[key].append({**blk, "줄": lines, "표지": tags}) out["자원"] = {f"{LINK[k[0]]}:{k[1]}": v for k, v in res.items()} + from M02_MasterTemplete.M02_San_MyMachine import est_makes + + out["기계구성"] = est_makes(t) # 821-4 못 이은 기계 → 내 기계 값 out["표지"] = sorted(warn) + ["옵션(datestoption.opt) · 원가 서식(wongadat) 안 읽음"] return out diff --git a/M02_MasterTemplete/M02_San_MyMachine.py b/M02_MasterTemplete/M02_San_MyMachine.py new file mode 100644 index 000000000..ce545e86a --- /dev/null +++ b/M02_MasterTemplete/M02_San_MyMachine.py @@ -0,0 +1,312 @@ +"""불러오기 못 이은 기계 → 내 기계 자동 생성(PLAN 821-4 · 요구서_796 4 장 · 방향 장부 Q1 · S) — 서버 단독. + +구성 읽기(그 파일 값 → 우리 틀 값 · 경쟁사 조종원 노임 환산 ×16/12 ×25/20 은 안 가져옴): + EST 원파일 MachMainDat(`est_makes` · 올릴 때 둘 꼴 칸 `기계구성`) · EST 엑셀 「기계경비」 · + STmate 엑셀 「중기사용료」(`xlsx_makes` · 둔 산근 시트에서 그때 읽음) → {열쇠: 구성} + 구성 = {이름, 규격, 손료 [[계수 10⁻⁷, 가격 원, 달러?]], 주연료종류, 주연료 ℓ, 잡재료율 %, 조종원 인, + 조종원직종(글), 전력 kWh} · 열쇠 = 자원 코드 · 「이름|규격」(표기 정규화 `M02_Machine_Find.norm`). +고름(`plan`) = 가져온 로직 호표 기계 줄 가운데 이음 없음 · 찾기 판정 「못 찾음」(별칭 · 표기 · 같은 파일 대조 뒤 · + 후보 여럿 = 사람이 고름이라 안 만듦) · 조합 기계 = 조각마다(첫 조각 = 첫 손료 + 연료 · 조종원 · + 다음 조각 = 그 차례 손료만 · 구성 없는 조합 = 안 만듦). +몸 = 구성 있음 → 방식 바로(가격 천원 · 달러 = × 환율 FX000001 + 비고 · 손료계수 · 주연료 → 우리 유가 이름 · + 잡품 % · 조종원 → 우리 직종 원문번호 · 전력) · 못 이은 연료 · 직종 = 칸 비움 + 비고 「확인 필요」 · + 구성 없음 → 직접(그 파일 시간당 노 · 재 · 경). +잇기(`attach` · 층 저장 길 `M02_Sangeun_FromHopyo.make` 만) = 그 층 내 기계 같은 이름 · 규격이면 그 줄 · 없으면 + 새로(`M02_MyMachine.create`) · 바로 = 기계 줄 이음 키 · 로직 줄 = 시간당 부름 글 / 직접 = 이음 없이 자원 + `내기계` 키(셈 = 그 파일 값 · [우리 단가로] = 내 기계 시간당 · `M02_Sangeun_Imported.row_of`) · + 원본 `내기계` [{키, 이름, 규격, 새로}] · 개인 층 아님 = 안 만듦(원본 `내기계예정` [{이름, 규격}] 만). +금액(엑셀 단가 그대로)은 줄 값을 안 건드려 그대로. +""" + +from __future__ import annotations + +import re +from typing import Any + +from M02_MasterTemplete.M02_Machine_Find import NONE, norm + +NOTE = "불러오기 자동 생성 — {file} · 값 확인 필요" +CHECK = "확인 필요" +COST = {"L": "노무비", "M": "재료비", "S": "경비"} + + +def key_of(name: Any, spec: Any) -> str: + return f"{norm(name)}|{norm(spec)}" + + +def _num(v: Any) -> float | None: + if isinstance(v, bool) or v is None: + return None + if isinstance(v, (int, float)): + return float(v) + s = str(v).strip().replace(",", "") + return float(s) if re.fullmatch(r"-?\d+(?:\.\d*)?|-?\.\d+", s) else None + + +def _blank(name: str = "", spec: str = "") -> dict[str, Any]: + return {"이름": name, "규격": spec, "손료": []} + + +def _fuel(make: dict, kind: Any, litre: Any) -> None: + """주연료 종류 · ℓ(처음 것) — 종류 「전력」 = 전력 kWh 칸.""" + if re.sub(r"\s", "", str(kind or "")) == "전력" and (_num(litre) or 0) > 0: + make["전력"] = _num(litre) + elif make.get("주연료") is None and (_num(litre) or 0) > 0: + make["주연료종류"], make["주연료"] = str(kind or "").strip(), _num(litre) + + +# ── 구성 읽기 ───────────────────────────────────────────────────────── +def est_makes(t: dict[str, list[dict]]) -> dict[str, dict]: + """EST 원파일 MachMainDat → {자원 코드: 구성} — TitleCode 머리 다음 줄부터 빈 줄까지 · + Index -3 손료(GN 계수 · TU 가격 · 단위 「$」 = 달러) · -2 「주연료」(Size 종류) · 연료 이름 줄(선박용경유 등 ℓ) · + 잡품 · 잡유 % · 전력 KWH · -1 조종원(직종 · GN 인).""" + heads = {r.get("Code"): r for r in t.get("MachTitleDat", [])} + out: dict[str, dict] = {} + cur = None + for r in t.get("MachMainDat", []): + if r.get("TitleCode"): + head = heads.get(r["TitleCode"]) or r + cur = out.setdefault( + r["TitleCode"], _blank(head.get("Name") or "", head.get("Size") or "") + ) + elif not r.get("Name"): + cur = None + elif cur is not None: + i, gn, word = r.get("Index"), r.get("GN"), re.sub(r"\s", "", str(r["Name"])) + unit = str(r.get("Unite") or "").strip().upper() + if i == -3 and gn: + cur["손료"].append([gn, r.get("TU"), unit == "$"]) + elif i == -2 and word == "주연료": + _fuel(cur, r.get("Size"), gn) + elif i == -2 and word in ("잡품", "잡유") and cur.get("잡재료율") is None: + cur["잡재료율"] = gn + elif i == -2 and word == "전력": + cur["전력"] = gn + elif i == -2 and unit == "ℓ": + _fuel(cur, r["Name"], gn) + elif i == -1 and (gn or 0) > 0 and not cur.get("조종원"): + cur["조종원"], cur["조종원직종"] = gn, str(r["Name"]).strip() + return {k: v for k, v in out.items() if v["손료"]} + + +def _est_sheet(rows: list[list]) -> dict[str, dict]: + """EST 엑셀 「기계경비」 — 「N. 이름 · 규격」 머리 아래 빈 줄까지: 「손료 가격 ₩×(달러 $×) 계수 × 10(-7)」 · + 「주연료 종류 ℓ」 · 「잡품 주연료의 % 」 · 「직종 인수 인」 · 「전력 … kWh」.""" + out: dict[str, dict] = {} + cur = None + for row in rows: + v = [c for c in row if c not in (None, "")] + head = re.match(r"\d+\.\s*(.+)", str(v[0])) if v else None + if head: + spec = v[1] if len(v) > 1 and isinstance(v[1], str) else "" + cur = out.setdefault(key_of(head.group(1), spec), _blank(head.group(1).strip(), spec)) + elif not v: + cur = None + elif cur is not None: + word = re.sub(r"\s", "", str(v[0])) + mark = next((x for x in v if x in ("₩×", "$×")), None) + if "손료" in word and mark and (at := v.index(mark) + 1) < len(v): + cur["손료"].append([_num(v[at]), _num(v[1]), mark == "$×"]) + elif word == "주연료" and len(v) > 2: + _fuel(cur, v[1], v[2]) + elif word in ("잡품", "잡유") and len(v) > 2 and cur.get("잡재료율") is None: + cur["잡재료율"] = _num(v[2]) + elif word == "전력" and len(v) > 2: + cur["전력"] = _num(v[1]) if _num(v[1]) is not None else _num(v[2]) + elif len(v) > 2 and v[2] == "인" and (_num(v[1]) or 0) > 0 and not cur.get("조종원"): + cur["조종원"], cur["조종원직종"] = _num(v[1]), str(v[0]).strip() + elif len(v) > 3 and str(v[3]).upper() in ("ℓ", "L") and (_num(v[2]) or 0) > 0: + _fuel(cur, v[0], v[2]) + return {k: m for k, m in out.items() if m["손료"] and None not in m["손료"][0][:2]} + + +def _stm_sheet(rows: list[list]) -> dict[str, dict]: + """STmate 엑셀 「중기사용료」 — 「제 N 호표」 뒤 머리 [이름, 규격, 시간, 코드] · 손료 [이름, 규격, 계수(천원당), + 「천원」, 가격 천원 …] · 조종원 [직종, 인, 「인」 …] · 연료 [이름, 등급, ℓ, 「L」 …] · 잡품 [잡품, 주연료의%, % …] · + 「합 계」 끝 → {코드 · 이름|규격: 구성}(손료계수 = 계수 × 10⁴ · 가격 = 천원 × 1000 원).""" + out: dict[str, dict] = {} + cur = None + for row in rows: + v = [c for c in row if c not in (None, "")] + first = re.sub(r"\s", "", str(v[0])) if v else "" + if re.fullmatch(r"제\d+호표", first): + cur = {} + continue + if cur is None or not v: + continue + if first == "합계": + cur = None + continue + if ( + not cur + and len(v) >= 4 + and str(v[2]) in ("시간", "hr") + and re.fullmatch(r"X\d+", str(v[-1])) + ): + cur.update(_blank(str(v[0]), str(v[1]))) + out[str(v[-1])] = out[key_of(v[0], v[1])] = cur + elif not cur: + continue + elif len(v) > 4 and str(v[3]) == "천원" and _num(v[2]) and _num(v[4]): + cur["손료"].append([_num(v[2]) * 10_000, _num(v[4]) * 1000, False]) + elif len(v) > 2 and str(v[2]) == "인" and (_num(v[1]) or 0) > 0 and not cur.get("조종원"): + cur["조종원"], cur["조종원직종"] = _num(v[1]), str(v[0]).strip() + elif first == "잡품" and len(v) > 2 and cur.get("잡재료율") is None: + cur["잡재료율"] = _num(v[2]) + elif len(v) > 3 and str(v[3]).upper() in ("L", "ℓ"): + _fuel(cur, v[0], v[2]) + elif len(v) > 3 and str(v[3]).upper() == "KWH": + cur["전력"] = _num(v[2]) + return {k: m for k, m in out.items() if m.get("손료")} + + +def xlsx_makes(sheets: dict[str, list]) -> dict[str, dict]: + """엑셀 산근 시트 → 구성(EST 「기계경비」 · STmate 「중기사용료」 · 없음 = {}).""" + if sheets.get("기계경비"): + return _est_sheet(sheets["기계경비"]) + if sheets.get("중기사용료"): + return _stm_sheet(sheets["중기사용료"]) + return {} + + +# ── 고름 · 몸 ───────────────────────────────────────────────────────── +def _todo(logic: dict) -> list[tuple[dict, int]]: + """(줄, 조각 차례) — 기계 줄 · 이음 없음 · 찾기 판정 「못 찾음」.""" + out, n = [], 0 + for line in logic.get("호표") or []: + res = line.get("자원") or {} + if res.get("갈래") != "기계": + continue + n = n + 1 if res.get("조합") else 0 + if line.get("이음") or res.get("조합식") or res.get("내기계"): + continue + if (res.get("찾기") or {}).get("판정") == NONE: + out.append((line, n)) + return out + + +def _make_of(res: dict, makes: dict) -> dict | None: + return makes.get(str(res.get("코드") or "")) or makes.get( + key_of(res.get("이름"), res.get("규격")) + ) + + +def _kinds(make: dict, notes: list[str]) -> dict[str, Any]: + """연료 · 조종원 → 우리 유가 이름 · 직종 원문번호(못 이음 = 칸 비움 + 비고).""" + from M01_MasterData import M01_MasterData_Store as m01 + + index, out = m01.loaded()[1], {} + fuel, litre = str(make.get("주연료종류") or ""), make.get("주연료") or 0 + if litre: + ours = next( + (n for n in ("경유", "휘발유", "실내등유") if n == re.sub(r"\s", "", fuel)), None + ) + ours = ours or next((n for n in ("경유", "휘발유", "실내등유") if fuel.startswith(n)), None) + if ours and f"{ours}:전국평균" in (index.numbers.get("MO") or {}): + out |= {"주연료": litre, "주연료종류": ours} + out |= {"잡재료율": make["잡재료율"]} if make.get("잡재료율") else {} + else: + notes.append(f"주연료 「{fuel}」 {litre:g}ℓ — 우리 유가에 못 이음({CHECK})") + if make.get("조종원"): + job = norm(make.get("조종원직종")) + hit = [k for k, r in (index.numbers.get("LB") or {}).items() if norm(r.get("이름")) == job] + if len(hit) == 1: + out |= {"조종원": make["조종원"], "조종원직종": hit[0]} + else: + notes.append( + f"조종원 「{make.get('조종원직종')}」 {make['조종원']:g}인 — 우리 직종에 못 이음({CHECK})" + ) + if make.get("전력"): + out["전력"] = make["전력"] + return out + + +def body(make: dict | None, n: int, line: dict, file: str) -> dict | None: + """내 기계 몸(`M02_MyMachine.create`) — 구성 있음 = 바로 · 없음 = 직접(조합 조각 = 안 만듦).""" + from M01_MasterData import M01_MasterData_Store as m01 + + res = line.get("자원") or {} + found = res.get("찾기") or {} + name = str(found.get("이름") or res.get("이름") or line.get("이름") or "").strip() + spec = str((found.get("규격") if found.get("이름") else res.get("규격")) or "").strip() + head = {"이름": name, "규격": spec} + notes = [NOTE.format(file=file)] + if make is None: + if n or res.get("조합첫"): + return None + got = res.get("단가") or {} + pack = line.get("값묶음") or {} + hour = {v: _num(got.get(c)) or _num(pack.get(v)) or 0 for c, v in COST.items()} + return {**head, "방식": "직접", "시간당": hour, "비고": " · ".join(notes)} + if n >= len(make["손료"]): + return None + coef, price, usd = make["손료"][n] + if usd: + rate = float(m01.loaded()[1].index["FX"]["FX000001"]["값"]) + notes.append(f"가격 ${price:g} × 환율 {rate:,} 원/USD") + price = price * rate + vals: dict[str, Any] = {"가격": round(float(price) / 1000, 4), "손료계수": coef} + if n == 0: + vals |= _kinds(make, notes) + return {**head, "방식": "바로", "값": vals, "비고": " · ".join(notes)} + + +def plan(logic: dict, makes: dict, file: str) -> list[dict]: + """만들(또는 이을) 내 기계 [{줄, 열쇠, 몸}] — 글 · 층 안 건드림.""" + out = [] + for line, n in _todo(logic): + got = body(_make_of(line.get("자원") or {}, makes), n, line, file) + if got: + out.append({"줄": line, "열쇠": key_of(got["이름"], got["규격"]), "몸": got}) + return out + + +def preview(out: dict, makes: dict) -> None: + """미리 보기 · 분석 길 — 원본 `내기계예정` [{이름, 규격}](안 만듦).""" + todo = plan(out["로직"], makes, str(out["원본"].get("파일") or "")) + seen = {t["열쇠"]: {"이름": t["몸"]["이름"], "규격": t["몸"]["규격"]} for t in todo} + if seen: + out["원본"]["내기계예정"] = list(seen.values()) + + +# ── 잇기(층 쓰기) ───────────────────────────────────────────────────── +def link(line: dict, row: dict) -> None: + """가져온 줄 → 내 기계 — 바로 = 기계 줄 키 · 로직 줄 시간당 부름 글 / 직접 = 자원 `내기계` 만.""" + from M02_MasterTemplete.M02_San_Link import hourly_text + + line["자원"]["내기계"] = row["키"] + if row.get("방식") == "직접": + return + line["이음"] = hourly_text(row["키"]) if line.get("종류") == "로직" else row["키"] + + +def attach(out: dict, makes: dict) -> list[dict]: + """층 저장 길 — 못 이은 기계 줄을 그 층 내 기계(같은 이름 · 규격) · 새 내 기계에 이음 · 원본 `내기계`.""" + from M02_MasterTemplete import M02_MasterTemplete_Store as store + from M02_MasterTemplete import M02_MyMachine as mym + + src = out["원본"] + todo = plan(out["로직"], makes, str(src.get("파일") or "")) + if not todo: + return [] + try: + mym._user() + except store.StoreError: # 개인 층 아님 — 안 만듦(예정만) + preview(out, makes) + return [] + done: dict[str, dict] = {} + for one in todo: + if one["열쇠"] not in done: + have = next( + (r for r in mym.rows() if key_of(r.get("이름"), r.get("규격")) == one["열쇠"]), None + ) + row = have or mym.create(one["몸"])["줄"] + done[one["열쇠"]] = {"줄": row, "새로": have is None} + link(one["줄"], done[one["열쇠"]]["줄"]) + made = [ + {**{k: d["줄"][k] for k in ("키", "이름", "규격", "방식")}, "새로": d["새로"]} + for d in done.values() + ] + src["내기계"] = made + src.pop("내기계예정", None) + return made diff --git a/M02_MasterTemplete/M02_Sangeun_FromHopyo.py b/M02_MasterTemplete/M02_Sangeun_FromHopyo.py index 9aed1d97d..30613e4a9 100644 --- a/M02_MasterTemplete/M02_Sangeun_FromHopyo.py +++ b/M02_MasterTemplete/M02_Sangeun_FromHopyo.py @@ -25,6 +25,7 @@ from M02_MasterTemplete import M02_Doc_FromNative as fn from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_San_Emit as em +from M02_MasterTemplete import M02_San_MyMachine as mm DIRNAME = "산근" RELOAD = "가져온 식 자료 없음 — 엑셀을 다시 올림" @@ -109,7 +110,15 @@ def has(folder: str | Path, key: str) -> bool: def emit(folder: str | Path, key: str, no: int) -> dict[str, Any]: - """HP 단산 블록 하나 → `{로직, 원본}`(원파일 우선 · 아니면 엑셀 읽기기 같은 길 · 이음 = M01 키).""" + """HP 단산 블록 하나 → `{로직, 원본}`(원파일 우선 · 아니면 엑셀 읽기기 같은 길 · 이음 = M01 키) · + 못 이은 기계 = 원본 `내기계예정`(821-4 · 안 만듦).""" + out, makes = _emit(folder, key, no) + mm.preview(out, makes) + return out + + +def _emit(folder: str | Path, key: str, no: int) -> tuple[dict[str, Any], dict]: + """(블록 `{로직, 원본}`, 그 파일 기계 구성 — 원파일 둘 꼴 `기계구성` · 엑셀 = 둔 시트 · 821-4).""" hopyo_store.read(folder, key) # 없는 HP = 404 path = _path(folder, key) try: @@ -118,13 +127,13 @@ def emit(folder: str | Path, key: str, no: int) -> dict[str, Any]: raise store.StoreError(409, RELOAD) from e native = saved.get("원파일") if isinstance(native, dict) and int(no) in fn.numbers(native): # 원파일 우선(810-2) - return fn.emit(native, int(no)) + return fn.emit(native, int(no)), native.get("기계구성") or {} if not saved.get("시트"): raise store.StoreError(404, f"없는 단산{no}") excel = _excel_of(path, saved) if int(no) not in fn.numbers(excel): raise store.StoreError(404, f"없는 단산{no}") - return fn.emit(excel, int(no)) + return fn.emit(excel, int(no)), mm.xlsx_makes(saved["시트"]) def _inner( @@ -173,7 +182,7 @@ def make(source: Path, key: str, no: int, name: str = "", _trail: tuple = ()) -> 안쪽 산근 블록은 형제 SK 를 먼저 만들어 참조(797-1).""" from M02_MasterTemplete import M02_Sangeun_Imported as imp - out = emit(source, key, no) + out, makes = _emit(source, key, no) out["원본"]["가져온때"] = utc_now_text() # 원문 패널 「어디서 왔나」(807-1) _link_inner(source, key, no, out, _trail) try: # 소요량 · 계수 후보 표지(797-2 · 사람 확인) — 못 셈 = 표지 없이 만듦 @@ -182,13 +191,15 @@ def make(source: Path, key: str, no: int, name: str = "", _trail: tuple = ()) -> out["원본"]["표지"] += qty.flags(out["로직"]) except Exception: # noqa: BLE001 — 역색인 짓기 실패가 가져온 식 만들기를 막지 않음 pass + made = mm.attach(out, makes) # 821-4 못 이은 기계 = 내 기계(그 층 같은 것 · 새로) + mine = {"내기계": made} if made else {} title = str(name or "").strip() or str(out["로직"].get("이름") or f"단산{no}") try: got = imp.create(out["로직"], out["원본"], title) - return {**got, "다시씀": False} + return {**got, "다시씀": False, **mine} except store.StoreError as e: same = (e.detail or {}).get("같은것") if isinstance(e.detail, dict) else None if e.status != 409 or not same: raise got = imp.create(out["로직"], out["원본"], title, use=same[0]["키"]) - return {**got, "다시씀": True} + return {**got, "다시씀": True, **mine} diff --git a/M02_MasterTemplete/M02_Sangeun_Imported.py b/M02_MasterTemplete/M02_Sangeun_Imported.py index ae10be471..ddfa375dd 100644 --- a/M02_MasterTemplete/M02_Sangeun_Imported.py +++ b/M02_MasterTemplete/M02_Sangeun_Imported.py @@ -63,9 +63,10 @@ def _mf() -> Any: def _whole() -> Any: - from M01_MasterData import M01_MasterData_Store as m01 + """M01 + 지금 층 내 기계(821-4 · 못 이은 기계를 이은 개인 키 `EQP…`).""" + from M02_MasterTemplete import M02_MyMachine as mym - return m01.loaded()[1] + return mym.whole() def is_imported(doc: Any) -> bool: @@ -80,7 +81,7 @@ def hourly_of(key: str) -> dict: """이음 기계(EQ) 시간당 노 · 재 · 경 — M01 판이 바뀌면 다시(캐시 = M01 묶음 하나에 매임).""" from M02_MasterTemplete import M02_Machine_Hourly as machine - whole = _whole() + whole = _whole() # 내 기계 고침 = 색인이 바뀜 → 다시 if _HOURLY["whole"] is not whole: _HOURLY.update(whole=whole, rows={}) if key not in _HOURLY["rows"]: @@ -112,6 +113,8 @@ def row_of(doc: Any) -> dict | None: "참조" ): # 안쪽 산근 = 형제 SK 1 단위 값(셈이 채움 · 엑셀 값묶음은 얼린 사본으로 둠) item |= {"요소": None, "값묶음": None, "기준": "우리" if ours else "엑셀"} + if ours and not link and (mine := (one.get("자원") or {}).get("내기계")): + _direct(item, one, mine) # 821-4 시간당 값 직접 내기계 = 그 줄 값 if ours and link: item.pop("단가", None) item.pop("값묶음", None) @@ -124,6 +127,18 @@ def row_of(doc: Any) -> dict | None: return {**row, "키": doc.get("키"), "호표": items} +def _direct(item: dict, one: dict, key: str) -> None: + """[우리 단가로] 이음 없이 이은 내 기계(시간당 값 직접 · 821-4) — 단가 = 그 비목 · 값묶음 = 셋 · + 지운 내 기계 = 엑셀 값 그대로.""" + got = hourly_of(key) + if not got.get("ok"): + return + if "단가" in item and one.get("비목") in COSTS: + item["단가"] = sd.plain(Decimal(str(got[one["비목"]]))) + elif "값묶음" in item: + item["값묶음"] = {k: sd.plain(Decimal(str(got[k]))) for k in COSTS} + + def base_row(doc: dict) -> dict[str, Any]: """바탕 없는 SK 바탕 줄(`sd.empty_row`) — 가져온 식 = 그 줄 · 아니면 입력 · 줄 없음.""" return row_of(doc) or { @@ -314,7 +329,7 @@ def unlinked(doc: dict) -> list[dict]: {"자리": ids[n], "이름": one.get("이름") or "", "까닭": link.why(one)} | ({"견적": True} if (one.get("자원") or {}).get("갈래") == "고정" else {}) for n, one in enumerate(doc[KEY]["로직"].get("호표") or []) - if not one.get("이음") and not one.get("참조") + if not one.get("이음") and not one.get("참조") and not (one.get("자원") or {}).get("내기계") ] diff --git a/M02_MasterTemplete/logic/M02_Hopyo_Imported_Model.ts b/M02_MasterTemplete/logic/M02_Hopyo_Imported_Model.ts index cde16bbac..6d5a8598e 100644 --- a/M02_MasterTemplete/logic/M02_Hopyo_Imported_Model.ts +++ b/M02_MasterTemplete/logic/M02_Hopyo_Imported_Model.ts @@ -27,6 +27,10 @@ export interface ImportedMeta { 갈래?: string; 엑셀금액?: Money4; 글줄?: { 행?: number | null; 글?: string; 자리?: string; 값?: number | null }[]; + /** 못 이은 기계 → 내 기계(821-4 · 새로 = 이번에 만듦 · 아니면 그 층 같은 것) */ + 내기계?: { 키: string; 이름: string; 규격?: string; 방식?: string; 새로?: boolean }[]; + /** 개인 층 밖에서 연 것 — 만들어질 내 기계(안 만듦) */ + 내기계예정?: { 이름: string; 규격?: string }[]; }; 검산?: { 같음: boolean;