Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
노임 = LB 이름 글자 묶음 닮음 · 단가 = LB 값
|
||||
기계 = EQ 갈래 겹침 · 규격 수(같음 1 · 겹침 0.5) · 이름 낱말 덤 · 괄호 낱말 어긋남 깎음 ·
|
||||
단가 없음(기계경비는 로직 셈) · `machine` = 하나로 가름(778-1)
|
||||
차례 = 점수(0.1 단위로 접음) › 엑셀 단가에 가까운 것(엑셀 단가 = 블록 글 「이름 단가 × 식 = 금액」 · 모르면 점수만).
|
||||
차례 = 점수(0.1 단위로 접음) › 엑셀 단가에 가까운 것(엑셀 단가 = 블록 단위 줄 단가 · 모르면 점수만).
|
||||
서버 단독 · 읽기만.
|
||||
"""
|
||||
|
||||
@@ -17,10 +17,10 @@ import re
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import mtypes, nfkc, parse_rhs, specnums
|
||||
from M02_MasterTemplete import M02_Hopyo_UnitLine as unit_line
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import mtypes, nfkc, specnums
|
||||
|
||||
TOP = 3
|
||||
POOL = 5
|
||||
@@ -35,16 +35,12 @@ PARTS = ("자동덮개", "부착용", "콤팩터", "습지")
|
||||
|
||||
|
||||
def prices(block: dict[str, Any] | None) -> dict[str, float]:
|
||||
"""블록 글 → `{이름: 엑셀 단가}`(직종 줄 「직종 : 단가 × 식」 · 자재 줄 「이름 단가 × 식 = 금액」)."""
|
||||
"""블록 단위 줄(노무 · 자재 · 값) → `{이름: 엑셀 단가}`(첫 것 · 776-3 — 글 다시 안 풂)."""
|
||||
out: dict[str, float] = {}
|
||||
for raw in (block or {}).get("글") or []:
|
||||
row = str(raw).rstrip()
|
||||
cm = finger._COLON.match(row)
|
||||
mm = None if cm else finger._MAT.match(row)
|
||||
name = (cm or mm).group("name").strip() if (cm or mm) else ""
|
||||
got = parse_rhs((cm or mm).group("rhs")) if name else None
|
||||
if got and got.get("단가"):
|
||||
out.setdefault(re.sub(r"^[0-9가-힣]\.", "", name).strip(), float(got["단가"]))
|
||||
for one in (block or {}).get("항목") or []:
|
||||
price = unit_line.money(one.get("단가")) if one.get("묶음") in ("L", "M", "E") else None
|
||||
if price:
|
||||
out.setdefault(re.sub(r"^[0-9가-힣]\.", "", str(one.get("이름") or "")).strip(), price)
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -18,8 +18,9 @@ STmate 꼴(「일위대가표」 · 「단가산출근거」 시트) = `M02_Hopy
|
||||
어긋남은 글 목록(막지 않음).
|
||||
행(329-1) = 호표 [제N호표 줄, 마지막 구성 줄] · 구성 줄 한 줄(엑셀 1부터) ·
|
||||
근거(330-1) = 단산N참조 줄에 「단가산출」 블록 `{출처, 직종, 행}`(`M02_Hopyo_Basis.read`) ·
|
||||
`단산` = 블록마다 `{번, 이름, 단위, 행, 출처, 직종, 글}`(338 · 글 = 머리 뺀 몸 줄) ·
|
||||
`꼴` = 「엑셀꼴」.
|
||||
`단산` = 블록마다 `{번, 이름, 단위, 행, 출처, 직종, 글, 금액, 항목}`(338 · 글 = 머리 뺀 몸 줄) ·
|
||||
`꼴` = 「엑셀꼴」 · 두 형식 끝에 `UnitLine.settle`(단위 줄 하위 · 재현 · 덮음 · 일위대가 줄
|
||||
투영 · 778-3).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -31,6 +32,7 @@ from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_ReadEst as hopyo_basis # EST 단가산출 어댑터(776-2)
|
||||
from M02_MasterTemplete import M02_Hopyo_Stmate as hopyo_stmate
|
||||
from M02_MasterTemplete import M02_Hopyo_UnitLine as unit_line
|
||||
|
||||
SHEET = "일위대가"
|
||||
SUMMARY = "일위대가총괄표"
|
||||
@@ -300,7 +302,7 @@ def read_book(name: str, data: bytes) -> dict[str, Any]:
|
||||
sheets, xls = _open(name, data, LISTS)
|
||||
_OPENED = (data, sheets, xls) # 격자(`Grid.read`)가 이어 씀 — 한 번 열기
|
||||
if hopyo_stmate.is_stmate(sheets):
|
||||
return hopyo_stmate.read(sheets)
|
||||
return unit_line.settle(hopyo_stmate.read(sheets))
|
||||
rows = sheets.get(SHEET)
|
||||
if rows is None:
|
||||
raise ExcelError(NO_SHEET)
|
||||
@@ -365,13 +367,14 @@ def read_book(name: str, data: bytes) -> dict[str, Any]:
|
||||
]
|
||||
keep = ("번", "이름", "단위", "행", "출처", "직종", "글", "금액", "항목")
|
||||
basis = [{k: b[k] for k in keep} for b in blocks.values()]
|
||||
return {
|
||||
book = {
|
||||
"꼴": "엑셀꼴",
|
||||
"시트": SHEET,
|
||||
"호표": out,
|
||||
"단산": basis,
|
||||
"검산": {"어긋남": wrong, "총괄표": len(totals)},
|
||||
}
|
||||
return unit_line.settle(book) # 단위 줄 하위 · 재현 · 덮음 · 일위대가 줄 투영(778-3)
|
||||
|
||||
|
||||
def _json(value: Any) -> Any:
|
||||
|
||||
@@ -1,102 +1,45 @@
|
||||
"""M02 일위대가 실무자료 엑셀 지문(PLAN 338-1 · 설계 2-2) — 호표 줄 + 이어진 단가산출 블록 →
|
||||
직종 품 · 장비 시간 · 자재 수량 · 율 · 계수 · 조건 · 조항 · 몫. 금액 안 씀(식 맨 앞 단가는 뗌).
|
||||
"""M02 일위대가 실무자료 엑셀 지문(PLAN 338-1 · 설계 2-2 · 776-3) — 호표 줄 + 이어진 단가산출 블록 →
|
||||
직종 품 · 장비 시간 · 자재 수량 · 율 · 계수 · 조건 · 조항 · 몫. 금액 안 씀.
|
||||
|
||||
엑셀꼴 = 블록 `글`(머리 뺀 몸 줄)을 줄마다 풂 — 절 `^\\d+\\.` · 작은 절 `^[가-하]\\.` ·
|
||||
직종 줄 「직종 : 단가 × 식 = 금액」(우리 인력 이름 · 옛이름) · 장비 = 「노무비 · 경비 · 재료비 :
|
||||
단가 ÷ Q」
|
||||
+ 지금 장비(노무비 > 경비 > 재료비) · 자재 = 「이름 단가 × 수량 = 금액」 · 「: 별도계상」 ·
|
||||
못 풀면 금액 ÷ 단가(「역산」 · 품 되찾기만).
|
||||
STmate = 블록 `자원`(L · X · M · S) 품이 곧 품(노무비 · 재료비 · 경비 세 줄 같은 품은 한 번) ·
|
||||
섞음 = 이름 · 규격 몫 둘(작은 쪽 인력 · 큰 쪽 장비).
|
||||
몫(조합용 · 설계 2-7) = 맨 윗 절마다 노임 · 장비 벡터(`M02_Hopyo_Shares`).
|
||||
형식 모름 — 블록 단위 줄(`항목` · `M02_Hopyo_UnitLine`)의 투영:
|
||||
L 노무 = 직종 품 · 식이 있으면 맨 인자가 섞음 비율 후보(호표 이름 · 규격 · 절 제목의 (N%) ·
|
||||
8:2)와 같을 때 율로 가름 · 식 없는 줄(QTY)은 호표 이름 비율 둘 이상이면 작은 쪽(인력)
|
||||
X 기계 = 장비 {갈래, 규격, 시간, 밑시간, 율, 절, 글} · 식 없는 줄은 큰 비율(장비)로 가름
|
||||
M 자재 = 식(수량) · 별도계상(품 없음) · E 값 줄에 수량이 있으면 장비 글이면 장비 · 아니면 자재
|
||||
셈 밖 · 율 · 하위 줄 = 안 씀(율 % 는 글에서).
|
||||
글(블록 머리 「N.이름 [단위]」 + 몸 글) = 조건 · 옛 판 · 계수(q · K · f …) · 글 속 「…의 N %」.
|
||||
몫(조합용 · 설계 2-7) = 절마다 노임 · 장비 벡터(`M02_Hopyo_Shares.sections`).
|
||||
블록 없는 기계(hr) 줄 = `machine` · 노임 · 장비를 바로 적은 호표 = `direct`.
|
||||
값 · 규칙은 336 실험(C4 `s1_excel_fp` · K5 `stmate`) 그대로.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections import Counter
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis
|
||||
from M02_MasterTemplete import M02_Hopyo_ReadEst as hopyo_est
|
||||
from M02_MasterTemplete import M02_Hopyo_Shares as shares
|
||||
from M02_MasterTemplete.M02_Hopyo_Shares import trade
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import conds, mtypes, parse_rhs, specnums
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import conds, mtypes, specnums
|
||||
|
||||
COST = {"노무비", "재료비", "경비"}
|
||||
SKIPW = {
|
||||
"소계",
|
||||
"계",
|
||||
"합계",
|
||||
"인부소계",
|
||||
"자재소계",
|
||||
"주자재소계",
|
||||
"운반",
|
||||
"상차",
|
||||
"기계",
|
||||
"다짐",
|
||||
"적사",
|
||||
"살수",
|
||||
"집토",
|
||||
"암절취",
|
||||
"채집",
|
||||
"인력",
|
||||
"장비",
|
||||
"작업조건",
|
||||
"1일시공량",
|
||||
"작업면적",
|
||||
"1시간작업량",
|
||||
"기계채집",
|
||||
"구조물터파기",
|
||||
}
|
||||
_COLON = re.compile(
|
||||
r"^\s*(?:\d+\)|[0-9가-힣]\.)?\s*(?P<name>[^::=]{1,40}?)\s*[::]\s*(?P<rhs>.*)$"
|
||||
)
|
||||
_MAT = re.compile(
|
||||
r"^\s*(?P<name>[^\d\s::=][^::=]*?[A-Za-z가-힣)\]])\s+(?P<rhs>[\d,]+(?:\.\d+)?\s*[×xX*÷/].*=.*)$"
|
||||
)
|
||||
_PCT = re.compile(r"([가-힣]+)의\s*([\d.]+)\s*%")
|
||||
_COEF = re.compile(
|
||||
r"(?<![A-Za-z가-힣])(q|qs|q1|K|k|f|f1|f2|E|Es|Cm|L|V1|V2|T|rt|n|t1|t2|t3|t4)\s*=\s*"
|
||||
r"(\d+(?:\.\d+)?)(?![\d.]*\s*[×x*÷/])"
|
||||
)
|
||||
_HEAD = re.compile(r"^\s*(\d+)\.\s*(.*)$")
|
||||
_SUB = re.compile(r"^\s*([가-하])\.\s*(.*)$")
|
||||
_SECTION = re.compile(r"^\s*(\d+\.|[가-하]\.)")
|
||||
OLD = re.compile(
|
||||
r"((?<![\d,.])20(0\d|1\d|20)(?![\d,.])|(?<!\d)(1\d|20)년|임도품셈|임도표준품셈|사방품셈|사방사업표준품셈)"
|
||||
)
|
||||
_LASTNUM = re.compile(r"=\s*([\d,]+(?:\.\d+)?)\s*$")
|
||||
_HOURS = re.compile(r"(hr|시간)", re.I)
|
||||
TRADE_UNITS = ("인",)
|
||||
rates_in = hopyo_est.rates_in
|
||||
|
||||
|
||||
def _flat(s: Any) -> str:
|
||||
return re.sub(r"\s+", "", str(s or ""))
|
||||
|
||||
|
||||
def _ratio(rhs: str) -> float | None:
|
||||
"""식을 못 풀 때만 — 금액 ÷ 단가(품 되찾기용)."""
|
||||
m = re.match(r"^\s*([\d,]+(?:\.\d+)?)", rhs)
|
||||
n = _LASTNUM.search(rhs)
|
||||
if not m or not n:
|
||||
return None
|
||||
p, a = float(m.group(1).replace(",", "")), float(n.group(1).replace(",", ""))
|
||||
return a / p if p else None
|
||||
|
||||
|
||||
def rates_in(text: Any) -> set[float]:
|
||||
"""글 속 섞음 비율 후보 — (N%) · 8:2 · 9:1."""
|
||||
t = str(text or "")
|
||||
out = {round(float(n) / 100, 4) for n in re.findall(r"(\d+(?:\.\d+)?)\s*%", t)}
|
||||
out = {v for v in out if 0 < v < 1}
|
||||
for a, b in re.findall(r"(\d+)\s*:\s*(\d+)", t):
|
||||
a, b = float(a), float(b)
|
||||
if a and b and a + b <= 20:
|
||||
out |= {round(a / (a + b), 4), round(b / (a + b), 4)}
|
||||
return out
|
||||
|
||||
|
||||
def empty(line: dict[str, Any], block: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
block = block or {}
|
||||
basis = line.get("근거") or {}
|
||||
@@ -133,12 +76,10 @@ def _round(fp: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
|
||||
def excel(block: dict[str, Any] | None, line: dict[str, Any]) -> dict[str, Any]:
|
||||
"""HP 블록(설계 3-1) + 그 줄(이름 · 규격 · 단위) → 지문 · 블록이 없거나 글 · 자원이 없으면
|
||||
"""HP 블록(설계 3-1) + 그 줄(이름 · 규격 · 단위) → 지문 · 블록이 없거나 단위 줄 · 글이 없으면
|
||||
기계(hr) 줄 지문(장비 1 시간) 또는 빈 지문."""
|
||||
if block and block.get("자원"):
|
||||
return _round(_stmate(block, line))
|
||||
if block and block.get("글"):
|
||||
return _round(_text(block, line))
|
||||
if block and (block.get("항목") or block.get("글")):
|
||||
return _round(_units(block, line))
|
||||
return machine(line)
|
||||
|
||||
|
||||
@@ -155,124 +96,59 @@ def machine(line: dict[str, Any]) -> dict[str, Any]:
|
||||
return fp
|
||||
|
||||
|
||||
def _text(block: dict[str, Any], line: dict[str, Any]) -> dict[str, Any]:
|
||||
"""엑셀꼴 — 블록 글 줄마다 풂(C4 `north`)."""
|
||||
fp = empty(line, block)
|
||||
def _text(fp: dict[str, Any], block: dict[str, Any]) -> None:
|
||||
"""블록 글 → 조건(머리 + 앞 넉 줄 + ※ · 작업조건 줄) · 옛 판 · 계수 · 「…의 N %」."""
|
||||
head = f"{block.get('번', '')}.{block.get('이름', '')} [{block.get('단위', '')}]"
|
||||
text = [head, *[str(t) for t in block.get("글") or []]]
|
||||
front = [t for t in text[:5]] + [t for t in text if "※" in t or "작업조건" in t]
|
||||
head_text = " ".join([fp["이름"], fp["규격"], *front])
|
||||
fp["조건"] = {k: sorted(v) for k, v in conds(head_text).items()}
|
||||
front = text[:5] + [t for t in text if "※" in t or "작업조건" in t]
|
||||
got = conds(" ".join([fp["이름"], fp["규격"], *front]))
|
||||
fp["조건"] = {k: sorted(v) for k, v in got.items()}
|
||||
fp["옛판"] = bool(OLD.search(" ".join(text[:6])))
|
||||
sec, sub, mach, label = 0, "", None, ""
|
||||
got_cost: dict[tuple, tuple] = {}
|
||||
item_rates = rates_in(f"{fp['이름']} {fp['규격']}")
|
||||
sec_rates: set[float] = set()
|
||||
|
||||
def unmix(got: dict | None) -> dict | None:
|
||||
"""율 메모가 없는데 맨 인자가 섞음 비율 후보와 같으면 율로 가름."""
|
||||
if got and not got["율"]:
|
||||
for v in got.get("인자") or []:
|
||||
if round(v, 4) in item_rates | sec_rates and 0 < v < 1:
|
||||
return {**got, "밑품": got["밑품"] / v, "율": [[round(v, 4), "섞음"]]}
|
||||
return got
|
||||
|
||||
for raw in text[1:]:
|
||||
row = raw.rstrip()
|
||||
h, s = _HEAD.match(row), _SUB.match(row)
|
||||
if h or s:
|
||||
if h:
|
||||
sec, sub = int(h.group(1)), ""
|
||||
sec_rates = rates_in(h.group(2))
|
||||
else:
|
||||
sub = s.group(1)
|
||||
sec_rates = sec_rates | rates_in(s.group(2))
|
||||
title = (h or s).group(2)
|
||||
ts = mtypes(title)
|
||||
mach = (ts, specnums(title)) if ts else None
|
||||
label = title.strip() if ts else ""
|
||||
for k, v in _PCT.findall(row):
|
||||
fp["율"].append([k, float(v)])
|
||||
cm = _COLON.match(row)
|
||||
name = re.sub(r"^[0-9가-힣]\.", "", _flat(cm.group("name"))) if cm else ""
|
||||
who = hopyo_basis.trade_names().get(name) if cm else None
|
||||
if who:
|
||||
rhs = cm.group("rhs")
|
||||
got = unmix(parse_rhs(rhs))
|
||||
if got:
|
||||
_labor(fp, who, got["품"], got["밑품"], got["율"], "식")
|
||||
elif r := _ratio(rhs):
|
||||
_labor(fp, who, r, r, [], "역산")
|
||||
continue
|
||||
if cm and name in COST:
|
||||
got = unmix(parse_rhs(cm.group("rhs")))
|
||||
if got and mach:
|
||||
key = (sec, sub, label)
|
||||
prio = 0 if (name == "노무비" and got["단가"] > 0) else 1 if name == "경비" else 2
|
||||
if key not in got_cost or prio < got_cost[key][0]:
|
||||
got_cost[key] = (prio, mach, got)
|
||||
continue
|
||||
if cm and name not in SKIPW and re.search(r"(별도|별산)", cm.group("rhs")):
|
||||
fp["자재"].append({"이름": cm.group("name").strip(), "품": None, "밑품": None})
|
||||
continue
|
||||
mm = _MAT.match(row)
|
||||
if mm and not cm and (got := parse_rhs(mm.group("rhs"))):
|
||||
fp["자재"].append(
|
||||
{"이름": mm.group("name").strip(), "품": got["품"], "밑품": got["밑품"]}
|
||||
)
|
||||
continue
|
||||
if not (h or s):
|
||||
ts = mtypes(row)
|
||||
if ts and "%" not in row and not cm:
|
||||
mach, label = (ts, specnums(row)), row.strip()
|
||||
for row in text[1:]:
|
||||
fp["율"] += [[k, float(v)] for k, v in _PCT.findall(row)]
|
||||
if not _SECTION.match(row):
|
||||
for k, v in _COEF.findall(row):
|
||||
fp["계수"].setdefault(k, float(v))
|
||||
for (sec_, sub_, lab), (_, (ts, sp), got) in got_cost.items():
|
||||
where = f"{sec_}.{sub_}" if sub_ else str(sec_)
|
||||
one = {"갈래": sorted(ts), "규격": sorted(sp), "시간": got["품"], "밑시간": got["밑품"]}
|
||||
fp["장비"].append({**one, "율": got["율"], "절": where, "글": lab})
|
||||
fp["몫"] = shares.sections(text[1:])
|
||||
return fp
|
||||
|
||||
|
||||
def _stmate(block: dict[str, Any], line: dict[str, Any]) -> dict[str, Any]:
|
||||
"""STmate — 자원 품이 곧 품(K5 `stmate`)."""
|
||||
def _unmix(one: dict[str, Any], rates: set[float]) -> tuple[float, list]:
|
||||
"""(밑품, 율) — 율 메모가 없는데 맨 인자가 섞음 비율 후보와 같으면 율로 가름."""
|
||||
if not one.get("율"):
|
||||
for v in one.get("인자") or []:
|
||||
if round(v, 4) in rates and 0 < v < 1:
|
||||
return one["밑품"] / v, [[round(v, 4), "섞음"]]
|
||||
return one["밑품"], one.get("율") or []
|
||||
|
||||
|
||||
def _units(block: dict[str, Any], line: dict[str, Any]) -> dict[str, Any]:
|
||||
fp = empty(line, block)
|
||||
text = [str(t) for t in block.get("글") or []][:60]
|
||||
fp["조건"] = {
|
||||
k: sorted(v) for k, v in conds(" ".join([fp["이름"], fp["규격"], *text[:4]])).items()
|
||||
}
|
||||
fp["옛판"] = bool(OLD.search(" ".join(text[:4])))
|
||||
for t in text:
|
||||
for k, v in _COEF.findall(t):
|
||||
fp["계수"].setdefault(k, float(v))
|
||||
for k, v in _PCT.findall(t):
|
||||
fp["율"].append([k, float(v)])
|
||||
rates = rates_in(f"{fp['이름']} {fp['규격']}")
|
||||
lab_rate = min(rates) if len(rates) >= 2 else None
|
||||
mach_rate = max(rates) if len(rates) >= 2 else None
|
||||
seen: Counter = Counter()
|
||||
for x in block.get("자원") or []:
|
||||
kind, q = x.get("갈래"), x.get("품")
|
||||
nm, sp = str(x.get("이름") or ""), str(x.get("규격") or "")
|
||||
if not q:
|
||||
_text(fp, block)
|
||||
item_rates = rates_in(f"{fp['이름']} {fp['규격']}")
|
||||
pair = sorted(item_rates) if len(item_rates) >= 2 else []
|
||||
lab_rate, mach_rate = (pair[0], pair[-1]) if pair else (None, None)
|
||||
for one in block.get("항목") or []:
|
||||
kind, q, how = one.get("묶음"), one.get("품"), one.get("어떻게")
|
||||
if one.get("밖") or (q is None and how != "별도"):
|
||||
continue
|
||||
seen[(x.get("코드"), round(float(q), 9))] += 1
|
||||
if seen[(x.get("코드"), round(float(q), 9))] > 1: # 노무비 · 재료비 · 경비 세 줄 같은 품
|
||||
continue
|
||||
q = float(q)
|
||||
ts = mtypes(f"{nm} {sp}")
|
||||
if kind == "L":
|
||||
if who := trade(nm):
|
||||
base = q / lab_rate if lab_rate else q
|
||||
_labor(fp, who, q, base, [[lab_rate, "섞음"]] if lab_rate else [], "QTY")
|
||||
elif kind == "X" or (kind == "S" and ts):
|
||||
one = {"갈래": sorted(ts), "규격": sorted(specnums(sp) | specnums(nm)), "시간": q}
|
||||
base = q / mach_rate if mach_rate else q
|
||||
fp["장비"].append({**one, "밑시간": base, "율": [], "절": "0", "글": f"{nm} {sp}"})
|
||||
elif kind in ("M", "S"):
|
||||
fp["자재"].append({"이름": f"{nm} {sp}".strip(), "품": q, "밑품": q})
|
||||
fp["몫"] = shares.sections_stm(block.get("자원") or [], text)
|
||||
text = f"{one.get('이름') or ''} {one.get('규격') or ''}".strip()
|
||||
rates = item_rates | set(one.get("절율") or [])
|
||||
if kind == "L" and (who := one.get("직종") or trade(one.get("이름"))):
|
||||
if how == "QTY":
|
||||
base, r = (q / lab_rate, [[lab_rate, "섞음"]]) if lab_rate else (q, [])
|
||||
else:
|
||||
base, r = _unmix(one, rates) if how == "식" else (one["밑품"], one["율"])
|
||||
_labor(fp, who, q, base, r, how)
|
||||
elif kind == "X" or (kind == "E" and mtypes(text)):
|
||||
if how == "QTY":
|
||||
base, r = (q / mach_rate if mach_rate else q), []
|
||||
else:
|
||||
base, r = _unmix(one, rates)
|
||||
sizes = specnums(one.get("규격") or "") | specnums(one.get("이름") or "")
|
||||
got = {"갈래": sorted(mtypes(text)), "규격": sorted(sizes), "시간": q}
|
||||
fp["장비"].append({**got, "밑시간": base, "율": r, "절": one.get("절"), "글": text})
|
||||
elif kind in ("M", "E") and (how in ("식", "별도", "QTY")):
|
||||
fp["자재"].append({"이름": text, "품": q, "밑품": one.get("밑품", q)})
|
||||
fp["몫"] = shares.sections(block.get("항목") or [])
|
||||
return fp
|
||||
|
||||
|
||||
@@ -282,7 +158,7 @@ def direct(hopyo: dict[str, Any]) -> dict[str, Any] | None:
|
||||
fp = empty({k: hopyo.get(k) for k in ("이름", "규격", "단위")})
|
||||
texts = [str(t) for t in hopyo.get("근거글") or []]
|
||||
texts += [f"{x.get('이름')} {x.get('규격')}" for x in lines if x.get("갈래") == "근거글"]
|
||||
fp["출처"] = hopyo_basis.sources(" ".join(texts))
|
||||
fp["출처"] = hopyo_est.sources(" ".join(texts))
|
||||
hit = False
|
||||
for x in lines:
|
||||
q = x.get("수량")
|
||||
|
||||
@@ -20,6 +20,7 @@ import hashlib
|
||||
import json
|
||||
import queue
|
||||
import threading
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -295,9 +296,21 @@ def _hp_path(folder: Path, key: str) -> Path:
|
||||
return hopyo_store._path(folder, key)
|
||||
|
||||
|
||||
@lru_cache(maxsize=64)
|
||||
def _hp_mark(path: str, stamp: tuple[int, int]) -> str:
|
||||
"""HP 지문 칸(올린 엑셀 sha256 + 읽기 판 · 776-3) — 없으면(붙여넣기) 파일 해시 · 파일이 그대로면
|
||||
(`stamp` = 바뀐 때 · 크기) 다시 안 읽음."""
|
||||
raw = Path(path).read_bytes()
|
||||
doc = json.loads(raw)
|
||||
mark = doc.get("지문") if isinstance(doc, dict) else None
|
||||
return f"{mark[:16]}{doc.get('판')}" if mark else hashlib.sha256(raw).hexdigest()[:16]
|
||||
|
||||
|
||||
def cache_path(folder: Path, key: str) -> Path:
|
||||
hp = hashlib.sha256(_hp_path(folder, key).read_bytes()).hexdigest()[:16]
|
||||
return ours.folder() / f"맞춤_{hp}_{ours.version()}_{CALC_VERSION}.json"
|
||||
hp = _hp_path(folder, key)
|
||||
got = hp.stat()
|
||||
mark = _hp_mark(str(hp), (got.st_mtime_ns, got.st_size))
|
||||
return ours.folder() / f"맞춤_{mark}_{ours.version()}_{CALC_VERSION}.json"
|
||||
|
||||
|
||||
def units(doc: dict[str, Any]) -> list[tuple[str, dict | None, dict]]:
|
||||
|
||||
@@ -343,7 +343,6 @@ def name_cands(line: dict[str, Any], got: dict[str, Any] | None = None) -> list[
|
||||
|
||||
# ── 인용 조항 · 이름 · 근거 차례(338 · 설계 2-4 B 차례) ─────────────────────
|
||||
_OLD_TEXT = re.compile(r"(20(1\d|2[0-4])|1\d년|2[0-4]년|임도품셈|임도표준품셈|사방품셈)")
|
||||
_TYPO_CLAUSE = re.compile(r"^(\d+)-(\d)(\d)(\d)$") # 13-114 → 13-11-4
|
||||
_GENERIC = frozenset(
|
||||
"인력 기계 장비 소계 합계 노무비 재료비 경비 적용 기준 별도계상 별도 인부 보통인부 특별인부 "
|
||||
"설치 해체 운반 작업 시공 사용 포함 공구손료 경장비 손료 기계경비 재료 인건비 품셈 표준품셈 "
|
||||
@@ -352,32 +351,14 @@ _GENERIC = frozenset(
|
||||
|
||||
|
||||
def fix_typo(theirs: list) -> list[list[str]]:
|
||||
"""엑셀 인용 조항 오타 — 「13-114」 → 「13-11-4」."""
|
||||
out = []
|
||||
for book, n in theirs or []:
|
||||
m = _TYPO_CLAUSE.match(str(n))
|
||||
out.append([book, f"{m.group(1)}-{m.group(2)}{m.group(3)}-{m.group(4)}" if m else n])
|
||||
return out
|
||||
"""엑셀 인용 조항 사본 — 오타(「13-114」 → 13-11-4)는 읽기(`ReadEst.sources`)가 이미 고침."""
|
||||
return [list(x) for x in theirs or []]
|
||||
|
||||
|
||||
def clause(theirs: list, our: dict[str, Any]) -> tuple[float, list[str], str]:
|
||||
"""(점수, 맞은 우리 조항 글, 꼬리표) — 같음 1 · 윗마디 0.9(엑셀 6-2 ↔ 우리 6-2-2) · 앞 마디 0.5
|
||||
·
|
||||
나 창 `Basis.clause` 가 있으면 그것."""
|
||||
if hasattr(hopyo_basis, "clause"):
|
||||
return hopyo_basis.clause(theirs, our)
|
||||
nums = [n for book, n in theirs if book == our["책"]]
|
||||
same = sorted(n for n in nums if n in our["조항"])
|
||||
if same:
|
||||
return 1.0, [f"{our['구분']} {n}" for n in same], "조항 같음"
|
||||
kids = sorted(c for c in our["조항"] for n in nums if c.startswith(n + "-"))
|
||||
if kids:
|
||||
return 0.9, [f"{our['구분']} {n}" for n in kids], "조항 같음"
|
||||
heads = {n.rsplit("-", 1)[0] for n in nums}
|
||||
near_ = sorted(n for n in our["조항"] if n.rsplit("-", 1)[0] in heads)
|
||||
if near_:
|
||||
return 0.5, [f"{our['구분']} {n}" for n in near_], "앞 마디 같음"
|
||||
return 0.0, [], ""
|
||||
"""(점수, 맞은 우리 조항 글, 꼬리표) — `M02_Hopyo_Basis.clause`(같음 1 · 윗마디 0.9 ·
|
||||
앞 마디 0.5)."""
|
||||
return hopyo_basis.clause(theirs, our)
|
||||
|
||||
|
||||
def _strong_basis(line: dict[str, Any], cand: dict[str, Any]) -> bool:
|
||||
@@ -393,9 +374,6 @@ def b_order(line: dict[str, Any], got: dict[str, Any] | None = None, text: Any =
|
||||
근거: {로직: 근거}}` — 이름 후보 + 근거 후보 합침(접힘 없음) · 「같음」도 근거 검사(인용 조항이
|
||||
다른 로직을 가리키면 후보) · 옛 판 글이면 같음 그대로 · 씨앗은 이름 같음 자리."""
|
||||
got = got or targets()
|
||||
basis_ = dict(line.get("근거") or {})
|
||||
if basis_.get("출처"):
|
||||
line = {**line, "근거": {**basis_, "출처": fix_typo(basis_["출처"])}}
|
||||
basis = hopyo_basis.candidates(line, got)
|
||||
named = candidates(line, "로직", got, None)
|
||||
merged_c = [c for c in hopyo_basis.merge(basis, named) if c.get("로직")]
|
||||
|
||||
@@ -9,16 +9,15 @@
|
||||
글 = 머리 뺀 몸 줄 `GLINES` 줄 · 줄마다 `GCHARS` 자 안 · 산출근거 열이 빈 줄은 그 오른쪽 ~ 돈 머리
|
||||
(맨 오른쪽 묶음) 앞 칸을 빈칸으로 이어 붙임(거창 꼴 · 돈 머리 없으면 `WIDE` 열) ·
|
||||
금액 = 머리 줄 돈 칸(1 단위).
|
||||
항목(Book 블록 항목) = 글 줄 풀이 — 줄 율(호표 줄 이름 · 규격)에 기대지 않는 것만 ·
|
||||
지문 단계가 율을 가름:
|
||||
항목(Book 블록 항목 · 778-3) = 글 줄 → 단위 줄(꼴 = `M02_Hopyo_UnitLine`) · 줄 금액 = 「= 금액」 ·
|
||||
끝 점(「28,500. ×」) 뗌 · 율(호표 줄 이름 · 규격)에 기대지 않는 것만(섞음 율은 지문 몫):
|
||||
L 직종 줄 「직종 : 단가 × 식 = 금액」(못 풀면 금액 ÷ 단가 「역산」 · `직종` = 우리 이름) ·
|
||||
X 장비 = 장비 절 · 장비 글 줄 뒤 「노무비 · 경비 · 재료비 : 단가 ÷ Q」 — 절 · 장비마다 하나
|
||||
(노무비(단가 > 0) > 경비 > 재료비 줄의 품) · 이름 = 장비 글 · 단가 = 비목마다 ·
|
||||
M 자재 = 「이름 단가 × 수량 = 금액」 · 「이름 : 별도계상」(품 없음).
|
||||
항목 = `{갈래, 이름, 규격, 품, 밑품, 율, 인자, 단가, 절, 절율, 어떻게, 행}`(L 은 + 직종) ·
|
||||
STmate 어댑터(`M02_Hopyo_Stmate`)도 같은 꼴.
|
||||
풀이 규칙은 지문(`M02_Hopyo_Finger._text`)과 같음 — 776-3 이 지문을 항목 위로 옮기면
|
||||
그쪽 사본을 걷음.
|
||||
(품 = 노무비(단가 > 0) › 경비 › 재료비 줄 · 비목마다 매개값이 다르면 `계수`) ·
|
||||
M 자재 = 「이름 단가 × 수량 = 금액」(이름 속 = · / 도) · 「이름 : 별도계상」(품 없음) ·
|
||||
R 율 = 「기준 × N% = 금액」(라벨 = 이 줄 · 앞 줄 · 기준 = 「노무비의」 앞 낱말) ·
|
||||
E 값 = 셈 글 아닌 「이름 : … = 금액」(하위 가름은 `UnitLine.settle`) ·
|
||||
절 = 제목(작은 절 = 「윗 제목 · 작은 제목」) · 합계 · 소계 절 안 줄 = `밖`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -28,6 +27,7 @@ from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_UnitLine as unit_line
|
||||
from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import mtypes, parse_rhs
|
||||
|
||||
@@ -53,6 +53,26 @@ _MAT = re.compile(
|
||||
_SEC = re.compile(r"^\s*(\d+)\.\s*(.*)$")
|
||||
_SUB = re.compile(r"^\s*([가-하])\.\s*(.*)$")
|
||||
_LASTNUM = re.compile(r"=\s*([\d,]+(?:\.\d+)?)\s*$")
|
||||
# 단위 줄(778-3) — 줄 금액 · 끝 점 · 율 · 느슨한 자재 · 셈 글 · 합계 절
|
||||
NUM = r"[\d,]+(?:\.\d+)?"
|
||||
_AMOUNT = re.compile( # 끝 「= 금액」 — 원 · ₩ · 「\/㎡」 단위 · 「(지주)」 꼬리 허용
|
||||
r"=\s*[A-Z]?\s*(" + NUM + r")\s*\.?\s*(?:원|₩)?\s*(?:\\?/\s*\S+)?\s*(?:\([^()]*\))?\s*$"
|
||||
)
|
||||
_DOT = re.compile(r"(\d)\.(?=\s|$|%)")
|
||||
_DECR = re.compile(r"[×xX*]\s*(0?\.\d+)\s*=")
|
||||
_PCTN = re.compile(r"(\d+(?:\.\d+)?)\s*%")
|
||||
_BASEW = re.compile(r"([가-힣]+)의\s*[\d.]+\s*%")
|
||||
_TOTAL = re.compile(r"(합\s*계|소\s*계|총\s*계)")
|
||||
_NUMBERED = re.compile(r"^\s*(?:\d+|[가-하])\s*[.)]\s*") # 「3.」 · 「가.」 · 「1)」 머리
|
||||
_TOP_L = re.compile(r"^\s*\d+\s*\.\s*(.*)$")
|
||||
_SUB_L = re.compile(r"^\s*[가-하]\s*\.\s*(.*)$")
|
||||
_DECIMAL = re.compile(r"^\s*\d+\.\d")
|
||||
_LOOSE = re.compile(
|
||||
r"^\s*(?P<name>[^\d\s::].*?[A-Za-z가-힣)\]])\s+(?P<rhs>" + NUM + r"\s*[×xX*÷/].*=.*)$"
|
||||
)
|
||||
_CALC = re.compile(
|
||||
r"(^|[\s(])(Q\d?|q\w?|K|k|f\d?|E\w?|Cm|L|V\d|T|rt|n|t\d|A|N|W|V|S)\s*[=:]|─|㎥/hr|m3/hr|/hr\b"
|
||||
)
|
||||
|
||||
|
||||
def _flat(value: Any) -> str:
|
||||
@@ -187,7 +207,7 @@ def read(rows: list[list[Any]] | None) -> dict[int, dict[str, Any]]:
|
||||
return blocks
|
||||
|
||||
|
||||
# ── 글 → 항목 ─────────────────────────────────────────────────────────
|
||||
# ── 글 → 단위 줄 ───────────────────────────────────────────────────────
|
||||
def _ratio(rhs: str) -> float | None:
|
||||
"""식을 못 풀 때만 — 금액 ÷ 단가(품 되찾기용)."""
|
||||
m = re.match(r"^\s*([\d,]+(?:\.\d+)?)", rhs)
|
||||
@@ -198,22 +218,82 @@ def _ratio(rhs: str) -> float | None:
|
||||
return a / p if p else None
|
||||
|
||||
|
||||
def _num(text: str) -> float | None:
|
||||
try:
|
||||
return float(text.replace(",", ""))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def amount(row: str) -> float | None:
|
||||
"""줄 끝 「= 금액」(원 · 끝 점 · 앞 글자 하나 허용)."""
|
||||
m = _AMOUNT.search(row.replace("W", ""))
|
||||
return _num(m.group(1)) if m else None
|
||||
|
||||
|
||||
def _item(kind: str, name: str, got: dict[str, Any], how: str, **more: Any) -> dict[str, Any]:
|
||||
keep = {k: got.get(k) for k in ("품", "밑품", "율", "인자")}
|
||||
return {"갈래": kind, "이름": name, "규격": "", **keep, "어떻게": how, **more}
|
||||
keep = {"품": got.get("품"), "밑품": got.get("밑품")}
|
||||
keep |= {"율": got.get("율") or [], "인자": got.get("인자") or []}
|
||||
return {"묶음": kind, "이름": name, "규격": "", **keep, "어떻게": how, **more}
|
||||
|
||||
|
||||
def _material(hit: Any, row: str, money: float | None, place: dict) -> dict[str, Any]:
|
||||
"""자재 줄 「이름 단가 × 수량 [× 손료율] = 금액」 — 손료율 = 식 속 % · 끝 「× 0.xx =」."""
|
||||
got = parse_rhs(hit.group("rhs"))
|
||||
more = {"단가": {"재료비": got["단가"]} if got else {}, "금액": money, **place}
|
||||
one = _item("M", hit.group("name").strip(), got or {}, "식", **more)
|
||||
pct = [v for v, note in (got or {}).get("율") or [] if note == "%"]
|
||||
dec = _DECR.search(row)
|
||||
dec = float(dec.group(1)) if dec and 0.5 <= float(dec.group(1)) < 1 else None
|
||||
if (
|
||||
pct or dec
|
||||
): # ponytail: 끝 「× 0.95 =」 = 손료율(0.5~1) 어림 — 글에 「손료」 표지가 생기면 그것으로
|
||||
one["손료율"] = pct[-1] if pct else dec
|
||||
return one
|
||||
|
||||
|
||||
def _rate(row: str, prev: str, cm: Any) -> dict[str, Any]:
|
||||
"""율 줄 「기준 × N% = 금액」(라벨 = 이 줄 이름 · 없으면 앞 줄) →
|
||||
{이름, 품 = 율, 기준값, 기준}."""
|
||||
pm = _PCTN.findall(row) or _PCTN.findall(prev)
|
||||
rate = float(pm[-1]) / 100 if pm else None
|
||||
dec = _DECR.search(row)
|
||||
if dec and not _PCTN.findall(row):
|
||||
rate = float(dec.group(1))
|
||||
body = row.split(":", 1)[-1] if cm else row
|
||||
par = re.search(r"\(([^()]*\d[^()]*)\)\s*[×xX*]", body)
|
||||
if par and "+" in par.group(1):
|
||||
base: float | None = sum(_num(x) or 0 for x in re.findall(NUM, par.group(1)))
|
||||
else:
|
||||
hit = re.search(r"(" + NUM + r")\s*[×xX*]\s*(?:" + NUM + r")\s*(?:%|=)", body)
|
||||
hit = hit or re.search(r"(" + NUM + r")", body)
|
||||
base = _num(hit.group(1)) if hit else None
|
||||
word = _BASEW.search(row) or _BASEW.search(prev)
|
||||
label = _NUMBERED.sub("", (cm.group("name") if cm else prev)).strip()[:40]
|
||||
return {"이름": label, "품": rate, "기준값": base, "기준": word.group(1) if word else ""}
|
||||
|
||||
|
||||
def items(text: list[str], rows: list[int] | None = None) -> list[dict[str, Any]]:
|
||||
"""블록 몸 글(머리 뺌) → 항목 목록(L · M 은 글 차례 · X 는 절 · 장비 처음 나온 차례로 뒤에)."""
|
||||
"""블록 몸 글(머리 뺌) → 단위 줄(글 차례 · 기계 = 절 · 장비마다 하나 · 첫 비목 줄 자리 ·
|
||||
품 = 노무비(단가 > 0) › 경비 › 재료비 줄) · 합계 · 소계 절 안 = `밖` · 값 → 하위 가름은 책
|
||||
단위(`M02_Hopyo_UnitLine.settle`)."""
|
||||
rows = rows or [0] * len(text)
|
||||
names = trade_names()
|
||||
out: list[dict[str, Any]] = []
|
||||
machines: dict[tuple, dict[str, Any]] = {}
|
||||
sec, sub, mach, label = 0, "", False, ""
|
||||
sec, sub, mach, label, total, prev = 0, "", False, "", False, ""
|
||||
top = path = "" # 절 제목 · 작은 절이면 「윗 제목 · 작은 제목」
|
||||
whole_out = False # 합계 · 소계 맨 윗 절 안
|
||||
sec_rates: set[float] = set()
|
||||
for raw, at in zip(text, rows, strict=True):
|
||||
row = str(raw).rstrip()
|
||||
h, s = _SEC.match(row), _SUB.match(row)
|
||||
if (lt := _TOP_L.match(row)) and not _DECIMAL.match(row): # 몫 절 · 셈 밖(띄어 쓴 「가 .」)
|
||||
top = path = lt.group(1).strip()
|
||||
whole_out = total = bool(_TOTAL.search(top))
|
||||
elif ls := _SUB_L.match(row):
|
||||
total = whole_out or bool(_TOTAL.search(ls.group(1)))
|
||||
path = path if total else f"{top} · {ls.group(1).strip()}"
|
||||
if h or s:
|
||||
if h:
|
||||
sec, sub = int(h.group(1)), ""
|
||||
@@ -225,46 +305,87 @@ def items(text: list[str], rows: list[int] | None = None) -> list[dict[str, Any]
|
||||
ts = mtypes(title)
|
||||
mach = bool(ts)
|
||||
label = title.strip() if ts else ""
|
||||
where = f"{sec}.{sub}" if sub else str(sec)
|
||||
place = {"절": where, "절율": sorted(sec_rates), "행": at}
|
||||
else:
|
||||
row = _DOT.sub(r"\1", row) # 「28,500. ×」 끝 점
|
||||
place = {
|
||||
"절": path,
|
||||
"윗절": top,
|
||||
"절율": sorted(sec_rates),
|
||||
"행": at,
|
||||
**({"밖": True} if total else {}),
|
||||
}
|
||||
money = amount(row)
|
||||
cm = _COLON.match(row)
|
||||
name = re.sub(r"^[0-9가-힣]\.", "", _flat(cm.group("name"))) if cm else ""
|
||||
who = names.get(name) if cm else None
|
||||
if who:
|
||||
rhs = cm.group("rhs")
|
||||
raw_name, how = cm.group("name").strip(), {"직종": who, **place}
|
||||
raw_name, how = cm.group("name").strip(), {"직종": who, "금액": money, **place}
|
||||
if got := parse_rhs(rhs):
|
||||
out.append(_item("L", raw_name, got, "식", 단가={"노무비": got["단가"]}, **how))
|
||||
elif r := _ratio(rhs):
|
||||
got = {"품": r, "밑품": r, "율": [], "인자": []}
|
||||
out.append(_item("L", raw_name, got, "역산", **how))
|
||||
got = {"품": r, "밑품": r}
|
||||
out.append(_item("L", raw_name, got, "역산", 단가={}, **how))
|
||||
elif money is not None: # 품 없는 「직종 : P」 선언 줄은 뺌(값은 뒤 줄)
|
||||
out.append(_item("L", raw_name, {}, "식", 단가={}, **how))
|
||||
prev = row
|
||||
continue
|
||||
if cm and name in COST:
|
||||
got = parse_rhs(cm.group("rhs"))
|
||||
if got and mach:
|
||||
if mach and (got or money is not None):
|
||||
key = (sec, sub, label)
|
||||
prio = 0 if (name == "노무비" and got["단가"] > 0) else 1 if name == "경비" else 2
|
||||
one = machines.get(key)
|
||||
if one is None or prio < one["_순"]:
|
||||
price = one["단가"] if one else {}
|
||||
one = _item("X", label, got, "식", 단가=price, _순=prio, **place)
|
||||
if one is None:
|
||||
one = _item("X", label, {}, "식", 단가={}, 금액=0.0, _순=9, _q={}, **place)
|
||||
machines[key] = one
|
||||
one["단가"][name] = got["단가"]
|
||||
out.append(one)
|
||||
one["금액"] = round(one["금액"] + (money or 0), 6)
|
||||
if got:
|
||||
prio = (
|
||||
0 if (name == "노무비" and got["단가"] > 0) else 1 if name == "경비" else 2
|
||||
)
|
||||
if prio < one["_순"]:
|
||||
one.update({k: got[k] for k in ("품", "밑품", "율", "인자")}, _순=prio)
|
||||
one["단가"][name] = got["단가"]
|
||||
one["_q"][name] = got["품"]
|
||||
prev = row
|
||||
continue
|
||||
if cm and name not in SKIPW and re.search(r"(별도|별산)", cm.group("rhs")):
|
||||
got = {"품": None, "밑품": None, "율": [], "인자": []}
|
||||
out.append(_item("M", cm.group("name").strip(), got, "별도", **place))
|
||||
more = {"단가": {}, "금액": 0.0, **place}
|
||||
out.append(_item("M", cm.group("name").strip(), {}, "별도", **more))
|
||||
prev = row
|
||||
continue
|
||||
mm = _MAT.match(row)
|
||||
if mm and not cm and (got := parse_rhs(mm.group("rhs"))):
|
||||
price = {"재료비": got["단가"]}
|
||||
out.append(_item("M", mm.group("name").strip(), got, "식", 단가=price, **place))
|
||||
if mm and not cm:
|
||||
out.append(_material(mm, row, money, place))
|
||||
prev = row
|
||||
continue
|
||||
if not (h or s):
|
||||
ts = mtypes(row)
|
||||
if ts and "%" not in row and not cm:
|
||||
mach, label = True, row.strip()
|
||||
if (
|
||||
money is None
|
||||
or total
|
||||
or (cm and (name in ("소계", "계", "합계") or name.endswith("소계")))
|
||||
):
|
||||
prev = row
|
||||
continue
|
||||
dec = _DECR.search(row)
|
||||
loose = None if cm else _LOOSE.match(row)
|
||||
if loose and (names.get(_flat(loose.group("name"))) or _BASEW.search(loose.group("name"))):
|
||||
loose = None # 직종 · 「…의 N%」 이름 = 자재 아님
|
||||
if loose: # 자재(이름 속 = · / · 손료율 % 도)
|
||||
out.append(_material(loose, row, money, place))
|
||||
elif "%" in row or (dec and "%" in prev):
|
||||
got = {"묶음": "R", "규격": "", **_rate(row, prev, cm), "단가": {}, "금액": money}
|
||||
out.append({**got, "어떻게": "율", **place})
|
||||
elif cm and not _CALC.search(row):
|
||||
first = re.search(r"(" + NUM + r")\s*[×xX*]", cm.group("rhs"))
|
||||
price = {"계": _num(first.group(1))} if first else {}
|
||||
one = {"묶음": "E", "이름": cm.group("name").strip()[:40], "규격": "", "품": None}
|
||||
out.append({**one, "단가": price, "금액": money, "어떻게": "값", **place})
|
||||
prev = row
|
||||
for one in machines.values():
|
||||
del one["_순"]
|
||||
out.append(one)
|
||||
unit_line.machine_done(one)
|
||||
return out
|
||||
|
||||
@@ -1,45 +1,18 @@
|
||||
"""M02 일위대가 맞춤 몫(PLAN 338-1 · 설계 2-2 · 2-7) — 단가산출 블록을 맨 윗 절(1. 2. 3.)
|
||||
· 작은 절(가. 나.)로 갈라 몫마다 노임(직종 → 인) · 장비(「굴착기|0.6」 → 시간) 벡터.
|
||||
|
||||
엑셀꼴 = 「직종 : 노임 × 식 = 금액」(식을 못 풀면 금액 ÷ 노임) · 장비 이름 줄 뒤 「노무비 : 단가 ÷ Q
|
||||
× 몫」 ·
|
||||
소계 · 합계 절 뺌. STmate = 자원 L · X(절 = 자원 `절` · 없으면 한 몫). 금액 안 씀.
|
||||
규칙은 336 실험(C5 `c01_trees.sections` · `sections_stm`) 그대로 · 장비 갈래는
|
||||
`M02_Hopyo_Words.mtypes`.
|
||||
"""M02 일위대가 맞춤 몫(PLAN 338-1 · 설계 2-2 · 2-7 · 776-3) — 단가산출 블록 단위 줄을 절(맨 윗 절 ·
|
||||
작은 절)마다 묶어 노임(직종 → 인) · 장비(「굴착기|0.6」 → 시간) 벡터. 형식 모름(단위 줄만 ·
|
||||
`M02_Hopyo_UnitLine`) · 셈 밖 줄 뺌 · 금액 안 씀 · 장비 갈래 = `M02_Hopyo_Words.mtypes`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import operator
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis
|
||||
from M02_MasterTemplete.M02_Hopyo_ReadEst import trade # 노임 이름 → 우리 인력(B09 도 씀)
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import mtypes, nfkc
|
||||
|
||||
__all__ = ["eq_key", "sections", "trade"]
|
||||
_SIZE = re.compile(r"(\d+(?:\.\d+)?)\s*(㎥|m3|톤|ton|t\b|kw|hp)", re.I)
|
||||
_OPS = {
|
||||
ast.Mult: operator.mul,
|
||||
ast.Div: operator.truediv,
|
||||
ast.Add: operator.add,
|
||||
ast.Sub: operator.sub,
|
||||
}
|
||||
_TRADE = re.compile(
|
||||
r"^[\s*·\-○◇]*([가-힣][가-힣\s]*?)\s*:\s*([\d,]+(?:\.\d+)?)\s*(.*?)(?:=\s*([\d,]+(?:\.\d+)?))?\s*$"
|
||||
)
|
||||
_SECTION = re.compile(r"^\s*(\d+)\s*\.\s*(.*)$")
|
||||
_EQ_COST = re.compile(r"^\s*노\s*무\s*비\s*:\s*([\d,]+(?:\.\d+)?)\s*(.*?)(?:=\s*[\d,.]+)?\s*$")
|
||||
_SKIP_SEC = re.compile(r"^(합\s*계|소\s*계|계)")
|
||||
_SUBLABEL = re.compile(r"^\s*[가-하]\s*\.\s*(.*)$")
|
||||
_COSTLINE = re.compile(r"^\s*(노\s*무\s*비|재\s*료\s*비|경\s*비)\s*:")
|
||||
|
||||
|
||||
def trade(name: Any) -> str | None:
|
||||
"""노임 이름 → 우리 인력 이름(빈칸 · 옛이름 · 괄호 글 풀이) · 아니면 None."""
|
||||
names = hopyo_basis.trade_names()
|
||||
flat = re.sub(r"\s+", "", str(name or ""))
|
||||
return names.get(flat) or names.get(re.sub(r"\(.*?\)", "", flat))
|
||||
|
||||
|
||||
def eq_key(text: Any) -> str | None:
|
||||
@@ -54,124 +27,23 @@ def eq_key(text: Any) -> str | None:
|
||||
return f"{'+'.join(sorted(kinds))}|{size}"
|
||||
|
||||
|
||||
def _eval(node: ast.AST) -> float:
|
||||
if isinstance(node, ast.Expression):
|
||||
return _eval(node.body)
|
||||
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
|
||||
return float(node.value)
|
||||
if isinstance(node, ast.BinOp) and type(node.op) in _OPS:
|
||||
return _OPS[type(node.op)](_eval(node.left), _eval(node.right))
|
||||
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
|
||||
return -_eval(node.operand)
|
||||
raise ValueError("식 아님")
|
||||
|
||||
|
||||
def qty_of(expr: str) -> float | None:
|
||||
"""「273,540 × 3인 ÷ 55㎥ × 2」 → 3/55×2 · 「59,020 ÷ 23.1 × 90%」 → 1/23.1×0.9."""
|
||||
s = re.sub(r"\([^()0-9]*\)", "", str(expr)).replace(",", "")
|
||||
s = re.sub(r"(\d+(?:\.\d+)?)\s*%", r"(\1/100)", s)
|
||||
s = s.replace("×", "*").replace("÷", "/").replace("X", "*").replace("x", "*")
|
||||
s = re.sub(r"/\s*(?=[^\d(\s.])", " ", s) # 「인/㎥」 의 / 는 단위
|
||||
s = re.sub(r"[^\d.+\-*/() ]", " ", s)
|
||||
s = re.sub(r"(?<=\d)\.(?!\d)", "", s) # 「4.」 → 4
|
||||
s = re.sub(r"\(\s*\)", " ", s).strip()
|
||||
m = re.match(r"\s*\d+(?:\.\d+)?", s)
|
||||
if not m:
|
||||
return None
|
||||
s = "1" + s[m.end() :]
|
||||
s = re.sub(r"(?<=[\d)])\s+(?=[\d(])", "*", s) # 빈칸으로 붙은 수는 곱
|
||||
try:
|
||||
v = _eval(ast.parse(s, mode="eval"))
|
||||
except (SyntaxError, ValueError, ZeroDivisionError):
|
||||
return None
|
||||
return v if 0 < v < 1e4 else None
|
||||
|
||||
|
||||
def _part(title: str, parent: str) -> dict[str, Any]:
|
||||
return {"제목": title, "절": parent, "노임": {}, "장비": {}}
|
||||
|
||||
|
||||
def sections(texts: list[str]) -> list[dict[str, Any]]:
|
||||
"""블록 몸 글 → 몫마다 {제목, 절, 노임, 장비} — 노임 · 장비가 하나도 없는 몫은 뺌."""
|
||||
out: list[dict[str, Any]] = []
|
||||
cur: dict[str, Any] | None = None
|
||||
eq, parent = None, ""
|
||||
for raw in texts:
|
||||
line = str(raw).rstrip()
|
||||
sec = _SECTION.match(line)
|
||||
if sec and not re.match(r"^\s*\d+\.\d", line):
|
||||
title = sec.group(2).strip()
|
||||
if _SKIP_SEC.match(title.replace(" ", "")):
|
||||
cur = None
|
||||
continue
|
||||
parent = title
|
||||
cur = _part(title, title)
|
||||
out.append(cur)
|
||||
eq = eq_key(title)
|
||||
def sections(units: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""블록 단위 줄(`M02_Hopyo_UnitLine`) → 몫마다 {제목 = 줄 절, 절 = 윗절, 노임, 장비} — 셈 밖 ·
|
||||
매개값 없는 줄 뺌 · 장비 = 시간 50 아래 · 노임 · 장비가 하나도 없는 몫은 뺌."""
|
||||
out: dict[str, dict[str, Any]] = {}
|
||||
for one in units:
|
||||
q, kind = one.get("품"), one.get("묶음")
|
||||
if one.get("밖") or not q or kind not in ("L", "X"):
|
||||
continue
|
||||
sub = _SUBLABEL.match(line) if cur is not None else None
|
||||
if sub and not _SKIP_SEC.match(sub.group(1).replace(" ", "")):
|
||||
cur = _part(f"{parent} · {sub.group(1).strip()}", parent)
|
||||
out.append(cur)
|
||||
if cur is None:
|
||||
if out or not line.strip():
|
||||
continue
|
||||
cur = _part("", "")
|
||||
out.append(cur)
|
||||
if re.search(r"(소계|합계|인부소계)", line.replace(" ", "")):
|
||||
eq = None
|
||||
continue
|
||||
sub = _SUBLABEL.match(line)
|
||||
if sub:
|
||||
eq = eq_key(sub.group(1))
|
||||
elif not _COSTLINE.match(line) and (k := eq_key(line)):
|
||||
eq = k
|
||||
m = _EQ_COST.match(line)
|
||||
if m and eq:
|
||||
q = qty_of(f"{m.group(1)} {m.group(2)}")
|
||||
if q and q < 50:
|
||||
cur["장비"][eq] = cur["장비"].get(eq, 0) + q
|
||||
continue
|
||||
t = _TRADE.match(line)
|
||||
if t and (who := trade(t.group(1))):
|
||||
q = qty_of(f"{t.group(2)} {t.group(3)}")
|
||||
if q is None and t.group(4): # 식을 못 풀면 금액 ÷ 노임(품 되찾기만)
|
||||
q = float(t.group(4).replace(",", "")) / float(t.group(2).replace(",", ""))
|
||||
if q:
|
||||
cur["노임"][who] = cur["노임"].get(who, 0) + q
|
||||
return [s for s in out if s["노임"] or s["장비"]]
|
||||
|
||||
|
||||
def sections_stm(res: list[dict[str, Any]], texts: list[str]) -> list[dict[str, Any]]:
|
||||
"""STmate 자원(L · X) → 몫(자원 `절` = 글 줄 차례면 그 앞 맨 윗 절 · 글이면 그 제목 · 없으면 한
|
||||
몫)."""
|
||||
marks = []
|
||||
for at, t in enumerate(texts):
|
||||
sec = _SECTION.match(t)
|
||||
if sec and not re.match(r"^\s*\d+\.\d", t):
|
||||
if not _SKIP_SEC.match(sec.group(2).replace(" ", "")):
|
||||
marks.append((at, sec.group(2).strip()))
|
||||
|
||||
def which(where: Any) -> str:
|
||||
if isinstance(where, str):
|
||||
return where
|
||||
if isinstance(where, int) and not isinstance(where, bool):
|
||||
got = [m for m in marks if m[0] <= where]
|
||||
return got[-1][1] if got else ""
|
||||
return ""
|
||||
|
||||
secs: dict[str, dict[str, Any]] = {}
|
||||
seen = set()
|
||||
for x in res:
|
||||
q, kind = x.get("품"), x.get("갈래")
|
||||
if not q or kind not in ("L", "X") or (x.get("코드"), q, x.get("절")) in seen:
|
||||
continue
|
||||
seen.add((x.get("코드"), q, x.get("절")))
|
||||
title = which(x.get("절"))
|
||||
s = secs.setdefault(title, _part(title, title))
|
||||
if kind == "L" and (who := trade(x.get("이름"))):
|
||||
s["노임"][who] = s["노임"].get(who, 0) + float(q)
|
||||
elif kind == "X":
|
||||
k = eq_key(f"{x.get('이름') or ''} {x.get('규격') or ''}") or f"?{x.get('코드')}"
|
||||
s["장비"][k] = s["장비"].get(k, 0) + float(q)
|
||||
return [s for s in secs.values() if s["노임"] or s["장비"]]
|
||||
title = str(one.get("절") or "")
|
||||
part = out.setdefault(title, _part(title, str(one.get("윗절") or title)))
|
||||
if kind == "L" and (who := one.get("직종") or trade(one.get("이름"))):
|
||||
part["노임"][who] = part["노임"].get(who, 0) + float(q)
|
||||
elif kind == "X" and q < 50:
|
||||
k = eq_key(f"{one.get('이름') or ''} {one.get('규격') or ''}") or f"?{one.get('코드')}"
|
||||
part["장비"][k] = part["장비"].get(k, 0) + float(q)
|
||||
return [p for p in out.values() if p["노임"] or p["장비"]]
|
||||
|
||||
@@ -15,9 +15,12 @@ PR · 단위 % = % · 그 밖 기타.
|
||||
비면 목록표 · 기계 셋 줄(노무비 · 재료비 · 경비)은 하나로 · 절 = 그 앞 번호 절 제목) ·
|
||||
머리 이름 · 규격 · 단위 = 단가산출근거목록표 · 출처 = 블록 글 `ReadEst.sources` ·
|
||||
직종 = 자원 L 이름(우리 인력 · 옛이름 = 지금 이름) ·
|
||||
금액 = 머리 줄 C~F(1 단위) · 항목(776-2 · Book 블록 항목 · EST 어댑터 `M02_Hopyo_ReadEst` 와
|
||||
같은 꼴) = 자원 + `{직종(L), 밑품 = 품, 율 [], 인자 [], 단가 I~L, 금액 C~F, 어떻게 「QTY」, 행}`
|
||||
(기계 셋 줄 = 더함).
|
||||
금액 = 머리 줄 C~F(1 단위) · 항목(778-3 · 단위 줄 꼴 = `M02_Hopyo_UnitLine`) = 행마다
|
||||
자원 L 노무 · X 기계 · M 자재 · S 경비 = E 값(품 = H · 단가 I~L · 줄 금액 C · 어떻게 「QTY」) ·
|
||||
기계 셋 줄 = 같은 코드 · 절 · 아직 빈 비목일 때만 하나(같은 코드 두 묶음 = 따로 ·
|
||||
비목마다 H 가 다르면 `계수`) · G 열 「&자원& * N'%'」 = R 율(기준값 = I) ·
|
||||
PR · RR 행 = R 율(PR = N % · RR = 곱 · 기준 = G 열 표지 「&PA01M:M&」 앞) ·
|
||||
D 코드 행 = S 하위 · N 열 「C…」 = `밖`(소계(제외금액)) · 자원 = 자원 코드 줄 옛 칸.
|
||||
검산 안 함 — `검산 {어긋남: [], 총괄표: 0}`.
|
||||
"""
|
||||
|
||||
@@ -28,6 +31,7 @@ from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel # 서로 부름 — 부를 때만 씀
|
||||
from M02_MasterTemplete import M02_Hopyo_ReadEst as hopyo_est
|
||||
from M02_MasterTemplete import M02_Hopyo_UnitLine as unit_line
|
||||
|
||||
SHEET = "일위대가표"
|
||||
CALC = "단가산출근거"
|
||||
@@ -45,6 +49,17 @@ _COLS = {"합계": 4, "노무비": 6, "재료비": 8, "경비": 10}
|
||||
_CALC_AMOUNT = {"합계": 2, "노무비": 3, "재료비": 4, "경비": 5} # 단가산출근거 C~F 금액
|
||||
_CALC_PRICE = {"합계": 8, "노무비": 9, "재료비": 10, "경비": 11} # I~L 단가(T · L · M · S)
|
||||
NOTE, CODE_COLS, RES_COL, QTY_COL, NAME_COL = 12, range(12, 26), 12, 7, 23
|
||||
FORMULA_COL, OUT_COL = 6, 13 # G 식 · N 합 갈 곳(C = 셈 밖 「소계(제외금액)」)
|
||||
COSTS = ("노무비", "재료비", "경비")
|
||||
_KIND = {"L": "L", "X": "X", "M": "M", "S": "E"} # 자원 코드 머리 → 단위 줄 묶음(S 경비 = 값)
|
||||
_RES_CODE = re.compile(r"^[LMXS]\d{5}$")
|
||||
_RATE = re.compile(r"^(?:PR|RR)[A-Z]?_") # 율 행 「PR_1」 · 「RRS_(8)_1」
|
||||
_SUB_D = re.compile(r"^(D\d{5})_\d+$") # 다른 블록 부름
|
||||
_PCT_REF = re.compile(r"&([A-Z][A-Z0-9_]*?)(?::[A-Z])?&\s*\*\s*[\d.]+\s*'%'\s*=") # 율 꼴 자원 행
|
||||
_LOSS = re.compile(r"\*\s*([\d.]+)\s*'%'") # 자재 손료율 「* 0.443'%'」
|
||||
_BASE_WORD = re.compile(r"([가-힣]+)의\s*[\d.]+\s*%") # 율 기준글 「(인건비의 3%)」
|
||||
_NUMBERED = re.compile(r"^\s*(?:\d+|[가-하])\s*[.)]\s*")
|
||||
_TAG = re.compile(r"&([A-Z][A-Z0-9_]*?)(?::[A-Z])?&") # 율 기준 표지 「&PA01M:M&」 → PA01M
|
||||
|
||||
|
||||
def is_stmate(names: Any) -> bool:
|
||||
@@ -100,12 +115,78 @@ def _lists(sheets: dict[str, list[list[Any]]]) -> dict[str, dict[str, str]]:
|
||||
|
||||
|
||||
# ── 단가산출근거 블록 ──────────────────────────────────────────────────
|
||||
def _part(row: list[Any]) -> str | None:
|
||||
"""기계 셋 줄 가운데 이 줄 비목(I~L 단가 칸 · 노무비 › 재료비 › 경비)."""
|
||||
return next((k for k in COSTS if _cell(row, _CALC_PRICE[k]) not in (None, "")), None)
|
||||
|
||||
|
||||
def _same_machine(last: dict[str, Any] | None, code: str, title: str, part: str | None) -> bool:
|
||||
"""바로 앞 기계 줄과 한 기계(셋 줄)인가 — 같은 코드 · 절 · 이 비목이 아직 빔
|
||||
(같은 코드 두 묶음 = 따로 · 울진대흥 1129)."""
|
||||
return bool(
|
||||
last
|
||||
and last["묶음"] == "X"
|
||||
and last["코드"] == code
|
||||
and last["절"] == title
|
||||
and part
|
||||
and part not in last["_q"]
|
||||
)
|
||||
|
||||
|
||||
def _unit(row: list[Any], code: str, title: str, info: dict[str, str]) -> dict[str, Any]:
|
||||
"""자원 행 → 단위 줄(L 노무 · X 기계 · M 자재 · S 경비 = E 값 · G 열 「'%'」 = R 율)."""
|
||||
name, _, spec = _s(_cell(row, NAME_COL)).partition("/")
|
||||
qty = hopyo_excel._num(_cell(row, QTY_COL))
|
||||
q = float(qty) if qty is not None else None
|
||||
one = {
|
||||
"묶음": _KIND.get(code[0], "E"),
|
||||
"코드": code,
|
||||
"이름": name.strip() or info.get("이름", ""),
|
||||
"규격": spec.strip() if name.strip() else info.get("규격", ""),
|
||||
"단위": info.get("단위", ""),
|
||||
"품": q,
|
||||
"절": title,
|
||||
}
|
||||
if rate := _PCT_REF.search(_s(_cell(row, FORMULA_COL))): # 「기구손료 : &L00020& * 2'%'」
|
||||
base = hopyo_excel._num(_cell(row, _CALC_PRICE["합계"]))
|
||||
more = {"기준": rate.group(1), "기준값": float(base) if base is not None else None}
|
||||
return {**one, "묶음": "R", "품": q / 100 if q is not None else None, **more}
|
||||
if one["묶음"] == "M" and (loss := _LOSS.search(_s(_cell(row, FORMULA_COL)))):
|
||||
one["손료율"] = round(float(loss.group(1)) / 100, 9) # 「&M00081& * 1.03'㎡'* 0.443'%'」
|
||||
return one
|
||||
|
||||
|
||||
def _rate_row(row: list[Any], code: str, text: str, label: str) -> dict[str, Any]:
|
||||
"""PR · RR 율 행 → 단위 줄 R(기준 = G 열 표지 「&PA01M:M&」 앞 · PR = N % · RR = 곱) · 이름이
|
||||
없는 식 줄(「210,212.3 * 0.09」)은 위 라벨 줄(「기구손료 : (가공노력품의 9%)」)에서 이름 ·
|
||||
기준글 · 못 찾으면 이름 빈 줄 + `이름없음`."""
|
||||
qty = hopyo_excel._num(_cell(row, QTY_COL))
|
||||
base = hopyo_excel._num(_cell(row, _CALC_PRICE["합계"]))
|
||||
tag = _TAG.search(_s(_cell(row, FORMULA_COL)))
|
||||
rate = float(qty) / (100 if code.startswith("PR") else 1) if qty is not None else None
|
||||
name = text.split(":")[0] if ":" in text else ""
|
||||
if not re.search(r"[가-힣A-Za-z]", name):
|
||||
name = _NUMBERED.sub("", label.split(":")[0] if ":" in label else label)
|
||||
word = _BASE_WORD.search(f"{text} {label}")
|
||||
return {
|
||||
"묶음": "R",
|
||||
"코드": code,
|
||||
"이름": name.strip()[:40],
|
||||
"규격": "",
|
||||
"품": rate,
|
||||
"기준": tag.group(1) if tag else "",
|
||||
"기준값": float(base) if base is not None else None,
|
||||
**({"기준글": word.group(1)} if word else {}),
|
||||
**({} if name.strip() else {"이름없음": True}),
|
||||
}
|
||||
|
||||
|
||||
def _blocks(rows: list[list[Any]], table: dict[str, dict[str, str]]) -> dict[str, dict[str, Any]]:
|
||||
"""{D 코드: {번, 코드, 이름, 규격, 단위, 행, 출처, 직종, 글, 자원, 금액, 항목}}."""
|
||||
names = hopyo_est.trade_names()
|
||||
out: dict[str, dict[str, Any]] = {}
|
||||
block: dict[str, Any] | None = None
|
||||
title = ""
|
||||
title = label = "" # 절 제목 · 율 행 위 라벨 줄(코드 · 금액 없는 글)
|
||||
for at, row in enumerate(rows, 1):
|
||||
head = next(
|
||||
(hit for v in row[:8] if (hit := _HEAD_D.search(_s(v)))), None
|
||||
@@ -123,12 +204,11 @@ def _blocks(rows: list[list[Any]], table: dict[str, dict[str, str]]) -> dict[str
|
||||
"단위": info.get("단위", ""),
|
||||
"행": [at, at],
|
||||
"글": [],
|
||||
"자원": [],
|
||||
"금액": _calc_money(row, _CALC_AMOUNT),
|
||||
"항목": [],
|
||||
},
|
||||
)
|
||||
title = ""
|
||||
title = label = ""
|
||||
continue
|
||||
if block is None:
|
||||
continue
|
||||
@@ -137,16 +217,18 @@ def _blocks(rows: list[list[Any]], table: dict[str, dict[str, str]]) -> dict[str
|
||||
block["행"][1] = at
|
||||
block = None
|
||||
continue
|
||||
text = _s(_cell(row, 1))
|
||||
res = _RES.match(_s(_cell(row, RES_COL)))
|
||||
text, cell = _s(_cell(row, 1)), _s(_cell(row, RES_COL))
|
||||
res = _RES.match(cell)
|
||||
code = res.group(1) + res.group(2) if res else None
|
||||
if not code:
|
||||
alt = _RES_A.match(first) or _RES_G.search(_s(_cell(row, 6)))
|
||||
alt = _RES_A.match(first) or _RES_G.search(_s(_cell(row, FORMULA_COL)))
|
||||
code = alt.group(1) if alt else None
|
||||
rate = None if code else _RATE.match(cell)
|
||||
sub = None if code or rate else _SUB_D.match(cell)
|
||||
if not text and not code:
|
||||
continue
|
||||
block["행"][1] = at
|
||||
if _s(_cell(row, RES_COL)) == block["코드"]: # 머리 이름 줄 「이름 규격 / 단위」
|
||||
if cell == block["코드"]: # 머리 이름 줄 「이름 규격 / 단위」
|
||||
if not block["이름"]:
|
||||
name, _, unit = text.rpartition("/")
|
||||
block["이름"], block["단위"] = name.strip() or text, unit.strip()
|
||||
@@ -156,41 +238,56 @@ def _blocks(rows: list[list[Any]], table: dict[str, dict[str, str]]) -> dict[str
|
||||
block["글"].append(text[: hopyo_est.GCHARS])
|
||||
if (sec := _SECTION.match(text)) and not re.match(r"^\s*\d+\.\d", text):
|
||||
title = sec.group(1).strip()
|
||||
money = _calc_money(row, _CALC_AMOUNT)
|
||||
place = {
|
||||
"단가": _calc_money(row, _CALC_PRICE),
|
||||
"금액": money.get("계"),
|
||||
"행": at,
|
||||
"절": title,
|
||||
"윗절": title,
|
||||
**({"밖": True} if _s(_cell(row, OUT_COL)).startswith("C") else {}),
|
||||
}
|
||||
if rate:
|
||||
block["항목"].append({**_rate_row(row, cell, text, label), **place, "어떻게": "율"})
|
||||
continue
|
||||
if code or sub or money.get("계") is not None:
|
||||
label = "" # 라벨은 바로 위 글 줄만
|
||||
elif text:
|
||||
label = text.strip()
|
||||
if sub and sub.group(1) != block["코드"] and money.get("계"):
|
||||
qty = hopyo_excel._num(_cell(row, QTY_COL))
|
||||
one = {"묶음": "S", "코드": sub.group(1), "이름": text[:40], "규격": ""}
|
||||
more = {"품": float(qty) if qty is not None else None, "어떻게": "QTY"}
|
||||
block["항목"].append(
|
||||
{**one, **more, **place, "하위": {"종류": "단산", "번": int(sub.group(1)[1:])}}
|
||||
)
|
||||
continue
|
||||
if not code:
|
||||
continue
|
||||
price, amount = _calc_money(row, _CALC_PRICE), _calc_money(row, _CALC_AMOUNT)
|
||||
last = block["자원"][-1] if block["자원"] else None
|
||||
if code[0] == "X" and last and last["코드"] == code and last["절"] == title:
|
||||
_add(block["항목"][-1]["단가"], price) # 기계 노무비 · 재료비 · 경비 셋 줄 = 하나
|
||||
_add(block["항목"][-1]["금액"], amount)
|
||||
last, part = block["항목"][-1] if block["항목"] else None, _part(row)
|
||||
if code[0] == "X" and _same_machine(last, code, title, part):
|
||||
_add(last["단가"], place["단가"]) # 기계 노무비 · 재료비 · 경비 셋 줄 = 하나
|
||||
last["금액"] = hopyo_excel._out(
|
||||
hopyo_excel._num(last["금액"] or 0) + hopyo_excel._num(place["금액"] or 0)
|
||||
)
|
||||
last["_q"][part] = hopyo_excel._num(_cell(row, QTY_COL))
|
||||
continue
|
||||
name, _, spec = _s(_cell(row, NAME_COL)).partition("/")
|
||||
info = table.get(code) or {}
|
||||
qty = hopyo_excel._num(_cell(row, QTY_COL))
|
||||
res = {
|
||||
"갈래": code[0],
|
||||
"코드": code,
|
||||
"이름": name.strip() or info.get("이름", ""),
|
||||
"규격": spec.strip() if name.strip() else info.get("규격", ""),
|
||||
"품": float(qty) if qty is not None else None,
|
||||
"절": title,
|
||||
}
|
||||
block["자원"].append(res)
|
||||
who = hopyo_est.trade(res["이름"]) if code[0] == "L" else None
|
||||
block["항목"].append(
|
||||
{
|
||||
**res,
|
||||
**({"직종": who} if code[0] == "L" else {}),
|
||||
"밑품": res["품"],
|
||||
"율": [],
|
||||
"인자": [],
|
||||
"단가": price,
|
||||
"금액": amount,
|
||||
"어떻게": "QTY",
|
||||
"행": at,
|
||||
}
|
||||
)
|
||||
one = _unit(row, code, title, table.get(code) or {})
|
||||
who = hopyo_est.trade(one["이름"]) if one["묶음"] == "L" else None
|
||||
one = {**one, **({"직종": who} if one["묶음"] == "L" else {}), "밑품": one["품"]}
|
||||
one.update(율=[], 인자=[], 어떻게="율" if one["묶음"] == "R" else "QTY", **place)
|
||||
if one["묶음"] == "X":
|
||||
one["_q"] = {part: hopyo_excel._num(_cell(row, QTY_COL))} if part else {}
|
||||
block["항목"].append(one)
|
||||
for block in out.values():
|
||||
for one in block["항목"]:
|
||||
if "_q" in one:
|
||||
unit_line.machine_done(one)
|
||||
block["자원"] = [ # 옛 칸(S1 투영) — 자원 코드 줄만
|
||||
{"갈래": one["코드"][0], **{k: one[k] for k in ("코드", "이름", "규격", "품", "절")}}
|
||||
for one in block["항목"]
|
||||
if _RES_CODE.match(one["코드"])
|
||||
]
|
||||
block["출처"] = hopyo_est.sources(" ".join(block["글"]))
|
||||
trades = [names.get(_flat(r["이름"])) for r in block["자원"] if r["갈래"] == "L"]
|
||||
block["직종"] = list(dict.fromkeys(t for t in trades if t))
|
||||
|
||||
@@ -32,7 +32,7 @@ DONE = "불러옴"
|
||||
NO_GRID = "격자 없음 — 다시 올리면 보임"
|
||||
RELOAD = "읽기 꼴이 바뀐 옛 불러오기 — 엑셀을 다시 올림"
|
||||
EXPIRE = timedelta(days=1) # 작업 중 HP 보관 — 올린 뒤 하루
|
||||
BOOK = 2 # 읽기 꼴(Book) 판 — 읽기 · 정규화 출력 꼴을 바꾸면 올림
|
||||
BOOK = 3 # 읽기 꼴(Book) 판 — 읽기 · 정규화 출력 꼴을 바꾸면 올림
|
||||
_KEY = re.compile(r"^HP-(\d{6})$")
|
||||
# ponytail: 프로세스 안 잠금 — 서버를 여럿 띄우면 파일 잠금으로
|
||||
_LOCK = threading.Lock()
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
"""M02 일위대가 실무자료 단위 줄(PLAN 778-3 · D-1 · 요구 622) — 단가산출 블록 · 일위대가 줄을 같은
|
||||
「세부항목 계산 단위」 꼴로 · 자체 검산. 형식(EST · STmate)을 모름 — 읽기 어댑터
|
||||
(`M02_Hopyo_ReadEst.items` · `M02_Hopyo_Stmate._blocks`)가 블록 `항목` 을 단위 줄로 냄.
|
||||
|
||||
단위 줄 = `{묶음, 이름, 규격, 단위, 품, 밑품, 율, 인자, 계수, 단가, 금액, 밖, 기준, 기준값,
|
||||
하위, 할증, 재현, 직종, 절, 절율, 코드, 어떻게, 행}`:
|
||||
묶음 = L 노무 · X 기계 · M 자재 · R 율 · S 하위 · E 값
|
||||
품 = 매개값(L 품 · X 시간 · M 수량 · R 율 소수 · S 수량 · E 없음) · 밑품 · 율 · 인자 = 지문 몫
|
||||
계수 = X 비목마다 매개값이 다를 때 `{비목: 그 비목 매개값 ÷ 품}`(덤프 재료비 (Cm-t1)/Cm)
|
||||
단가 = {노무비 · 재료비 · 경비(· 계)} · 금액 = 줄 금액(None = 엑셀에 없음)
|
||||
밖 = 셈 밖(STmate N 열 C · EST 합계 · 소계 절) · 기준 · 기준값 = 율 줄 기준 표지 · 기준 금액 ·
|
||||
기준글 = 라벨 「…의 N%」 앞 낱말(STmate) · 기준꼴 · 기준줄 = 기준값이 어느 줄 합인가
|
||||
(`base_rows`) ·
|
||||
이름없음 = 율 줄 이름 못 찾음 · 손료율 = M 「수량 × 손료율 × 단가」 꼴 표지(율 줄 아님)
|
||||
하위 = S `{종류 단산|일위, 번}` · 할증 = X 이름 · 규격 「할증120%」 → 1.2
|
||||
재현 = 단가 × 매개값(× 계수) ≈ 금액(0.5 % · 1 원) · 율 = 기준값 × 율 · 별도 · 값 = 참
|
||||
블록 `줄합` = 밖 아닌 줄 금액 합 · `덮음` = 줄합 ≈ 블록 금액(금액 없으면 None).
|
||||
일위대가 줄 → `묶음` · `재현` · % 줄 `기준`(노무비 · 재료비 · 주재료비 · 경비 = 규격 글) ·
|
||||
호표 `줄합` · `덮음`.
|
||||
값 줄(E · 첫 수 = 다른 블록 · 호표 1 단위 금액 ±0.51 · 100 넘음) → S 하위.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
COSTS = ("노무비", "재료비", "경비")
|
||||
TOL = 0.005
|
||||
_SURCHARGE = re.compile(r"할증\s*(\d+(?:\.\d+)?)\s*%")
|
||||
_KIND = {
|
||||
"노임": "L",
|
||||
"기계": "X",
|
||||
"자재": "M",
|
||||
"별도계상": "M",
|
||||
"%": "R",
|
||||
"단가산출": "S",
|
||||
"호표": "S",
|
||||
}
|
||||
|
||||
|
||||
def near(a: Any, b: Any, tol: float = TOL) -> bool:
|
||||
if a is None or b is None:
|
||||
return False
|
||||
a, b = float(a), float(b)
|
||||
return abs(a - b) <= max(tol * max(abs(a), abs(b)), 1.0)
|
||||
|
||||
|
||||
def money(d: Any) -> float | None:
|
||||
"""금액 · 단가 칸 → 수(계 › 비목 합 · 수면 그대로)."""
|
||||
if isinstance(d, (int, float)) and not isinstance(d, bool):
|
||||
return float(d)
|
||||
if not isinstance(d, dict):
|
||||
return None
|
||||
if d.get("계") not in (None, ""):
|
||||
return float(d["계"])
|
||||
vals = [float(d[c]) for c in COSTS if d.get(c) not in (None, "")]
|
||||
return sum(vals) if vals else None
|
||||
|
||||
|
||||
def machine_done(one: dict[str, Any]) -> None:
|
||||
"""기계 줄 마무리(두 어댑터) — 비목마다 매개값이 다르면 `계수`."""
|
||||
one.pop("_순", None)
|
||||
got_q, main = one.pop("_q", {}), one.get("품")
|
||||
if main:
|
||||
ratio = {
|
||||
k: round(float(q) / main, 9)
|
||||
for k, q in got_q.items()
|
||||
if q is not None and abs(float(q) / main - 1) > 1e-6
|
||||
}
|
||||
if ratio:
|
||||
one["계수"] = ratio
|
||||
|
||||
|
||||
def _parts(price: dict[str, Any]) -> float | None:
|
||||
vals = [float(price[c]) for c in COSTS if price.get(c) not in (None, "")]
|
||||
return sum(vals) if vals else money(price)
|
||||
|
||||
|
||||
def redo(one: dict[str, Any]) -> bool:
|
||||
"""줄 재현 — 단가 × 매개값(× 계수) ≈ 금액."""
|
||||
kind, q, got = one["묶음"], one.get("품"), one.get("금액")
|
||||
if one.get("어떻게") == "별도" or (kind == "E" and q is None):
|
||||
return True
|
||||
if kind == "S" and q is None:
|
||||
return got is not None
|
||||
if kind == "R":
|
||||
return bool(q is not None and near((one.get("기준값") or 0) * q, got))
|
||||
price = one.get("단가") or {}
|
||||
if q is None or got is None:
|
||||
return False
|
||||
if kind == "X":
|
||||
ratio = one.get("계수") or {}
|
||||
want = sum(float(price[c]) * q * ratio.get(c, 1) for c in COSTS if price.get(c))
|
||||
return near(want, got)
|
||||
unit = _parts(price)
|
||||
return bool(unit is not None and (near(unit * q, got) or (unit == 0 and got == 0)))
|
||||
|
||||
|
||||
def _subs(units: list[dict[str, Any]], totals: dict[tuple[str, int], float]) -> None:
|
||||
"""값 줄 첫 수가 다른 블록 · 호표 1 단위 금액이면 하위."""
|
||||
for one in units:
|
||||
p = money(one.get("단가")) if one["묶음"] == "E" else None
|
||||
if not p or p < 100:
|
||||
continue
|
||||
hit = next((k for k, v in totals.items() if abs(p - v) <= 0.51), None)
|
||||
if hit:
|
||||
got = one.get("금액")
|
||||
q = round(float(got) / p, 6) if got is not None else None
|
||||
one.update(묶음="S", 품=q, 하위={"종류": hit[0], "번": hit[1]}, 어떻게="QTY")
|
||||
|
||||
|
||||
def _labor_part(one: dict[str, Any]) -> float:
|
||||
"""기계 줄 노무비 몫 = 노무 단가 × 시간(× 계수)."""
|
||||
price, q = (one.get("단가") or {}).get("노무비"), one.get("품")
|
||||
return float(price) * q * (one.get("계수") or {}).get("노무비", 1) if price and q else 0.0
|
||||
|
||||
|
||||
def base_rows(units: list[dict[str, Any]]) -> None:
|
||||
"""율 줄 기준값 = 어느 줄 합인가 → `기준꼴` · `기준줄 [행]`(못 찾으면 안 붙임) — 차례:
|
||||
바로 앞 같은 묶음 줄들(절 소계) › 앞 인력 › 앞 인력 + 기계 노무 › 앞 자재 › 앞 자재 + 율 ›
|
||||
앞 줄 모두 › 블록 인력 › 블록 자재."""
|
||||
live = [u for u in units if not u.get("밖") and u.get("금액")]
|
||||
for i, one in enumerate(units):
|
||||
if one["묶음"] != "R" or not one.get("기준값"):
|
||||
continue
|
||||
prev = [u for u in units[:i] if u in live]
|
||||
run: list[dict[str, Any]] = []
|
||||
for u in reversed(prev):
|
||||
if u["묶음"] == "R" or (run and u["묶음"] != run[0]["묶음"]):
|
||||
break
|
||||
run.insert(0, u)
|
||||
of = lambda us, k: [u for u in us if u["묶음"] in k] # noqa: E731
|
||||
pools = [
|
||||
("절 소계", run, None),
|
||||
("인력", of(prev, "L"), None),
|
||||
("노무비", of(prev, "LX"), "L+X노무"),
|
||||
("자재", of(prev, "M"), None),
|
||||
("자재 + 율", of(prev, "MR"), None),
|
||||
("앞 줄 모두", of(prev, "LXM"), None),
|
||||
("인력(블록)", of(live, "L"), None),
|
||||
("자재(블록)", of(live, "M"), None),
|
||||
]
|
||||
for name, pool, how in pools:
|
||||
total = sum(
|
||||
_labor_part(u) if how and u["묶음"] == "X" else float(u["금액"]) for u in pool
|
||||
)
|
||||
if pool and near(total, one["기준값"]):
|
||||
one["기준꼴"], one["기준줄"] = name, [u.get("행") for u in pool]
|
||||
break
|
||||
|
||||
|
||||
def check_block(block: dict[str, Any], totals: dict[tuple[str, int], float]) -> None:
|
||||
units = block.get("항목") or []
|
||||
own = ("단산", int(block["번"]))
|
||||
_subs(units, {k: v for k, v in totals.items() if k != own})
|
||||
base_rows(units)
|
||||
for one in units:
|
||||
if one["묶음"] == "X" and (hit := _SURCHARGE.search(f"{one['이름']} {one['규격']}")):
|
||||
one["할증"] = round(float(hit.group(1)) / 100, 4)
|
||||
one["재현"] = redo(one)
|
||||
total = money(block.get("금액"))
|
||||
lines = round(sum(float(one.get("금액") or 0) for one in units if not one.get("밖")), 4)
|
||||
block["줄합"] = lines
|
||||
block["덮음"] = near(lines, total) if total else None
|
||||
|
||||
|
||||
def check_hopyo(hopyo: dict[str, Any]) -> None:
|
||||
"""일위대가 줄 → 단위 줄 투영(묶음 · 재현 · % 기준) · 호표 줄합 · 덮음."""
|
||||
run = dict.fromkeys(COSTS, 0.0)
|
||||
main = lines = 0.0
|
||||
for line in hopyo.get("줄") or []:
|
||||
kind = _KIND.get(str(line.get("갈래")), "E")
|
||||
q, got = line.get("수량"), money(line.get("금액"))
|
||||
line["묶음"] = kind
|
||||
if line.get("갈래") == "별도계상":
|
||||
line["재현"] = True
|
||||
elif kind == "R":
|
||||
spec = re.sub(r"\s+", "", str(line.get("규격") or ""))
|
||||
base = (
|
||||
"주재료비"
|
||||
if "주재료" in spec
|
||||
else "재료비"
|
||||
if "재료비" in spec
|
||||
else "경비"
|
||||
if spec.startswith("경비")
|
||||
else "노무비"
|
||||
)
|
||||
line["기준"] = base
|
||||
value = main if base == "주재료비" else run[base]
|
||||
line["재현"] = bool(q and got is not None and near(value * float(q) / 100, got))
|
||||
else:
|
||||
p = money(line.get("단가"))
|
||||
line["재현"] = bool(q is not None and p is not None and near(float(q) * p, got))
|
||||
if kind != "R":
|
||||
for c in COSTS:
|
||||
run[c] += float((line.get("금액") or {}).get(c) or 0)
|
||||
if line.get("갈래") == "자재":
|
||||
main += float((line.get("금액") or {}).get("재료비") or 0)
|
||||
lines += got or 0
|
||||
total = money(hopyo.get("금액"))
|
||||
hopyo["줄합"] = round(lines, 4)
|
||||
hopyo["덮음"] = near(lines, total) if total else None
|
||||
|
||||
|
||||
def settle(book: dict[str, Any]) -> dict[str, Any]:
|
||||
"""읽은 책(HP 꼴) → 블록 단위 줄 하위 가름 · 재현 · 덮음 + 일위대가 줄 투영(제자리)."""
|
||||
totals: dict[tuple[str, int], float] = {}
|
||||
for block in book.get("단산") or []:
|
||||
if (v := money(block.get("금액"))) is not None:
|
||||
totals[("단산", int(block["번"]))] = v
|
||||
for hopyo in book.get("호표") or []:
|
||||
if (v := money(hopyo.get("금액"))) is not None:
|
||||
totals[("일위", int(hopyo["번"]))] = v
|
||||
for block in book.get("단산") or []:
|
||||
check_block(block, totals)
|
||||
for hopyo in book.get("호표") or []:
|
||||
check_hopyo(hopyo)
|
||||
return book
|
||||
Reference in New Issue
Block a user