"""从规矩的投标文件派生只带报价问题的投标文件(P01 超总价限价、P02 超采购包限价、P03 分项超单价上限)。 cd server && ../.venv/bin/python ../evals/check/price_defects.py <规矩的投标文件> <输出> [--variant ...] 然后 ../.venv/bin/python ../evals/check/render.py <输出> - 只改一个分项的单价,再重算该行合价、合计、开标一览表的小写和大写,算术前后一致:只触发要测的那一条规则; - 响应清单照抄规矩的那份(单元都是 expect=ok),rule_defects 记这一个问题;不挂标注单元,不需要复核(E1 只管单元); - 限价、单价上限取招标档案(evals/out/check/tenders/<招标文件>.json),和读标抽出的值一致时才有意义,生成后自己核对一遍。 """ from __future__ import annotations import argparse import json import pathlib import sys from decimal import ROUND_UP, Decimal from typing import Any ROOT = pathlib.Path(__file__).resolve().parent OUT = ROOT.parent / "out" / "check" sys.path.insert(0, str(ROOT)) from build import rmb_upper # noqa: E402 VARIANTS = {"P01": ["超 1 元", "超 1%"], "P02": ["超 1 元", "超 1%"], "P03": ["单价超 1%"]} def fmt(v: Decimal) -> str: return f"{v:,.2f}" def blocks(spec: dict[str, Any]) -> list[dict[str, Any]]: return [b for v in spec["volumes"] for b in v["blocks"]] def reprice(spec: dict[str, Any], index: int, unit_price: Decimal) -> Decimal: """把第 index 个分项的单价改成 unit_price,重算合价、合计、开标一览表;正文里出现的旧总价一并替换。返回新总价。""" items = spec["price"]["items"] old_total = Decimal(spec["price"]["total"]) items[index]["unit_price"] = f"{unit_price:.2f}" lines = [Decimal(str(i["qty"])) * Decimal(i["unit_price"]) for i in items] total = sum(lines, Decimal(0)).quantize(Decimal("0.01")) spec["price"]["total"] = f"{total:.2f}" table = next(b for b in blocks(spec) if b.get("price_items")) row = table["rows"][index] row["cells"][-2], row["cells"][-1] = fmt(unit_price), fmt(lines[index].quantize(Decimal("0.01"))) pairs = [(fmt(old_total), fmt(total)), (rmb_upper(old_total), rmb_upper(total))] for b in blocks(spec): if b.get("type") == "table": for r in b["rows"]: r["cells"] = [_swap(c, pairs) for c in r["cells"]] elif "text" in b: b["text"] = _swap(b["text"], pairs) if "lines" in b: b["lines"] = [_swap(x, pairs) for x in b["lines"]] return total def _swap(text: str, pairs: list[tuple[str, str]]) -> str: for a, b in pairs: text = text.replace(a, b) return text def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("clean") ap.add_argument("out") ap.add_argument("kind", choices=sorted(VARIANTS)) ap.add_argument("--variant") a = ap.parse_args() variant = a.variant or VARIANTS[a.kind][0] src = OUT / a.clean spec = json.loads((src / "spec.json").read_text()) manifest = json.loads((src / "manifest.json").read_text()) profile = json.loads((OUT / "tenders" / f"{spec['tender']}.json").read_text()) items = spec["price"]["items"] total = Decimal(spec["price"]["total"]) forms = [f for f in profile["forms"] if f.get("kind") == "price_items"] caps = {x["name"]: Decimal(str(x["ceiling"])) for x in (forms[0].get("items") or []) if x.get("ceiling")} if a.kind in ("P01", "P02"): ceiling = Decimal(str(spec["ceiling_total"])) target = ceiling + 1 if variant == "超 1 元" else (ceiling * Decimal("1.01")).quantize(Decimal("0.01")) i = next((n for n, x in enumerate(items) if Decimal(str(x["qty"])) == 1), 0) qty = Decimal(str(items[i]["qty"])) unit = Decimal(items[i]["unit_price"]) + ((target - total) / qty).quantize(Decimal("0.01"), rounding=ROUND_UP) new_total = reprice(spec, i, unit) assert new_total > ceiling, (new_total, ceiling) snippet = fmt(new_total) else: i = next(n for n, x in enumerate(items) if x["name"] in caps) unit = (caps[items[i]["name"]] * Decimal("1.01")).quantize(Decimal("0.01")) new_total = reprice(spec, i, unit) snippet = fmt(unit) out = OUT / a.out out.mkdir(parents=True, exist_ok=True) spec["bid"] = a.out manifest.update( bid=a.out, kind="sloppy", density="price", rule_defects=[ {"id": f"{a.kind}-1", "type": a.kind, "variant": variant, "t": snippet}, # 分项上限加起来常常就是限价:总价超限价时,被抬高的分项也超了上限,一并记 *( [{"id": "P03-2", "type": "P03", "variant": "随总价超限", "t": fmt(Decimal(x["unit_price"]))}] if a.kind != "P03" and (x := items[i])["name"] in caps and Decimal(x["unit_price"]) > caps[x["name"]] else [] ), ], ) (out / "spec.json").write_text(json.dumps(spec, ensure_ascii=False, indent=1)) (out / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=1)) print(f"{a.out}:{a.kind}({variant}),总价 {fmt(total)} → {fmt(new_total)},原文片段「{snippet}」") if __name__ == "__main__": main()