- 809_1 이름 = 진짜 흠 + 낡은 기대 — 읽기기(812-1)가 지은 율 줄 이름 「율」이 엑셀 글 머리로 안 바뀜 → `Import_Shape._MADE` 에 율 더함 · 합계 = 차림 글줄 · 기계 = 한 줄(이름 · 규격 칸) 은 낡은 기대 고침 · 금액 15,935 · 336 · 723 · 16,994 그대로 - 812_9 별칭 = 낡은 기대 — 815-2 가 아스팔트페이버 별칭을 마스터 기계 줄 칸으로 옮겨 「넣기 전 못 찾음」 이 안 맞음 · 시험은 마스터 칸으로 봄 · 부르는 곳 없는 `M02_Machine_Find.learn`(옛 장부 쓰기) · `by_code` · `same_kind`(걷은 연결 찾기 몫) 지움 - 804_2 hourly = 낡은 기대 — 814-1 이 GC000268 목록을 117 → 209 로 늘려 겹친 116 대 고름이 GC000269 로 뒤집힘(116 대 모두 원 단위 같음) · 같은 로직 줄만 대던 시험 → 같은 갈래 식(부착 · 손료만 · 조종원) 줄 모두 댐 - 진짜 흠(브레인 갈래 가) — GC000272 「조종원 없는 기종」 목록에 기계표 조종원 1 인 9 대가 겹쳐 목록 길이 차례로 고름 → `logic_of` = 기계표 조종원 > 0 이면 조종원 호표 든 식 먼저 · 불러오기 `hourly_link` 도 같은 길 · 바뀐 4 대 = 진동롤러(핸드가이드식) 0.7ton 6,557 → 41,969 · 래머 80kg 1,959 → 23,708 · 플레이트 콤팩터 1.5ton 2,855 → 24,604 · 발전기 100kW 45,535 → 67,284(M01 문서 금액 그대로) · 시험에 9 대 굳힘 - 778_5 기계 표 = 진짜 흠 — 812-9 찾기가 호표 기계 글 「운 반 : 덤프트럭(4.5Ton)」 · 「…[hr] : 90%」 · 「(90%)」 머리 꼬리를 안 뗌 → `Hopyo_Fixed._eqs` 쌍점 조각 · 율 꼬리 뗌 · 이어짐 187 · 고를 것 1 · 못 이음 2(조합 한 줄 · 괄호 깨짐) · 모두 이음 블록 161 - 721 단추 자리 = 낡은 기대(808-1 [단가 갱신][재생성] tools 더함) - 815-1 보고 요약에 「합친 뒤」 열 — 기계 이음 95.7 % · 우리 값 원파일 1,891 · 0 원 줄 블록 147 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
215 lines
11 KiB
Python
215 lines
11 KiB
Python
"""M02 실무자료 줄 → 고정 줄(PLAN 705-3 · 요구서 1장 · 4장 2 · 3 · 7) — 엑셀 숫자 그대로 · 빠짐 0.
|
|
|
|
맞춤 확인 안 한 줄 · 노임 · % · 「우리에 없음」 줄 = 빠진 줄 대신 고정 줄(705-1 그릇 `M02_Labor_Fixed`).
|
|
숫자는 서버가 실무자료 파일에서 읽음(브라우저 값 안 받음).
|
|
단가 = 엑셀 단가 노무비 · 재료비 · 경비(빈 칸 0) · 합계만 있는 줄 = 재료비(4장 3) ·
|
|
수량 없음 = 수량 1 · 단가 = 엑셀 금액(「금액만」).
|
|
이음 = 노임(LB) · 자재(MT) = 이름 · 규격 · 단위 · 단가 다 같은 것만 · `by_name`(729-2 불러오기 길 ·
|
|
요구서 내역19 3장 7) = 노임 = 이름 같은 LB 하나(값 = 지금 단가 책) · 기계 = 이름(갈래) · 규격 수 같은 EQ
|
|
하나 · 못 이은 짝 = `후보` 수만 · 사용자가 고른 이음(`force` {노임|기계|자재: 키} · {비율: 기준}) = 그대로.
|
|
% 줄 = 비율 줄 `비율 {기준, 비목}` — 엑셀 밑수(단가 계) = 다른 줄 금액[기준] 합일 때만 ·
|
|
아니면(주재료비 등) 수량 1 · 「식」 · 단가 = 엑셀 금액.
|
|
같은 이름 · 규격 · 단위 둘째부터 = 이름 끝 「 (2)」(줄 가름 · 705-1 400 피함).
|
|
출처 = `{파일, 호표, 줄}`(줄 = 엑셀 줄 차례 0부터 · [품셈으로 바꾸기] 짝).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from decimal import Decimal
|
|
from typing import Any
|
|
|
|
from M01_MasterData import M01_MasterData_Store as m01
|
|
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
|
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
|
from M02_MasterTemplete import M02_Hopyo_Shares as shares
|
|
from M02_MasterTemplete import M02_Labor_Doc as labor
|
|
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
|
from M02_MasterTemplete import M02_Machine_Find as mfind
|
|
|
|
COSTS = ("노무비", "재료비", "경비")
|
|
_BASE = re.compile(r"(?<!주)(노무비|재료비|경비|인건비)\s*의")
|
|
GAP = Decimal(1) # 밑수 · 단가 같음 판정 끝수(원)
|
|
_RATE = re.compile(r"\s*\(\s*\d+(?:\.\d+)?\s*%\s*\)\s*$") # 「…[hr] (90%)」 꼬리
|
|
|
|
|
|
_d, _plain = fixed._num, fixed._plain
|
|
|
|
|
|
def kind_of(line: dict[str, Any]) -> str:
|
|
"""저장 줄 갈래 + 「노임」(단위 인 + 우리 직종 · 옛 파일도 · 새 파일은 `Excel.kind_of` 가 읽을 때 정함)."""
|
|
kind = str(line.get("갈래") or "")
|
|
unit = re.sub(r"\s+", "", str(line.get("단위") or ""))
|
|
if kind in ("기타", "자재", "노임") and unit == "인" and shares.trade(line.get("이름")):
|
|
return "노임"
|
|
return kind
|
|
|
|
|
|
mm_plain = m01.cm.mf.mp.mm.plain
|
|
|
|
|
|
def _parts(money: Any) -> dict[str, Decimal]:
|
|
"""엑셀 단가 · 금액 → 세 비목(빈 칸 0) · 합계만 있으면 재료비(4장 3)."""
|
|
money = money if isinstance(money, dict) else {}
|
|
out = {k: _d(money.get(k)) or Decimal(0) for k in COSTS}
|
|
total = _d(money.get("계"))
|
|
if total and not any(_d(money.get(k)) is not None for k in COSTS):
|
|
out["재료비"] = total
|
|
return out
|
|
|
|
|
|
def _ratio(line: dict[str, Any], others: list[dict[str, Any]]) -> dict[str, str] | None:
|
|
"""% 줄 → `{기준, 비목}` — 밑수 = 다른 줄 엑셀 금액[기준] 합(끝수 1 원 안) · 금액 비목 하나."""
|
|
hit = _BASE.search(f"{line.get('규격') or ''} {line.get('이름') or ''}")
|
|
under = _d((line.get("단가") or {}).get("계"))
|
|
paid = [k for k, v in _parts(line.get("금액")).items() if v]
|
|
if not hit or under is None or len(paid) != 1:
|
|
return None
|
|
base = "노무비" if hit.group(1) == "인건비" else hit.group(1)
|
|
total = sum((_parts(x.get("금액"))[base] for x in others), Decimal(0))
|
|
return {"기준": base, "비목": paid[0]} if abs(total - under) <= GAP else None
|
|
|
|
|
|
def linked(one: dict[str, Any], decided: bool = False) -> bool:
|
|
"""일위대가 줄 하나가 M01 · 기초 · SK 에 이어짐 — 로직 · 참조 · 비율 · 이음 있는 고정 줄 ·
|
|
`decided` = 사용자가 「우리에 없음」으로 고른 줄."""
|
|
fx = one.get("고정") if isinstance(one, dict) else None
|
|
if not isinstance(fx, dict):
|
|
return True
|
|
return decided or bool(fx.get("비율")) or any((fx.get("이음") or {}).values())
|
|
|
|
|
|
class Linker:
|
|
"""이음 — 노임(LB 이름) · 기계(EQ 갈래 · 규격) · 자재(이름 · 규격 같은 한 품목 · 단위 · 단가 같음)."""
|
|
|
|
def __init__(self, by_name: bool = False) -> None:
|
|
self.by_name = (
|
|
by_name # 729-2 불러오기 길만 — 붙여넣기 · B09 통째 = 단가 같을 때만(엑셀 합계 그대로)
|
|
)
|
|
self.whole = m01.loaded()[1]
|
|
self.got = hm.targets()
|
|
self.labor: dict[str, list[tuple[str, Decimal | None]]] = {}
|
|
for row in self.whole.numbers.get("LB", {}).values():
|
|
pair = (str(row.get("키")), _d(row.get("값")))
|
|
self.labor.setdefault(str(row.get("이름") or ""), []).append(pair)
|
|
self.machines = [
|
|
(str(row.get("키")), mm_plain(row.get("이름")), mm_plain(row.get("규격")))
|
|
for row in self.whole.numbers.get("EQ", {}).values()
|
|
]
|
|
self.near = 0 # 못 이은 이음 후보 수
|
|
|
|
def link(self, line: dict[str, Any], price: dict[str, Decimal]) -> dict[str, str | None]:
|
|
out: dict[str, str | None] = {"노임": None, "자재": None, "기계": None}
|
|
kind = kind_of(line)
|
|
if kind == "노임":
|
|
pairs = self.labor.get(shares.trade(line.get("이름")) or "", [])
|
|
one = [code for code, _ in pairs]
|
|
same = one[0] if self.by_name and len(one) == 1 else None
|
|
out["노임"] = same or self._same(pairs, price["노무비"])
|
|
elif kind == "기계" and self.by_name:
|
|
out["기계"] = self._machine(line)
|
|
elif kind in hm.MATERIAL_KINDS and price["재료비"]:
|
|
out["자재"] = self._same(self._mats(line), price["재료비"])
|
|
return out
|
|
|
|
def _eqs(self, line: dict[str, Any]) -> list[str]:
|
|
"""기계 줄 → EQ 후보 — 이름 · 규격 글 같음 › 기계 찾기(812-9 `M02_Machine_Find`)."""
|
|
name, spec = mm_plain(line.get("이름")), mm_plain(line.get("규격"))
|
|
same = [c for c, n, s in self.machines if n == name and s == spec]
|
|
if len(same) != 1:
|
|
text = str(line.get("이름") or "")
|
|
# 엑셀 호표 글 「운 반 : 덤프트럭(4.5Ton)」 · 「굴착기(…)[hr] : 90%」 = 쌍점 조각마다
|
|
parts = [text, *(text.split(":") if ":" in text else [])]
|
|
tries = [mfind.find(_RATE.sub("", p).strip(), line.get("규격")) for p in parts]
|
|
got = next((g for g in tries if g["판정"] in (mfind.LINKED, mfind.ALIAS)), None)
|
|
got = got or next((g for g in tries if g["판정"] != mfind.NONE), tries[0])
|
|
same = [] if got["판정"] == mfind.NONE else [c["키"] for c in got["후보"]]
|
|
return same
|
|
|
|
def candidates(self, line: dict[str, Any]) -> list[str]:
|
|
"""못 이은 줄의 이음 후보 키(809-1 [수정] 보기 「후보 N」) — 노임 같은 직종 · 기계 EQ · 같은 자재."""
|
|
kind = kind_of(line)
|
|
if kind == "노임":
|
|
return [code for code, _ in self.labor.get(shares.trade(line.get("이름")) or "", [])]
|
|
if kind == "기계":
|
|
return self._eqs(line)
|
|
return [code for code, _ in self._mats(line)] if kind in hm.MATERIAL_KINDS else []
|
|
|
|
def _machine(self, line: dict[str, Any]) -> str | None:
|
|
"""기계 줄 → EQ 하나(하나일 때만)."""
|
|
same = self._eqs(line)
|
|
if len(same) == 1:
|
|
return same[0]
|
|
self.near += bool(same)
|
|
return None
|
|
|
|
def _same(self, pairs: list[tuple[str, Decimal | None]], want: Decimal) -> str | None:
|
|
same = [code for code, value in pairs if value is not None and abs(value - want) < GAP]
|
|
if len(same) == 1:
|
|
return same[0]
|
|
self.near += bool(pairs)
|
|
return None
|
|
|
|
def _mats(self, line: dict[str, Any]) -> list[tuple[str, Decimal | None]]:
|
|
me, _ = hmat.clean(line)
|
|
found = hm._exact(me, "자재", self.got)
|
|
if len(found) != 1:
|
|
found = [r["one"] for r in hmat.sure(me)[0]]
|
|
out = []
|
|
for one in found:
|
|
code = one.get("자재")
|
|
if not code or hm.factor(line.get("단위"), one.get("단위"))["배"] != 1:
|
|
continue
|
|
try:
|
|
price = fixed.mm.element(self.whole, code, {}, line.get("단위") or None)[0]
|
|
except fixed.mf.FormulaError:
|
|
price = None
|
|
out.append((code, _d(price)))
|
|
return out
|
|
|
|
|
|
def line_of(
|
|
line: dict[str, Any],
|
|
others: list[dict[str, Any]],
|
|
where: dict[str, Any],
|
|
taken: set[str],
|
|
linker: Linker,
|
|
force: dict[str, str] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""엑셀 줄 하나 → 일위대가 고정 줄 `{고정, 별칭, 메모, 수량}` — `taken` = 이 호표 줄 가름(채워 감) ·
|
|
`force` = 사용자가 고른 이음 `{노임|기계|자재: 키}` · 비율 `{비율: 기준 비목}`(수량 = 율 %)."""
|
|
qty = _d(line.get("수량"))
|
|
spec, unit = str(line.get("규격") or "").strip(), str(line.get("단위") or "").strip()
|
|
rate = _ratio(line, others) if kind_of(line) == "%" else None
|
|
if force and force.get("비율"):
|
|
paid = [k for k, v in _parts(line.get("금액")).items() if v]
|
|
rate = {"기준": force["비율"], "비목": paid[0] if len(paid) == 1 else force["비율"]}
|
|
memo = f"실무 {qty if qty is not None else ''}{unit} 그대로"
|
|
if rate:
|
|
price = dict.fromkeys(COSTS, Decimal(0))
|
|
elif qty is None or kind_of(line) == "%": # 금액만 · 밑수 다른 % 줄
|
|
price, qty, memo = _parts(line.get("금액")), Decimal(1), f"{memo} · 금액만"
|
|
unit = "식" if kind_of(line) == "%" else unit
|
|
else:
|
|
price = _parts(line.get("단가"))
|
|
name = str(line.get("이름") or "").strip() or "이름 없음"
|
|
head, n = name, 1
|
|
while fixed.fixed_id({"이름": name, "규격": spec, "단위": unit}) in taken:
|
|
n += 1
|
|
name = f"{head} ({n})"
|
|
fx = {
|
|
"이름": name,
|
|
"규격": spec,
|
|
"단위": unit,
|
|
"단가": {k: _plain(v) for k, v in price.items()},
|
|
}
|
|
fx["이음"] = dict.fromkeys(fixed.LINKS) if rate else linker.link(line, price)
|
|
if force and not rate:
|
|
fx["이음"] = {k: force.get(k) for k in fixed.LINKS}
|
|
fx["출처"] = {**where, "줄": int(line.get("차례", 0))}
|
|
if rate:
|
|
fx["비율"] = rate
|
|
taken.add(fixed.fixed_id(fx))
|
|
alias = str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None
|
|
return {"고정": fx, "별칭": alias, "메모": memo, "수량": {"손": _plain(qty)}}
|