Spaces:
Running
Running
File size: 7,652 Bytes
dd6303a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | """
Enhanced Tender Runner
----------------------
One high-level API for local use and the Gradio dashboard.
Rules:
- input files live in: tender_engine/input/<tender_id>/
- output files go to: tender_engine/output/<tender_id>/
- each tender may have: tender_engine/input/<tender_id>/context.json
"""
from __future__ import annotations
import json
import pathlib
import shutil
from dataclasses import asdict
from typing import Dict, List, Optional
from .context import create_default_context, load_context, merge_context_into_firm_config, save_context
from .pipeline import run_pipeline
from .checker import check_rates, summary_to_dict
from .reports import generate_rate_check_excel, generate_summary_txt
from .sor import parse_bwdb_sor, parse_lged_sor, build_sor_lookup, detect_bwdb_zone
from .models import BOQItem
from .local_features import (
scan_required_documents, save_checklist_report, save_cache,
build_search_index, detect_duplicates, export_review_markdown,
)
BASE_DIR = pathlib.Path(__file__).parent
INPUT_DIR = BASE_DIR / "input"
OUTPUT_DIR = BASE_DIR / "output"
TEMPLATE_DIR = BASE_DIR / "templates"
ROOT_DIR = BASE_DIR.parent
def ensure_tender_folders(tender_id: str) -> Dict[str, pathlib.Path]:
"""Create input/<tender_id> and output/<tender_id>."""
in_dir = INPUT_DIR / tender_id
out_dir = OUTPUT_DIR / tender_id
in_dir.mkdir(parents=True, exist_ok=True)
out_dir.mkdir(parents=True, exist_ok=True)
ctx_path = in_dir / "context.json"
if not ctx_path.exists():
create_default_context(tender_id)
return {"input": in_dir, "output": out_dir, "context": ctx_path}
def copy_uploaded_files(tender_id: str, file_paths: List[str]) -> List[str]:
"""Copy uploaded PDFs into input/<tender_id>."""
folders = ensure_tender_folders(tender_id)
copied = []
for fp in file_paths or []:
src = pathlib.Path(fp)
if not src.exists():
continue
dst = folders["input"] / src.name
shutil.copy2(src, dst)
copied.append(dst.name)
return copied
def generate_tender(tender_id: str, context_updates: Optional[dict] = None, run_rate_check: bool = True) -> dict:
"""
Generate all documents for a tender.
Returns a structured status dict for CLI and GUI.
"""
folders = ensure_tender_folders(tender_id)
ctx = load_context(tender_id)
if context_updates:
ctx.update({k: v for k, v in context_updates.items() if v is not None})
save_context(tender_id, ctx)
checklist = scan_required_documents(str(folders["input"]))
if not checklist["ready_to_generate"]:
return build_status(tender_id, extra={"error": "Missing critical documents", "checklist": checklist})
firm_config = merge_context_into_firm_config(tender_id)
output_folder = run_pipeline(
input_folder=str(folders["input"]),
template_folder=str(TEMPLATE_DIR),
output_base=str(OUTPUT_DIR),
firm_config=firm_config,
)
save_checklist_report(str(folders["input"]), str(folders["output"]))
rate_summary = None
if run_rate_check:
try:
rate_summary = run_rate_cross_check(tender_id)
except Exception as exc:
rate_summary = {"error": str(exc)}
duplicate_matches = detect_duplicates(tender_id)
review_report = export_review_markdown(tender_id)
status = build_status(tender_id, extra={
"rate_check": rate_summary,
"duplicates": duplicate_matches,
"review_report": review_report,
})
save_cache(tender_id, status)
build_search_index()
return status
def run_rate_cross_check(tender_id: str, sor_source: str = "BWDB") -> dict:
"""Create rate check Excel + text summary for an already generated tender."""
folders = ensure_tender_folders(tender_id)
data_path = folders["output"] / "extracted_data.json"
if not data_path.exists():
raise FileNotFoundError("Generate documents first; extracted_data.json is missing.")
data = json.loads(data_path.read_text(encoding="utf-8"))
ctx = load_context(tender_id)
location = ctx.get("location") or data.get("location") or ""
zone = (ctx.get("zone") or detect_bwdb_zone(location) or "A").upper()
boq_items = [_boq_item_from_dict(x) for x in data.get("boq_items", [])]
if not boq_items:
raise ValueError("No BOQ items found in extracted_data.json.")
firm = merge_context_into_firm_config(tender_id)
if sor_source.upper() == "LGED":
sor_pdf = firm.get("sor_pdf_lged") or str(ROOT_DIR.parent / "LGED Revised Rate Schedule,2023.pdf")
sor_items = parse_lged_sor(sor_pdf)
else:
sor_pdf = firm.get("sor_pdf_bwdb") or str(ROOT_DIR.parent / "BWDB Revised Rate Schedule,2023.pdf")
sor_items = parse_bwdb_sor(sor_pdf)
lookup = build_sor_lookup(sor_items)
summary = check_rates(boq_items, lookup, zone, tender_id)
report_xlsx = folders["output"] / f"Rate_Check-{tender_id}.xlsx"
report_txt = folders["output"] / f"Summary-{tender_id}.txt"
generate_rate_check_excel(summary, str(report_xlsx))
generate_summary_txt(summary, str(report_txt))
summary_json = folders["output"] / f"Summary-{tender_id}.json"
summary_json.write_text(json.dumps(summary_to_dict(summary), indent=2), encoding="utf-8")
return summary_to_dict(summary)
def build_status(tender_id: str, extra: Optional[dict] = None) -> dict:
folders = ensure_tender_folders(tender_id)
input_files = sorted([p.name for p in folders["input"].glob("*") if p.is_file()])
output_files = sorted([p.name for p in folders["output"].glob("*") if p.is_file()])
result = {
"tender_id": tender_id,
"input_folder": str(folders["input"]),
"output_folder": str(folders["output"]),
"context_file": str(folders["context"]),
"input_files": input_files,
"output_files": output_files,
"output_count": len(output_files),
}
if extra:
result.update(extra)
return result
def list_all_tender_statuses() -> List[dict]:
if not INPUT_DIR.exists():
return []
ids = sorted([p.name for p in INPUT_DIR.iterdir() if p.is_dir()])
return [build_status(tid) for tid in ids]
def create_batch_script(tender_id: str) -> pathlib.Path:
"""Create batch_GEN_<tender_id>.py at project root."""
ensure_tender_folders(tender_id)
script = ROOT_DIR / f"batch_GEN_{tender_id}.py"
content = f'''"""Generate all tender files for Tender ID {tender_id}."""
import pathlib, sys
BASE = pathlib.Path(__file__).parent
sys.path.insert(0, str(BASE))
from tender_engine.enhanced_runner import generate_tender
if __name__ == "__main__":
result = generate_tender("{tender_id}", run_rate_check=True)
print("Generated Tender:", result["tender_id"])
print("Output folder:", result["output_folder"])
print("Files:")
for name in result["output_files"]:
print(" -", name)
'''
script.write_text(content, encoding="utf-8")
return script
def _boq_item_from_dict(d: dict) -> BOQItem:
return BOQItem(
item_no=int(d.get("item_no", 0)),
item_code=str(d.get("item_code", "")),
description=str(d.get("description", "")),
quantity=float(d.get("quantity", 0) or 0),
unit=str(d.get("unit", "")),
bwdb_rate=float(d.get("bwdb_rate", 0) or 0),
bwdb_amount=float(d.get("bwdb_amount", 0) or 0),
quoted_rate=float(d.get("quoted_rate", 0) or d.get("bwdb_rate", 0) or 0),
quoted_amount=float(d.get("quoted_amount", 0) or 0),
percent_diff=float(d.get("percent_diff", 0) or 0),
)
|