Files
Aislo/resources/master_data/scripts/master_doc_work.py
T
eomsangdonandClaude Opus 5.5 a53fae747c feat(846-1): 기계 줄 시간당 작업량 — 기계 값 칸 작업량 · 덮음 일당 | 시간당 · 계산기 짝(금액 불변)
- 기계 자료: M01 기계 221 대 값 칸 `작업량` = [{식 · 단위 · 변수(규격에서 정해지는 칸)}] (건설 공통 8-2 장 작업량 틀 · 빌더 build_기계_작업량_846_1.py · --check 0 · 다른 칸 · 시간당 비용 그대로)
- 계산기 짝 master_doc_work.py ↔ common_util_formula_doc_work.ts: 기계 줄 수량에서 시간당 작업량 자리 읽기(일당 = 시공량 ÷ (8 × 대수) · 식 = …/hr 나눔 변수) · 셈 답 줄 칸 `작업량` · 덮음 `작업량: {방식 시간당, 식, 변수?}` = 그 자리를 1 ÷ Q(기계 작업량 식)로 바꿔 셈 · 부착 기계(브레이커 · 집게) 줄 따로 · 식 없는 기계 · 단위 안 맞음 = 시간당 거절
- 덮음 짝(master_doc_cover · common_util_formula_doc_cover): 작업량은 기계 식 문서에 안 얹음 · prune 이 작업량 정리(일당 · 기본과 같은 변수 = 지움) · 몸 검사 · CoverEntry · CoverBase 꼴
- 저장 길: 사본 부름 묶음에 시간당 식 키(M02_Doc_Copy.calls) · 내 기계 clean 이 작업량 받음 · 연결 읽기 작업량 칸(master_doc_link)
- 기계 보기: 작업량 칸 = 제어 구역 몫(ownedCols — 바탕 칸 숫자 입력으로 안 그림 · 그리기는 846-2)
- 지킴: 1,520 문서 · 기계 655 대 · 견본 SK 금액 전후 다름 0 · 빌더 802-2 --check 1,520 같음(옵션 표본 18,873) · 둘 다 가능 줄 전수 시간당 셈 멈춤 0
- 시험: test_846_1_machine_work(일당 · 시간당 손셈 · E · K 덮음 · 정리 · 거절 · 부착 · 거울 · 저장 길 · 내 기계) 신설 · test_816_1 기대값 고침(까닭 — 4륜 덤프 1톤 기계 줄에 작업량 칸이 생겨 값 칸 비교에서 뺌)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-16 00:00:06 +09:00

396 lines
16 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.
# -*- coding: utf-8 -*-
"""기계 줄 시간당 작업량 (PLAN 846-1 · 요구 881 · 882 · 883) — 기계 줄 수량을 「1 ÷ 시간당 작업량」 으로 읽고
단가산출 덮음으로 그 기계의 작업량 식(시간당 방식)으로 바꿔 셈.
읽기(`sites`) — 기계 줄 수량 식의 시간당 작업량 자리(저장 식 글은 안 바꿈 · 만약 갈래 안도 찾음):
일당 = 곱 · 나눔 줄에 8 과 나눔 하나(시공량 D) → 시간당 작업량 = D ÷ (8 × 대수)
(대수 = 곱의 다른 변수 — 단위 % · `r_` 율은 곱에 남김)
식 = 나눔으로만 쓰는 변수 · 단위 「…/hr」(작업량 식 부품 · 표 값)
기계 값 칸 `작업량` = [{식: 작업량 틀 키, 단위, 변수: {기계 규격에서 정해지는 칸}}](빌더 `build_machine_work.py`).
덮음 = `기계덮음` 기계 칸 `작업량: {방식: '시간당', 식?, 변수?}` — 그 기계 줄의 작업량 자리를 `1 ÷ Q` 로 바꿈
(Q = 덮음 식 › 단위 맞는 첫 식을 식 문서 기본 › 기계 변수 › 덮음 변수로 셈) · 다른 곱(율 등) 그대로 ·
기계에 작업량 칸 없음 · 단위 맞는 식 없음 · Q 못 셈 = 그 줄 멈춤(까닭).
셈 답 기계 줄 칸 `작업량` = {기계, 꼴(일당 · 식 · 시간당), 율, 값, 단위, 식들, 변수?, 까닭?}(산출근거 자료).
파이썬 · TS 짝 — 짝 = `common_util/common_util_formula_doc_work.ts` · 거울 시험
`resources/tester/test_846_1_machine_work.py`.
"""
from __future__ import annotations
import re
from decimal import Decimal
import master_doc as md
WORK = "작업량"
HOURLY, DAILY = "시간당", "일당"
Q_NEW = "Q_시간당작업량" # 셈 범위 이름(문서 기호 아님)
EIGHT = Decimal(8)
def norm_unit(u) -> str:
t = str(u or "").strip().replace("m3", "㎥").replace("m2", "㎡")
return re.sub(r"/(시간|h|Hr|HR)$", "/hr", t)
def _var(doc: dict, name: str) -> dict:
return next((v for v in doc.get("변수") or [] if md.var_id(v) == name), {})
def _per_hour(doc: dict) -> str:
"""문서 결과 단위(원/X · 원/100㎡) → X/hr · 모름 = ""."""
t = str(doc.get("결과단위") or "")
low = re.sub(r"^[\d.,]+", "", t.split("/", 1)[1]).strip() if "/" in t else ""
return f"{norm_unit(low)}/hr" if low else ""
def unit_of(doc: dict, name: str, depth: int = 0) -> str:
"""변수 단위 — 빈 칸이면 정의 식에서 짐작(…/hr 변수를 씀 · 3600 이 든 식 = 문서 결과 단위/hr)."""
u = norm_unit(_var(doc, name).get("단위"))
line = next((x for x in doc.get("식") or [] if x.get("기호") == name), None)
if u or depth > 3 or line is None:
return u
node = _parse(line.get("식"))
if node is None:
return ""
for n in md.names(node):
if (got := unit_of(doc, n, depth + 1)).endswith("/hr"):
return got
return _per_hour(doc) if any(x[0] == "num" and x[1] == 3600 for x in md.walk(node)) else ""
def _held(doc: dict, name: str) -> bool:
"""곱에 남기는 변수(율) — 대수가 아님."""
return name.startswith("r_") or unit_of(doc, name) == "%"
# ── 식 나무 ───────────────────────────────────────────────────────────
def _chain(node) -> bool:
return node[0] == "bin" and node[1] in ("*", "/")
def _factors(node, sign: int = 1, out: list | None = None) -> list:
out = [] if out is None else out
if _chain(node):
_factors(node[2], sign, out)
_factors(node[3], sign if node[1] == "*" else -sign, out)
else:
out.append((sign, node))
return out
def _build(fs: list):
"""(부호, 노드) 목록 → 곱 · 나눔 나무(차례 그대로 · 곱 먼저)."""
tops = [n for s, n in fs if s > 0] or [("num", Decimal(1))]
node = tops[0]
for n in tops[1:]:
node = ("bin", "*", node, n)
for s, n in fs:
if s < 0:
node = ("bin", "/", node, n)
return node
def _kids(node) -> list:
if node[0] == "call":
return list(node[2])
return [x for x in node[1:] if isinstance(x, tuple)]
def _uses(node, sign: int, out: dict) -> None:
"""변수 쓰임 — {이름: [나눔으로만?]} (곱 · 나눔 줄 잎 = 부호 · 그 밖 = 0)."""
if _chain(node):
for s, n in _factors(node):
if n[0] == "var":
out.setdefault(n[1], []).append(s * sign)
else:
_uses(n, 0, out)
return
if node[0] == "var":
out.setdefault(node[1], []).append(0)
return
for k in _kids(node):
_uses(k, 0, out)
def _rates(doc: dict, node) -> set:
uses: dict = {}
_uses(node, 1, uses)
return {
n
for n, signs in uses.items()
if all(s < 0 for s in signs) and unit_of(doc, n).endswith("/hr")
}
def _daily(doc: dict, node, rates: set):
"""곱 · 나눔 줄 하나 → 일당 자리 | None."""
fs = _factors(node)
eight = [i for i, (s, n) in enumerate(fs) if s > 0 and n[0] == "num" and n[1] == EIGHT]
den = [
i for i, (s, n) in enumerate(fs) if s < 0 and n[0] != "num"
] # 수 나눔(÷ 100) = 곱에 남김
if not eight or len(den) != 1 or (fs[den[0]][1][0] == "var" and fs[den[0]][1][1] in rates):
return None
crew = [i for i, (s, n) in enumerate(fs) if s > 0 and n[0] == "var" and not _held(doc, n[1])]
d = fs[den[0]][1]
per = ("num", EIGHT)
for i in crew:
per = ("bin", "*", per, fs[i][1])
rate = ("bin", "/", d, per)
names = md.names(d)
u = unit_of(doc, names[0]) if len(names) == 1 else ""
unit = f"{u}/hr" if u else _per_hour(doc) # 시공량 단위 없음 = 문서 결과 단위
return {"꼴": DAILY, "노드": rate, "단위": unit, "뺌": {eight[0], den[0], *crew}}
def _find(doc: dict, node, rates: set, test, out: list) -> None:
if node[0] == "call" and node[1] == "만약" and test is not None and len(node[2]) == 3:
t = test(node[2][0])
if t is not None:
_find(doc, node[2][1] if t else node[2][2], rates, test, out)
return
if _chain(node):
site = _daily(doc, node, rates)
if site:
out.append(site)
return
for s, n in _factors(node):
if s < 0 and n[0] == "var" and n[1] in rates:
out.append({"꼴": "식", "노드": n, "단위": unit_of(doc, n[1])})
elif n[0] != "var":
_find(doc, n, rates, test, out)
return
for k in _kids(node):
_find(doc, k, rates, test, out)
def _parse(text):
try:
return md.parse("1" if text in (None, "") else text)
except md.DocError:
return None
def sites(doc: dict, text, test=None) -> list[dict]:
"""수량 글 → 시간당 작업량 자리 [{꼴, 노드, 율(글), 단위}] · test(조건 노드) → 참 · 거짓 · None(두 갈래 다)."""
node = _parse(text)
if node is None:
return []
out: list = []
_find(doc, node, _rates(doc, node), test, out)
for s in out:
s["율"] = md.text_of(s["노드"])
return out
def _swap(doc: dict, node, rates: set):
if _chain(node):
site = _daily(doc, node, rates)
fs = _factors(node)
if site:
keep = [f for i, f in enumerate(fs) if i not in site["뺌"]]
return _build([*keep, (-1, ("var", Q_NEW))])
if any(s < 0 and n[0] == "var" and n[1] in rates for s, n in fs):
return _build(
[
(s, ("var", Q_NEW) if s < 0 and n[0] == "var" and n[1] in rates else n)
for s, n in fs
] # fmt: skip
)
kids = [(s, n if n[0] == "var" else _swap(doc, n, rates)) for s, n in fs]
return node if all(a[1] is b[1] for a, b in zip(fs, kids)) else _build(kids)
kids = [_swap(doc, k, rates) for k in _kids(node)]
if all(a is b for a, b in zip(_kids(node), kids)):
return node # 자리 없음 = 그대로(글 차례 안 바뀜)
if node[0] == "call":
return ("call", node[1], kids)
if node[0] == "field":
return ("field", kids[0], node[2])
return (node[0], *node[1:2], *kids) if node[0] == "bin" else (node[0], kids[0])
def swap(doc: dict, text) -> str:
"""수량 글의 작업량 자리(모든 갈래)를 `1 ÷ Q_NEW` 로 바꾼 글."""
node = _parse(text)
return md.text_of(_swap(doc, node, _rates(doc, node))) if node is not None else str(text)
# ── 기계 작업량 · 덮음 ──────────────────────────────────────────────────
def specs_of(got) -> list[dict]:
"""기계 값 칸 `작업량` → [{식, 단위, 변수}] (꼴 틀림 = 뺌)."""
return [
x
for x in (got if isinstance(got, list) else [])
if isinstance(x, dict) and isinstance(x.get("식"), str) and x["식"]
]
def fits(specs: list[dict], unit: str) -> list[str]:
"""시간당 작업량 단위에 맞는 식 키(단위 모름 = 전부)."""
return [s["식"] for s in specs if not unit or norm_unit(s.get("단위")) == unit]
def defaults(tpl: dict | None, spec: dict | None) -> dict:
"""식 문서 입력 · 선택 기본 › 기계 변수."""
out = {}
for v in (tpl or {}).get("변수") or []:
link = v.get("연결") or {}
if link.get("갈래") in ("입력", "선택", "숫자") and v.get("열쇠"):
out[str(v["열쇠"])] = link.get("값")
return {**out, **((spec or {}).get("변수") or {})}
def _same(a, b) -> bool:
try:
return abs(Decimal(str(a)) - Decimal(str(b))) <= Decimal("1e-9") * max(
Decimal(1), abs(Decimal(str(a)))
)
except Exception:
return str(a) == str(b)
def prune_work(want, base: dict | None = None) -> dict | None:
"""작업량 덮음 정리 — 방식 시간당만 · 기본(`defaults`)과 같은 변수 뺌 · 남는 것 없음 = None."""
if not isinstance(want, dict) or want.get("방식") != HOURLY:
return None
base = base or {}
got = {
k: v
for k, v in (want.get("변수") or {}).items()
if v not in (None, "") and not (k in base and _same(v, base[k]))
}
out: dict = {"방식": HOURLY}
if want.get("식"):
out["식"] = str(want["식"])
if got:
out["변수"] = got
return out
def clean_work(want, where: str) -> dict:
"""몸 검사 — 틀리면 ValueError."""
if not isinstance(want, dict) or want.get("방식") not in (DAILY, HOURLY):
raise ValueError(f"{where} 작업량 — 방식 일당 · 시간당")
out: dict = {"방식": want["방식"]}
if want.get("식") not in (None, ""):
if not isinstance(want["식"], str):
raise ValueError(f"{where} 작업량 식 — 글")
out["식"] = want["식"]
elif want["방식"] == HOURLY: # 사본 부름 묶음(`cover_calls`)에 그 식을 담으려면 필요
raise ValueError(f"{where} 작업량 — 시간당은 식 키 필요")
vals = want.get("변수")
if vals is not None:
if not isinstance(vals, dict) or any(
isinstance(v, (dict, list, bool)) for v in vals.values()
):
raise ValueError(f"{where} 작업량 변수 — {{열쇠: 수 · 글}}")
out["변수"] = {str(k): v for k, v in vals.items() if v not in (None, "")}
return out
def cover_calls(doc: dict) -> list[str]:
"""문서 `기계덮음` 의 시간당 작업량 식 키 — 사본 부름 닫힌 묶음에 더함(`M02_Doc_Copy.calls`)."""
got = doc.get("기계덮음") if isinstance(doc, dict) else None
return [
str(w["식"])
for e in (got.values() if isinstance(got, dict) else [])
if isinstance(e, dict)
and isinstance(w := e.get(WORK), dict)
and w.get("방식") == HOURLY
and w.get("식")
]
# ── 셈(계산기 `_money` 가 기계 줄마다 부름) ─────────────────────────────
NOT_MACHINE = ("지역", "보정작업", "조종원직종", "계약종별") # 기계 식 입력 가운데 기계 아닌 것
def _machine_of(run, line: dict):
"""금액 줄 단가가 부르는 기계 식 변수 → (작업 기계 입력 식, 변수 이름) · 기계 줄 아님 = None ·
부착 기계(브레이커 · 집게 입력) 줄 = 그 부착 기계(일하는 기계 — 굴착기 줄과 따로)."""
node = _parse(line.get("단가"))
for name in md.names(node) if node is not None else []:
link = (run.vars.get(name) or {}).get("연결") or {}
given = link.get("입력") or {}
if link.get("갈래") == "단가산출" and "기계" in given:
tool = [v for k, v in given.items() if k != "기계" and k not in NOT_MACHINE]
return str(tool[-1] if tool else given["기계"]), name
return None
def line_work(run, line: dict, scope: dict):
"""기계 줄 하나 → (바꾼 수량 글 | 멈춤(표지는 계산기가 담) | None, 범위 더함 {}, 답 칸 | None) —
계산기 `_money` 가 부름."""
import master_doc_calc as mc
import master_doc_cover as mcv
got = _machine_of(run, line)
if not got:
return None, {}, None
where = str(line.get("이름") or "")
def quiet(fn):
"""흔적 없는 셈 — 표지 · 기억한 값을 되돌림(진짜 셈이 표지를 다시 냄)."""
n, seen = len(run.marks), set(run.values)
try:
return fn()
finally:
del run.marks[n:]
for k in set(run.values) - seen:
del run.values[k]
key_val = quiet(lambda: run.expr(got[0], where, scope))
if mc._bad(key_val):
return None, {}, None
key = mcv.machine_key({"기계": mc.plain_text(key_val)})
def test(cond):
v = quiet(lambda: run.ev(cond, scope, where))
return None if mc._bad(v) else bool(v)
found = sites(run.doc, line.get("수량"), test)
want = ((run.cover or {}).get(key) or {}).get(WORK)
hourly = isinstance(want, dict) and want.get("방식") == HOURLY
if not found and not hourly:
return None, {}, None
try:
specs = specs_of(run.links.element({"키": f"EQ:{key}", "칸": WORK}))
except (mc.Gap, mc.Broken):
specs = []
unit = found[0]["단위"] if found else ""
info = {"기계": key, "꼴": found[0]["꼴"] if found else "", "율": found[0]["율"] if found else "",
"값": None, "단위": unit, "식들": fits(specs, unit)} # fmt: skip
if not hourly:
v = quiet(lambda: run.ev(found[0]["노드"], scope, where))
info["값"] = v if isinstance(v, Decimal) else None
return None, {}, info
pick = str(want.get("식") or "") or next(iter(info["식들"]), "")
spec = next((s for s in specs if s["식"] == pick), None)
why = (
"시간당 작업량 식 없는 기계"
if not specs
else "시간당 작업량 자리 없는 줄"
if not found
else f"단위 {unit} 맞는 작업량 식 없음"
if not spec or pick not in info["식들"]
else ""
)
info |= {"꼴": HOURLY, "율": pick}
if why:
info["까닭"] = why
return mc.Stop(f"{where} 시간당 거절 — {why}", "시간당 거절", where), {}, info
given = {**(spec.get("변수") or {}), **(want.get("변수") or {})}
tpl = None
try:
q = run.sub({"키": pick, "입력": given}, where)
tpl = run.links.doc(pick)
except (mc.Gap, mc.Broken) as e:
q = mc.Stop(str(e))
info["변수"] = {**defaults(tpl, spec), **(want.get("변수") or {})}
if not isinstance(q, Decimal) or q <= 0:
info["까닭"] = f"작업량 식 {pick} 못 셈" + (f" — {q.why}" if mc._bad(q) else "")
return mc.Stop(f"{where} {info['까닭']}", "시간당 거절", where), {}, info
info["값"] = q
return swap(run.doc, line.get("수량")), {Q_NEW: q}, info