from flask import Blueprint, render_template, request, send_file, flash import pandas as pd import io import re from collections import defaultdict from app.models.subcontractor_model import Subcontractor from app.models.trench_excavation_model import TrenchExcavation from app.models.manhole_excavation_model import ManholeExcavation from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber from app.models.laying_model import Laying from app.models.tr_ex_client_model import TrenchExcavationClient from app.models.mh_ex_client_model import ManholeExcavationClient from app.models.mh_dc_client_model import ManholeDomesticChamberClient from app.models.laying_client_model import LayingClient from app.utils.helpers import login_required generate_report_bp = Blueprint("generate_report", __name__, url_prefix="/report") # --- REGEX PATTERNS FOR TOTALING --- PIPE_MM_PATTERN = re.compile(r"^pipe_\d+_mm$") D_RANGE_PATTERN = re.compile(r"^d_\d+(?:_\d+)?_to_\d+(?:_\d+)?$") # --- UTILITIES --- def normalize_key(value): if value is None: return "" return str(value).strip().upper() def format_header(header): if "-" in header: prefix, rest = header.split("-", 1) prefix = prefix.title() else: prefix, rest = None, header parts = rest.split("_") result = [] i = 0 while i < len(parts): if i + 1 < len(parts) and parts[i].isdigit() and parts[i + 1].isdigit(): result.append(f"{parts[i]}.{parts[i + 1]}") i += 2 else: result.append(parts[i].title()) i += 1 final_text = " ".join(result) return f"{prefix}-{final_text}" if prefix else final_text def make_lookup(rows, key_field): """Creates a mapping of (Location, Key) to a list of records.""" lookup = defaultdict(list) for r in rows: # Check both capitalized and lowercase keys for robustness loc = normalize_key(r.get("Location") or r.get("location")) key = normalize_key(r.get(key_field) or r.get(key_field.lower())) if loc and key: lookup[(loc, key)].append(r) return lookup def calculate_row_total(row_dict): """Calculates total based on _total suffix or regex patterns.""" return sum( float(v or 0) for k, v in row_dict.items() if k.endswith("_total") or D_RANGE_PATTERN.match(k) or PIPE_MM_PATTERN.match(k) ) # --- CORE COMPARISON LOGIC --- def build_comparison(client_rows, contractor_rows, key_field): # 1. Create Lookup for Subcontractors contractor_lookup = {} for r in contractor_rows: loc = normalize_key(r.get("Location") or r.get("location")) key = normalize_key(r.get(key_field) or r.get(key_field.lower())) if loc and key: contractor_lookup[(loc, key)] = r output = [] # 2. Iterate through Client rows for c in client_rows: loc_raw = c.get("Location") or c.get("location") key_raw = c.get(key_field) or c.get(key_field.lower()) loc_norm = normalize_key(loc_raw) key_norm = normalize_key(key_raw) # Match check s = contractor_lookup.get((loc_norm, key_norm)) # We only include the row if there is a match (Inner Join) if s: client_total = calculate_row_total(c) sub_total = calculate_row_total(s) row = { "Location": loc_raw, key_field.replace("_", " "): key_raw } # Add Client Data for k, v in c.items(): if k in ["id", "created_at"]: continue row[f"Client-{k}"] = v row["Client-Total"] = round(client_total, 2) row[" "] = "" # Spacer # Add Subcontractor Data (Aligned on same row) for k, v in s.items(): if k in ["id", "created_at", "subcontractor_id"]: continue row[f"Subcontractor-{k}"] = v row["Subcontractor-Total"] = round(sub_total, 2) row["Diff"] = round(client_total - sub_total, 2) output.append(row) # 3. Handle the "Empty/Blank" scenario using pd.concat if not output: # Create a basic dataframe with a message so the Excel file isn't empty/corrupt return pd.DataFrame([{"Location": "N/A", "Message": "No matching data found"}]) df = pd.DataFrame(output) df.columns = [format_header(col) for col in df.columns] return df # --- EXCEL WRITER --- def write_sheet(writer, df, sheet_name, subcontractor_name): if df.empty: return workbook = writer.book df.to_excel(writer, sheet_name=sheet_name, index=False, startrow=3) ws = writer.sheets[sheet_name] # Formats title_fmt = workbook.add_format({"bold": True, "font_size": 14}) client_header_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#B6DAED", "align": "center"}) sub_header_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F3A081", "align": "center"}) total_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F7D261", "align": "center"}) diff_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#82DD49", "align": "center"}) default_header_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#E7E6E6", "align": "center"}) # Header Titles ws.merge_range(0, 0, 0, len(df.columns) - 1, "CLIENT vs SUBCONTRACTOR COMPARISON", title_fmt) ws.merge_range(1, 0, 1, len(df.columns) - 1, f"Subcontractor: {subcontractor_name}", title_fmt) for col_num, col_name in enumerate(df.columns): if col_name.startswith("Client-"): fmt = client_header_fmt elif col_name.startswith("Subcontractor-"): fmt = sub_header_fmt elif "Total" in col_name: fmt = total_fmt elif col_name == "Diff": fmt = diff_fmt else: fmt = default_header_fmt ws.write(3, col_num, col_name, fmt) ws.set_column(col_num, col_num, 18) # --- ROUTES --- @generate_report_bp.route("/comparison_report", methods=["GET", "POST"]) @login_required def comparison_report(): subcontractors = Subcontractor.query.all() if request.method == "POST": subcontractor_id = request.form.get("subcontractor_id") if not subcontractor_id: flash("Please select a subcontractor", "danger") return render_template("generate_comparison_report.html", subcontractors=subcontractors) subcontractor = Subcontractor.query.get_or_404(subcontractor_id) # Build Dataframes for each section sections = [ (TrenchExcavationClient, TrenchExcavation, "Tr.Ex"), (ManholeExcavationClient, ManholeExcavation, "Mh.Ex"), (ManholeDomesticChamberClient, ManholeDomesticChamber, "MH & DC"), (LayingClient, Laying, "Laying") ] output = io.BytesIO() filename = f"{subcontractor.subcontractor_name}_Comparison_Report.xlsx" with pd.ExcelWriter(output, engine="xlsxwriter") as writer: for client_model, sub_model, sheet_name in sections: c_data = [r.serialize() for r in client_model.query.all()] s_data = [r.serialize() for r in sub_model.query.filter_by(subcontractor_id=subcontractor_id).all()] df = build_comparison(c_data, s_data, "MH_NO") write_sheet(writer, df, sheet_name, subcontractor.subcontractor_name) output.seek(0) return send_file( output, as_attachment=True, download_name=filename, mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) return render_template("generate_comparison_report.html", subcontractors=subcontractors)