MEGA-BOQ-WEBAPP/run.py
MEGA INFRA 301d0fff44 MEGA INFRA BOQ Converter — Candy PDF to consultant Excel
Reads our Candy PDF (deterministically, no OCR or model needed) and populates
the consultant's own workbook using their formulas, column layout and PAYMENT
column. Nothing is ever silently overwritten: every disagreement is reported,
can be settled inside the app, and regenerated.

- extract   Candy text PDF -> tree (x-position column detection)
- template  layout detect, sequence align, write, totals, overrides
- validate  qty x rate and children-vs-subtotal reconciliation
- profiles  per-consultant layout profiles, stored on a volume

Shipped with a Dockerfile + docker-compose for Coolify.
2026-10-01 02:32:13 +02:00

76 lines
2.5 KiB
Python

#!/usr/bin/env python3
"""
BOQ PDF -> Excel + CSV
python3 run.py tender.pdf # writes out/BOQ_<name>.xlsx + .csv
python3 run.py tender.pdf --report # also prints the validation report
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from boq_engine.extract import extract
from boq_engine.populate import flatten, populate_template, to_csv
from boq_engine.validate import validate
def safe(name: str) -> str:
return re.sub(r"[^A-Za-z0-9]+", "_", name).strip("_")[:60]
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("pdf")
ap.add_argument("-o", "--outdir", default="out")
ap.add_argument("--contract", default="")
ap.add_argument("--title", default="BILL OF QUANTITIES")
ap.add_argument("--vat", type=float, default=15.0)
ap.add_argument("--report", action="store_true")
ap.add_argument("--json", dest="json_out")
args = ap.parse_args()
base = safe(os.path.splitext(os.path.basename(args.pdf))[0])
os.makedirs(args.outdir, exist_ok=True)
xlsx = os.path.join(args.outdir, f"{base}.xlsx")
csvp = os.path.join(args.outdir, f"{base}.csv")
jsonp = args.json_out or os.path.join(args.outdir, f"{base}.json")
print(f"reading {args.pdf}")
tree = extract(args.pdf)
rep = validate(tree)
stats = populate_template(tree, xlsx, title=args.title,
contract=args.contract, vat_pct=args.vat)
nrows = to_csv(tree, csvp)
with open(jsonp, "w", encoding="utf-8") as fh:
json.dump({"tree": tree, "validation": rep.as_dict()}, fh,
indent=1, default=str)
print(f"\n priced lines {stats['lines']}")
print(f" groups {stats['groups']}")
print(f" 'Rate only' {stats['rate_only']}")
print(f" odd units {stats['unknown_units']} (highlighted in the workbook)")
print(f" csv rows {nrows}")
print(f" validation {len(rep.errors)} error(s), {len(rep.warnings)} warning(s)")
if rep.errors or args.report:
print("\nVALIDATION")
for i in rep.issues:
if i.severity == "info" and not args.report:
continue
mark = {"error": "ERROR", "warn": "WARN ", "info": "INFO "}[i.severity]
print(f" {mark} {i.code}")
print(f" {i.where}")
print(f" {i.detail}")
print(f"\nwrote {xlsx}\nwrote {csvp}\nwrote {jsonp}")
return 1 if rep.errors else 0
if __name__ == "__main__":
sys.exit(main())