Author SHA1 Message Date
eomsangdonandClaude Sonnet 5.5 2094436577 feat(802-2a): GC000100 암파쇄(유압식 할암공법) 수식 문서 넣음 — 단가산출 부름 값 = 끝수 뒤 채택(요구 714-3)
- 빌더 ADOPTED(채택된 차이) — GC000100 지역=경북 재료비 1 원 · 목록에 남기고 저장은 막지 않음 · 요약에 「채택된 차이」
- 수식 문서 1,518 → 1,519(건설품셈 03장 토공사 파일에 GC000100 49 줄 더함 · 다른 문서 그대로)
- test_802_2 기대 바꿈(채택 차이 · 저장 1,519) · test_802_3 「빈 문서」 길은 임시 사본에서 GC000100 문서를 지워 봄(낡은 기대: GC000100 문서 없음)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 13:19:17 +09:00
eomsangdon 22588ea7c4 Merge branch 'sub_laptop_2' of C:/Aislo_wt/sub_laptop_2 into main_laptop_1 2026-11-09 10:10:09 +09:00
eomsangdonandClaude Opus 5.5 fbaf2f1d28 knowledge(812-2): 연결 판정 목록 — 전수 2,220 블록(확정 152 · 근접 588 · 없음 1,480) · 다른 항 · 참고 · 810-4 전후
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 10:09:21 +09:00
eomsangdonandClaude Opus 5.5 f1abfc2fb1 fix(812-2): 연결 찾기 로직 대조 다듬음 — 원파일 기계경비 묶음 ↔ 우리 기계 = 이름 종류 짝 · 율 대상 = (자원 갈래, 비목) · 번지 합 · 비목 이름 몫
- 기계경비 숫자 묶음(코드 없음) ↔ 우리 기계 = 이름 머리로 같은 기계(로울러 = 롤러 · 물탱크 = 살수차 등) · 시간당 단가 · 구성 = 참고
- 로직 같은 짝을 먼저 지음(같은 수량 줄 엇갈림 막음) · 한쪽 단가 없음(0) 비목 = 값
- 율 = 번지 합이면 핵심 줄 전부 · 「노무비 × n %」 = 그 비목 몫(기계 운전 노무비 포함) · 대상 = (갈래, 비목) 묶음
- test_812_2 — 기계 이름 짝 · 율 대상 범위 시험 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:38:19 +09:00
eomsangdonandClaude Opus 5.5 1e4ce036c6 feat(812-2): 연결 찾기 확정 = 로직 — 단가 · 끝수 = 참고(확정을 안 막음) · 율 값 = 비율 · 노임 · 자재 종류 · 산출 단가 = 다른 항
- M02_Doc_Match_Note(새) — 율 = 율 줄 셈값 ÷ 대상 줄 금액 합(대상 비목) · 짝 줄 다른 항 대 참고 가르기
- M02_Doc_Match — 같은 줄 = 로직 같은 줄(코드 다름은 짝 대조로) · 연결후보 `참고` 칸 · 참고 적은 조합 · 후보 먼저
- 띠 글 「확정 — 로직 같음 · 참고: 단가 다름 n 줄 · 우리 단가로 셈 ○○ 원」
- test_810_4 기대 고침(단가 차이 · 끝수 = 참고로 옮김) · 전수 도우미 확정 다시 셈 = 우리 금액 · 확정 참고 셈

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:03:34 +09:00
12 changed files with 580 additions and 53 deletions
+61 -31
View File
@@ -7,9 +7,10 @@
② 옵션 풀이 = 그 문서의 선택 · 입력 변수 값을 줄마다 좁힘(줄 수량 · 단가가 의존하는 변수만 돌림 ·
값이 가져온 줄 수량 · 단가 묶음에 있거나 0(꺼진 줄)인 값만 남김) → 남은 조합(상한 `CAP_ALL`)을
문서 셈 · 입력 변수 후보 = 기본값 · 0 · 가져온 식에 나온 수 · 선택은 가져온 글에 든 값부터.
③ 판정 = 줄 대조(수량 · 단가 · 비목 금액 · 율 줄) + 노 · 재 · 경 · 계 —
③ 판정 = 줄 대조 = 로직(812-2 · 요구 712 — `M02_Doc_Match_Note`): 다른 항 = 자원 · 기계 규격 · 수량 ·
비목 · 율 구조(율 = 비율) · 줄 없음 · 이름 · 참고 = 단가 · 끝수(값 — 확정을 안 막음) —
다른 항 0 = 「확정」 · 같은 줄이 하나 넘고 핵심 줄 반 넘게 짝 = 「근접」(다른 항 목록) · 그 밖 「없음」.
결과 `{판정, 키, 이름, 옵션, 다른, 금액 {가져온, 우리}, 후보수}` = 가져온 식 원본 `연결후보`.
결과 `{판정, 키, 이름, 옵션, 다른, 참고, 금액 {가져온, 우리}, 후보수}` = 가져온 식 원본 `연결후보`.
"""
from __future__ import annotations
@@ -22,6 +23,7 @@ from decimal import Decimal, InvalidOperation
from typing import Any
from M02_MasterTemplete import M02_Doc_Copy as dc
from M02_MasterTemplete import M02_Doc_Match_Note as note
md, mc, ml = dc.md, dc.mc, dc.ml
if not hasattr(
@@ -133,6 +135,7 @@ def facts(doc: dict, got: dict) -> list[dict]:
"합밖": bool(row.get("합밖")),
}
)
note.rates(doc, got, out)
return out
@@ -405,7 +408,7 @@ def _ratio(a: Any, b: Any) -> str | None:
def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]:
"""가져온 줄(mine) ↔ M01 줄(ours) → {같음, 짝, 핵심, 다른 [{항, 글}]}."""
"""가져온 줄(mine) ↔ M01 줄(ours) → {같음(로직 같은 줄), 짝, 핵심, 다른 [{항, 글}], 참고 [{항, 글}]}."""
left, right = list(mine), list(ours)
same = paired = 0
diffs: list[dict] = []
@@ -426,22 +429,29 @@ def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]:
def cls(a: dict, b: dict) -> bool:
return a["갈래"] == b["갈래"] or {a["갈래"], b["갈래"]} <= {"기계", "산출"}
notes: list[dict] = []
exact = take(
lambda a, b: (
a["수량"] == b["수량"] and a["단가"] == b["단가"] and a["비목"] == b["비목"]
if core(a, b)
else a["갈래"] == b["갈래"] == "율" and a["비목"] == b["비목"]
core(a, b)
and a["수량"] == b["수량"]
and a["단가"] == b["단가"]
and a["비목"] == b["비목"]
and (a["코드"] == b["코드"] or not (a["코드"] and b["코드"])) # 코드 다름 = 짝 대조
)
)
same += sum(1 for a, _ in exact if a["갈래"] != "율")
for a, b in take(lambda a, b: core(a, b) and cls(a, b) and a["수량"] == b["수량"]):
paired += 1
if a["갈래"] == "기계" and b["갈래"] == "기계" and a["코드"] != b["코드"]:
diffs.append({"항": "기계 규격", "글": f"{a['이름']} {a['코드']} ↔ {b['코드']}"})
elif a["단가"] != b["단가"]:
diffs.append({"항": "단가 차이", "글": f"{a['이름']} 단가 {a['단가']} ↔ {b['단가']}"})
else:
diffs.append({"항": "비목", "글": f"{a['이름']} 비목 다름"})
same += len(exact)
def twin(a: dict, b: dict) -> bool:
return core(a, b) and cls(a, b) and a["수량"] == b["수량"]
for a, b in take(lambda a, b: twin(a, b) and not note.pair(a, b)[0]): # 로직 같은 짝 먼저
same += 1
notes += note.pair(a, b)[1]
for a, b in take(twin):
bad, ref = note.pair(a, b)
same, paired = (same + 1, paired) if not bad else (same, paired + 1)
diffs += bad
notes += ref
for a, b in take(lambda a, b: core(a, b) and cls(a, b) and a["단가"] == b["단가"]):
paired += 1
how = _ratio(a["수량"], b["수량"])
@@ -452,10 +462,7 @@ def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]:
diffs.append(
{"항": "기계 규격", "글": f"{a['이름']} {a['코드']} ↔ {b['코드']} · 수량 다름"}
)
for a, b in take(lambda a, b: a["갈래"] == b["갈래"] == "율" and a["금액"] == b["금액"]):
diffs.append({"항": "비목", "글": f"율 줄 {a['이름']} 비목 다름"})
for a, b in take(lambda a, b: a["갈래"] == b["갈래"] == "율"):
diffs.append({"항": "율 값", "글": f"{a['이름']} {a['금액']} ↔ {b['이름']} {b['금액']}"})
diffs += note.rate_pairs(left, right, take)
for side, rows in (("가져온 쪽만", left), ("우리 쪽만", right)):
for x in rows:
if x["갈래"] == "율":
@@ -464,7 +471,7 @@ def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]:
what = "줄 없음"
diffs.append({"항": what, "글": f"{side} — {x['이름']}"})
n_core = sum(1 for a in mine if a["갈래"] != "율")
return {"같음": same, "짝": same + paired, "핵심": n_core, "다른": diffs}
return {"같음": same, "짝": same + paired, "핵심": n_core, "다른": diffs, "참고": notes}
def _totals(got: dict) -> dict:
@@ -473,27 +480,31 @@ def _totals(got: dict) -> dict:
EXPLAINED = (
"이름 다름",
"단가 차이",
"자원",
"산출 단가",
"율 대상",
"단위 환산",
"기계 규격",
"비목",
"율 값",
"율 줄",
"공구손료 유무",
"끝수",
)
def verdict(
mine: list[dict], ours: list[dict], got_a: dict, got_b: dict, off: list[dict] = ()
) -> dict:
"""확정 = 다른 항 0(옵션 글 근거까지 맞음) · 근접 = 핵심 줄 반 넘게 짝 + (같은 줄 있음 또는
다른 항이 모두 까닭 있는 갈래 — 단가 · 단위 환산 · 기계 규격 · 비목 · 율) · 그 밖 없음."""
"""확정 = 다른 항 0(옵션 글 근거까지 맞음 · 참고는 있어도 됨) · 근접 = 핵심 줄 반 넘게 짝 + (같은 줄
있음 또는 다른 항이 모두 까닭 있는 갈래 — 자원 · 단위 환산 · 기계 규격 · 비목 · 율) · 그 밖 없음 ·
끝수 = 줄 로직 · 단가 모두 같은데 합만 다름(참고)."""
res = compare(mine, ours)
res["다른"] = [*off, *res["다른"]]
ta, tb = _totals(got_a), _totals(got_b)
if not res["다른"] and ta != tb:
res["다른"].append({"항": "끝수", "글": f"줄은 같고 합이 다름 {ta} ↔ {tb}"})
if not res["다른"] and not res["참고"] and ta != tb:
res["참고"].append(
{"항": "끝수", "글": f"줄은 같고 합이 다름 {_sum_txt(ta)} ↔ {_sum_txt(tb)}"}
)
if not res["다른"] and res["같음"]:
res["판정"] = SURE
elif res["짝"] * 2 >= res["핵심"] + 1 and (
@@ -505,6 +516,10 @@ def verdict(
return res
def _sum_txt(t: dict) -> str:
return " · ".join(f"{k} {note.txt(v)}" for k, v in t.items() if v is not None)
# ── 한 문서 ───────────────────────────────────────────────────────────
def _calc(doc: dict, links: Any, given: dict) -> dict | None:
try:
@@ -533,12 +548,26 @@ def try_doc(
off = [_off(base, k, pick[k], ev[k]) for k in ev if k in pick and pick[k] not in ev[k]]
off += named
res = verdict(mine, facts(base, got), got_imp, got, off)
rank = (res["판정"] == SURE, -len(off), res["같음"], res["짝"], -len(res["다른"]))
rank = (
res["판정"] == SURE,
-len(off),
res["같음"],
res["짝"],
-len(res["다른"]),
-len(res["참고"]),
)
if best is None or rank > best["순위"]:
best = {**res, "순위": rank, "옵션": pick, "금액": _totals(got)}
if res["판정"] == SURE:
if res["판정"] == SURE and not res["참고"]:
break
return best or {"판정": NONE, "같음": 0, "짝": 0, "다른": [], "순위": (False, -9, 0, 0, 0)}
return best or {
"판정": NONE,
"같음": 0,
"짝": 0,
"다른": [],
"참고": [],
"순위": (False, -9, 0, 0, 0, 0),
}
def _off(doc: dict, vid: str, value: Any, seen: set) -> dict:
@@ -549,7 +578,7 @@ def _off(doc: dict, vid: str, value: Any, seen: set) -> dict:
def match(imported: dict, links_imp: Any, index: Index, links: Any) -> dict[str, Any]:
"""가져온 식 문서 → 연결후보 `{판정, 키, 이름, 옵션, 다른, 금액, 후보수}`."""
"""가져온 식 문서 → 연결후보 `{판정, 키, 이름, 옵션, 다른, 참고, 금액, 후보수}`."""
links = links if isinstance(links, Memo) else Memo(links)
got_imp = _calc(imported, links_imp, {})
if got_imp is None:
@@ -561,7 +590,7 @@ def match(imported: dict, links_imp: Any, index: Index, links: Any) -> dict[str,
res = try_doc(index.docs[key], links, imported, got_imp, mine, index.regions)
if best is None or res["순위"] > best["순위"]:
best, best_key = res, key
if res["판정"] == SURE:
if res["판정"] == SURE and not res["참고"]:
break
if best is None:
return {"판정": NONE, "후보수": 0}
@@ -575,6 +604,7 @@ def match(imported: dict, links_imp: Any, index: Index, links: Any) -> dict[str,
"이름": base.get("이름"),
"옵션": _shown(base, best["옵션"]),
"다른": best["다른"],
"참고": best["참고"],
"금액": {"가져온": _plain(_totals(got_imp)), "우리": _plain(best["금액"])},
"후보수": len(keys),
}
+220
View File
@@ -0,0 +1,220 @@
"""연결 찾기 — 로직 차이(다른 항) 대 값 차이(참고) 가르기(PLAN 812-2 · 요구 712) — `M02_Doc_Match` 가 부름.
확정을 막는 「다른 항」 = 로직 차이만 — 자원 갈래 · 기계 규격 · 노임 종류 · 자재 이름 종류 · 비목 · 산출 단가 ·
율 구조(있고 없음 · 대상 비목 · 율 = 비율) · 수량(옵션 풀이 뒤 · 원 단위 같아야 함).
「참고」 = 값 차이 — 인력 · 재료 · 기계 시간당 단가 · 끝수(합만 다름) · 그로 인한 비목 금액 · 계.
율 = 율 줄 셈값 ÷ 대상 금액 합 — 대상 = 율 식이 (거쳐서) 부르는 금액 줄 · 비목 이름이면 그 비목 몫 · 번지 합 = 핵심 줄 전부 ·
대상 금액 끝수 자리 차이만 지움(유효 6 자리 · 두 바탕 중 짧은 꼴) — 지어 맞추기 아님.
"""
from __future__ import annotations
import re
from decimal import Decimal, InvalidOperation
from typing import Any
from M02_MasterTemplete import M02_Doc_Copy as dc
md = dc.md
PRICED = ("노임", "재료", "기계") # 시간당 · 단위 단가 = 값(참고)
# ── 율 구조 ───────────────────────────────────────────────────────────
def _names(text: Any) -> set[str]:
if text in (None, ""):
return set()
try:
return set(md.names(md.parse(text)))
except md.DocError:
return set()
def _reach(doc: dict) -> dict[str, set[str]]:
"""기호 → 바로 부르는 이름(변수 연결 · 식 줄 · 금액 줄 수량 · 단가)."""
direct: dict[str, set[str]] = {}
for v in doc.get("변수") or []:
direct[md.var_id(v)] = set(md.link_names(v.get("연결") or {}))
for line in doc.get("식") or []:
direct[str(line.get("기호"))] = _names(line.get("식"))
for line in doc.get("금액") or []:
if line.get("기호"):
direct[str(line["기호"])] = _names(line.get("수량")) | _names(line.get("단가"))
return direct
def _close(start: set[str], direct: dict[str, set[str]]) -> set[str]:
seen, todo = set(start), list(start)
while todo:
for n in direct.get(todo.pop(), ()):
if n not in seen:
seen.add(n)
todo.append(n)
return seen
def _short(r: Decimal) -> Decimal:
return Decimal(f"{r:.6g}").normalize()
def rates(doc: dict, got: dict, facts: list[dict]) -> None:
"""율 사실에 `율`(비율 · 못 셈 None) · `대상`({(자원 갈래, 비목)})을 채움(제자리)."""
money, rows = doc.get("금액") or [], got.get("줄") or []
direct = _reach(doc)
core = [f for f in facts if f["갈래"] != "율" and not f["합밖"]]
for f in facts:
if f["갈래"] != "율":
continue
line, row = money[f["번"]], rows[f["번"]]
reach = _close(_names(line.get("수량")) | _names(line.get("단가")), direct)
costs = reach & set(md.COSTS)
hits = [c for c in core if _hit(money[c["번"]], reach)]
if hits: # 줄 기호 · 줄 수량 · 단가 이름 = 그 줄 금액 전부
parts = [(c, tuple(c["비목"])) for c in hits]
elif costs: # 비목 이름(「노무비 × 4 %」) = 그 비목 몫만
parts = [(c, tuple(k for k in c["비목"] if k in costs)) for c in core]
else: # 번지 합 · 못 찾음 = 블록 핵심 줄 전부
parts = [(c, tuple(c["비목"])) for c in core]
parts = [(c, ks) for c, ks in parts if ks]
f["대상"] = frozenset((_cls(c["갈래"]), k) for c, ks in parts for k in ks)
f["율"] = None
amount = _mul(row.get("수량"), row.get("단가"))
if amount is None:
continue
bases = [sum((_num(c["비목"][k]) for c, ks in parts for k in ks), Decimal(0))]
if hits:
each = (_mul(rows[c["번"]].get("수량"), rows[c["번"]].get("단가")) for c in hits)
bases.append(sum((x or 0 for x in each), Decimal(0)))
found = [_short(amount / b) for b in bases if b]
if found:
f["율"] = min(found, key=lambda r: len(r.as_tuple().digits))
def _hit(line: dict, reach: set[str]) -> bool:
parts = _names(line.get("수량")) | _names(line.get("단가"))
return bool(line.get("기호") and str(line["기호"]) in reach) or bool(parts and parts <= reach)
def _cls(kind: str) -> str:
return "기계" if kind == "산출" else kind # 기계경비 묶음(코드 없음) = 기계
def _num(v: Any) -> Decimal:
if isinstance(v, dict):
return sum((_num(v.get(k)) for k in md.COSTS), Decimal(0))
try:
return Decimal(str(v)) if v is not None else Decimal(0)
except InvalidOperation:
return Decimal(0)
def _mul(q: Any, p: Any) -> Decimal | None:
if q is None or p is None:
return None
return _num(q) * _num(p)
def pct(r: Any) -> str:
"""비율 → 「9 %」."""
if not isinstance(r, Decimal):
return "?"
return f"{(r * 100).normalize():f} %"
# ── 짝 줄 ─────────────────────────────────────────────────────────────
def txt(v: Any) -> str:
"""단가 글 — 수 = 원 단위 쉼표 · 기계 묶음 = 노 · 재 · 경."""
if isinstance(v, tuple):
return " · ".join(f"{k[0]} {txt(x)}" for k, x in zip(md.COSTS, v) if x)
if isinstance(v, Decimal):
return f"{v.normalize():,f}"
return str(v)
_ALIAS = (("로울러", "롤러"), ("굴삭", "굴착"), ("브레이카", "브레이커"), ("로우더", "로더"))
_ALIAS += (("물탱크", "살수차"), ("아스팔트페이버", "아스팔트피니셔")) # 같은 기계 다른 이름
def _plain(text: str) -> str:
t = re.sub(r"\s", "", text)
for old, new in _ALIAS:
t = t.replace(old, new)
return t
def _head(text: str) -> str:
"""기계 이름 머리(괄호 · 규격 앞 한글) — 「머캐덤로울러(자주식)」 → 「머캐덤롤러」."""
m = re.match(r"[가-힣]+", _plain(text))
return m.group(0) if m else ""
def _same_machine(a: str, b: str) -> bool:
x, y = _head(a), _head(b)
return bool(x and y) and (x in y or y in x)
def _grams(text: str) -> set[str]:
t = _plain(text)
return {run[i : i + 2] for run in re.findall(r"[가-힣]+", t) for i in range(len(run) - 1)}
def pair(a: dict, b: dict) -> tuple[list[dict], list[dict]]:
"""수량 같은 짝 줄 → (다른 항, 참고) — 단가만 다르면 참고 · 자원 · 비목이 다르면 다른 항."""
diffs: list[dict] = []
kinds = {a["갈래"], b["갈래"]}
# 기계경비 숫자 묶음(코드 없음 · 원파일 값) ↔ 우리 기계 = 이름 종류로 같은 기계인지
loose = kinds == {"기계", "산출"} and not (a if a["갈래"] == "산출" else b)["코드"]
if a["갈래"] == b["갈래"] == "기계" and a["코드"] != b["코드"]:
diffs.append({"항": "기계 규격", "글": f"{a['이름']} {a['코드']} ↔ {b['코드']}"})
elif loose and not _same_machine(a["이름"], b["이름"]):
diffs.append({"항": "기계 규격", "글": f"{a['이름']} ↔ {b['이름']} {b['코드']}"})
elif a["갈래"] == b["갈래"] == "노임" and a["코드"] != b["코드"]:
diffs.append({"항": "자원", "글": f"{a['이름']} {a['코드']} ↔ {b['이름']} {b['코드']}"})
elif a["갈래"] == b["갈래"] == "재료" and not _same_stuff(a, b):
diffs.append({"항": "자원", "글": f"{a['이름']} ↔ {b['이름']}"})
machine = a["갈래"] == b["갈래"] == "기계" or loose # 시간당 단가 구성(운전 노무비 등) = 값
priced = a["비목"] and b["비목"] # 한쪽 단가 없음(0) = 값
if priced and not machine and set(a["비목"]) != set(b["비목"]):
diffs.append(
{"항": "비목", "글": f"{a['이름']} 비목 {'·'.join(a['비목'])} ↔ {'·'.join(b['비목'])}"}
)
if a["단가"] == b["단가"]:
return diffs, []
say = f"{a['이름']} 단가 {txt(a['단가'])} ↔ {txt(b['단가'])}"
if (a["갈래"] == b["갈래"] and a["갈래"] in PRICED) or loose:
return diffs, [{"항": "단가 차이", "글": say}]
return [*diffs, {"항": "산출 단가", "글": say}], []
def _same_stuff(a: dict, b: dict) -> bool:
"""자재 종류 같음 — 두 코드가 있으면 코드 · 없으면 이름 낱말(한글 두 글자) 겹침."""
if a["코드"] and b["코드"] and a["코드"] == b["코드"]:
return True
return bool(_grams(a["이름"]) & _grams(b["이름"]))
def rate_pairs(left: list[dict], right: list[dict], take: Any) -> list[dict]:
"""율 줄 짝 — 같은 구조(율 · 대상 · 비목) 먼저 · 그다음 비목만 · 대상만 · 율만 다름 순."""
diffs: list[dict] = []
def is_rate(a: dict, b: dict) -> bool:
return a["갈래"] == b["갈래"] == "율"
def same(a: dict, b: dict, *keys: str) -> bool:
return is_rate(a, b) and all(
(set(a[k]) == set(b[k])) if k == "비목" else a.get(k) == b.get(k) for k in keys
)
def name(a: dict, b: dict) -> str:
return f"{a['이름']} {pct(a.get('율'))} ↔ {b['이름']} {pct(b.get('율'))}"
def target(x: dict) -> str:
return "·".join(f"{k} {c}" for k, c in sorted(x.get("대상") or ())) or "없음"
take(lambda a, b: same(a, b, "율", "대상", "비목")) # 로직 같음
for a, b in take(lambda a, b: same(a, b, "율", "대상")):
diffs.append({"항": "비목", "글": f"율 줄 {name(a, b)} 비목 다름"})
for a, b in take(lambda a, b: same(a, b, "율")):
diffs.append({"항": "율 대상", "글": f"{name(a, b)} 대상 {target(a)} ↔ {target(b)}"})
for a, b in take(lambda a, b: same(a, b, "대상") or is_rate(a, b)):
diffs.append({"항": "율 값", "글": f"{name(a, b)} · 대상 {target(a)} ↔ {target(b)}"})
return diffs
+22 -4
View File
@@ -3,6 +3,7 @@
* 가져온 식 ↔ 우리 M01 기초 연결 판정 띠 자료(PLAN 810-4 · 요구 707 · 709 · 703) — 원문 패널 카드 위.
* SK `가져온.원본.연결후보`(서버 `M02_Doc_Match` 가 적음) → 띠 글 · 다른 항 · 단추 하나:
* 확정 = [우리 기초 사본으로 바꾸기] · 근접 · 없음 = 단추 없음(703) · 안 찾음 = [연결 찾기] ·
* 다른 항 = 로직 차이(확정을 막음) · 참고 = 단가 · 끝수(값 차이 — 「우리 단가로 셈 ○○ 원」 · 812-2) ·
* 바꾼 SK(`가져온보관` 있음) = 「우리 기초 사본 ○○」 + [가져온 식으로 되돌리기]
* 화면 · DOM · fetch 없음
* ========================================================================== */
@@ -15,6 +16,8 @@ export interface MatchFound {
이름?: string;
옵션?: Record<string, string>;
다른?: { 항: string; 글: string }[];
/** 값 차이(단가 · 끝수) — 확정을 안 막음 */
참고?: { 항: string; 글: string }[];
금액?: { 가져온?: Record<string, number | null>; 우리?: Record<string, number | null> };
후보수?: number;
가까움?: { 키?: string; 이름?: string; 다른?: { 항: string; 글: string }[] };
@@ -44,6 +47,21 @@ const opts = (o?: Record<string, unknown>): string => {
const lines = (d?: { 항: string; 글: string }[]): string[] =>
(d ?? []).map((x) => `${x.항} — ${x.글}`);
const won = (n?: number | null): string =>
n == null ? "" : `${Math.round(n).toLocaleString("ko-KR")} 원`;
/** 참고 요약 — 「참고: 단가 다름 3 줄 · 우리 단가로 셈 53,218 원」 · 참고 없음 = "" */
const noteText = (f: MatchFound): string => {
const n = f.참고 ?? [];
if (!n.length) return "";
const price = n.filter((x) => x.항 === "단가 차이").length;
const bits = [
price ? `단가 다름 ${price} 줄` : "",
n.some((x) => x.항 === "끝수") ? "끝수 다름" : "",
].filter(Boolean);
const ours = won(f.금액?.우리?.계);
return ` · 참고: ${bits.join(" · ")}${ours ? ` · 우리 단가로 셈 ${ours}` : ""}`;
};
/** SK → 띠 자료 · 가져온 식도 · 바꾼 SK 도 아니면 null */
export function matchBand(sk: SkLike | null | undefined): MatchBand | null {
const kept = sk?.가져온보관;
@@ -64,8 +82,8 @@ export function matchBand(sk: SkLike | null | undefined): MatchBand | null {
if (f.판정 === "확정") {
return {
tone: "success",
text: `우리 기초 ${base} 와 확정 일치`,
details: [],
text: `확정 — 로직 같음 · 우리 기초 ${base}${noteText(f)}`,
details: lines(f.참고),
action: "바꾸기",
};
}
@@ -73,8 +91,8 @@ export function matchBand(sk: SkLike | null | undefined): MatchBand | null {
const kinds = [...new Set((f.다른 ?? []).map((x) => x.항))].join(" · ");
return {
tone: "warning",
text: `근접 — ${base} · 다른 항: ${kinds}`,
details: lines(f.다른),
text: `근접 — ${base} · 다른 항: ${kinds}${noteText(f)}`,
details: [...lines(f.다른), ...lines(f.참고).map((x) => `참고 ${x}`)],
action: null,
};
}
Binary file not shown.
Binary file not shown.
@@ -12,7 +12,9 @@
· 계(돈 아닌 로직 = 결과)
원 단위 — 기본 옵션 전부 + 옵션 표본(로직마다 고르기 입력의 다른 갈래 하나씩 · 나머지 기본).
둘 다 못 셈 = 같음(따로 셈). 보정 없음 — 다르면 목록에 까닭.
- 저장 = 같은 것만(요구서_664 1 장 3) — 모든 대조가 같고 부르는 단가산출도 저장되는 문서 ·
- 채택된 차이(`ADOPTED`) = 사용자 요구 714-3 「1원 차이는 로직만 맞다면 문제 없음」 — 식 속 단가산출
부름 값 = 끝수 뒤(새 계산기 그대로) · 목록에 남기고 저장은 막지 않음.
- 저장 = 같은 것만(요구서_664 1 장 3) — 모든 대조가 같고(채택된 차이 빼고) 부르는 단가산출도 저장되는 문서 ·
`resources/master_data/수식문서/수식_<옛 로직 파일 꼬리>.json`(로직 파일마다
· 옛 로직 JSON 은 안 씀).
- 목록 = `docs/raw/요구_1104/_목록_802-2_변환대조.xlsx`(로직 · 옵션 · 옛 · 새 · 차이 · 까닭 + 요약).
@@ -44,6 +46,8 @@ RAW_SUB = (
"식 속 단가산출 부름 = 끝수 뒤 값"
"(옛 엔진 = 끝수 전 날 값 · `master_formula.run` 결과를 식에 씀)"
)
# 채택된 차이 — (로직, 옵션) · 요구 714-3
ADOPTED = {("GC000100", "지역=경북")}
def rows_of(files: dict) -> list[tuple[str, dict]]:
@@ -108,7 +112,7 @@ def why(doc: dict, old, new) -> str:
if isinstance(new, str) and not isinstance(old, str):
return f"새 계산기 못 셈 — {new}"
if _uses_raw_sub(doc):
return RAW_SUB
return RAW_SUB + " · 채택(요구 714-3)"
return "까닭 모름"
@@ -151,6 +155,7 @@ def build(log=print) -> dict:
)
for one in runs:
if not one["같음"]:
one["채택"] = (one["로직"], one["옵션"]) in ADOPTED
one["까닭"] = why(docs[one["로직"]], one["옛"], one["새"])
keep = _keep(docs, runs)
for one in runs: # 같아도 못 넣는 것 — 부르는 단가산출이 빠짐
@@ -165,6 +170,7 @@ def build(log=print) -> dict:
"옵션 표본": sum(1 for x in runs if x["옵션"]),
"옵션 같음": sum(1 for x in runs if x["옵션"] and x["같음"]),
"옵션 다름": sum(1 for x in runs if x["옵션"] and not x["같음"]),
"채택된 차이": sum(1 for x in runs if x.get("채택")),
"둘 다 못 셈": both_out,
"저장": len(keep),
"변환 알림": sum(1 for v in notes.values() if v),
@@ -192,7 +198,7 @@ def _calls(doc: dict) -> set[str]:
def _keep(docs: dict, runs: list[dict]) -> set[str]:
"""저장할 문서 — 모든 대조 같음 + 부르는 단가산출도 저장(닫힘).
키 구멍 부름은 그 로직 대조로 판정."""
keep = set(docs) - {x["로직"] for x in runs if not x["같음"]}
keep = set(docs) - {x["로직"] for x in runs if not x["같음"] and not x.get("채택")}
while True:
drop = {k for k in keep if not _calls(docs[k]) <= keep}
if not drop:
@@ -641,6 +641,55 @@
"끝전": "비목버림"
}
},
{
"꼴": "수식문서",
"판": 1,
"키": "GC000100",
"이름": "암파쇄(유압식 할암공법)",
"결과단위": "원/㎥",
"출처": "건설품셈 공통 3-3-8",
"변수": [
{"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}},
{"열쇠": "보정작업", "기호": "S", "아래": "보정작업", "위": "", "이름": "보정작업", "단위": "", "출처": "건설품셈 공통 8-1-7 1", "연결": {"갈래": "선택", "목록": ["해당 없음", "암석작업(연암・보통암・경암)", "전석섞인토사"], "값": "해당 없음"}},
{"열쇠": "드릴경비", "기호": "C", "아래": "드릴", "위": "", "이름": "드릴경비", "단위": "", "연결": {"갈래": "식"}},
{"열쇠": "브레이커경비", "기호": "C", "아래": "브레이커", "위": "", "이름": "브레이커경비", "단위": "", "연결": {"갈래": "식"}},
{"열쇠": "P_기계시간당", "기호": "P", "아래": "기계시간당", "위": "", "이름": "기계 시간당 기계경비(손료 보정 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000268", "입력": {"기계": "'5405-0110'", "지역": "S_지역"}, "칸": "계"}},
{"열쇠": "Q_유압식크롤드^표", "기호": "Q", "아래": "유압식크롤드", "위": "표", "이름": "암파쇄(유압식 할암공법).유압식크롤러드릴", "단위": "hr", "연결": {"갈래": "표", "표": "QC000201", "조건": {}, "칸": "유압식크롤러드릴"}},
{"열쇠": "P_굴착기무한궤", "기호": "P", "아래": "굴착기무한궤", "위": "", "이름": "굴착기(무한궤도)+대형브레이커 시간당 기계경비", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000271", "입력": {"기계": "'0201-0100'", "브레이커": "'0230-0010'", "지역": "S_지역", "보정작업": "S_보정작업"}, "칸": "계"}},
{"열쇠": "Q_굴착기대형브^표", "기호": "Q", "아래": "굴착기대형브", "위": "표", "이름": "암파쇄(유압식 할암공법).굴착기_대형브레이커", "단위": "hr", "연결": {"갈래": "표", "표": "QC000201", "조건": {}, "칸": "굴착기_대형브레이커"}},
{"열쇠": "Q_기계설비공^표", "기호": "Q", "아래": "기계설비공", "위": "표", "이름": "암파쇄(유압식 할암공법).기계설비공", "단위": "인", "연결": {"갈래": "표", "표": "QC000201", "조건": {}, "칸": "기계설비공"}},
{"열쇠": "w_기계설비공", "기호": "w", "아래": "기계설비공", "위": "", "이름": "기계설비공노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000051"}},
{"열쇠": "Q_특별인부^표", "기호": "Q", "아래": "특별인부", "위": "표", "이름": "암파쇄(유압식 할암공법).특별인부", "단위": "인", "연결": {"갈래": "표", "표": "QC000201", "조건": {}, "칸": "특별인부"}},
{"열쇠": "w_특별인부", "기호": "w", "아래": "특별인부", "위": "", "이름": "특별인부노임", "단위": "", "연결": {"갈래": "요소", "키": "LB000003"}},
{"열쇠": "P_기계시간당2", "기호": "P", "아래": "기계시간당2", "위": "", "이름": "기계 시간당 기계경비(손료 보정 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000268", "입력": {"기계": "'5405-0110'", "지역": "S_지역"}}},
{"열쇠": "Q_발전기^표", "기호": "Q", "아래": "발전기", "위": "표", "이름": "암파쇄(유압식 할암공법).발전기", "단위": "hr", "연결": {"갈래": "표", "표": "QC000201", "조건": {}, "칸": "발전기"}},
{"열쇠": "P_기계시간당3", "기호": "P", "아래": "기계시간당3", "위": "", "이름": "기계 시간당 기계경비(손료 보정 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000268", "입력": {"기계": "'7505-0025'", "지역": "S_지역"}}},
{"열쇠": "Q_유압식할암기^표", "기호": "Q", "아래": "유압식할암기", "위": "표", "이름": "암파쇄(유압식 할암공법).유압식할암기", "단위": "hr", "연결": {"갈래": "표", "표": "QC000201", "조건": {}, "칸": "유압식할암기"}},
{"열쇠": "P_기계시간당4", "기호": "P", "아래": "기계시간당4", "위": "", "이름": "기계 시간당 손료(연료·조종원 없는 기종)", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000273", "입력": {"기계": "'5501-0080'"}}},
{"열쇠": "P_굴착기무한2", "기호": "P", "아래": "굴착기무한2", "위": "", "이름": "굴착기(무한궤도)+대형브레이커 시간당 기계경비", "단위": "원/시간", "연결": {"갈래": "단가산출", "키": "GC000271", "입력": {"기계": "'0201-0100'", "브레이커": "'0230-0010'", "지역": "S_지역", "보정작업": "S_보정작업"}}},
{"열쇠": "Q_기계경비의^표", "기호": "Q", "아래": "기계경비의", "위": "표", "이름": "소모자재 요율.기계경비의", "단위": "%", "연결": {"갈래": "표", "표": "QC000202", "조건": {"장비": "'유압식 크롤러드릴'"}, "칸": "기계경비의"}},
{"열쇠": "Q_기계경비의2^표", "기호": "Q", "아래": "기계경비의2", "위": "표", "이름": "소모자재 요율.기계경비의", "단위": "%", "연결": {"갈래": "표", "표": "QC000202", "조건": {"장비": "'굴착기+대형브레이커'"}, "칸": "기계경비의"}}
],
"식": [
{"기호": "C_드릴", "식": "P_기계시간당 × Q_유압식크롤드^표"},
{"기호": "C_브레이커", "식": "P_굴착기무한궤 × Q_굴착기대형브^표"}
],
"금액": [
{"이름": "기계설비공", "단위": "인", "비목": "노무비", "수량": "Q_기계설비공^표", "단가": "w_기계설비공", "묶음": "인력"},
{"이름": "특별인부", "단위": "인", "비목": "노무비", "수량": "Q_특별인부^표", "단가": "w_특별인부", "묶음": "인력"},
{"이름": "유압식 크롤러드릴 110㎾", "단위": "시간", "비목": "묶음", "수량": "Q_유압식크롤드^표", "단가": "P_기계시간당2", "묶음": "유압식 크롤러드릴 110㎾"},
{"이름": "발전기 25㎾", "단위": "시간", "비목": "묶음", "수량": "Q_발전기^표", "단가": "P_기계시간당3", "묶음": "발전기 25㎾"},
{"이름": "유압식할암기 ∅80", "단위": "시간", "비목": "묶음", "수량": "Q_유압식할암기^표", "단가": "P_기계시간당4", "묶음": "유압식할암기 ∅80"},
{"이름": "굴착기+대형브레이커 1.0㎥", "단위": "시간", "비목": "묶음", "수량": "Q_굴착기대형브^표", "단가": "P_굴착기무한2", "묶음": "굴착기+대형브레이커 1.0㎥"},
{"이름": "소모자재(크롤러드릴)", "단위": "식", "비목": "재료비", "단가": "C_드릴 × Q_기계경비의^표 ÷ 100", "출처": "건설품셈 공통 3-3-8 주④", "묶음": "소모자재(크롤러드릴) · 소모자재(대형브레이커)"},
{"이름": "소모자재(대형브레이커)", "단위": "식", "비목": "재료비", "단가": "C_브레이커 × Q_기계경비의2^표 ÷ 100", "출처": "건설품셈 공통 3-3-8 주④", "묶음": "소모자재(크롤러드릴) · 소모자재(대형브레이커)"}
],
"끝수": {
"줄": {"방법": "버림", "자리": 1},
"비목": {"방법": "버림", "자리": 0},
"끝전": "비목버림"
}
},
{
"꼴": "수식문서",
"판": 1,
+12 -3
View File
@@ -2,7 +2,7 @@
파일마다 프로세스 · 안쪽 산근(형제)은 같은 파일 블록을 우리 단가로 먼저 셈해 묶음 값으로 넣음(저장 층의
형제 SK 읽기와 같은 값) · 확정은 M01 문서를 찾은 옵션(입력값 꼴)으로 다시 셈해 금액 대조.
마지막 줄 = JSON {블록, 판정, 근접항, 오류, 확정다시셈다름} · `--rows <pkl>` = 줄 목록 저장(목록 xlsx 용).
마지막 줄 = JSON {블록, 판정, 근접항, 확정참고, 확정참고블록, 오류, 확정다시셈다름} · `--rows <pkl>` = 줄 목록 저장(목록 xlsx 용).
"""
from __future__ import annotations
@@ -96,9 +96,12 @@ def sweep(arg: tuple[str, str, int]) -> list[dict]:
row |= {"규격": (doc.get("가져온") or {}).get("규격"), **res}
if res.get("판정") == dm.SURE:
again = dm._calc(docs[res["키"]], dm.ml.Links(whole, docs), dict(res["옵션"]))
money = res["금액"] # 다시 셈 = 우리 · 참고 없으면 가져온 = 우리(812-2)
row["다시셈같음"] = (
again is not None and dm._plain(dm._totals(again)) == res["금액"]["가져온"]
) # noqa: E501
again is not None
and dm._plain(dm._totals(again)) == money["우리"]
and (bool(res.get("참고")) or money["우리"] == money["가져온"])
)
row["초"] = round(time.time() - start, 2)
rows.append(row)
return rows
@@ -121,6 +124,12 @@ def main() -> None:
"근접항": dict(
collections.Counter(d["항"] for r in real if r["판정"] == "근접" for d in r["다른"])
), # noqa: E501
"확정참고": dict(
collections.Counter(
d["항"] for r in real if r["판정"] == "확정" for d in r.get("참고") or []
)
),
"확정참고블록": sum(1 for r in real if r["판정"] == "확정" and r.get("참고")),
"오류": sum(1 for r in real if r["판정"] == "오류"),
"확정다시셈다름": sum(1 for r in real if r.get("다시셈같음") is False),
}
+16 -6
View File
@@ -4,7 +4,7 @@
① --check — 다시 돌린 결과 = 저장본(`resources/master_data/수식문서/`) · 목록 xlsx.
② 로직 1,519 전수 기본 옵션 = 옛 엔진 노 · 재 · 경 · 계(결과) 원 단위 같음.
③ 옵션 표본 — 로직마다 고르기 입력의 모든 갈래 한 번 이상 · 같음 수 · 다름 = 목록 · 까닭 모름 0.
④ 저장 = 같은 것만(부르는 단가산출까지) · 저장본을 읽어 다시 셈해도 같음.
④ 저장 = 같은 것만(부르는 단가산출까지 · 채택된 차이 GC000100 지역=경북 재료비 1 원 빼고) · 저장본을 읽어 다시 셈해도 같음.
⑤ 791 실무 손 옮김 견본 넷(번지 · 단계 · 합밖 · 줄 자리 · 끝전 · 조정 · 값묶음)도 변환 셈 = 엑셀 금액.
"""
@@ -56,7 +56,7 @@ def test_옵션_표본_모든_갈래_한_번_이상(result):
assert (row["키"], f"{spec['이름']}={opt}") in seen
info = result["요약"]
assert info["옵션 표본"] == want == info["옵션 같음"] + info["옵션 다름"]
assert info["옵션 같음"] >= 18700 and info["옵션 다름"] <= 1
assert info["옵션 같음"] >= 18700 and info["옵션 다름"] == info["채택된 차이"] == 1
def _whole():
@@ -69,15 +69,25 @@ def test_다름_목록_까닭(result):
assert sheet[0] == list(build.HEAD) and sheet[1:] == rows
diff = [x for x in result["대조"] if not x["같음"]]
assert all(x["까닭"] != "까닭 모름" for x in diff)
assert [(x["로직"], x["옵션"], x["까닭"]) for x in diff] == [
("GC000100", "지역=경북", build.RAW_SUB)
]
# 채택된 차이(요구 714-3) — 식 속 단가산출 부름 값 = 끝수 뒤 · 재료비 1 원 · 저장은 함
assert [(x["로직"], x["옵션"], x["채택"]) for x in diff] == [("GC000100", "지역=경북", True)]
assert diff[0]["까닭"].startswith(build.RAW_SUB)
assert diff[0]["옛"]["재료비"] != diff[0]["새"]["재료비"]
assert abs(diff[0]["새"]["재료비"] - diff[0]["옛"]["재료비"]) == 1
assert {k: diff[0]["옛"][k] for k in ("노무비", "경비")} == {
k: diff[0]["새"][k] for k in ("노무비", "경비")
}
def test_저장_같은_것만_다시_셈(result):
saved = ml.load_all()
assert set(saved) == result["저장"]
assert "GC000100" not in saved and len(saved) == result["요약"]["저장"] >= 1518
assert "GC000100" in saved and len(saved) == result["요약"]["저장"] >= 1519
base = next(x for x in result["대조"] if x["로직"] == "GC000100" and not x["옵션"])
assert (
base["같음"]
and build.new_answer(ml.Links(_whole(), saved), saved["GC000100"]) == base["옛"]
)
for key in ("GC000211", "GC001147", "GF000294", "GC000087"):
doc = saved[key]
got = build.new_answer(ml.Links(_whole(), saved), doc)
+5 -1
View File
@@ -67,7 +67,11 @@ def test_읽기_미리셈_연결기록(client):
def test_없는문서_빈문서_없는키_404(client):
fresh = _read(client, "GC000100") # 802-2a 로 문서 안 넣음
assert (
_read(client, "GC000100")["새"] is False
) # 802-2a 로 문서 넣음 — 임시 사본에서 지워 빈 문서 길을 봄
doc_screen.drop("GC000100")
fresh = _read(client, "GC000100")
assert fresh["새"] is True and fresh["문서"]["식"] == [] and fresh["문서"]["금액"] == []
assert fresh["셈"]["셈됨"] is False and fresh["셈"]["계"] is None
assert client.get("/api/m01/logic/doc", params={"key": "GX999999"}).status_code == 404
+8 -5
View File
@@ -5,7 +5,7 @@
근접(합판거푸집 4회 = GC000211 보통 · 4회 — 단가 차이 · 비목 · 율) · 요구 704 파형강관 매설 Φ800 =
GF000179 관경 800 근접 · 다른 항 = 단위 환산(× 6) · 기계 규격 · 율 줄 · 없음(후보 0)
(b) 옵션 풀이 — 줄 수량이 의존하는 변수만 돌려 좁힘 · 글 근거 값과 다르면 「옵션 글」 다른 항(확정 아님)
(c) 줄 대조 — 같음 · 단가 차이 · 단위 환산 · 율 줄 · 끝수(지어 맞추기 없음 — 1 원 달라도 확정 아님)
(c) 줄 대조 — 같음 · 단위 환산 · 율 줄 = 다른 항 · 단가 차이 · 끝수 = 참고(812-2 · 확정을 안 막음)
(d) 바꾸기 — 임시 층 개인: 확정 SK = 같은 키에 M01 사본(바탕 · 입력값) · 금액 = M01 문서 셈 · 원래 몸 보관 →
되돌리기 = 원래 금액 · 근접 SK 바꾸기 = 409 · M01 문서 파일 그대로
(e) 전수(1,518 문서 × 원파일 산근 2,220 블록 · 파일마다 프로세스) — 확정은 모두 다시 셈해도 금액 같음 ·
@@ -70,7 +70,8 @@ def test_near_formwork_lists_differences() -> None:
assert got["판정"] == dm.NEAR and got["키"] == "GC000211"
assert got["옵션"]["사용횟수"] == "4회" and got["옵션"]["유형"] == "보통"
kinds = {d["항"] for d in got["다른"]}
assert "단가 차이" in kinds and "비목" in kinds # 대전 합판 단가 · 공구손료 재료비 ↔ 경비
assert kinds == {"비목"} # 공구손료 1 % 재료비 ↔ 경비 = 로직(812-2)
assert [d["항"] for d in got["참고"]] == ["단가 차이"] # 대전 합판 단가 = 값(참고)
def test_req704_pipe_is_near_with_unit_rate_machine() -> None:
@@ -118,10 +119,11 @@ def test_compare_kinds_and_no_tolerance() -> None:
tot = {"노무비": 9, "재료비": 0, "경비": 0, "계": 9}
assert dm.verdict(a, list(a), tot, tot)["판정"] == dm.SURE
off = dm.verdict(a, list(a), tot, {**tot, "노무비": 10, "계": 10})
assert off["판정"] != dm.SURE and off["다른"][0]["항"] == "끝수" # 1 원도 확정 아님
assert off["판정"] == dm.SURE and off["참고"][0]["항"] == "끝수" # 끝수 = 참고(812-2)
b = [_line("인부", "노임", "0.24", "100"), _line("공", "노임", "0.5", "11")]
res = dm.verdict(a, b, tot, tot)
assert [d["항"] for d in res["다른"]] == ["단가 차이", "단위 환산"] and res["판정"] == dm.NEAR
assert [d["항"] for d in res["다른"]] == ["단위 환산"] and res["판정"] == dm.NEAR
assert [d["항"] for d in res["참고"]] == ["단가 차이"]
rate = {**_line("율", "율", "1", "3", "경비"), "수량": None, "단가": None}
res = dm.verdict([*a, rate], a, tot, tot)
assert res["다른"] == [{"항": "율 줄", "글": "가져온 쪽만 — 율"}] and res["판정"] == dm.NEAR
@@ -197,7 +199,8 @@ def test_every_block_table() -> None:
assert got["블록"] == 2220 and got["오류"] == 0 and got["확정다시셈다름"] == 0
assert set(got["판정"]) <= {dm.SURE, dm.NEAR, dm.NONE}
assert sum(got["판정"].values()) == 2220 and got["판정"].get(dm.SURE, 0) >= 1
assert collections.Counter(got["근접항"]).get("단가 차이", 0) >= 1
assert not {"단가 차이", "끝수"} & set(got["근접항"]) # 값 차이 = 참고(812-2)
assert collections.Counter(got["확정참고"]).get("단가 차이", 0) >= 1
def test_parent_reads_swapped_sibling(world) -> None:
+178
View File
@@ -0,0 +1,178 @@
"""812-2 지킴 — 연결 찾기 확정 = 로직(요구 712) · 값 차이 = 참고(`M02_Doc_Match_Note`).
(a) 단가만 다름 = 확정 + 참고 「단가 차이」(원파일 기계경비 묶음 ↔ 우리 기계 = 이름 종류 같을 때) ·
끝수만 다름 = 확정 + 참고 「끝수」
(b) 로직 차이 = 다른 항 — 율 비율 다름(「율 값」) · 기계 규격 · 노임 종류(「자원」) · 산출 단가 · 비목
(c) 율 = 비율 — 거창 합판거푸집(블록 41): 가져온 「주자재소계 × 9 %」 = M01 소모자재 9 % 짝 · 율_2 1 % 재료비 ↔
공구손료 1 % 경비 = 「비목」 · 대전 합판 단가 = 참고
(d) 요구 704 배수관부설 D=800 ↔ GF000179 = 여전히 근접(단위 환산 × 6 · 기계 규격 · 율 줄)
(e) 전수(S8122_ALL=1) — 2,220 블록 · 확정 다시 셈 같음 · 근접 까닭에 단가 차이 · 끝수 없음 · 확정 참고 있음
원파일 없으면 (c) · (d) · (e) skip(다른 PC).
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
from decimal import Decimal as D
from pathlib import Path
import pytest
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
for p in (ROOT, HERE):
if str(p) not in sys.path:
sys.path.insert(0, str(p))
from M02_MasterTemplete import M02_Doc_Match as dm # noqa: E402
TOT = {"노무비": 9, "재료비": 0, "경비": 0, "계": 9}
def _line(name, kind, qty, price, cost="노무비", code=""):
q, p = D(qty), D(price)
return {"번": 0, "이름": name, "갈래": kind, "코드": code, "수량": dm._key(q),
"단가": dm._key(p), "비목": {cost: dm._key(q * p)}, "금액": dm._key(q * p), "합밖": False} # fmt: skip
def _rate(name, rate, target=(("노임", "노무비"),), cost="경비"):
return {"번": 0, "이름": name, "갈래": "율", "코드": "", "수량": None, "단가": None,
"비목": {cost: D(1)}, "금액": D(1), "합밖": False, "율": D(rate), "대상": frozenset(target)} # fmt: skip
A = [_line("인부", "노임", "0.04", "100", code="LB1"), _line("공", "노임", "0.5", "10", code="LB2")]
# ── (a) 값 차이 = 참고 ───────────────────────────────────────────────────
def test_price_only_is_sure_with_note() -> None:
b = [_line("인부", "노임", "0.04", "120", code="LB1"), A[1]]
res = dm.verdict(A, b, TOT, {**TOT, "노무비": 10, "계": 10})
assert res["판정"] == dm.SURE and not res["다른"] and res["같음"] == 2
assert [d["항"] for d in res["참고"]] == ["단가 차이"]
mach = [_line("굴삭기", "기계", "0.1", "500", "경비", "0201-0070")]
other = [_line("굴삭기", "기계", "0.1", "530", "경비", "0201-0070")]
assert dm.verdict(mach, other, TOT, TOT)["판정"] == dm.SURE # 기계 시간당 단가 = 참고
file = [
_line("머캐덤로울러(자주식)", "산출", "0.1", "530", "경비")
] # 원파일 기계경비 묶음(코드 없음)
ours = [_line("탠덤롤러", "기계", "0.1", "500", "경비", "1206-0008")]
ours.append(_line("머캐덤롤러 10∼12ton", "기계", "0.1", "500", "경비", "1106-0012"))
res = dm.verdict(file, ours, TOT, TOT) # 이름 종류 같은 기계와 짝 · 단가 = 참고
assert (
not [d for d in res["다른"] if d["항"] != "줄 없음"] and res["참고"][0]["항"] == "단가 차이"
)
run = [{**_line("드릴", "산출", "0.1", "530", "경비"), "비목": {"노무비": D(1), "경비": D(2)}}]
res = dm.verdict(run, [_line("드릴", "기계", "0.1", "500", "경비", "0901-0010")], TOT, TOT)
assert res["판정"] == dm.SURE # 기계경비 구성(운전 노무비 포함) 차이 = 시간당 단가 = 참고
none = [{**_line("모래", "재료", "1", "0", "재료비"), "비목": {}}]
assert (
dm.verdict([_line("모래", "재료", "1", "9", "재료비")], none, TOT, TOT)["판정"] == dm.SURE
)
res = dm.verdict(file, ours[:1], TOT, TOT)
assert [d["항"] for d in res["다른"]] == ["기계 규격"]
def test_rounding_only_is_sure_with_note() -> None:
res = dm.verdict(A, list(A), TOT, {**TOT, "노무비": 10, "계": 10})
assert res["판정"] == dm.SURE and [d["항"] for d in res["참고"]] == ["끝수"]
# ── (b) 로직 차이 = 다른 항 ──────────────────────────────────────────────
def test_rate_ratio_differs_blocks() -> None:
res = dm.verdict([*A, _rate("공구손료", "0.02")], [*A, _rate("공구손료", "0.03")], TOT, TOT)
assert [d["항"] for d in res["다른"]] == ["율 값"] and res["판정"] == dm.NEAR
same = dm.verdict([*A, _rate("율", "0.02")], [*A, _rate("공구손료", "0.02")], TOT, TOT)
assert same["판정"] == dm.SURE # 율 · 대상 · 비목 같음 = 이름 달라도 같은 율 줄
tgt = dm.verdict(
[*A, _rate("율", "0.02", (("재료", "재료비"),))], [*A, _rate("율", "0.02")], TOT, TOT
)
assert [d["항"] for d in tgt["다른"]] == ["율 대상"]
cost = dm.verdict([*A, _rate("율", "0.02", cost="재료비")], [*A, _rate("율", "0.02")], TOT, TOT)
assert [d["항"] for d in cost["다른"]] == ["비목"]
def test_machine_labor_kind_differs_blocks() -> None:
a = [*A, _line("굴삭기", "기계", "0.1", "500", "경비", "0201-0070")]
b = [*A, _line("굴삭기", "기계", "0.1", "500", "경비", "0201-0080")]
res = dm.verdict(a, b, TOT, TOT)
assert [d["항"] for d in res["다른"]] == ["기계 규격"] and res["판정"] == dm.NEAR
c = [_line("특별인부", "노임", "0.04", "120", code="LB9"), A[1]]
res = dm.verdict(A, c, TOT, TOT)
assert [d["항"] for d in res["다른"]] == ["자원"] and res["판정"] != dm.SURE
sub = [_line("모르타르", "산출", "0.1", "500", "재료비")]
res = dm.verdict(sub, [_line("모르타르", "산출", "0.1", "520", "재료비")], TOT, TOT)
assert [d["항"] for d in res["다른"]] == ["산출 단가"] # 안쪽 산출 = 로직 결과
# ── (c) · (d) 원파일 블록 ────────────────────────────────────────────────
def _judge(no: int) -> dict:
t4 = pytest.importorskip("test_810_4_match")
try:
return t4.judge(no)
except FileNotFoundError:
pytest.skip("원파일 없음")
def test_formwork_rate_ratio_pairs() -> None:
got = _judge(41) # 합판거푸집 4회 ↔ GC000211
assert got["판정"] == dm.NEAR and got["키"] == "GC000211"
assert [d["항"] for d in got["다른"]] == ["비목"] and "1 %" in got["다른"][0]["글"]
assert [d["항"] for d in got["참고"]] == ["단가 차이"]
def test_req704_still_near() -> None:
got = _judge(19) # 배수관부설 D=800 ↔ GF000179
kinds = [d["항"] for d in got["다른"]]
assert got["판정"] == dm.NEAR and got["키"] == "GF000179"
assert kinds.count("단위 환산") == 2 and "기계 규격" in kinds and "율 줄" in kinds
# ── (e) 전수 ─────────────────────────────────────────────────────────────
@pytest.mark.skipif(os.environ.get("S8122_ALL") != "1", reason="전수 = S8122_ALL=1")
def test_every_block_logic() -> None:
out = subprocess.run(
[sys.executable, str(HERE / "helper_810_4_sweep.py")], capture_output=True, text=True
)
assert out.returncode == 0, out.stderr[-2000:]
got = json.loads(out.stdout.strip().splitlines()[-1])
assert got["블록"] == 2220 and got["오류"] == 0 and got["확정다시셈다름"] == 0
assert not {"단가 차이", "끝수"} & set(got["근접항"])
assert got["확정참고블록"] >= 1 and got["판정"][dm.SURE] >= 98
def test_rate_target_scope() -> None:
"""율 대상 — 줄 기호 = 그 줄 · 비목 이름 = 그 비목 몫(기계 운전 노무비 포함) · 번지 합 = 핵심 줄 전부."""
from M02_MasterTemplete import M02_Doc_Match_Note as note
def run(rate_text: str) -> dict:
money = [
{"이름": "인부", "수량": "1", "단가": "100", "기호": "C_인부"},
{"이름": "굴삭기", "수량": "1", "단가": "300"},
{"이름": "율", "단가": rate_text},
]
rows = [
{"수량": D(1), "단가": D(100), "금액": D(100)},
{"수량": D(1), "단가": D(300), "금액": D(300)},
{"수량": D(1), "단가": D(0), "금액": D(0)},
]
facts = [
{"번": 0, "갈래": "노임", "비목": {"노무비": D(100)}, "합밖": False},
{"번": 1, "갈래": "산출", "비목": {"노무비": D(100), "경비": D(200)}, "합밖": False},
{"번": 2, "갈래": "율", "비목": {"경비": D(4)}, "합밖": False},
]
doc = {"변수": [], "식": [], "금액": money}
rows[2]["단가"] = {"C_인부 × 4%": D(4), "노무비 × 2%": D(4), "번지.A01.계 × 1%": D(4)}[
rate_text
]
note.rates(doc, {"줄": rows}, facts)
return facts[2]
one = run("C_인부 × 4%")
assert one["율"] == D("0.04") and one["대상"] == {("노임", "노무비")}
labor = run("노무비 × 2%") # 노무비 200(인부 100 + 굴삭기 운전 100)
assert labor["율"] == D("0.02") and labor["대상"] == {("노임", "노무비"), ("기계", "노무비")}
whole = run("번지.A01.계 × 1%") # 400 × 1 %
assert whole["율"] == D("0.01") and ("기계", "경비") in whole["대상"]