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