"""불러오기 못 이은 기계 → 내 기계 자동 생성(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`) · 원본 `내기계` [{키, 이름, 규격, 새로}] · 개인 층 아님 = 안 만듦(원본 `내기계예정` [{이름, 규격}] 만). 금액(엑셀 단가 그대로)은 줄 값을 안 건드려 그대로. 기계 한 대 안 여러 줄(842-4) = 구성에 `인력줄` [[직종 글, 인]] · `연료줄` [[종류, ℓ]] · `잡품줄` [율 %] · `재료줄` [[이름, 수량, 단위]] (읽은 차례 · 첫 줄 = 옛 단일 칸 그대로) → 몸 값 = `master_machine_crew` 꼴 `인력` · `잡품` 목록(옛 조종원 · 잡재료율 칸 없음) · 둘째 연료부터 · 소모 재료 = 잡품 키 줄(우리 유가 · 자재 하나에 이어질 때 · 율 100) · 잡품 율 = 연료 줄 차례로 짝 · 못 이은 직종 · 연료 · 재료 · 짝 없는 율 = 줄 안 만들고 비고 「확인 필요」(지어내지 않음 · 버린 줄 0). """ 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": "경비"} FUELS = ("경유", "휘발유", "실내등유") 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 (_num(litre) or 0) > 0: make.setdefault("연료줄", []).append([str(kind or "").strip(), _num(litre)]) if make.get("주연료") is None: make["주연료종류"], make["주연료"] = str(kind or "").strip(), _num(litre) def _crew(make: dict, job: Any, n: Any) -> None: """운전사 줄 — 모두 `인력줄` · 첫 줄 = 조종원 · 조종원직종(842-4).""" make.setdefault("인력줄", []).append([str(job).strip(), _num(n)]) if not make.get("조종원"): make["조종원"], make["조종원직종"] = _num(n), str(job).strip() def _misc(make: dict, rate: Any) -> None: """잡품 율 — 모두 `잡품줄` · 첫 율 = 잡재료율(842-4).""" if _num(rate) is not None: make.setdefault("잡품줄", []).append(_num(rate)) if make.get("잡재료율") is None: make["잡재료율"] = _num(rate) MISC_WORDS = ("잡품", "잡유", "잡비", "잡재료", "잡재료비") # ── 구성 읽기 ───────────────────────────────────────────────────────── 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 MISC_WORDS: _misc(cur, gn) elif i == -2 and word == "전력": cur["전력"] = gn elif i == -2 and unit == "ℓ": _fuel(cur, r["Name"], gn) elif i == -2 and (gn or 0) > 0: # 소모 재료(치즐 EA 등 · 842-4) cur.setdefault("재료줄", []).append([str(r["Name"]).strip(), gn, unit]) elif i == -1 and (gn or 0) > 0: _crew(cur, r["Name"], gn) 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 MISC_WORDS and len(v) > 2: _misc(cur, 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: _crew(cur, v[0], v[1]) 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 엑셀 「중기사용료」 — 칸 차례 [명칭, 규격, 수량, 단위, 단가 …](첫 글 칸부터 · 빈 규격도 칸 자리 지킴 · 843-2 — 규격 빈 머리 · 손료 줄을 못 읽던 것) · 「제 N 호표」 뒤 머리(단위 「시간」 · 끝 칸 코드 X…) · 손료(단위 「천원」 · 수량 = 계수(천원당) · 단가 = 가격 천원) · 조종원(「인」) · 연료(「L」) · 전력(「kWh」) · 잡품(명칭 잡품 · 수량 = %) · 「합 계」 끝 → {코드 · 이름|규격: 구성}(손료계수 = 계수 × 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 at = next(i for i, c in enumerate(row) if c not in (None, "")) name, spec, qty, unit, price = (*row[at : at + 5], *[None] * 5)[:5] spec, qty, price, unit = spec or "", _num(qty), _num(price), str(unit or "").strip() if not cur and unit in ("시간", "hr") and re.fullmatch(r"X\d+", str(v[-1])): cur.update(_blank(str(name), str(spec))) out[str(v[-1])] = out[key_of(name, spec)] = cur elif not cur: continue elif unit == "천원" and qty and price: cur["손료"].append([qty * 10_000, price * 1000, False]) elif unit == "인" and (qty or 0) > 0: _crew(cur, name, qty) elif first in MISC_WORDS and qty is not None: _misc(cur, qty) elif unit.upper() in ("L", "ℓ"): _fuel(cur, name, qty) elif unit.upper() == "KWH": cur["전력"] = qty 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: rate = ( f"(잡품 {make['잡재료율']:g}%)" if make.get("잡재료율") else "" ) # 842-4 율도 적음 notes.append(f"주연료 「{fuel}」 {litre:g}ℓ{rate} — 우리 유가에 못 이음({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["전력"] if not litre and make.get("잡재료율"): # 연료 없는 전력 기계 잡품 = 전력의 %(842-4) out["잡재료율"] = make["잡재료율"] crew, misc = _extra(make, out, index, notes) if crew or misc: # 둘째 줄부터(842-4) — 첫 줄 몸은 옛 칸 그대로 바꿈 + 덧줄 import master_machine_crew as mcrew out = mcrew.from_old(out) out[mcrew.CREW] = [*out.get(mcrew.CREW, []), *crew] out[mcrew.MISC] = [*out.get(mcrew.MISC, []), *misc] return out def _job(index: Any, name: Any) -> str | None: hit = [ k for k, r in (index.numbers.get("LB") or {}).items() if norm(r.get("이름")) == norm(name) ] return hit[0] if len(hit) == 1 else None def _oil(index: Any, fuel: Any) -> dict | None: t = re.sub(r"\s", "", str(fuel or "")) ours = next((n for n in FUELS if n == t), None) or next( (n for n in FUELS if t.startswith(n)), None ) return (index.numbers.get("MO") or {}).get(f"{ours}:전국평균") if ours else None def _goods(name: str, unit: str) -> dict | None: """소모 재료 → 우리 자재 하나(이름 표기 · 단위 같음) · 여럿 · 없음 = None.""" from M01_MasterData import M01_MasterData_Store as m01 q = re.sub(r"\(.*", "", re.sub(r"\s", "", name)) hit = [ x for x in m01.pick("price", q, 200)["items"] if norm(f"{x['이름']}{x.get('규격') or ''}") == norm(name) and norm(x.get("단위")) == norm(unit) ] return hit[0] if len(hit) == 1 else None def _extra(make: dict, out: dict, index: Any, notes: list[str]) -> tuple[list, list]: """기계 한 대 안 둘째 줄부터(842-4) → (인력 줄, 잡품 줄) — 못 이음 = 줄 없이 비고 「확인 필요」.""" crew, misc = [], [] for job, n in (make.get("인력줄") or [])[1:]: code = _job(index, job) if code: crew.append({"키": f"LB:{code}", "인": n, "이름": job}) else: notes.append(f"인력 「{job}」 {n:g}인 — 우리 직종에 못 이음({CHECK})") fuels = make.get("연료줄") or [] for fuel, litre in fuels[1:]: row = _oil(index, fuel) if row: misc.append({"키": row["키"], "이름": str(fuel), "수량": litre, "율": 100}) else: notes.append(f"연료 「{fuel}」 {litre:g}ℓ — 우리 유가에 못 이음({CHECK})") for i, rate in enumerate((make.get("잡품줄") or [])[1:], 1): row = _oil(index, fuels[i][0]) if i < len(fuels) else None if row: misc.append( {"키": row["키"], "이름": f"{fuels[i][0]} 잡품", "수량": fuels[i][1], "율": rate} ) elif out.get("주연료"): # 짝 연료 없음 = 주연료의 % misc.append({"줄": "주연료", "율": rate}) else: notes.append(f"잡품 {rate:g}% — 이을 연료 없음({CHECK})") for name, qty, unit in make.get("재료줄") or []: got = _goods(name, unit) if got: misc.append({"키": got["ref"], "이름": name, "수량": qty, "율": 100}) else: notes.append(f"재료 「{name}」 {qty:g}{unit} — 우리 자재에 못 이음({CHECK})") return crew, misc 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