Files
Aislo/M02_MasterTemplete/M02_San_MyMachine.py
T
eomsangdonandClaude Opus 5.5 8709dc9ffd feat(842-4): 불러오기 내 기계 — 기계 한 대 안 운전사 · 연료 · 잡품 · 소모 재료 줄을 모두 받음
- 구성 읽기 넷(EST 원파일 · EST 엑셀 · STmate 원파일 일위 구성 · STmate 엑셀) = 인력줄 · 연료줄 · 잡품줄 · 재료줄 목록(첫 줄 = 옛 단일 칸 그대로)
- 몸 = 842-3 꼴 — 둘째 운전사부터 인력 줄(LB:원문번호) · 둘째 연료 = 잡품 키 줄(우리 유가 · 율 100) · 잡품 율 = 연료 차례 짝 ·
  소모 재료(치즐 EA) = 우리 자재 하나에 이어지면 잡품 키 줄 · 못 이음 = 줄 없이 비고 「확인 필요」
- 덤: 「잡비」 = 잡품 · 첫 연료 못 이음 때 잡품 율을 비고에 · 연료 없는 전력 기계 잡품 = 전력의 %(알림 없이 버리던 것)
- 한 줄 구성 몸은 전과 같음 · 기계 두 대 조합 나누기 그대로
- 견본 1,528 구성 · 여럿 69(다산소프트 main 만 · 실무 0) · 버린 줄 0 · 시험 test_842_4 새로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-15 18:46:40 +09:00

416 lines
20 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`) ·
원본 `내기계` [{키, 이름, 규격, 새로}] · 개인 층 아님 = 안 만듦(원본 `내기계예정` [{이름, 규격}] 만).
금액(엑셀 단가 그대로)은 줄 값을 안 건드려 그대로.
기계 한 대 안 여러 줄(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 엑셀 「중기사용료」 — 「제 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:
_crew(cur, v[0], v[1])
elif first in MISC_WORDS and len(v) > 2:
_misc(cur, 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:
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