Files
Aislo/M02_MasterTemplete/M02_Machine_Hourly.py
T
eomsangdonandClaude Opus 5.5 3eeb7c3cc3 fix(822-2b): 내 기계 연료 + 전력 둘 다 허용 · 새 식 둘(GC001200 연료 + 전력 · GC001201 + 조종원)
- 내 기계 저장의 「주연료 · 전력은 하나만」 막음 뺌 — 불러오기 자동 생성(경쟁사 파워트로웰 = 손료 + 휘발유 + 잡품 + 전력)이 막히던 것
- 꼴 고름(logic_key) 연료 + 전력 → GC001200 · + 조종원(직종 있음) → GC001201 · 직종 없는 조종원 = 까닭
- 조종원 줄을 한 식에 넣지 않음 — 수식 계산기가 빈 직종 「LB:」 를 못 받음(전력 식 GC001196 · 1197 짝과 같은 꼴)
- 수식 문서 두 줄 더함 · 802-2 대조 목록 다시 씀(두 줄 같음) · M01 정본 기계 · 다른 식 금액 그대로
- 낡은 시험: test_821_4 키대장 다음 번호 수 박은 기대 → 쓴 · 버린 번호보다 뒤(결번 재사용 없음) ·
  test_822_2 연료 + 전력 None · 저장 400 기대 → 새 식 · 미리보기 = 서버 값

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 18:57:42 +09:00

219 lines
10 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.
"""M02 기계(M01 EQ) 한 시간 값 — [항목 추가] 창에서 기계를 고르면 고정 줄 단가(PLAN 736-3).
기계 줄 = 일위대가 · 단가산출 더한줄의 고정 줄(이음 기계 EQ · 수량 1) — 단가 = 이 값(노 · 재 · 경).
셈 = M01 「건설기계」 장 기계경비 로직 가운데 그 기종을 받는 짝 차례 첫째
(`로직(GC, 기계='원문번호')` · 옵션은 기본값). 짝 차례 · 셈은
`DataFlow/M02_DF_Values._machine_hourly` 와 같은 규칙(그 폴더는 시험 길이라 따로 둠).
마스터 파일은 읽기만 · 로그인 사용자 모두.
기계표 = M01 + 지금 층 내 기계(821-1 · `M02_MyMachine` — 식은 채운 칸으로 고른 같은 GC 식 · 직접 = 그 값) ·
층을 모르는 길(`/api/m02/logic-lib/machine-hourly`)은 M01 만.
식 고름 한 곳(822-2 · `pick`) = ㉠ 원문번호가 식 「기계」 목록에 있으면 그 식(짝 차례) ㉡ 그 기계만의 식
(`_own`) ㉢ 없으면 꼴(채운 칸 · `M02_MyMachine.logic_key`) — M01 [새 기계] · 불러오기 · 데이터 흐름 ·
줄 쌓기 · 단가산출 기초가 다 이것을 씀 · 못 고름 · 값 모자람 = 까닭 글(`why`).
"""
from __future__ import annotations
import re
from typing import Any
from M01_MasterData import M01_MasterData_Store as store
from M02_MasterTemplete import M02_MyMachine as mym
cm = store.cm
_PLAIN = {
"기계",
"지역",
"보정작업",
"조종원직종",
"전력 계약종별",
} # 이 밖의 입력 = 부착 장비 전용
_TEXT = {"주연료종류", "조종원직종"} # 글 칸 — 나머지 기계 줄 값 칸은 수
_FIELD = re.compile(r"EQ:\{기계\}\.([^\s*/+()\-,]+)")
HOUR_UNIT = "10^-7/시간"
def _family() -> list[dict]:
"""기계 시간당 경비 로직 — 「건설기계」 장 파일 안 · 입력 「기계」 고르기가 있는 돈 로직."""
return [
row
for name, data in store.loaded()[0].items()
if data.get("그룹") == "로직" and "건설기계" in name
for row in data.get("줄") or []
if str(row.get("결과단위") or "").startswith("원")
and any(i.get("이름") == "기계" and i.get("고르기") for i in row.get("입력") or [])
]
def _codes(row: dict) -> list:
return next(i["고르기"] for i in row["입력"] if i.get("이름") == "기계")
def _rank(row: dict) -> tuple:
"""부착 전용 · 손료만 · 보정 · 기종 수 · 키 — 작을수록 먼저."""
names = [i.get("이름") for i in row.get("입력") or []]
attach = any(n not in _PLAIN for n in names)
loss = "지역" not in names
return (attach, loss, loss and "보정작업" in names, len(_codes(row)), str(row.get("키")))
def _operated(code: str) -> bool:
"""기계표(원문 8-4) 조종원 > 0 — 그 원문번호 기계 가운데 하나라도."""
rows = mym.whole().index.get("EQ", {}).values()
return any(
str(r.get("원문번호")) == code and float((r.get("값") or {}).get("조종원") or 0) > 0
for r in rows
)
def logic_of(code: str) -> dict | None:
"""기계 원문번호 → 그 기계 시간당 경비 로직 · 없으면 None(813-2 M01 화면 · 불러오기
`M02_Doc_Import.hourly_link` 도 씀) — `pick` 과 같음(그 원문번호 기계 줄로)."""
index = mym.whole()
row = (index.numbers.get("EQ") or {}).get(code) or {"원문번호": code}
return pick(row)
def listed(code: str) -> bool:
"""원문번호가 어느 기계경비식 「기계」 목록에 있는지(M01 [새 기계] 식 등록 · 상세 「쓰는 계산식」)."""
return bool(code) and any(code in [str(c) for c in _codes(r)] for r in _family())
def pick(row: dict) -> dict | None:
"""기계 줄 → 시간당 경비 식 — ㉠ 목록 ㉡ 그 기계만의 식 ㉢ 꼴 · 못 고름 = None(까닭 = `why`)."""
code = str(row.get("원문번호") or "")
fit = [r for r in _family() if code and code in [str(c) for c in _codes(r)]]
if not fit:
if got := _own(code):
return got
key = None if _day_loss(row) else mym.logic_key(row)
return mym.whole().logic(key) if key else None
op = _operated(code)
def order(row: dict) -> tuple:
has = any(x.get("이름") == "조종원" for x in row.get("호표") or [])
rank = _rank(row)
return (*rank[:3], op and not has, *rank[3:])
return min(fit, key=order)
def _own(code: str) -> dict | None:
"""그 기계만의 시간당 경비 식(816-1 · 4륜구동 1톤 덤프트럭 = GF000145 · 816-3 체인톱 = GF000337) —
「기계」 입력 없는 기계경비식이 그 원문번호 기계 줄 값(`EQ….가격` · `EQ….손료계수`)을 읽으면 그 식."""
rows = mym.whole().index.get("EQ", {}).values()
keys = [f"{r.get('키')}." for r in rows if code and str(r.get("원문번호")) == code]
for data in store.loaded()[0].values():
for row in data.get("줄") or [] if data.get("그룹") == "로직" else []:
if (row.get("부품") or {}).get("갈래") != "기계경비식" or takes_machine(row):
continue
if any(k in str(row.get("중간")) + str(row.get("호표")) for k in keys):
return row
return None
def _day_loss(row: dict) -> str:
"""손료계수 단위가 시간당(10⁻⁷)이 아님(산림품셈 「1일 1대당」) — 그 단위 글 · 아니면 빈 글."""
unit = str((row.get("단위") or {}).get("손료계수") or HOUR_UNIT)
return "" if unit == HOUR_UNIT or "손료계수" not in (row.get("값") or {}) else unit
def _is_num(v: Any) -> bool:
try:
float(v)
except (TypeError, ValueError):
return False
return not isinstance(v, bool)
def why(row: dict, logic: dict | None) -> str:
"""시간당 경비를 못 내는 까닭 한 줄(값이 정말 모자람) · 셀 수 있으면 빈 글."""
vals = row.get("값") or {}
if logic is None:
if unit := _day_loss(row):
return f"손료계수 단위 「{unit}」 — 시간당 식 없음(일 단위 손료)"
if not _is_num(vals.get("주연료")) and vals.get("주연료") not in (None, ""):
return f"주연료 「{vals['주연료']}」 — 수 아님"
if float(vals.get("조종원") or 0) > 0 and not vals.get("조종원직종"):
return "조종원직종 없음"
return "손료 계수 셋(상각 · 정비 · 관리) 없음"
text = " ".join(str(x.get(k)) for k in ("식", "수량") for p in ("중간", "호표", "덧줄")
for x in logic.get(p) or []) # fmt: skip
lost, odd = [], []
for name in dict.fromkeys(_FIELD.findall(text)):
v = vals.get(name)
if v in (None, ""):
lost.append(name)
elif name not in _TEXT and not _is_num(v):
odd.append(f"{name} 「{v}」 — {'범위' if '∼' in str(v) else '수 아님'}")
elif name == "주연료종류" and f"{v}:전국평균" not in (mym.whole().numbers.get("MO") or {}):
odd.append(f"주연료종류 「{v}」 — 유가 표에 없음")
return " · ".join([*([f"{' · '.join(lost)} 없음"] if lost else []), *odd])
def takes_machine(logic: dict) -> bool:
"""식이 「기계」 입력(원문번호 고르기)을 받는지 — 안 받는 식(GF000145)엔 기계='…' 를 안 넘김."""
return any(i.get("이름") == "기계" for i in logic.get("입력") or [])
def hourly(key: str) -> dict[str, Any]:
"""`{ok, 이름, 규격, 로직, 원문번호, 노무비, 재료비, 경비, 계}` · 못 셈 = `{ok: False, reason}`."""
if not key.startswith("EQ"):
return {"ok": False, "reason": f"「{key}」 는 기계 아님"}
try:
row = (whole := mym.whole()).get(key)
except cm.mf.FormulaError as e:
return {"ok": False, "reason": str(e)}
if mym.mine(key):
return mym.hourly(row, whole)
code = str(row.get("원문번호") or "")
logic = pick(row)
if lack := why(row, logic):
return {"ok": False, "reason": lack, **({"로직": logic["키"]} if logic else {})}
args = "".join( # 빈칸 든 입력 이름(「전력 계약종별」 · 820-1 파워트로웰)은 따옴표로
f", {n if (n := i['이름']).isidentifier() else repr(n)}='{(i.get('고르기') or [''])[0]}'"
for i in logic.get("입력") or []
if i.get("이름") != "기계"
)
mine = f", 기계='{code}'" if takes_machine(logic) else ""
call = f"로직({logic['키']}{mine}{args})"
line = {"종류": "로직", "요소": call, "이름": "기계", "단위": "시간", "수량": "1"}
holder = {
"키": "", "원문번호": "", "이름": "기계 시간", "결과단위": "원/식", "출처": "자체",
"입력": [], "중간": [], "호표": [line], "덧줄": [], "끝수": None,
} # fmt: skip
got = store.calc("", {}, holder, None)
if not got.get("ok") or "sums" not in got:
return {"ok": False, "reason": got.get("reason") or "셈 못 함"}
sums = {k: float(got["sums"][k]) for k in (*cm.mf.COST_ITEMS, "계")}
# 804-3 줄 쌓기 기계 줄 = 이 로직 + 기계='원문번호'
return {
"ok": True,
"이름": row.get("이름"),
"규격": row.get("규격"),
"로직": logic["키"],
"원문번호": code,
**sums,
}
def combo(main: str, part: str) -> tuple[str, str] | None:
"""조합 기계(굴착기 + 대형 브레이커 · 부착용 집게 · 814-1) → 우리 조합 식 (키, 부름 글)
`로직(GC000271, 기계='…', 브레이커='…', 지역='전국평균', …)` — 두 기계가 그 식 고르기에 있을 때만."""
try:
codes = [str(store.loaded()[1].get(k).get("원문번호") or "") for k in (main, part)]
except cm.mf.FormulaError:
return None
for row in sorted(_family(), key=lambda r: str(r.get("키"))):
ins = row.get("입력") or []
attach = [i for i in ins if i.get("이름") not in _PLAIN and i.get("고르기")]
if len(attach) != 1 or codes[0] not in [str(c) for c in _codes(row)]:
continue
if codes[1] not in [str(c) for c in attach[0]["고르기"]]:
continue
given = {"기계": codes[0], attach[0]["이름"]: codes[1]}
given |= {i["이름"]: (i.get("고르기") or [""])[0] for i in ins if i["이름"] not in given}
args = "".join(f", {k}='{v}'" for k, v in given.items())
return str(row["키"]), f"로직({row['키']}{args})"
return None