fix(812-2): 연결 찾기 로직 대조 다듬음 — 원파일 기계경비 묶음 ↔ 우리 기계 = 이름 종류 짝 · 율 대상 = (자원 갈래, 비목) · 번지 합 · 비목 이름 몫
- 기계경비 숫자 묶음(코드 없음) ↔ 우리 기계 = 이름 머리로 같은 기계(로울러 = 롤러 · 물탱크 = 살수차 등) · 시간당 단가 · 구성 = 참고 - 로직 같은 짝을 먼저 지음(같은 수량 줄 엇갈림 막음) · 한쪽 단가 없음(0) 비목 = 값 - 율 = 번지 합이면 핵심 줄 전부 · 「노무비 × n %」 = 그 비목 몫(기계 운전 노무비 포함) · 대상 = (갈래, 비목) 묶음 - test_812_2 — 기계 이름 짝 · 율 대상 범위 시험 더함 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -440,7 +440,14 @@ def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]:
|
||||
)
|
||||
)
|
||||
same += len(exact)
|
||||
for a, b in take(lambda a, b: core(a, b) and cls(a, b) and a["수량"] == b["수량"]):
|
||||
|
||||
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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
확정을 막는 「다른 항」 = 로직 차이만 — 자원 갈래 · 기계 규격 · 노임 종류 · 자재 이름 종류 · 비목 · 산출 단가 ·
|
||||
율 구조(있고 없음 · 대상 비목 · 율 = 비율) · 수량(옵션 풀이 뒤 · 원 단위 같아야 함).
|
||||
「참고」 = 값 차이 — 인력 · 재료 · 기계 시간당 단가 · 끝수(합만 다름) · 그로 인한 비목 금액 · 계.
|
||||
율 = 율 줄 셈값 ÷ 대상 줄 금액 합 — 대상 = 율 식이 (거쳐서) 부르는 금액 줄 · 비목 이름이면 그 비목 줄 ·
|
||||
율 = 율 줄 셈값 ÷ 대상 금액 합 — 대상 = 율 식이 (거쳐서) 부르는 금액 줄 · 비목 이름이면 그 비목 몫 · 번지 합 = 핵심 줄 전부 ·
|
||||
대상 금액 끝수 자리 차이만 지움(유효 6 자리 · 두 바탕 중 짧은 꼴) — 지어 맞추기 아님.
|
||||
"""
|
||||
|
||||
@@ -57,7 +57,7 @@ def _short(r: Decimal) -> Decimal:
|
||||
|
||||
|
||||
def rates(doc: dict, got: dict, facts: list[dict]) -> None:
|
||||
"""율 사실에 `율`(비율 · 못 셈 None) · `대상`(대상 비목 묶음)을 채움(제자리)."""
|
||||
"""율 사실에 `율`(비율 · 못 셈 None) · `대상`({(자원 갈래, 비목)})을 채움(제자리)."""
|
||||
money, rows = doc.get("금액") or [], got.get("줄") or []
|
||||
direct = _reach(doc)
|
||||
core = [f for f in facts if f["갈래"] != "율" and not f["합밖"]]
|
||||
@@ -67,33 +67,37 @@ def rates(doc: dict, got: dict, facts: list[dict]) -> None:
|
||||
line, row = money[f["번"]], rows[f["번"]]
|
||||
reach = _close(_names(line.get("수량")) | _names(line.get("단가")), direct)
|
||||
costs = reach & set(md.COSTS)
|
||||
hits = []
|
||||
for c in core:
|
||||
mine = money[c["번"]]
|
||||
parts = _names(mine.get("수량")) | _names(mine.get("단가"))
|
||||
if (
|
||||
(mine.get("기호") and str(mine["기호"]) in reach)
|
||||
or (parts and parts <= reach)
|
||||
or (set(c["비목"]) & costs)
|
||||
):
|
||||
hits.append(c)
|
||||
f["대상"] = frozenset(k for c in hits for k in c["비목"])
|
||||
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(rows[c["번"]].get("금액")) for c in hits), Decimal(0)),
|
||||
sum(
|
||||
(_mul(rows[c["번"]].get("수량"), rows[c["번"]].get("단가")) or 0 for c in hits),
|
||||
Decimal(0),
|
||||
),
|
||||
]
|
||||
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))
|
||||
@@ -126,28 +130,57 @@ def txt(v: Any) -> str:
|
||||
return str(v)
|
||||
|
||||
|
||||
def _grams(text: str) -> set[str]:
|
||||
_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['이름']}"})
|
||||
if set(a["비목"]) != set(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:
|
||||
if (a["갈래"] == b["갈래"] and a["갈래"] in PRICED) or loose:
|
||||
return diffs, [{"항": "단가 차이", "글": say}]
|
||||
return [*diffs, {"항": "산출 단가", "글": say}], []
|
||||
|
||||
@@ -175,7 +208,7 @@ def rate_pairs(left: list[dict], right: list[dict], take: Any) -> list[dict]:
|
||||
return f"{a['이름']} {pct(a.get('율'))} ↔ {b['이름']} {pct(b.get('율'))}"
|
||||
|
||||
def target(x: dict) -> str:
|
||||
return "·".join(sorted(x.get("대상") or ())) or "없음"
|
||||
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, "율", "대상")):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""812-2 지킴 — 연결 찾기 확정 = 로직(요구 712) · 값 차이 = 참고(`M02_Doc_Match_Note`).
|
||||
|
||||
(a) 단가만 다름 = 확정 + 참고 「단가 차이」 · 끝수만 다름 = 확정 + 참고 「끝수」
|
||||
(a) 단가만 다름 = 확정 + 참고 「단가 차이」(원파일 기계경비 묶음 ↔ 우리 기계 = 이름 종류 같을 때) ·
|
||||
끝수만 다름 = 확정 + 참고 「끝수」
|
||||
(b) 로직 차이 = 다른 항 — 율 비율 다름(「율 값」) · 기계 규격 · 노임 종류(「자원」) · 산출 단가 · 비목
|
||||
(c) 율 = 비율 — 거창 합판거푸집(블록 41): 가져온 「주자재소계 × 9 %」 = M01 소모자재 9 % 짝 · 율_2 1 % 재료비 ↔
|
||||
공구손료 1 % 경비 = 「비목」 · 대전 합판 단가 = 참고
|
||||
@@ -37,7 +38,7 @@ def _line(name, kind, qty, price, cost="노무비", code=""):
|
||||
"단가": dm._key(p), "비목": {cost: dm._key(q * p)}, "금액": dm._key(q * p), "합밖": False} # fmt: skip
|
||||
|
||||
|
||||
def _rate(name, rate, target=("노무비",), cost="경비"):
|
||||
def _rate(name, rate, target=(("노임", "노무비"),), cost="경비"):
|
||||
return {"번": 0, "이름": name, "갈래": "율", "코드": "", "수량": None, "단가": None,
|
||||
"비목": {cost: D(1)}, "금액": D(1), "합밖": False, "율": D(rate), "대상": frozenset(target)} # fmt: skip
|
||||
|
||||
@@ -54,6 +55,24 @@ def test_price_only_is_sure_with_note() -> None:
|
||||
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:
|
||||
@@ -67,7 +86,9 @@ def test_rate_ratio_differs_blocks() -> None:
|
||||
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)
|
||||
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["다른"]] == ["비목"]
|
||||
@@ -120,3 +141,38 @@ def test_every_block_logic() -> None:
|
||||
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["대상"]
|
||||
|
||||
Reference in New Issue
Block a user