Files
Aislo/resources/tester/test_707_4_bill_link.py
T
eomsangdonandClaude Opus 5.5 fce824b471 refactor(815-2): 옛 별칭 장부 걷기 — M02 장부 화면 · 장부 길 · 불러오기 확인 장부 쓰기 · 공사 내역 맞춤 기억
- M02 왼쪽 [별칭 장부] · M02_MasterTemplete_Hopyo_Ledger.ts · 장부 타입 · css 지움
- 길 GET · PUT /api/m02/layers/{층}/hopyo-ledger 지움 · M02_Hopyo_Ledger 의 장부 화면 줄(rows · 승인 상태) 지움
- 일위대가 불러오기 확인(M02_Hopyo_Build) = 장부에 안 적음 · 가져온 식 저장(M02_Doc_Router) = 기계 장부에 안 적음
- B09 공사 내역 맞춤 기억(내역맞춤.json 읽기 · 쓰기 · 이 짝 잊기 길 · 단추) 걷음 — 제안은 짝 표만
- storage 옛 장부 파일(맞춤 · 내역맞춤 · 별칭반려)은 읽지 않게만 · 지우지 않음
- M02_Hopyo_Ledger 모듈은 남김 — 짝 검사 · 열쇠 도우미 · 요소 잇기의 옛 장부 읽기(M02_Machine_Find · M02_Hopyo_Units · M02_Hopyo_Sangeun — 815-1 몫)
- 시험: test_718_2_alias_gather · test_739_1_alias_central · test_m02_hopyo_build 지움(걷은 길 · 화면 지킴) · test_707_4 기억 층 · 잊기 시험을 「걷음」 시험으로 · test_m02_hopyo_ledger rows 줄 · test_m02_hopyo_screen · test_721 장부 화면 줄 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 15:02:00 +09:00

274 lines
14 KiB
Python

"""707-4 내역 줄 ↔ 우리 공종 잇기 — 짝 표 · 맞춤 기억 · 묶음 통째 잇기(`Bill_Link`) ·
기억 층(`Router_Link`).
- 짝 표 = 우리 토공 줄(`Bill_Items.EARTH`) 모두 · 실무 이름(EST 거창 · STmate 봉화) → 우리 줄 ·
암 갈래(EST 연암 · 보통암) = 후보 둘 · 하나 아님 · 여러 줄 → 한 줄 = 하나 아님
- 맞춤 기억 = 거창 엑셀에서 이은 짝 → 같은 회사 장수 · 진안 .estx 저절로 이음 · 두 번 셈 0
- 수량 갈래 take = 엑셀 수량 덮개 / price = B08 · 엑셀 = 비교 칸
- 묶음 ↔ 구조물 줄 — take = 안 줄이 구조물 줄 아래 / price = 1 단위 고정 단가 · 안 줄 지움 ·
금액 = 엑셀
- 못 잇는 짝(남의 줄 · 자재 줄 · 이미 이음 · 꼴 다름) · 기억 층 = 회사 관리자 회사 · 아니면 개인
- 셈 짝(`Calc_Bill` py · ts) 안 바뀜 — 거울은 test_707_1 · test_145_3 그대로
"""
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B09_Estimation import B09_Estimation_Bill_Import as imp # noqa: E402
from B09_Estimation import B09_Estimation_Bill_Link as lk # noqa: E402
from B09_Estimation import B09_Estimation_Estx as ex # noqa: E402
from B09_Estimation import B09_Estimation_Router_Link as route # noqa: E402
from B09_Estimation import B09_Estimation_Store as store # noqa: E402
from B09_Estimation.B09_Estimation_Bill_Excel import read_bill # noqa: E402
from B09_Estimation.B09_Estimation_Bill_Items import EARTH, earth_items # noqa: E402
from B09_Estimation.B09_Estimation_Calc_Bill import compute_estimation # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store # noqa: E402
K = ROOT / "resources" / "knowledge" / "original" / "실무문서"
GEO = (
K / "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx"
)
BONG = K / "04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx"
ESTX = {
"장수": K / "02.산불 장수/01.내역서/00.단가변경_26년_산불_무주(장수,장수,덕산,산54-1).estx",
"진안": K / "03.산불진안/01.내역서/00.단가변경_26년_산불_무주(진안,백운,백암,산1-2).estx",
}
ST = "st:pipe:box|1"
ITEMS = [{**i, "qty": 100} for i in earth_items(None)] + [
{"key": ST, "section": "2.2", "name": "BOX 암거", "unit": "m", "qty": 10,
"ref": {"tab": "sg_list", "key": "X"}},
] # fmt: skip
INPUTS = {"materials": [], "bill": {"items": ITEMS}, "hopyo": {"X": {
"labor": 100, "material": 0, "expense": 0}}} # fmt: skip
def _bill(user):
return compute_estimation(INPUTS, user, None, {})["bill"]
def _load(parsed):
"""707-1 먼저 싣기(붙임 없음) → (사용자 값, 셈 줄)."""
user, _ = imp.apply(parsed, {}, _bill({})["rows"], "abcd1234", attach=False)
user = store.clean_user(user)
return user, _bill(user)["rows"]
def _link(user, rows, pairs, mode="take", **kw):
made, report = lk.link(user, rows, pairs, mode, **kw)
made = store.clean_user(made)
return made, report, _bill(made)
@pytest.fixture(scope="module")
def geo():
return read_bill(GEO.name, GEO.read_bytes())
def _pick(suggestions):
"""사용자 고름 꼴 — 보통암 = 둘째(암발파) · 그 밖 = 첫 후보 · 자동 줄 하나에 한 줄."""
out, used = [], set()
for s in suggestions:
keys = [c["key"] for c in s["후보"]]
goal = keys[1] if len(keys) > 1 and "보통암" in s["spec"] else keys[0]
if goal not in used:
out.append((s["key"], goal))
used.add(goal)
return out
def test_짝_표_우리_토공_줄_모두():
names = {name for _s, name, _u, _w in EARTH}
assert len(names) == len(EARTH) == 17
assert all(lk.table(n, "") == (n,) for n in names) # 우리 이름 그대로
assert all(set(t) <= names for _a, _p, t in lk.PAIRS) # 짝 표 = 있는 줄만
assert set(lk.EARTH_KEY.values()) == {f"earth:{n}" for n in range(1, 18)}
def test_짝_표_실무_이름_암_갈래(geo):
user, rows = _load(geo)
got = {(s["name"], s["spec"]): s for s in lk.suggest(rows, {})}
one = {k: s["후보"][0]["name"] for k, s in got.items() if s["하나"]}
assert one == {
("토사절취", "백호0.7㎥"): "절토(토사)",
("측구터파기", "토사"): "측구파기(토사)",
("성토사면다짐", "B/H0.7+유압식콤팩터"): "성토사면다짐",
("토사운반", "굴삭기+덤프+굴삭기"): "덤프운반",
("수평규준틀", ""): "규준틀설치 종단규준틀",
("비탈규준틀", ""): "규준틀설치 횡단규준틀",
("구조물 터파기(토사)", "육샹토사(0~2m,굴삭기0.2)"): "구조물터파기(토사)",
("터파기(암)", ""): "구조물터파기(연암)",
("되메우기", "백호0.7㎥(100%)"): "구조물 되메우기",
}
rock = got[("암석절취", "보통암")]
assert [c["name"] for c in rock["후보"]] == ["절토(연암)", "암발파"]
assert not rock["하나"] and rock["까닭"] == "한 줄 ↔ 한 줄 아님 — 고름"
side = got[("측구터파기", "연암")] # 연암 · 보통암 둘 → 측구파기(연암) 하나
assert not side["하나"] and side["까닭"] == "여러 줄 → 한 줄 — 고름"
assert all(s["꼬리표"] == lk.TABLE for s in got.values())
bong = lk.suggest(_load(read_bill(BONG.name, BONG.read_bytes()))[1], {})
named = {s["name"]: s["후보"][0]["name"] for s in bong if s["하나"]}
assert named["토사절취(흙깎기)"] == "절토(토사)"
assert named["구조물터파기(육상암0~1m)"] == "구조물터파기(연암)"
assert named["비탈규준틀(@60M)"] == "규준틀설치 횡단규준틀"
def test_맞춤_기억_두_번째_공사_저절로(geo, tmp_path):
user, rows = _load(geo)
made, report, bill = _link(user, rows, _pick(lk.suggest(rows, {})))
assert report["이음"] == 12 and report["못함"] == []
hopyo_store.remember(tmp_path, report["기억"], lk.MEMORY)
assert hopyo_store.memory(tmp_path) == {} # 실무자료 맞춤과 다른 파일
memory = hopyo_store.memory(tmp_path, lk.MEMORY)
assert memory["암석절취|보통암|m3"]["확정"] == "earth:6"
for name, path in ESTX.items():
parsed = ex.read_bill(path.name, path.read_bytes())
user, rows = _load(parsed)
lines = lk.suggest(rows, memory)
assert sum(s["꼬리표"] == lk.RECALL for s in lines) == 12, name
pairs = lk.auto_pairs(lines)
assert len(pairs) == 11, name # 측구 연암 = 보통암 줄과 한 자동 줄 → 고름
made, report, bill = _link(user, rows, pairs)
assert report["이음"] == 11 and report["못함"] == []
keys = [r["key"] for r in bill["rows"]]
assert len(keys) == len(set(keys)) # 두 번 셈 0 — 줄 하나씩
assert not {k for k, _t in pairs} & set(keys) # 이은 불러온 줄 = 지움
# take = 엑셀 수량 · 고정 단가 → 직접비(아는 금액) = 파일 합 + 안 이은 구조물 줄
assert bill["totals"]["direct"]["sum"] == parsed["합계"]["직접"] + 1000, name
assert lk.auto_pairs(lk.suggest(bill["rows"], memory)) == [] # 다시 = 이음 없음
def test_수량_갈래_가_나(geo):
user, rows = _load(geo)
line = next(r for r in rows if r["name"] == "토사절취")
for mode, qty in (("take", 7641), ("price", 100)):
made, report, bill = _link(user, rows, [(line["key"], "earth:4")], mode)
cut = next(r for r in bill["rows"] if r["key"] == "earth:4")
assert (cut["qty"], cut["src_qty"], cut["fixed"]) == (qty, 7641, True)
assert cut["parent"] == line["parent"] # 불러온 자리
assert cut["price_sum"] == line["price_sum"]
assert line["key"] not in {r["key"] for r in bill["rows"]}
def test_묶음_통째_잇기(geo):
user, rows = _load(geo)
base = _bill(user)["totals"]["direct"]["sum"]
group = next(r for r in rows if r["name"] == "콘크리트구조물")
inner = [r["key"] for r in rows if r["parent"] == group["key"]]
made, report, bill = _link(user, rows, [(group["key"], ST)], "take")
got = {r["key"]: r for r in bill["rows"]}
assert report["묶음"] == 1 and group["key"] not in got
assert got[ST]["kind"] == "group" and got[ST]["sum"] == group["sum"]
assert all(got[k]["parent"] == ST for k in inner) # 안 줄 = 딸려 감(1 : N)
assert bill["totals"]["direct"]["sum"] == base - 1000 # 구조물 줄 제 금액 = 안 줄 합으로
assert report["기억"] == {"콘크리트구조물||": {"확정": ST, "이름": "BOX 암거"}}
made, report, bill = _link(user, rows, [(group["key"], ST)], "price")
got = {r["key"]: r for r in bill["rows"]}
assert report["지움"] == len(inner) and not set(inner) & set(got)
assert got[ST]["kind"] == "item" and (got[ST]["qty"], got[ST]["src_qty"]) == (10, 10)
assert abs(got[ST]["sum"] - group["sum"]) <= 3 # B08 수량 × 1 단위 단가 · 끝수
made, report, bill = _link(user, rows, [(group["key"], ST)], "price", counts={group["key"]: 5})
assert next(r for r in bill["rows"] if r["key"] == ST)["sum"] == pytest.approx(
group["sum"] * 2, abs=3
) # 경쟁사 5 개 · B08 10 개 = 두 배
memory = {"콘크리트구조물||": {"확정": "st:other", "이름": "BOX 암거"}}
assert lk.auto_pairs(lk.suggest(rows, memory)) == [(group["key"], ST)] # 키 없음 = 같은 이름
def test_못_잇는_짝(geo):
user, rows = _load(geo)
line = next(r for r in rows if r["name"] == "토사절취")
group = next(r for r in rows if r["name"] == "콘크리트구조물")
pairs = [
("earth:3", "earth:4"),
(line["key"], "mat:x"),
(line["key"], ST),
(group["key"], "earth:4"),
(line["key"], "earth:4"),
(next(r for r in rows if r["name"] == "수평규준틀")["key"], "earth:4"),
]
_made, report, _bill = _link(user, rows, pairs)
assert [m["까닭"] for m in report["못함"]] == [
"불러온 줄이 아님",
"이을 수 없는 줄",
"묶음 ↔ 구조물 줄만",
"묶음 ↔ 구조물 줄만",
"이미 이은 줄",
]
assert report["이음"] == 1
with pytest.raises(ValueError):
lk.link(user, rows, [], "both")
assert lk.link(user, rows, [], "take")[1]["이음"] == 0
def test_기억_길_걷음():
"""공사 내역 맞춤 기억(층 `내역맞춤.json` 읽기 · 쓰기 · 잊기)은 걷음(815-2 · 요구 765-2)."""
assert not any(hasattr(route, x) for x in ("recalled", "keep", "forget", "ForgetBody"))
def _same(a, b):
"""두 셈 = 같은 줄 · 같은 직접비."""
assert [r["key"] for r in a["rows"]] == [r["key"] for r in b["rows"]]
assert a["totals"]["direct"]["sum"] == b["totals"]["direct"]["sum"]
@pytest.mark.parametrize("mode", ["take", "price"])
def test_잇기_풀기_제자리(geo, mode):
user, rows = _load(geo)
before = _bill(user)
group = next(r for r in rows if r["name"] == "콘크리트구조물")
pairs = [*_pick(lk.suggest(rows, {})), (group["key"], ST)]
made, report, bill = _link(user, rows, pairs, mode)
assert report["이음"] == 12 and report["묶음"] == 1
linked = [e for e in made["bill"]["rows"] if e.get("was")]
assert len(linked) == 13 # 저장 다듬기 뒤에도 되돌릴 것 남음
for entry in linked[::2] + linked[1::2]: # 이은 차례와 다르게
made, why = lk.unlink(made, bill["rows"], entry["key"])
assert why == ""
made = store.clean_user(made)
bill = _bill(made)
_same(bill, before)
assert lk.unlink(made, bill["rows"], "earth:4")[1] == "이은 줄이 아님"
def test_줄마다_갈래_판(geo):
user, rows = _load(geo)
cut = next(r for r in rows if r["name"] == "토사절취")
side = next(r for r in rows if r["name"] == "측구터파기" and r["spec"] == "토사")
pairs = [(cut["key"], "earth:4"), (side["key"], "earth:11")]
made, report, bill = _link(user, rows, pairs, "take", modes={side["key"]: "price"})
got = {r["key"]: r for r in bill["rows"]}
assert got["earth:4"]["qty"] == cut["qty"] and got["earth:11"]["qty"] == 100
with pytest.raises(ValueError):
lk.link(user, rows, pairs, "take", modes={cut["key"]: "both"})
board = lk.board(made, bill["rows"])
left = {r["key"] for r in board["남은"]}
assert cut["key"] not in left and side["key"] not in left
assert {r["key"] for r in board["이은"]} == {"earth:4", "earth:11"}
assert board["이은"][0]["memo"] in report["기억"]
assert not {"earth:4", "earth:11"} & {r["key"] for r in board["자리"]}
group = next(r for r in rows if r["name"] == "콘크리트구조물")
inner = {r["key"] for r in rows if r["parent"] == group["key"]}
made, _r, bill = _link(made, bill["rows"], [(group["key"], ST)], "take")
left = {r["key"] for r in lk.board(made, bill["rows"])["남은"]}
assert not inner & left # 구조물 묶음에 딸린 줄 = 남은 줄 아님
def test_잊은_짝_짝_표_제안에도_안_나옴(geo):
_user, rows = _load(geo)
now = {s["key"]: s for s in lk.suggest(rows, {})}
line = next(r for r in rows if (r["name"], r.get("spec")) == ("토사절취", "백호0.7㎥"))
assert now[line["key"]]["꼬리표"] == lk.TABLE
memory = {lk.key_of(line): {lk.FORGOT: True}}
after = {s["key"]: s for s in lk.suggest(rows, memory)}
assert line["key"] not in after # 짝 표 제안도 잊음
assert set(now) - set(after) == {line["key"]} # 다른 줄은 그대로
memory[lk.key_of(line)] = {"확정": "earth:4", "이름": "절토(토사)"} # 다시 이음 = 풀림
assert lk.suggest(rows, memory)[0] is not None
assert line["key"] in {s["key"] for s in lk.suggest(rows, memory)}