- 需人工核对:份数、密封、递交、CA 签章加密、解密、保证金这类文件里看不出来的要求, 从正式清单按主题合并、带招标原文出处;manual-checks 接口按项目存勾选、写勾选人和时间, 复查沿用;查标页加「需人工核对」一块,覆盖范围写明列了几条 - 「报价」类清单项:读标从采购需求的清单表规则读出分项 / 单价上限(checklist/price.py); 查标交给规则 PRICE.OVER_CEILING_ITEM 比对分项报价表,不交给模型;规则集 2026.11-r2 - 版面工具(分页、分行、归栏)挪到 document/pipeline/layout.py,读标和查标共用 - 评测:price_defects.py 从规矩投标文件派生只带报价问题的 4 份(P01、P02、P03) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
119 lines
5.2 KiB
Python
119 lines
5.2 KiB
Python
"""从规矩的投标文件派生只带报价问题的投标文件(P01 超总价限价、P02 超采购包限价、P03 分项超单价上限)。
|
||
|
||
cd server && ../.venv/bin/python ../evals/check/price_defects.py <规矩的投标文件> <输出> <P01|P02|P03> [--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()
|