- M02_Machine_Hourly.pick = 목록 › 그 기계 식 › 꼴(내 기계와 같은 규칙) · why = 값 모자란 까닭(가격 없음 · 범위 · 일 단위 손료 …) - 데이터 흐름 · 사본 모으기 · M01 · 불러오기 · 줄 쌓기 모두 pick 을 씀(데이터 흐름 옛 짝 고름 지움) - 내 기계 꼴: 전력 · 전력 + 조종원 · 직종 입력 · 손료계수 한 칸 · 직접(GC001199 — 줄 쌓기 회색 풀림) · 연료 + 전력 = 400 - M01 [새 기계] 저장 = 꼴 식 「기계」 목록에 등록 · M01 에서 만든 기계 지우면 목록에서 뺌 · 상세 「쓰는 계산식」(POST /machine/formula) · 목록 시간당 「—」 대신 까닭 글 - 미리보기 짝: 잡품 = (주연료 + 전력) × %(전력 기종 = 전력비의 %) - 시험 test_822_2 새로 · test_804_2 갈래에 전력 줄 더함(손료만 식과 전력 식은 다른 갈래 — 모르타르 믹서) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
221 lines
10 KiB
Python
221 lines
10 KiB
Python
"""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 "조종원직종 없음"
|
||
if vals.get("주연료") and 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
|