- 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>
274 lines
14 KiB
Python
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)}
|