Merge branch 'main_laptop_1' of C:/Program_coding/임도설계 및 견적자동화 프로그램 개발 into sub_laptop_3
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""M01 로직 단위 글을 건설품셈 원문 꼴로 (PLAN 801-5 · 요구 663-2) — 글만 바꿈 · 셈 지문 그대로.
|
||||
|
||||
돌리기: ./venv/Scripts/python.exe resources/master_data/scripts/build_단위표기_801_5.py [--check | --list 파일.xlsx]
|
||||
- 대상 = M01 로직 줄의 입력 · 중간 변수 `단위` · `결과단위` — 규칙 · 근거 절 = `master_unit_text.RULES`.
|
||||
- 계수 · 소요량 표 값칸 · 호표 줄 단위 · 사용자가 이미 원문 꼴로 쓴 칸은 안 건드림(이미 같으면 그대로 — 두 번 돌려도 같음).
|
||||
- 쓰기 전 지문 대조 = 줄마다 엔진 셈 · 끝수 지문이 전후 같을 때만 씀(다르면 멈춤).
|
||||
- `--check` = 쓰지 않고 쓸 것이 남았는지만(남았으면 종료 코드 1) · `--list` = 검토 목록 xlsx 만 씀(마스터 안 씀).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE.parents[2]))
|
||||
import master_auto as ma # noqa: E402
|
||||
import master_cut as mcut # noqa: E402
|
||||
import master_formula as mf # noqa: E402
|
||||
import master_unit_text as ut # noqa: E402
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as st # noqa: E402
|
||||
|
||||
|
||||
def plan(files: dict[str, dict]) -> tuple[list[dict], list[dict], dict[str, dict]]:
|
||||
"""(저장 묶음, 목록 줄들, 새 files) — 읽기 전용."""
|
||||
batch, rows, new_files = [], [], {}
|
||||
for name, data in files.items():
|
||||
if data.get("그룹") != "로직":
|
||||
new_files[name] = data
|
||||
continue
|
||||
changes, new_data = [], copy.deepcopy(data)
|
||||
for row in new_data.get("줄", []):
|
||||
hit = False
|
||||
for kind in ("입력", "중간"):
|
||||
for spec in row.get(kind) or []:
|
||||
now = ut.canon(spec.get("단위"))
|
||||
if now != (spec.get("단위") or ""):
|
||||
rows.append(_item(row, kind, spec["이름"], spec["단위"], now))
|
||||
spec["단위"], hit = now, True
|
||||
if row.get("결과단위"):
|
||||
now = ut.canon(row["결과단위"])
|
||||
if now != row["결과단위"]:
|
||||
rows.append(_item(row, "결과단위", "", row["결과단위"], now))
|
||||
row["결과단위"], hit = now, True
|
||||
if hit:
|
||||
changes.append({"op": "edit", "key": row["키"], "row": row})
|
||||
new_files[name] = new_data
|
||||
if changes:
|
||||
batch.append({"file": name, "version": st.read(name)[1], "changes": changes})
|
||||
return batch, rows, new_files
|
||||
|
||||
|
||||
def _item(row: dict, kind: str, var: str, before: str, after: str) -> dict:
|
||||
return {
|
||||
"로직": row["키"],
|
||||
"이름": row.get("이름", ""),
|
||||
"칸": kind,
|
||||
"변수": var,
|
||||
"지금": before,
|
||||
"원문 꼴": after,
|
||||
"규칙": ut.rule_of(before),
|
||||
}
|
||||
|
||||
|
||||
def fingerprint(whole: mf.Master, row: dict) -> str:
|
||||
"""줄 하나의 엔진 셈 · 끝수 지문 — 멈춤도 지문."""
|
||||
try:
|
||||
key = str(row["키"])
|
||||
raw = mf.run(
|
||||
whole,
|
||||
key,
|
||||
{
|
||||
**ma.values(whole, row),
|
||||
**{k: v for k, v in ma.defaults(whole, row).items() if v is not None},
|
||||
},
|
||||
)
|
||||
text = json.dumps(raw, default=str, sort_keys=True, ensure_ascii=False)
|
||||
text += json.dumps(
|
||||
mcut.engine(whole, key, raw), default=str, sort_keys=True, ensure_ascii=False
|
||||
)
|
||||
except Exception as e: # noqa: BLE001
|
||||
text = f"ERR {type(e).__name__} {e}"
|
||||
return hashlib.sha256(text.encode()).hexdigest()[:16]
|
||||
|
||||
|
||||
def differing(old: dict[str, dict], new: dict[str, dict]) -> list[str]:
|
||||
"""지문이 바뀐 로직 키들 — 고친 줄만 대조."""
|
||||
before, after = mf.Master(old), mf.Master(new)
|
||||
out = []
|
||||
for name, data in new.items():
|
||||
if data.get("그룹") != "로직":
|
||||
continue
|
||||
was = {r["키"]: r for r in old[name]["줄"]}
|
||||
for row in data["줄"]:
|
||||
if row != was[row["키"]] and fingerprint(before, was[row["키"]]) != fingerprint(
|
||||
after, row
|
||||
):
|
||||
out.append(row["키"])
|
||||
return out
|
||||
|
||||
|
||||
def write_list(rows: list[dict], path: Path) -> None:
|
||||
import openpyxl
|
||||
|
||||
book = openpyxl.Workbook()
|
||||
sheet = book.active
|
||||
sheet.title = "바뀜"
|
||||
cols = ["로직", "이름", "칸", "변수", "지금", "원문 꼴", "규칙"]
|
||||
sheet.append(cols)
|
||||
for r in rows:
|
||||
sheet.append([r[c] for c in cols])
|
||||
rule = book.create_sheet("대응표")
|
||||
rule.append(["규칙", "바꿀 글", "근거 절(건설공사 표준품셈 본문 아래)", "바뀐 칸 수"])
|
||||
for name, _, to, _, files in ut.RULES:
|
||||
rule.append([name, to, " · ".join(files), sum(name in r["규칙"] for r in rows)])
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
book.save(path)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
files, _ = st.loaded()
|
||||
batch, rows, new_files = plan(files)
|
||||
print(f"쓸 것 — 로직 {len({r['로직'] for r in rows})} · 칸 {len(rows)}")
|
||||
if "--list" in sys.argv:
|
||||
write_list(rows, Path(sys.argv[sys.argv.index("--list") + 1]))
|
||||
return
|
||||
if "--check" in sys.argv:
|
||||
raise SystemExit(1 if batch else 0)
|
||||
if batch:
|
||||
bad = differing(files, new_files)
|
||||
if bad:
|
||||
raise SystemExit(f"셈 지문이 달라지는 로직 {len(bad)} — 멈춤: {bad[:10]}")
|
||||
print(st.save(batch))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -28,6 +28,7 @@ from pathlib import Path
|
||||
|
||||
import master_auto as ma
|
||||
import master_cut as mcut
|
||||
import master_equation_group as grp
|
||||
import master_equation_view as view
|
||||
import master_formula as mf
|
||||
import master_material as mm
|
||||
@@ -401,36 +402,30 @@ def _line(ctx: _Ctx, kind: str, key: str, text: str, value, **more) -> dict:
|
||||
|
||||
|
||||
def subtotals(logic: dict, cut: dict, total: dict) -> dict:
|
||||
"""호표 줄 → 소계(799-1 · 보이기만) — 하위 로직 줄 = 블록마다 · 그 밖 자원 · 덧줄은 한 줄씩 + 끝수.
|
||||
"""호표 줄 → 소계 묶음(799-1 · 801-3 · 보이기만) — 부른 단가산출 = 묶음 하나씩 · 직접 줄 = 한 묶음 + 끝수.
|
||||
|
||||
값 = `master_cut.engine` 이 버린 줄 비목(0.1 원) 그대로 · 합계 = 엔진 비목 합(1 원 버림) ·
|
||||
끝수 = 합계 − 줄 합(금액 셈은 안 바꿈 · 사용자 확정 뒤에 정함).
|
||||
끝수 = 합계 − 묶음 합(금액 셈은 안 바꿈 · 사용자 확정 뒤에 정함). 묶음 꼴 = `master_equation_group`.
|
||||
"""
|
||||
items, adds = logic.get("호표") or [], logic.get("덧줄") or []
|
||||
items = logic.get("호표") or []
|
||||
skip = mcut.ms.skipped(logic) if mcut.ms.uses(logic) else set()
|
||||
rows: list[dict] = []
|
||||
pool: dict[str, dict] = {}
|
||||
for n, line in enumerate(cut.get("줄") or []):
|
||||
if n in skip:
|
||||
continue
|
||||
split = {k: line.get("비목", {}).get(k) or Decimal(0) for k in mf.COST_ITEMS}
|
||||
if n < len(items) and items[n].get("종류") == "로직":
|
||||
rows.append({"갈래": "블록", "이름": str(items[n].get("이름") or ""), **split})
|
||||
continue
|
||||
kind = "덧줄" if n >= len(items) else "자원"
|
||||
one = pool.setdefault(
|
||||
kind, {"갈래": kind, "이름": kind, **dict.fromkeys(mf.COST_ITEMS, Decimal(0))}
|
||||
item = items[n] if n < len(items) else {}
|
||||
kind = "블록" if item.get("종류") == "로직" else "자원" if item else "덧줄"
|
||||
rows.append(
|
||||
{"갈래": kind, "이름": str(item.get("이름") or ""), "종류": item.get("종류"), **split}
|
||||
| {"계": sum(split.values(), Decimal(0))}
|
||||
)
|
||||
for k in mf.COST_ITEMS:
|
||||
one[k] += split[k]
|
||||
rows += [pool[k] for k in ("자원", "덧줄") if k in pool]
|
||||
for one in rows:
|
||||
one["계"] = sum((one[k] for k in mf.COST_ITEMS), Decimal(0))
|
||||
got = grp.bundles(rows)
|
||||
rest = {
|
||||
k: (total.get(k) or Decimal(0)) - sum((one[k] for one in rows), Decimal(0))
|
||||
k: (total.get(k) or Decimal(0)) - sum((one[k] for one in got["줄"]), Decimal(0))
|
||||
for k in mf.BUNDLE
|
||||
}
|
||||
return {"줄": rows, "끝수": rest}
|
||||
return {"줄": got["줄"], "끝수": rest}
|
||||
|
||||
|
||||
def export(master, key: str, given: dict | None = None, depth: int = 0) -> dict:
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""수식 탭 소계 묶음(PLAN 801-3 · 요구 662-3) — 돈 묶음 한 곳 · M01(`master_equation`) · M02(`M02_Sangeun_Equation_Full`) 같은 꼴.
|
||||
|
||||
묶음 = 부른 · 더한 단가산출(갈래 블록) 하나씩 + 직접 줄(인력 · 재료 · 기계 · 덧줄 · 율) 전부 한 묶음(갈래 직접).
|
||||
돈 없는 줄(계 0 · 바탕) · 뺀 줄 = 묶음 아님(식 줄로만 보임) · 합계 밖 줄 = `밖` 으로만.
|
||||
머리 · 소계 · 합계 · 계 글자 판정 = 화면 부품(`ui_template_equation_sum.ts`) — 묶음 둘 이상 = 머리 · 소계 ·
|
||||
단가산출(블록) 둘 이상 = 합계 · 아니면 계.
|
||||
시험 `resources/tester/test_801_3_groups.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
COSTS = ("노무비", "재료비", "경비")
|
||||
_WORDS = ("인력", "재료", "기계") # 직접 묶음 이름 차례
|
||||
|
||||
|
||||
def _zero(row: dict) -> bool:
|
||||
"""돈 없는 바탕 줄 · 뺀 줄 — 묶음 아님."""
|
||||
if row.get("상태") == "뺌":
|
||||
return True
|
||||
return row["계"] == 0 and row.get("상태") in (None, "바탕")
|
||||
|
||||
|
||||
def _name(direct: list[dict]) -> str:
|
||||
"""직접 묶음 이름 — 자원 갈래 말(인력 · 재료 · 기계) · 율 줄 = 이름 (율 %) · 덧줄뿐 = 비목 말."""
|
||||
words = [w for w in _WORDS if any(r.get("종류") == w for r in direct)]
|
||||
words += [
|
||||
f"{r['이름']} ({r['수량']}%)" if r.get("수량") not in (None, "") else str(r["이름"])
|
||||
for r in direct
|
||||
if r["갈래"] == "율"
|
||||
]
|
||||
if not words:
|
||||
words = [k for k in COSTS if any(r[k] != 0 for r in direct)]
|
||||
return " · ".join(words)
|
||||
|
||||
|
||||
def bundles(rows: list[dict]) -> dict:
|
||||
"""줄(갈래 블록 | 자원 | 덧줄 | 율 · 비목 칸 · 계) → `{줄: 묶음들, 밖: 합계 밖 줄들}` — 금액은 더하기만(셈 안 바꿈)."""
|
||||
blocks: list[dict] = []
|
||||
direct: list[dict] = []
|
||||
out: list[dict] = []
|
||||
for row in rows:
|
||||
if row.get("합밖"):
|
||||
out.append({"이름": row["이름"], "밖금액": row.get("밖금액")})
|
||||
elif _zero(row):
|
||||
continue
|
||||
elif row["갈래"] == "블록":
|
||||
blocks.append(row)
|
||||
else:
|
||||
direct.append(row)
|
||||
lines = list(blocks)
|
||||
if direct:
|
||||
one = {
|
||||
"갈래": "직접",
|
||||
"이름": _name(direct),
|
||||
**{k: sum((r[k] for r in direct), Decimal(0)) for k in COSTS},
|
||||
}
|
||||
one["계"] = sum((one[k] for k in COSTS), Decimal(0))
|
||||
mine = [ # 더한 · 덮은 줄 — 묶음 속에 한 줄씩(고친 줄을 숨기지 않음)
|
||||
{k: r[k] for k in ("이름", "상태", "계", "원래") if r.get(k) is not None}
|
||||
for r in direct
|
||||
if r.get("상태") in ("더함", "덮음")
|
||||
]
|
||||
if mine:
|
||||
one["낱줄"] = mine
|
||||
lines.append(one)
|
||||
return {"줄": lines, "밖": out}
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""M01 로직 단위 글 → 건설품셈 원문 꼴(PLAN 801-5 · 요구 663-2) — 글만 바꿈 · 셈 · 환산 안 바꿈.
|
||||
|
||||
`canon(단위 글)` = 아래 `RULES` 를 차례로 적용(이미 원문 꼴이면 그대로 · 두 번 돌려도 같음).
|
||||
`RULES` 한 줄 = (이름, 지금 글 정규식, 바꿀 글, 원문 글, 근거 절들) — 근거 절 = `EVIDENCE_ROOT` 아래 파일 ·
|
||||
그 파일 안에 원문 글이 실제로 있어야 함(시험 `test_801_5_units`). 원문이 두 꼴을 다 쓰는 단위(㎞ · km · 분 · min)는 안 건드림.
|
||||
단위 칸 중 원문 표를 그대로 옮긴 계수 · 소요량 표 값칸 · 호표 줄 단위(요소 단위)는 대상 아님.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
EVIDENCE_ROOT = Path("resources/knowledge/original/원가계산/건설공사_표준품셈/본문")
|
||||
_A = r"(?<![A-Za-z])" # 영문 낱말 한가운데는 안 건드림
|
||||
_Z = r"(?![A-Za-z])"
|
||||
|
||||
# (이름, 지금 글 정규식, 바꿀 글, 원문 글, 근거 절들)
|
||||
RULES: list[tuple[str, str, str, str, list[str]]] = [
|
||||
(
|
||||
"시간당 ㎥ · ㎡ → hr",
|
||||
r"(?<=[㎥㎡])/시간(?![가-힣])",
|
||||
"/hr",
|
||||
"/hr",
|
||||
[
|
||||
"01_공통부문/제08장_건설기계/8-03-05_[40]콘크리트기계.md", # ㎥/hr
|
||||
"02_토목부문/제07장_항만공사/7-02_사석.md", # ㎡/hr
|
||||
],
|
||||
),
|
||||
(
|
||||
"사이클 시간 초 → sec",
|
||||
r"(?<![가-힣])초(?![가-힣])",
|
||||
"sec",
|
||||
"sec",
|
||||
[
|
||||
"01_공통부문/제05장_기초공사/5-02_연약지반처리.md", # cm : 1회 싸이클 타임(sec)
|
||||
"01_공통부문/제08장_건설기계/8-02-02_리퍼(유압식).md",
|
||||
],
|
||||
),
|
||||
(
|
||||
"mm → ㎜",
|
||||
rf"{_A}mm{_Z}",
|
||||
"㎜",
|
||||
"㎜",
|
||||
["04_기계설비부문/제13장_플랜트설비공사/13-01_플랜트_배관.md"],
|
||||
),
|
||||
(
|
||||
"cm → ㎝",
|
||||
rf"{_A}cm{_Z}",
|
||||
"㎝",
|
||||
"㎝",
|
||||
["01_공통부문/제08장_건설기계/8-03-06_[50]골재생산기계_등.md"],
|
||||
),
|
||||
("kg → ㎏", rf"{_A}kg{_Z}", "㎏", "㎏", ["01_공통부문/제01장_적용기준/1-02_설계_및_수량.md"]),
|
||||
(
|
||||
"km/hr → ㎞/hr",
|
||||
rf"{_A}km/hr",
|
||||
"㎞/hr",
|
||||
"㎞/hr",
|
||||
["01_공통부문/제08장_건설기계/8-02-09_롤러.md"],
|
||||
),
|
||||
(
|
||||
"TON · t → ton",
|
||||
rf"{_A}(?:TON|t){_Z}",
|
||||
"ton",
|
||||
"ton",
|
||||
["01_공통부문/제08장_건설기계/8-02-12_크러셔.md"],
|
||||
),
|
||||
]
|
||||
_COMPILED = [(name, re.compile(pat), to, tok, files) for name, pat, to, tok, files in RULES]
|
||||
|
||||
|
||||
def canon(unit) -> str:
|
||||
"""원문 꼴 단위 글 — 규칙에 안 걸리면 그대로."""
|
||||
text = str(unit or "")
|
||||
for _, pat, to, _, _ in _COMPILED:
|
||||
text = pat.sub(to, text)
|
||||
return text
|
||||
|
||||
|
||||
def rule_of(before: str) -> str:
|
||||
"""바뀐 단위가 걸린 규칙 이름들(검토 목록 · 대응표용)."""
|
||||
out, text = [], str(before or "")
|
||||
for name, pat, to, _, _ in _COMPILED:
|
||||
if pat.search(text):
|
||||
out.append(name)
|
||||
text = pat.sub(to, text)
|
||||
return " · ".join(out)
|
||||
Reference in New Issue
Block a user