77 lines
2.5 KiB
Python
77 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())
|