"""「报价」类清单项(C7):从招标文件的清单表读分项 / 单价上限,查标时和分项报价表比对。内容虚构。""" from __future__ import annotations from decimal import Decimal from uuid import uuid4 from bid_doctor.check import applicability, match, rules from bid_doctor.checklist import price from bid_doctor.document.pipeline.text_layer import Sentence COLS = [(80, 110), (110, 300), (300, 340), (340, 380), (380, 470)] def row(seq: int, y: float, texts: list[str], page: int = 1) -> list[Sentence]: out = [] for n, (t, (x0, x1)) in enumerate(zip(texts, COLS, strict=False)): s = Sentence( key=f"t:p{page}:s{seq + n}", page_no=page, seq=seq + n, text=t, quads=[[x0 + 2, y + 4, x1 - 2, y + 16]] ) s.cell = [x0, y, x1, y + 20] out.append(s) return out def line(seq: int, y: float, text: str, page: int = 1) -> Sentence: return Sentence(key=f"t:p{page}:s{seq}", page_no=page, seq=seq, text=text, quads=[[74, y, 500, y + 12]]) def tender() -> list[Sentence]: return [ line(1, 60, "采购包2:数据中心扩容"), *row(10, 80, ["序号", "分项名称", "数量", "单位", "单价上限(万元)"]), *row(20, 100, ["1", "服务器", "4", "台", "12.5"]), *row(30, 120, ["2", "服务器机柜", "2", "个", "0.8"]), line(40, 160, "采购包一览:各包的最高限价见下表,投标报价超过所投采购包最高限价的投标无效。"), *row(50, 180, ["序号", "采购包名称", "数量", "单位", "最高限价(元)"]), # 采购包一级:不是分项 *row(60, 200, ["1", "采购包2", "1", "项", "600000"]), ] def test_caps_from_tender_table() -> None: caps = price.caps(tender()) assert [(c.lot_no, c.name, c.cap, c.per, c.qty) for c in caps] == [ ("2", "服务器", Decimal("125000.0"), "unit", Decimal(4)), ("2", "服务器机柜", Decimal("8000.0"), "unit", Decimal(2)), ] assert caps[0].title == "服务器:单价不超过 125,000.00 元" and caps[0].sentences[0].text == "服务器" assert caps[0].spec()["price"] == { "kind": "unit_cap", "name": "服务器", "cap": "125000.00", "qty": "4", "lot_no": "2", } def test_price_items_are_checked_by_rule_not_model() -> None: d = applicability.decide({"categories": ["price"], "title": "服务器:单价不超过 125,000.00 元"}, {}) assert (d.match, d.skip) == (False, "price") def bid_row(seq: int, y: float, texts: list[str]) -> list[Sentence]: out = [] for n, (t, (x0, x1)) in enumerate( zip(texts, [(80, 100), (100, 260), (260, 300), (300, 380), (380, 470)], strict=True) ): s = Sentence(key=f"b:p1:s{seq + n}", page_no=1, seq=seq + n, text=t, quads=[[x0 + 2, y + 4, x1 - 2, y + 16]]) s.cell = [x0, y, x1, y + 20] out.append(s) return out def test_item_over_cap_is_fatal_and_similar_names_do_not_match() -> None: bid = [ *bid_row(1, 80, ["序号", "货物名称", "数量", "单价(元)", "合价(元)"]), *bid_row(10, 100, ["1", "服务器", "4", "126,000.00", "504,000.00"]), *bid_row(20, 120, ["2", "服务器机柜", "2", "7,900.00", "15,800.00"]), ] item = uuid4() facts = rules.Facts( caps=[ rules.ItemCap("服务器", Decimal(125_000), "unit", item), rules.ItemCap("服务器机柜", Decimal(8_000), "unit"), ] ) hits, cov = rules.run(facts, [match.BidDoc(uuid4(), 1, "报价分册", bid)], []) (h,) = [h for h in hits if h.code == "PRICE.OVER_CEILING_ITEM"] assert h.severity == "fatal" and h.item == item and h.basis == f"cap:{item}" and "服务器" in h.title assert "126,000.00 元" in h.explanation and "125,000.00 元" in h.explanation assert not any("其余没有比对" in x for x in cov["not_checked"]) # 两条上限都对上了名称 assert rules._match_cap("服务器", [rules.ItemCap("服务器机柜", Decimal(1), "unit")]) is None