feat(729-1): 실무 단가산출 → 기초 · SK · 고름 대기 — 블록 맞춤 판정 → 엑셀 품 그대로 덮개 · 더한줄 · 뺀줄 · 별칭 제안
- 새 M02_Hopyo_SkPatch: 판정 맞음 = 기초(바탕 = 1위 기초 · 덮개 없음) · 다른 점 = 바탕 기초 + 인력 덮개(qty · 배율) · 엑셀만 직종 · 장비(GC000267 · GC000268 기계 옵션 찾기) 더한줄 · 우리만 줄 뺀줄 · 블록 안 자재 덮개 · 옮기지 못한 차이 = 본뜬.못옮김 · 후보 = 고름 대기
- M02_Hopyo_Sangeun: 바탕 없는 SK + 고정 한 줄 걷음 · 블록마다 기초 · SK · 고름 대기 · 고른 기초(바탕 {번: 로직}) · 별칭 장부 짝 = 기초까지만 · 맞춤 셈 전 409
- 여덟 파일 206 블록 = 기초 82 · SK 40 · 고름 대기 84 · SK 품 어긋남 0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
"""M02 일위대가 불러오기 → 단가산출(PLAN 718-1 · 요구서 내역12 4장 22) —
|
||||
산근(단산) 블록 = 바탕 없는 단가산출.
|
||||
"""M02 일위대가 불러오기 → 단가산출(PLAN 718-1 · 729-1 · 요구서 내역19 1장 7 · 3장 6) —
|
||||
산근(단산) 블록마다 매칭 → 비교 → 기초 · SK · 고름 대기.
|
||||
|
||||
블록마다 SK 하나 — 그 블록을 부르는 첫 엑셀 줄(「단산N참조」)의 1 단위 단가
|
||||
(노무비 · 재료비 · 경비)를 고정 더한줄 하나로(`M02_Hopyo_Fixed` 꼴 · 엑셀 숫자 그대로 ·
|
||||
서버가 파일에서 읽음) · 이름 = 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) ·
|
||||
구분 「실무자료」 · 상세구분 = 파일 · 본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본,
|
||||
확인} · 같은 본뜬이 이 층에 있으면 그대로(겹침 없음) · 만드는 층 = 부르는 층(마법사에서 고른 층).
|
||||
블록마다 그 블록을 부르는 첫 엑셀 줄(「단산N참조」)의 맞춤셈(`M02_Hopyo_Fit` 캐시 · 없으면 그 블록만
|
||||
셈) → `M02_Hopyo_SkPatch.plan`:
|
||||
기초 = SK 바탕 = 1위 기초 · 입력값 · 덮개 없음 · SK = 바탕 기초 + 덮개 · 더한줄 · 뺀줄(엑셀 품 그대로) ·
|
||||
고름 대기 = SK 안 만듦(바탕 없는 SK 로 몰래 채우지 않음) · 후보 다섯 → 사용자가 고른 기초 `바탕 {번: {로직,
|
||||
입력값?}}` 로 다시 부르면 그 기초에 견줘 SK(이미 있으면 그 키에 덮음).
|
||||
SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 ·
|
||||
본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래, 엑셀품, 못옮김} · 같은 본뜬(키 · 원본)이
|
||||
이 층에 있으면 그대로(겹침 없음) · 만드는 층 = 부르는 층.
|
||||
별칭 제안 = 기초 · SK 만든 블록의 엑셀 줄 → 이 층 별칭 장부 짝 = 바탕 기초까지만(차이 SK 는 이 층 문서 ·
|
||||
요구서 3장 6) · 장부에 이미 짝 · 갈래가 있는 줄은 그대로 · 승인 길 = M01 별칭 장부(716).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -15,10 +20,13 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_time import utc_now_text
|
||||
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
ORIGIN = "실무자료"
|
||||
KIND = "sangeun"
|
||||
@@ -28,76 +36,154 @@ def mark(key: str, no: int) -> str:
|
||||
return f"{key} 단산{no}"
|
||||
|
||||
|
||||
def made(folder: str | Path, key: str | None = None) -> dict[str, str]:
|
||||
"""이 층 실무자료 단가산출 `{본뜬 키: SK 키}`(`key` = 그 HP 것만)."""
|
||||
def made(
|
||||
folder: str | Path, key: str | None = None, source: refs.Here | None = None
|
||||
) -> dict[str, str]:
|
||||
"""이 층 실무자료 단가산출 `{본뜬 키: SK 키}`(`key` = 그 HP 것만 · `source` = 파일 층으로 거름)."""
|
||||
out = {}
|
||||
for path in sorted((Path(folder) / KIND).glob("SK-*.json")):
|
||||
try:
|
||||
got = json.loads(path.read_bytes()).get("본뜬")
|
||||
except (OSError, ValueError, AttributeError):
|
||||
continue
|
||||
if isinstance(got, dict) and got.get("층") == ORIGIN:
|
||||
if key is None or str(got.get("키") or "").startswith(f"{key} "):
|
||||
out[str(got.get("키"))] = path.stem
|
||||
if not isinstance(got, dict) or got.get("층") != ORIGIN:
|
||||
continue
|
||||
if key is not None and not str(got.get("키") or "").startswith(f"{key} "):
|
||||
continue
|
||||
if source is None or not got.get("원본") or tuple(got["원본"]) == tuple(source):
|
||||
out[str(got.get("키"))] = path.stem
|
||||
return out
|
||||
|
||||
|
||||
def _doc(line: dict, block: dict, no: int, hp: dict, stamp: str, who: Any) -> dict[str, Any]:
|
||||
def calls(hp: dict[str, Any]) -> tuple[dict[int, dict], dict[int, dict]]:
|
||||
"""(블록 {번: 블록}, 부르는 첫 줄 {번: 줄 + `_호표`})."""
|
||||
blocks = {int(b["번"]): b for b in hp.get("단산") or [] if b.get("번") is not None}
|
||||
first: dict[int, dict] = {}
|
||||
for h in hp.get("호표") or []:
|
||||
for line in h.get("줄") or []:
|
||||
ref = line.get("참조") or {}
|
||||
no = int(ref.get("번") or 0) if ref.get("종류") == "단산" else 0
|
||||
if no in blocks and no not in first:
|
||||
first[no] = {**line, "_호표": h.get("번")}
|
||||
return blocks, first
|
||||
|
||||
|
||||
def _bad(text: str, status: int = 400) -> store.StoreError:
|
||||
return store.StoreError(status, text)
|
||||
|
||||
|
||||
def _picks(raw: Any) -> dict[int, dict[str, Any]]:
|
||||
"""사용자가 고른 기초 `{번: {로직, 입력값?}}` 재검사 — 돈 로직만 · 입력 이름 = 그 로직 입력."""
|
||||
if not raw:
|
||||
return {}
|
||||
if not isinstance(raw, dict) or len(raw) > 500:
|
||||
raise _bad("바탕은 {블록 번: {로직, 입력값}}")
|
||||
logics = ledger._known()[0]
|
||||
out = {}
|
||||
for no, one in raw.items():
|
||||
if not isinstance(one, dict) or str(one.get("로직") or "") not in logics:
|
||||
raise _bad(f"단산{no} · 돈 로직이 아님 「{(one or {}).get('로직')}」")
|
||||
given = one.get("입력값")
|
||||
if given is not None:
|
||||
bad = set(given) - ledger.input_names(one["로직"]) if isinstance(given, dict) else {1}
|
||||
if bad:
|
||||
raise _bad(f"단산{no} · 모르는 입력 {sorted(map(str, bad))}")
|
||||
out[int(no)] = {"로직": one["로직"], **({"입력값": given} if given is not None else {})}
|
||||
return out
|
||||
|
||||
|
||||
def _doc(line: dict, block: dict, no: int, hp: dict, plan: dict, sure: dict) -> dict[str, Any]:
|
||||
name = str(block.get("이름") or line.get("이름") or "").strip() or f"단산{no}"
|
||||
unit = str(line.get("단위") or block.get("단위") or "").strip() or "식"
|
||||
spec = str(line.get("규격") or "").strip()
|
||||
price = {k: hfix._plain(v) for k, v in hfix._parts(line.get("단가")).items()}
|
||||
fx = {
|
||||
"이름": name,
|
||||
"규격": spec,
|
||||
"단위": unit,
|
||||
"단가": price,
|
||||
"이음": dict.fromkeys(fixed.LINKS),
|
||||
"출처": {"파일": hp.get("파일"), "호표": line.get("_호표"), "줄": int(line.get("차례", 0))},
|
||||
}
|
||||
keep = {k: plan[k] for k in ("갈래", "엑셀품", "못옮김")}
|
||||
return {
|
||||
"종류": "단가산출",
|
||||
"이름": name,
|
||||
"단위": unit,
|
||||
"구분": ORIGIN,
|
||||
"상세구분": str(hp.get("파일") or ""),
|
||||
"본뜬": {
|
||||
"층": ORIGIN,
|
||||
"키": mark(hp["키"], no),
|
||||
"파일": hp.get("파일"),
|
||||
"원본": list(refs.here()),
|
||||
"확인": {"누가": who, "때": stamp},
|
||||
},
|
||||
"바탕": None,
|
||||
"입력값": {},
|
||||
"본뜬": {"층": ORIGIN, "키": mark(hp["키"], no), "파일": hp.get("파일"), **sure, **keep},
|
||||
"바탕": plan["바탕"],
|
||||
"입력값": plan["입력값"],
|
||||
"열린입력": [],
|
||||
"고름": {},
|
||||
"자재표시": {},
|
||||
"덮개": {},
|
||||
"더한줄": [{"고정": fx, "이름": name, "단위": unit, "수량": 1}],
|
||||
"뺀줄": [],
|
||||
**{k: plan[k] for k in ("자재표시", "덮개", "더한줄", "뺀줄")},
|
||||
}
|
||||
|
||||
|
||||
def build(folder: Path, key: str, who: Any = None) -> dict[str, Any]:
|
||||
"""[단가산출 만들기] — 산근 블록마다 바탕 없는 단가산출 →
|
||||
`{블록, 만듦 [{번, 이름, 키}], 그대로}`."""
|
||||
hp = hopyo_store.read(folder, key)
|
||||
blocks = {int(b["번"]): b for b in hp.get("단산") or [] if b.get("번") is not None}
|
||||
calls: dict[int, dict] = {}
|
||||
for h in hp.get("호표") or []:
|
||||
for line in h.get("줄") or []:
|
||||
ref = line.get("참조") or {}
|
||||
no = int(ref.get("번") or 0) if ref.get("종류") == "단산" else 0
|
||||
if no in blocks and no not in calls:
|
||||
calls[no] = {**line, "_호표": h.get("번")}
|
||||
have, stamp = made(folder, key), utc_now_text()
|
||||
out: dict[str, Any] = {"블록": len(calls), "만듦": [], "그대로": 0}
|
||||
for no in sorted(calls):
|
||||
if mark(key, no) in have:
|
||||
def _alias(line: dict, plan: dict) -> dict[str, Any]:
|
||||
origin = {"품명": line.get("이름"), "규격": line.get("규격"), "단위": line.get("단위")}
|
||||
clauses = (line.get("근거") or {}).get("출처") or []
|
||||
pair = {"로직": plan["바탕"]["로직"], "입력값": plan["입력값"], "배": 1, "몫": None}
|
||||
return {
|
||||
"원래": {k: str(v or "") for k, v in origin.items()},
|
||||
"갈래": "짝",
|
||||
"짝": [pair],
|
||||
"엑셀조항": clauses if isinstance(clauses, list) else [],
|
||||
}
|
||||
|
||||
|
||||
class Maker:
|
||||
"""한 번 만들기 — 파일 · 맞춤셈 · 이 층 SK 를 한 번 읽음(`source` = 파일 층 · 쓰기 = `store.root()`)."""
|
||||
|
||||
def __init__(self, source: Path, key: str, who: Any = None) -> None:
|
||||
self.source, self.key, self.who = Path(source), key, who
|
||||
self.hp = hopyo_store.read(source, key)
|
||||
self.blocks, self.first = calls(self.hp)
|
||||
got = fit.result(source, key) or {}
|
||||
self.cache, self.done = got.get("블록") or {}, bool(got.get("끝"))
|
||||
self.here = refs.where(source)
|
||||
self.whole = m01.loaded()[1]
|
||||
self.sure = {"원본": list(self.here), "확인": {"누가": who, "때": utc_now_text()}}
|
||||
|
||||
def plan(self, no: int, pick: dict | None = None) -> dict[str, Any]:
|
||||
line, block = self.first[no], self.blocks[no]
|
||||
entry = self.cache.get(fit.unit_key(line))
|
||||
if entry is None and not pick:
|
||||
entry = fit.compute(block, line)
|
||||
return skpatch.plan(entry, block, line, self.whole, pick)
|
||||
|
||||
def write(self, no: int, plan: dict, have: str | None) -> str:
|
||||
doc = _doc(self.first[no], self.blocks[no], no, self.hp, plan, self.sure)
|
||||
if have:
|
||||
raw = (store.root() / KIND / f"{have}.json").read_bytes()
|
||||
store.write(KIND, have, store.version_of(raw), {**doc, "키": have})
|
||||
return have
|
||||
return refs.create(KIND, doc)["이름"]
|
||||
|
||||
|
||||
def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[str, Any]:
|
||||
"""[단가산출 만들기] — 산근 블록마다 기초 · SK · 고름 대기 → `{블록, 만듦 [{번, 이름, 키, 갈래}],
|
||||
그대로, 기초, SK, 고름대기 [{번, 이름, 단위, 까닭, 후보}], 별칭}` · `picks` = 고른 기초."""
|
||||
chosen = _picks(picks)
|
||||
maker = Maker(folder, key, who)
|
||||
bad = sorted(set(chosen) - set(maker.first))
|
||||
if bad:
|
||||
raise _bad(f"없는 블록 · 부르는 줄 없음 「단산{bad[0]}」", 404)
|
||||
if not maker.done and set(maker.first) - set(chosen):
|
||||
fit.ensure(folder, key)
|
||||
raise _bad("맞춤 셈 중 — 끝나면 다시 누름", 409)
|
||||
have = made(folder, key, maker.here)
|
||||
out: dict[str, Any] = {"블록": len(maker.first), "만듦": [], "그대로": 0, "고름대기": []}
|
||||
out.update({skpatch.BASE: 0, skpatch.SK: 0})
|
||||
books, alias = ledger.books(folder), {}
|
||||
for no in sorted(maker.first):
|
||||
old = have.get(mark(key, no))
|
||||
if old and no not in chosen:
|
||||
out["그대로"] += 1
|
||||
continue
|
||||
doc = _doc(calls[no], blocks[no], no, hp, stamp, who)
|
||||
got = refs.create(KIND, doc)
|
||||
out["만듦"].append({"번": no, "이름": doc["이름"], "키": got["이름"]})
|
||||
plan = maker.plan(no, chosen.get(no))
|
||||
line = maker.first[no]
|
||||
if plan["갈래"] == skpatch.WAIT:
|
||||
head = {"번": no, "이름": maker.blocks[no].get("이름") or line.get("이름")}
|
||||
out["고름대기"].append({**head, "단위": line.get("단위"), **plan})
|
||||
continue
|
||||
got = maker.write(no, plan, old)
|
||||
out[plan["갈래"]] += 1
|
||||
name = maker.blocks[no].get("이름") or line.get("이름")
|
||||
out["만듦"].append({"번": no, "이름": name, "키": got, "갈래": plan["갈래"]})
|
||||
found = ledger.recall(line, folder, books)
|
||||
if no in chosen or found is None or found.get("규격다름"):
|
||||
alias[ledger.key(line)] = _alias(line, plan)
|
||||
out["별칭"] = ledger.write(folder, alias, who, maker.hp.get("파일") or None)
|
||||
return out
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
"""M02 실무 단가산출(산근 블록) → 단가산출(SK) 꼴(PLAN 729-1 · 요구서 내역19 1장 7 · 3장 6 ·
|
||||
분석 19-B D-1).
|
||||
|
||||
블록 맞춤셈 한 칸(`M02_Hopyo_Verdict.block` 꼴 · 캐시 `M02_Hopyo_Fit.result`) → 갈래 셋:
|
||||
기초 = 판정 맞음(또는 견줘 차이 없음) — 바탕 = 1위 기초 · 입력값 = 옵션 거꾸로 찾은 값 · 덮개 없음
|
||||
SK = 다른 점 — 바탕 = 1위 기초(조합 둘째부터 = 더한줄 로직 · 수량 = 몫) + 차이 = 엑셀 품 그대로
|
||||
인력 = 직종마다 덮개 qty(줄 하나) · 배율(여럿) · 엑셀만 직종 = 더한줄 인력 · 우리만 = 뺀줄
|
||||
장비 = 시간 같은 같은 갈래 줄은 그대로 · 같은 갈래 남은 줄 = 덮개 qty · 엑셀만 = 더한줄
|
||||
기계경비 로직(굴착기 GC000267 · 그 밖 GC000268 · 기계 = 옵션 거꾸로 찾기) · 남은 우리 = 뺀줄
|
||||
자재 = 같은 이름 우리 줄 수량 덮개 · 옮기지 못한 차이 = `못옮김`(버리지 않고 본뜬에 적음)
|
||||
고름 대기 = 후보 · 숫자 없음 — SK 안 만듦 · 후보 다섯(사용자가 고른 기초 `pick` = 견줘 SK · 기초).
|
||||
cid = `kind:키#n`(첫 기초 바탕 줄만 · `M02_Sangeun_Doc.line_ids`) · 더한줄 · 덮개 qty = 바탕 행 몫
|
||||
(1 단위 품 × 바탕 결과단위 앞 수 · 셈이 ÷ 배) · 더한줄 로직 수량 = 몫 × 바탕 배 ÷ 그 기초 배.
|
||||
품 견줌 = 우리 셈(`M02_Sangeun_Calc.run` 줄 수량 ÷ 배) · 같음 = `Option.near`(0.5 % + 엑셀 자릿수 반올림).
|
||||
우리만 자재 줄은 그대로(판정 견줌도 안 봄 · 엑셀은 자재를 일위대가 줄로 따로 적는 일이 많음).
|
||||
서버 단독 · 읽기만(쓰기 = `M02_Hopyo_Sangeun`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Combine as combine
|
||||
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Hopyo_Option as option
|
||||
from M02_MasterTemplete import M02_Hopyo_Ours as ours
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import mtypes, specnums
|
||||
|
||||
BASE, SK, WAIT = "기초", "SK", "고름 대기"
|
||||
KEYS = ("덮개", "자재표시", "더한줄", "뺀줄", "못옮김")
|
||||
TINY = 1e-9
|
||||
_ADD = re.compile(r"^add:#(\d+)$")
|
||||
|
||||
|
||||
def _near(a: float, b: float) -> bool:
|
||||
return abs(a - b) <= TINY or option.near(a, b, fit.TOL[0], option.ru(a))
|
||||
|
||||
|
||||
def _num(x: float) -> int | float:
|
||||
got = Decimal(repr(float(x))).normalize()
|
||||
return int(got) if got == got.to_integral_value() else float(got)
|
||||
|
||||
|
||||
# ── 우리 셈 → 1 단위 품 ──────────────────────────────────────────────
|
||||
def _types(sub: str, item: dict, args: str | None, given: dict, auto: dict) -> tuple[set, set]:
|
||||
"""기계경비 줄 갈래 · 규격 — `M02_Hopyo_Ours._engine` 과 같은 읽기(중간값 없이)."""
|
||||
ts = set(ours.FIXED_MACH.get(sub, set())) | mtypes(item.get("이름"))
|
||||
sp = set(specnums(item.get("이름")))
|
||||
for v in ours._args(args).values():
|
||||
lit = v.strip("'\"")
|
||||
if not ours.EQ_CODE.match(lit):
|
||||
lit = str(given.get(lit) or auto.get(lit) or "")
|
||||
if ours.EQ_CODE.match(lit):
|
||||
a, b = ours.eq_of(lit)
|
||||
ts, sp = ts | a, sp | b
|
||||
return ts, sp
|
||||
|
||||
|
||||
def parts(doc: dict, whole: Any) -> list[dict[str, Any]]:
|
||||
"""SK 문서 → 줄마다 `{cid, 갈래 인력|장비|자재|기초|그밖, 이름 · 종류 · 규격, 품(1 단위)}` ·
|
||||
더한줄 기초 = 그 기초 1 단위 품 × 줄 몫(`L` · `X`)."""
|
||||
got = calc.resolve(doc, whole)
|
||||
items = got["줄"]["호표"]
|
||||
ran = calc.run(doc, whole)
|
||||
times = Decimal(str(ran["배"]))
|
||||
labor = ours._labor_names()
|
||||
auto = {}
|
||||
if (doc.get("바탕") or {}).get("로직"):
|
||||
auto = option.m01.mau.values(whole, whole.logic(doc["바탕"]["로직"]))
|
||||
qty = {x["cid"]: x.get("수량") for x in ran.get("줄") or [] if x.get("cid")}
|
||||
out = []
|
||||
for item, cid in zip(items, got["정체"]): # 별도계상 재료 = 셈 줄에서 빠짐 → 품 None
|
||||
raw = qty.get(cid)
|
||||
one: dict[str, Any] = {"cid": cid, "갈래": "그밖"}
|
||||
one["품"] = None if raw is None else float(Decimal(str(raw)) / times)
|
||||
kind, found = item.get("종류"), ours._SUB.match(str(item.get("요소") or ""))
|
||||
if kind == "인력":
|
||||
one.update(갈래="인력", 이름=labor.get(str(item.get("요소")), str(item.get("이름"))))
|
||||
elif kind == "재료":
|
||||
one.update(갈래="자재", 이름=str(item.get("이름") or ""))
|
||||
elif kind == "로직" and found:
|
||||
sub = found.group(1)
|
||||
if ours.is_mach_logic(whole.logic(sub)):
|
||||
ts, sp = _types(sub, item, found.group(2), got["입력값"], auto)
|
||||
one.update(갈래="장비", 종류=ts, 규격=sp)
|
||||
elif hit := _ADD.match(cid):
|
||||
given = (doc.get("더한줄") or [])[int(hit[1])].get("입력값") or {}
|
||||
v = ours.run(sub, given) or {"L": {}, "X": []}
|
||||
k = (one["품"] or 0.0) * ours.times_of(whole.logic(sub))
|
||||
one.update(갈래="기초", 로직=sub, L={t: q * k for t, q in v["L"].items()})
|
||||
one["X"] = [(set(a), set(b), q * k) for a, b, q in v["X"]]
|
||||
out.append(one)
|
||||
return out
|
||||
|
||||
|
||||
def measure(doc: dict, whole: Any) -> dict[str, Any]:
|
||||
"""SK 1 단위 품 — `{인력 {직종: 품}, 장비 [{갈래, 시간}], 자재 {이름: 수량}}`(시험 · 견줌)."""
|
||||
labor: dict[str, float] = {}
|
||||
machines, mats = [], {}
|
||||
for one in parts(doc, whole):
|
||||
if one["갈래"] == "인력" and one["품"]:
|
||||
labor[one["이름"]] = labor.get(one["이름"], 0.0) + one["품"]
|
||||
elif one["갈래"] == "장비" and one["품"]:
|
||||
machines.append({"갈래": sorted(one["종류"]), "시간": one["품"]})
|
||||
elif one["갈래"] == "자재" and one["품"]:
|
||||
mats[one["이름"]] = mats.get(one["이름"], 0.0) + one["품"]
|
||||
elif one["갈래"] == "기초":
|
||||
for t, q in one["L"].items():
|
||||
labor[t] = labor.get(t, 0.0) + q
|
||||
machines += [{"갈래": sorted(a), "시간": q} for a, _, q in one["X"] if q]
|
||||
return {"인력": labor, "장비": machines, "자재": mats}
|
||||
|
||||
|
||||
# ── 차이 → 덮개 · 더한줄 · 뺀줄 ──────────────────────────────────────
|
||||
class _Plan:
|
||||
def __init__(self, doc: dict, whole: Any) -> None:
|
||||
self.doc, self.whole = doc, whole
|
||||
self.tb = float(calc.times_of(whole.logic(doc["바탕"]["로직"])))
|
||||
self.lines = parts(doc, whole)
|
||||
self.lost: list[str] = []
|
||||
self.lb = {name: key for key, name in ours._labor_names().items()}
|
||||
|
||||
def cover(self, cid: str, **one: float) -> None:
|
||||
self.doc["덮개"][cid] = {k: _num(v) for k, v in one.items()}
|
||||
|
||||
def drop(self, cid: str) -> None:
|
||||
if cid not in self.doc["뺀줄"]:
|
||||
self.doc["뺀줄"].append(cid)
|
||||
|
||||
def add(self, one: dict[str, Any]) -> None:
|
||||
self.doc["더한줄"].append(one)
|
||||
|
||||
def labor(self, excel: dict[str, float]) -> None:
|
||||
mine = [x for x in self.lines if x["갈래"] == "인력" and not x["cid"].startswith("add:")]
|
||||
mine = [{**x, "품": x["품"] or 0.0} for x in mine]
|
||||
extra: dict[str, float] = {}
|
||||
for x in self.lines:
|
||||
for t, q in (x.get("L") or {}).items():
|
||||
extra[t] = extra.get(t, 0.0) + q
|
||||
for t in sorted(set(excel) | {x["이름"] for x in mine if x["품"]} | set(extra)):
|
||||
a, rows = excel.get(t, 0.0), [x for x in mine if x["이름"] == t]
|
||||
b, f = sum(x["품"] for x in rows), extra.get(t, 0.0)
|
||||
if a and _near(a, b + f):
|
||||
continue
|
||||
want = a - f
|
||||
if want <= TINY:
|
||||
for x in rows:
|
||||
self.drop(x["cid"])
|
||||
if want < -TINY:
|
||||
self.lost.append(f"{t} 엑셀 {a:.6g} · 더한 기초만 {f:.6g}")
|
||||
elif not rows:
|
||||
if t in self.lb:
|
||||
unit = "인"
|
||||
self.add({"종류": "인력", "요소": self.lb[t], "이름": t, "단위": unit,
|
||||
"수량": _num(want * self.tb)}) # fmt: skip
|
||||
else:
|
||||
self.lost.append(f"{t} 엑셀 {a:.6g} — 우리 인력에 없음")
|
||||
elif len(rows) == 1 or not b:
|
||||
self.cover(rows[0]["cid"], qty=want * self.tb)
|
||||
for x in rows[1:]:
|
||||
self.drop(x["cid"])
|
||||
else:
|
||||
for x in rows:
|
||||
self.cover(x["cid"], 배율=want / b)
|
||||
|
||||
def machines(self, excel: list[dict[str, Any]], line: dict[str, Any]) -> None:
|
||||
mine = [x for x in self.lines if x["갈래"] == "장비" and x["품"]]
|
||||
fixed = [(a, q) for x in self.lines for a, _, q in x.get("X") or [] if q]
|
||||
used, rest = set(), []
|
||||
for m in excel:
|
||||
kinds, h = set(m["갈래"]), m["시간"]
|
||||
hit = next((x for x in mine if x["cid"] not in used and x["종류"] & kinds
|
||||
and _near(h, x["품"])), None) # fmt: skip
|
||||
if hit is not None:
|
||||
used.add(hit["cid"])
|
||||
continue
|
||||
at = next((i for i, (a, q) in enumerate(fixed) if a & kinds and _near(h, q)), None)
|
||||
if at is not None:
|
||||
fixed.pop(at)
|
||||
continue
|
||||
rest.append(m)
|
||||
for m in rest:
|
||||
kinds, sp = set(m["갈래"]), set(m.get("규격") or [])
|
||||
same = [x for x in mine if x["cid"] not in used and x["종류"] & kinds]
|
||||
same.sort(key=lambda x: not (sp and sp & x["규격"]))
|
||||
if same:
|
||||
used.add(same[0]["cid"])
|
||||
self.cover(same[0]["cid"], qty=m["시간"] * self.tb)
|
||||
else:
|
||||
self._add_machine(m, line)
|
||||
for x in mine:
|
||||
if x["cid"] not in used:
|
||||
self.drop(x["cid"])
|
||||
self.lost += [f"우리만 장비 {'·'.join(sorted(a))} — 더한 기초 안" for a, _ in fixed]
|
||||
|
||||
def _add_machine(self, m: dict[str, Any], line: dict[str, Any]) -> None:
|
||||
"""엑셀만 장비 → 기계경비 로직 줄(기계 = 그 갈래 · 규격 옵션 거꾸로 찾기)."""
|
||||
fp = finger.empty(line)
|
||||
fp["장비"] = [{**m, "시간": 1.0, "밑시간": 1.0, "율": []}]
|
||||
kinds = set(m["갈래"])
|
||||
first = next((c for k, c in combine.MACHINE.items() if k in kinds), combine.MACHINE_ANY)
|
||||
for code in [first, *sorted(combine.MACHINE_SET - {first})]:
|
||||
got = option.find(code, fp, [])
|
||||
if got["수준"] <= 1:
|
||||
name = " ".join([*m["갈래"], *(f"{x:g}" for x in m.get("규격") or [])])
|
||||
self.add({"종류": "로직", "로직": code, "입력값": got["입력"], "이름": name,
|
||||
"단위": "시간", "수량": _num(m["시간"] * self.tb)}) # fmt: skip
|
||||
return
|
||||
self.lost.append(f"엑셀만 장비 {'·'.join(m['갈래'])} {m['시간']:.6g} 시간 — 기계 못 찾음")
|
||||
|
||||
def materials(self, excel: list[dict[str, Any]]) -> None:
|
||||
mine = [x for x in self.lines if x["갈래"] == "자재" and not x["cid"].startswith("add:")]
|
||||
for m in excel:
|
||||
words = option.mat_words(m["이름"])
|
||||
rows = [x for x in mine if option._named(words, x["이름"])]
|
||||
if rows and all(x["품"] is None for x in rows): # 우리 = 별도계상 · 엑셀 = 블록 안
|
||||
self.doc["자재표시"][rows[0]["cid"]] = {"별도계상": "직접계산"}
|
||||
self.cover(rows[0]["cid"], qty=m["품"] * self.tb)
|
||||
continue
|
||||
rows = [x for x in rows if x["품"] is not None]
|
||||
if rows and _near(m["품"], sum(x["품"] for x in rows)):
|
||||
continue
|
||||
if rows:
|
||||
self.cover(rows[0]["cid"], qty=m["품"] * self.tb)
|
||||
for x in rows[1:]:
|
||||
self.drop(x["cid"])
|
||||
else:
|
||||
self.lost.append(f"엑셀만 자재 {m['이름']} {m['품']:.6g}")
|
||||
|
||||
|
||||
def _extras(entry: dict, code: str, whole: Any, tb: float) -> list[dict[str, Any]]:
|
||||
"""조합 둘째부터(체크된 로직) = 더한줄 로직 — 수량 = 몫 × 바탕 배 ÷ 그 기초 배."""
|
||||
out = []
|
||||
for c in entry.get("조합") or []:
|
||||
if not c.get("체크") or c["로직"] == code:
|
||||
continue
|
||||
given = ((entry.get("옵션") or {}).get(c["로직"]) or {}).get("입력") or {}
|
||||
times = ours.times_of(whole.logic(c["로직"]))
|
||||
qty = float(c.get("몫") or 1) * tb / times
|
||||
out.append({"종류": "로직", "로직": c["로직"], "입력값": given, "수량": _num(qty)})
|
||||
return out
|
||||
|
||||
|
||||
def _wait(entry: dict) -> dict[str, Any]:
|
||||
keep = ("로직", "이름", "단위", "판정", "글")
|
||||
cands = [{k: c.get(k) for k in keep} for c in (entry.get("후보") or [])[: fit.TOP_OPT]]
|
||||
why = (entry.get("판정") or {}).get("글") or "후보 없음"
|
||||
return {"갈래": WAIT, "까닭": f"맞는 기초 없음 — {why}", "후보": cands}
|
||||
|
||||
|
||||
def plan(
|
||||
entry: dict | None,
|
||||
block: dict | None,
|
||||
line: dict[str, Any],
|
||||
whole: Any,
|
||||
pick: dict | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""블록 한 칸 → `{갈래 기초|SK|고름 대기, 바탕, 입력값, 덮개, 자재표시, 더한줄, 뺀줄, 못옮김, 엑셀품,
|
||||
후보?}` ·
|
||||
`pick` = 사용자가 고른 기초 `{로직, 입력값?}`(옵션 없으면 거꾸로 찾음 · 늘 견줌)."""
|
||||
entry = entry or {}
|
||||
state = (entry.get("판정") or {}).get("상태")
|
||||
efp = finger.excel(block, line)
|
||||
if pick:
|
||||
code = str(pick["로직"])
|
||||
given = pick.get("입력값")
|
||||
if given is None:
|
||||
given = option.find(code, efp, (block or {}).get("글") or [])["입력"]
|
||||
elif state in ("맞음", "다른 점") and entry.get("차례"):
|
||||
code = entry["차례"][0]
|
||||
given = ((entry.get("옵션") or {}).get(code) or {}).get("입력") or {}
|
||||
else:
|
||||
return _wait(entry)
|
||||
doc = {"키": "SK-000000", "이름": "견줌", "단위": line.get("단위") or "식",
|
||||
"바탕": {"로직": code}, "입력값": dict(given), "덮개": {}, "뺀줄": [], "자재표시": {}} # fmt: skip
|
||||
tb = float(calc.times_of(whole.logic(code)))
|
||||
doc["더한줄"] = [] if pick else _extras(entry, code, whole, tb)
|
||||
from M02_MasterTemplete import M02_Hopyo_Verdict as verdict # 판정이 Fit 을 부름 — 늦게 읽음
|
||||
|
||||
excel = verdict.excel_qty(efp)
|
||||
head = {"바탕": doc["바탕"], "입력값": doc["입력값"], "엑셀품": excel}
|
||||
if state == "맞음" and not pick and not doc["더한줄"]:
|
||||
return {**head, "갈래": BASE, **dict.fromkeys(KEYS[:2], {}), **dict.fromkeys(KEYS[2:], [])}
|
||||
work = _Plan(doc, whole)
|
||||
work.labor({t: x["품"] for t, x in efp["인력"].items() if x.get("품")})
|
||||
work.machines([m for m in efp["장비"] if m.get("시간")], line)
|
||||
work.materials([m for m in efp["자재"] if m.get("품")])
|
||||
out = {k: doc[k] for k in KEYS[:4]}
|
||||
changed = any(out.values())
|
||||
return {**head, **out, "갈래": SK if changed else BASE, "못옮김": work.lost}
|
||||
Reference in New Issue
Block a user