Files
Aislo/M02_MasterTemplete/M02_San_MyMachine.py
T
eomsangdonandClaude Opus 5.5 4e71ec515b feat(821-4): 불러오기 못 이은 기계 = 내 기계 자동 생성 · 「내 기계 생성 n」 표시
- 새 M02_San_MyMachine — 그 파일 기계 구성(EST 원파일 MachMainDat · EST 엑셀 「기계경비」 · STmate 엑셀 「중기사용료」) → 우리 틀 값(가격 천원 · 달러 = 환율 FX000001 + 비고 · 손료계수 · 주연료 → 우리 유가 · 잡품 % · 조종원 인 + 우리 직종 · 전력) · 경쟁사 조종원 노임 환산 안 가져옴 · 구성 없음 = 시간당 값 직접 · 조합 기계 = 조각마다
- 만드는 때 = 층 저장 길(FromHopyo.make) 만 · 같은 이름 · 규격 내 기계 있으면 그것 · 다시 불러와도 중복 없음 · 미리 보기(emit) = 원본 「내기계예정」 만 · 개인 층 아님 = 안 만듦
- 불러오기 기계 찾기 = M01 만(Doc_FromNative.emit · Machine_Find.m01_only) — 내 기계 잇기는 한 곳(다시 불러와도 같은 SK)
- 가져온 식 색인 = M01 + 그 층 내 기계(Imported._whole) · 직접 꼴 = [우리 단가로] 내 기계 시간당 · 「못 이음」 에서 뺌 · 수식 문서 변수 설명 「내 기계 키」
- EST 원파일 둘 꼴 칸 기계구성 · STmate 엑셀 중기사용료 시트 둠
- 가져온 식 패널 — 「내 기계 생성 n」 · 「내 기계 이음 n」 · 줄마다 「내 기계 · 이름 규격」 꼬리표 · 「만들어질 내 기계 n」
- 금액(엑셀 단가 그대로) 줄 값 안 건드림

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 14:24:56 +09:00

313 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""불러오기 못 이은 기계 → 내 기계 자동 생성(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