"""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