"""불러오기 못 이은 기계 → 내 기계 자동 생성(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