Files
Aislo/M02_MasterTemplete/M02_Machine_Hourly.py
eomsangdonandClaude Opus 5.5 98f37c7c25 feat(827-1): 단가산출 기계 창 — 조회 · 바꾸기 · 고쳐 쓰기(초안 → [저장] 때 내 기계)
- 줄 쌓기 기계 줄 이름 단추 = 기계 창(왼쪽 찾기 · 오른쪽 공통 기계 양식 읽기 전용 · 지금 줄 = 사본 값 · 새 값 표시)
- [이 기계로 바꾸기] = repick 그대로(823-1 · 되돌리기) · M01 목록도 시간당(machine-rows)
- [고쳐 쓰기](개인 층 SK) = 서버 확인(쓰기 없음) → 문서 칸 내기계초안 · 줄 입력 개인 EQP초안-N
- [저장] 한 번에 서버가 초안 만들기 · 고침 → 키 바꿈 → 사본 박기 · 실패 = 만든 파일 지움 · 고친 줄 되돌림
- 미리 셈 = 초안을 그 요청 안만 셈 색인에 얹음 · 브라우저 = 셈 꼴 값을 캐시에
- 회사 · 마스터 층 · M01 기초 = 조회 · 바꾸기만(까닭 한 줄) · 옵션 가로 곁 [조회]
- 걷음: 줄 옆 「복사해 내 기계로」(line_stack_mine.ts) · 찾기 창 [새 기계 만들기] · 개인관리 › 기계 만들기 · 고치기(보기 · 쓰는 곳 · 지우기만)
- 내 기계 쓰는 곳 = 수식 길 사본도 셈
- 시험 test_827_1_machine_window(8)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 10:15:26 +09:00

259 lines
12 KiB
Python
Raw Permalink 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,
}
_BRIEF: dict[tuple, dict] = {}
def brief(keys: list[str]) -> list[dict[str, Any]]:
"""기계 창 목록 · 양식(827-1) — M01 기계 줄 값 + 시간당(`hourly` 와 같은 식 · 입력 기본값 ·
마스터를 다시 읽지 않고 색인 위에서 셈 · 색인마다 메모) · 키 · 원문번호로 찾음 · 없는 키 = 뺌."""
import master_cut as mcut
from M02_MasterTemplete.M02_Item_Prices import to_json
index, mf = store.loaded()[1], cm.mf
if len(_BRIEF) > 2000: # ponytail: 통째 비움 · 기계 수(655)의 세 배
_BRIEF.clear()
out = []
for key in keys[:60]:
if (got := _BRIEF.get((id(index), key))) is None:
try:
row = (index.numbers.get("EQ") or {}).get(key) or index.get(key)
except mf.FormulaError:
continue
if not str(row.get("키")).startswith("EQ"):
continue
logic = pick(row)
head = {k: row.get(k) for k in ("키", "원문번호", "이름", "규격")}
if lack := why(row, logic):
hour = {"ok": False, "reason": lack}
else:
given = {i["이름"]: (i.get("고르기") or [""])[0] for i in logic.get("입력") or []}
if takes_machine(logic):
given["기계"] = str(row.get("원문번호"))
sums = mcut.engine(
index, logic["키"], mf.run(index, logic["키"], mf.fill_empty(logic, given))
)
hour = {"ok": True, "로직": logic["키"]}
hour |= {k: float(sums[k]) for k in (*cm.mf.COST_ITEMS, "계")}
got = _BRIEF[(id(index), key)] = {**head, "값": to_json(row.get("값")), "시간당": hour}
out.append(got)
return out
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