Merge branch 'dev-anish' of http://gitea.lcepl.org/pjpatil12/Comparison_Project into dev-anish

This commit is contained in:
2026-02-02 14:57:20 +05:30
commit 37dd118cbd
57 changed files with 4063 additions and 0 deletions

0
app/routes/__init__.py Normal file
View File

48
app/routes/auth.py Normal file
View File

@@ -0,0 +1,48 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, session
from app.services.user_service import UserService
auth_bp = Blueprint("auth", __name__)
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
if session.get("user_id"):
return redirect(url_for("dashboard.dashboard"))
if request.method == "POST":
email = request.form.get("email")
password = request.form.get("password")
user = UserService.validate_login(email, password)
if user:
session["user_id"] = user.id
session["user_name"] = user.name
flash("Login successful", "success")
return redirect(url_for("dashboard.dashboard"))
flash("Invalid email or password", "danger")
return render_template("login.html", title="Login")
@auth_bp.route("/logout")
def logout():
session.clear()
flash("Logged out successfully", "info")
return redirect(url_for("auth.login"))
@auth_bp.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
name = request.form.get("name")
email = request.form.get("email")
password = request.form.get("password")
user = UserService.register_user(name, email, password)
if not user:
flash("Email already exists", "danger")
return redirect(url_for("auth.register"))
flash("User registered successfully", "success")
return redirect(url_for("auth.login"))
return render_template("register.html", title="Register")

136
app/routes/dashboard.py Normal file
View File

@@ -0,0 +1,136 @@
# import matplotlib
# matplotlib.use("Agg")
# from flask import Blueprint, render_template, session, redirect, url_for
# import matplotlib.pyplot as plt
# import io
# import base64
# from app.utils.plot_utils import plot_to_base64
# from app.services.dashboard_service import DashboardService
# dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/dashboard")
# # dashboard_bp = Blueprint("dashboard", __name__)
# # charts
# # def plot_to_base64():
# # img = io.BytesIO()
# # plt.savefig(img, format="png", bbox_inches="tight")
# # plt.close()
# # img.seek(0)
# # return base64.b64encode(img.getvalue()).decode()
# # bar chart
# def bar_chart():
# categories = ["Trench", "Manhole", "Pipe Laying", "Restoration"]
# values = [120, 80, 150, 60]
# plt.figure()
# plt.bar(categories, values)
# plt.title("Work Category Report")
# plt.xlabel("test Category")
# plt.ylabel("test Quantity")
# return plot_to_base64(plt)
# # Pie chart
# def pie_chart():
# labels = ["Completed", "In Progress", "Pending"]
# sizes = [55, 20, 25]
# plt.figure()
# plt.pie(sizes, labels=labels, autopct="%1.1f%%", startangle=140)
# plt.title("Project Status")
# return plot_to_base64(plt)
# # Histogram chart
# def histogram_chart():
# daily_work = [5, 10, 15, 20, 20, 25, 30, 35, 40, 45, 50]
# plt.figure()
# plt.hist(daily_work, bins=5)
# plt.title("Daily Work Distribution")
# plt.xlabel("Work Units")
# plt.ylabel("Frequency")
# return plot_to_base64(plt)
# # Dashboaed page
# @dashboard_bp.route("/")
# def dashboard():
# if not session.get("user_id"):
# return redirect(url_for("auth.login"))
# return render_template(
# "dashboard.html",
# title="Dashboard",
# bar_chart=bar_chart(),
# pie_chart=pie_chart(),
# histogram=histogram_chart()
# )
# # subcontractor dashboard
# @dashboard_bp.route("/subcontractor_dashboard", methods=["GET", "POST"])
# def subcontractor_dashboard():
# if not session.get("user_id"):
# return redirect(url_for("auth.login"))
# tr_dash = DashboardService().bar_chart_of_tr_ex
# return render_template(
# "subcontractor_dashboard.html",
# title="Dashboard",
# bar_chart=tr_dash
# )
from flask import Blueprint, render_template, session, redirect, url_for, jsonify
from sqlalchemy import func
from app import db
from app.models.trench_excavation_model import TrenchExcavation
from app.models.manhole_excavation_model import ManholeExcavation
from app.models.laying_model import Laying
dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/dashboard")
@dashboard_bp.route("/api/live-stats")
def live_stats():
try:
# 1. Overall Volume
t_count = TrenchExcavation.query.count()
m_count = ManholeExcavation.query.count()
l_count = Laying.query.count()
# 2. Location Distribution (Business reach)
loc_results = db.session.query(
TrenchExcavation.Location,
func.count(TrenchExcavation.id)
).group_by(TrenchExcavation.Location).all()
# 3. Work Timeline (Business productivity trend)
# Assuming your models have a 'created_at' field
timeline_results = db.session.query(
func.date(TrenchExcavation.created_at),
func.count(TrenchExcavation.id)
).group_by(func.date(TrenchExcavation.created_at)).order_by(func.date(TrenchExcavation.created_at)).all()
return jsonify({
"summary": {
"trench": t_count,
"manhole": m_count,
"laying": l_count,
"total": t_count + m_count + l_count
},
"locations": {row[0]: row[1] for row in loc_results if row[0]},
"timeline": {str(row[0]): row[1] for row in timeline_results}
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@dashboard_bp.route("/")
def dashboard():
if not session.get("user_id"):
return redirect(url_for("auth.login"))
return render_template("dashboard.html", title="Business Intelligence Dashboard")

30
app/routes/file_format.py Normal file
View File

@@ -0,0 +1,30 @@
from flask import Blueprint, render_template, send_from_directory, abort, current_app
from app.utils.helpers import login_required
import os
file_format_bp = Blueprint("file_format", __name__)
@file_format_bp .route("/file_format")
@login_required
def download_format():
return render_template("file_format.html", title="Download File Formats")
@file_format_bp .route("/file_format/download/<filename>")
@login_required
def download_excel_format(filename):
download_folder = os.path.join(
current_app.root_path, "static", "downloads/format"
)
file_path = os.path.join(download_folder, filename)
if not os.path.exists(file_path):
abort(404)
return send_from_directory(
directory=download_folder,
path=filename,
as_attachment=True
)

46
app/routes/file_import.py Normal file
View File

@@ -0,0 +1,46 @@
from flask import Blueprint, render_template, request, flash
from app.services.file_service import FileService
from app.models.subcontractor_model import Subcontractor
from app.utils.helpers import login_required
file_import_bp = Blueprint("file_import", __name__, url_prefix="/file")
# this is contractractor immport routes
@file_import_bp.route("/import_Subcontractor", methods=["GET", "POST"])
@login_required
def import_file():
subcontractors = Subcontractor.query.all()
if request.method == "POST":
file = request.files.get("file")
subcontractor_id = request.form.get("subcontractor_id")
RA_Bill_No = request.form.get("RA_Bill_No")
service = FileService()
success, msg = service.handle_file_upload(file, subcontractor_id, RA_Bill_No)
flash(msg, "success" if success else "danger")
return render_template(
"file_import_subcontractor.html",
title="Sub-cont. File Import",
subcontractors=subcontractors
)
# this is client import routes
@file_import_bp.route("/import_client", methods=["GET", "POST"])
@login_required
def client_import_file():
subcontractors = Subcontractor.query.all()
if request.method == "POST":
file = request.files.get("file")
RA_Bill_No = request.form.get("RA_Bill_No")
service = FileService()
success, msg = service.handle_client_file_upload(file, RA_Bill_No)
flash(msg, "success" if success else "danger")
return render_template("file_import_client.html", title="Client File Import", subcontractors=subcontractors)

214
app/routes/file_report.py Normal file
View File

@@ -0,0 +1,214 @@
import pandas as pd
import io
from flask import Blueprint, render_template, request, send_file, flash
from app.utils.helpers import login_required
from app.models.subcontractor_model import Subcontractor
from app.models.manhole_excavation_model import ManholeExcavation
from app.models.trench_excavation_model import TrenchExcavation
from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber
from app.models.laying_model import Laying
from app.models.mh_ex_client_model import ManholeExcavationClient
from app.models.tr_ex_client_model import TrenchExcavationClient
from app.models.mh_dc_client_model import ManholeDomesticChamberClient
from app.models.laying_client_model import LayingClient
# --- BLUEPRINT DEFINITION ---
file_report_bp = Blueprint("file_report", __name__, url_prefix="/file")
# --- Client class ---
class ClientBill:
def __init__(self):
self.df_tr = pd.DataFrame()
self.df_mh = pd.DataFrame()
self.df_dc = pd.DataFrame()
self.df_laying = pd.DataFrame()
def Fetch(self, RA_Bill_No):
trench = TrenchExcavationClient.query.filter_by(RA_Bill_No=RA_Bill_No).all()
mh = ManholeExcavationClient.query.filter_by(RA_Bill_No=RA_Bill_No).all()
dc = ManholeDomesticChamberClient.query.filter_by(RA_Bill_No=RA_Bill_No).all()
lay = LayingClient.query.filter_by(RA_Bill_No=RA_Bill_No).all()
self.df_tr = pd.DataFrame([c.serialize() for c in trench])
self.df_mh = pd.DataFrame([c.serialize() for c in mh])
self.df_dc = pd.DataFrame([c.serialize() for c in dc])
self.df_laying = pd.DataFrame([c.serialize() for c in lay])
drop_cols = ["id", "created_at", "_sa_instance_state"]
for df in [self.df_tr, self.df_mh, self.df_dc, self.df_laying]:
if not df.empty:
df.drop(columns=drop_cols, errors="ignore", inplace=True)
# --- Subcontractor class ---
class SubcontractorBill:
def __init__(self):
self.df_tr = pd.DataFrame()
self.df_mh = pd.DataFrame()
self.df_dc = pd.DataFrame()
self.df_laying = pd.DataFrame()
def Fetch(self, RA_Bill_No=None, subcontractor_id=None):
filters = {}
if subcontractor_id:
filters["subcontractor_id"] = subcontractor_id
if RA_Bill_No:
filters["RA_Bill_No"] = RA_Bill_No
trench = TrenchExcavation.query.filter_by(**filters).all()
mh = ManholeExcavation.query.filter_by(**filters).all()
dc = ManholeDomesticChamber.query.filter_by(**filters).all()
lay = Laying.query.filter_by(**filters).all()
self.df_tr = pd.DataFrame([c.serialize() for c in trench])
self.df_mh = pd.DataFrame([c.serialize() for c in mh])
self.df_dc = pd.DataFrame([c.serialize() for c in dc])
self.df_laying = pd.DataFrame([c.serialize() for c in lay])
drop_cols = ["id", "created_at", "_sa_instance_state"]
for df in [self.df_tr, self.df_mh, self.df_dc, self.df_laying]:
if not df.empty:
df.drop(columns=drop_cols, errors="ignore", inplace=True)
# --- subcontractor report only ---
@file_report_bp.route("/Subcontractor_report", methods=["GET", "POST"])
@login_required
def report_file():
subcontractors = Subcontractor.query.all()
tables = None
selected_sc_id = None
ra_bill_no = None
download_all = False
if request.method == "POST":
subcontractor_id = request.form.get("subcontractor_id")
ra_bill_no = request.form.get("ra_bill_no")
download_all = request.form.get("download_all") == "true"
action = request.form.get("action")
if not subcontractor_id:
flash("Please select a subcontractor.", "danger")
return render_template("subcontractor_report.html", subcontractors=subcontractors)
subcontractor = Subcontractor.query.get(subcontractor_id)
bill_gen = SubcontractorBill()
if download_all:
bill_gen.Fetch(subcontractor_id=subcontractor_id)
file_name = f"{subcontractor.subcontractor_name}_ALL_BILLS.xlsx"
else:
if not ra_bill_no:
flash("Please enter an RA Bill Number.", "danger")
return render_template("subcontractor_report.html", subcontractors=subcontractors)
bill_gen.Fetch(RA_Bill_No=ra_bill_no, subcontractor_id=subcontractor_id)
file_name = f"{subcontractor.subcontractor_name}_RA_{ra_bill_no}_Report.xlsx"
if bill_gen.df_tr.empty and bill_gen.df_mh.empty and bill_gen.df_dc.empty:
flash("No data found for this selection.", "warning")
return render_template("subcontractor_report.html", subcontractors=subcontractors)
# If download is clicked, return file immediately
if action == "download":
output = io.BytesIO()
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
bill_gen.df_tr.to_excel(writer, index=False, sheet_name="Tr.Ex.")
bill_gen.df_mh.to_excel(writer, index=False, sheet_name="MH.Ex.")
bill_gen.df_dc.to_excel(writer, index=False, sheet_name="MH & DC")
bill_gen.df_laying.to_excel(writer, index=False, sheet_name="Laying")
output.seek(0)
return send_file(output, download_name=file_name, as_attachment=True)
# We add bootstrap classes directly to the pandas output
table_classes = "table table-bordered table-striped table-hover table-sm mb-0"
tables = {
"tr": bill_gen.df_tr.to_html(classes=table_classes, index=False),
"mh": bill_gen.df_mh.to_html(classes=table_classes, index=False),
"dc": bill_gen.df_dc.to_html(classes=table_classes, index=False),
"laying": bill_gen.df_laying.to_html(classes=table_classes, index=False)
}
selected_sc_id = subcontractor_id
return render_template(
"subcontractor_report.html",
subcontractors=subcontractors,
tables=tables,
selected_sc_id=selected_sc_id,
ra_bill_no=ra_bill_no,
download_all=download_all
)
# --- client report only ---
@file_report_bp.route("/client_report", methods=["GET", "POST"])
@login_required
def client_vs_all_subcontractor():
tables = {"tr": None, "mh": None, "dc": None}
ra_val = ""
if request.method == "POST":
RA_Bill_No = request.form.get("RA_Bill_No")
ra_val = RA_Bill_No
if not RA_Bill_No:
flash("Please enter RA Bill No.", "danger")
return render_template("client_report.html", tables=tables, ra_val=ra_val)
clientBill = ClientBill()
clientBill.Fetch(RA_Bill_No=RA_Bill_No)
contractorBill = SubcontractorBill()
contractorBill.Fetch(RA_Bill_No=RA_Bill_No)
# --- SAFETY CHECK: Verify data exists before merging ---
if clientBill.df_tr.empty and clientBill.df_mh.empty:
flash(f"No Client records found for RA Bill {RA_Bill_No}", "warning")
return render_template("client_report.html", tables=tables, ra_val=ra_val)
qty_cols = [...] # (Keep your existing list)
mh_dc_qty_cols = [...] # (Keep your existing list)
mh_lay_qty_cols =[...]
def aggregate_df(df, group_cols, sum_cols):
if df.empty:
# Create an empty DF with the correct columns to avoid Merge/Key Errors
return pd.DataFrame(columns=group_cols + sum_cols)
existing_cols = [c for c in sum_cols if c in df.columns]
# Ensure group_cols exist in the DF
for col in group_cols:
if col not in df.columns:
df[col] = "N/A" # Fill missing join keys
return df.groupby(group_cols, as_index=False)[existing_cols].sum()
# Aggregate data
df_sub_tr_grp = aggregate_df(contractorBill.df_tr, ["Location", "MH_NO"], qty_cols)
df_sub_mh_grp = aggregate_df(contractorBill.df_mh, ["Location", "MH_NO"], qty_cols)
df_sub_dc_grp = aggregate_df(contractorBill.df_dc, ["Location", "MH_NO"], mh_dc_qty_cols)
df_sub_lay_grp = aggregate_df(contractorBill.df_dc, ["Location", "MH_NO"], mh_lay_qty_cols)
# --- FINAL MERGE LOGIC ---
# We check if "Location" exists in the client data. If not, we add it to prevent the KeyError.
for df_client in [clientBill.df_tr, clientBill.df_mh, clientBill.df_dc, clientBill.df_laying ]:
if not df_client.empty and "Location" not in df_client.columns:
df_client["Location"] = "Unknown"
try:
df_tr_cmp = clientBill.df_tr.merge(df_sub_tr_grp, on=["Location", "MH_NO"], how="left", suffixes=("_Client", "_Sub"))
df_mh_cmp = clientBill.df_mh.merge(df_sub_mh_grp, on=["Location", "MH_NO"], how="left", suffixes=("_Client", "_Sub"))
df_dc_cmp = clientBill.df_dc.merge(df_sub_dc_grp, on=["Location", "MH_NO"], how="left", suffixes=("_Client", "_Sub"))
df_lay_cmp = clientBill.df_laying.merge(df_sub_lay_grp, on=["Location", "MH_NO"], how="left", suffixes=("_Client", "_Sub"))
except KeyError as e:
flash(f"Merge Error: Missing column {str(e)}. Check if 'Location' is defined in your database models.", "danger")
return render_template("client_report.html", tables=tables, ra_val=ra_val)
# Convert to HTML for preview
tables["tr"] = df_tr_cmp.to_html(classes='table table-striped table-hover table-sm', index=False)
tables["mh"] = df_mh_cmp.to_html(classes='table table-striped table-hover table-sm', index=False)
tables["dc"] = df_dc_cmp.to_html(classes='table table-striped table-hover table-sm', index=False)
tables["laying"] = df_lay_cmp.to_html(classes='table table-striped table-hover table-sm', index=False)
return render_template("client_report.html", tables=tables, ra_val=ra_val)

View File

@@ -0,0 +1,352 @@
from flask import Blueprint, render_template, request, send_file, flash
from collections import defaultdict
import pandas as pd
import io
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
import re
generate_report_bp = Blueprint("generate_report", __name__, url_prefix="/report")
# sum field of pipe laying (pipe_150_mm)
PIPE_MM_PATTERN = re.compile(r"^pipe_\d+_mm$")
# sum fields of MH dc (d_0_to_0_75)
D_RANGE_PATTERN = re.compile( r"^d_\d+(?:_\d+)?_to_\d+(?:_\d+)?$")
# NORMALIZER
def normalize_key(value):
if value is None:
return None
return str(value).strip().upper()
# HEADER FORMATTER
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
# LOOKUP CREATOR
def make_lookup(rows, key_field):
lookup = {}
for r in rows:
location = normalize_key(r.get("Location"))
key_val = normalize_key(r.get(key_field))
if location and key_val:
lookup.setdefault((location, key_val), []).append(r)
return lookup
# COMPARISON BUILDER
def build_comparison(client_rows, contractor_rows, key_field):
contractor_lookup = make_lookup(contractor_rows, key_field)
output = []
used_index = defaultdict(int) # 🔥 THIS FIXES YOUR ISSUE
for c in client_rows:
client_location = normalize_key(c.get("Location"))
client_key = normalize_key(c.get(key_field))
if not client_location or not client_key:
continue
subs = contractor_lookup.get((client_location, client_key))
if not subs:
continue
idx = used_index[(client_location, client_key)]
# ❗ If subcontractor rows are exhausted, skip
if idx >= len(subs):
continue
s = subs[idx] # ✅ take NEXT subcontractor row
used_index[(client_location, client_key)] += 1
# ---- totals ----
client_total = sum(
float(v or 0)
for k, v in c.items()
if k.endswith("_total")
or D_RANGE_PATTERN.match(k)
or PIPE_MM_PATTERN.match(k)
)
sub_total = sum(
float(v or 0)
for k, v in s.items()
if k.endswith("_total")
or D_RANGE_PATTERN.match(k)
or PIPE_MM_PATTERN.match(k)
)
row = {
"Location": client_location,
key_field.replace("_", " "): client_key
}
for k, v in c.items():
if k not in ["id", "created_at"]:
row[f"Client-{k}"] = v
row["Client-Total"] = round(client_total, 2)
row[" "] = ""
for k, v in s.items():
if k not in ["id", "created_at", "subcontractor_id"]:
row[f"Subcontractor-{k}"] = v
row["Subcontractor-Total"] = round(sub_total, 2)
row["Diff"] = round(client_total - sub_total, 2)
output.append(row)
df = pd.DataFrame(output)
df.columns = [format_header(col) for col in df.columns]
return df
# EXCEL SHEET WRITER
def write_sheet(writer, df, sheet_name, subcontractor_name):
workbook = writer.book
df.to_excel(writer, sheet_name=sheet_name, index=False, startrow=3)
ws = writer.sheets[sheet_name]
title_fmt = workbook.add_format({"bold": True, "font_size": 14})
client_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#B6DAED"})
sub_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F3A081"})
total_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F7D261"})
diff_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#82DD49"})
default_header_fmt = workbook.add_format({"bold": True,"border": 1,"bg_color": "#E7E6E6","align": "center","valign": "vcenter"})
ws.merge_range(
0, 0, 0, len(df.columns) - 1,
"CLIENT vs SUBCONTRACTOR",
title_fmt
)
ws.merge_range(
1, 0, 1, len(df.columns) - 1,
f"Subcontractor Name - {subcontractor_name}",
title_fmt
)
for col_num, col_name in enumerate(df.columns):
if col_name.startswith("Client-"):
ws.write(3, col_num, col_name, client_fmt)
elif col_name.startswith("Subcontractor-"):
ws.write(3, col_num, col_name, sub_fmt)
elif col_name.endswith("_total") or col_name.endswith("_total") :
ws.write(3, col_num, col_name, total_fmt)
elif col_name == "Diff":
ws.write(3, col_num, col_name, diff_fmt)
else:
ws.write(3, col_num, col_name, default_header_fmt)
ws.set_column(col_num, col_num, 20)
# REPORT ROUTE
@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 subcontractor", "danger")
return render_template("generate_comparison_report.html",subcontractors=subcontractors)
subcontractor = Subcontractor.query.get_or_404(subcontractor_id)
# -------- DATA --------
tr_client = [r.serialize() for r in TrenchExcavationClient.query.all()]
tr_sub = [r.serialize() for r in TrenchExcavation.query.filter_by(
subcontractor_id=subcontractor_id
).all()]
df_tr = build_comparison(tr_client, tr_sub, "MH_NO")
mh_client = [r.serialize() for r in ManholeExcavationClient.query.all()]
mh_sub = [r.serialize() for r in ManholeExcavation.query.filter_by(
subcontractor_id=subcontractor_id
).all()]
df_mh = build_comparison(mh_client, mh_sub, "MH_NO")
dc_client = [r.serialize() for r in ManholeDomesticChamberClient.query.all()]
dc_sub = [r.serialize() for r in ManholeDomesticChamber.query.filter_by(
subcontractor_id=subcontractor_id
).all()]
df_dc = build_comparison(dc_client, dc_sub, "MH_NO")
# df_dc = build_comparison_mh_dc(dc_client, dc_sub, "MH_NO")
lay_client = [r.serialize() for r in LayingClient.query.all()]
lay_sub = [r.serialize() for r in Laying.query.filter_by(
subcontractor_id=subcontractor_id
).all()]
df_lay = build_comparison(lay_client, lay_sub, "MH_NO")
# df_lay = build_comparison_laying(lay_client, lay_sub, "MH_NO")
# -------- EXCEL --------
output = io.BytesIO()
filename = f"{subcontractor.subcontractor_name}_Comparison_Report.xlsx"
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
write_sheet(writer, df_tr, "Tr.Ex", subcontractor.subcontractor_name)
write_sheet(writer, df_mh, "Mh.Ex", subcontractor.subcontractor_name)
write_sheet(writer, df_dc, "MH & DC", subcontractor.subcontractor_name)
write_sheet(writer, df_lay, "Laying", 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)
# def build_comparison_mh_dc(client_rows, contractor_rows, key_field):
# contractor_lookup = make_lookup(contractor_rows, key_field)
# mh_dc_fields = ManholeDomesticChamberClient.sum_mh_dc_fields()
# output = []
# for c in client_rows:
# loc = normalize_key(c.get("Location"))
# key = normalize_key(c.get(key_field))
# if not loc or not key:
# continue
# s = contractor_lookup.get((loc, key))
# if not s:
# continue
# client_total = sum(float(c.get(f, 0) or 0) for f in mh_dc_fields)
# sub_total = sum(float(s.get(f, 0) or 0) for f in mh_dc_fields)
# row = {
# "Location": loc,
# key_field.replace("_", " "): key
# }
# # CLIENT ALL FIELDS
# 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[" "] = ""
# # SUBCONTRACTOR ALL FIELDS
# 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)
# df = pd.DataFrame(output)
# df.columns = [format_header(col) for col in df.columns]
# return df
# def build_comparison_laying(client_rows, contractor_rows, key_field):
# contractor_lookup = make_lookup(contractor_rows, key_field)
# laying_fields = Laying.sum_laying_fields()
# output = []
# for c in client_rows:
# loc = normalize_key(c.get("Location"))
# key = normalize_key(c.get(key_field))
# if not loc or not key:
# continue
# s = contractor_lookup.get((loc, key))
# if not s:
# continue
# client_total = sum(float(c.get(f, 0) or 0) for f in laying_fields)
# sub_total = sum(float(s.get(f, 0) or 0) for f in laying_fields)
# print("--------------",key,"----------")
# print("sum -client_total ",client_total)
# print("sum -sub_total ",sub_total)
# print("Diff ---- ",client_total - sub_total)
# print("------------------------")
# row = {
# "Location": loc,
# key_field.replace("_", " "): key
# }
# # CLIENT ALL FIELDS
# 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[" "] = ""
# # SUBCONTRACTOR ALL FIELDS
# 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)
# df = pd.DataFrame(output)
# df.columns = [format_header(col) for col in df.columns]
# return df

View File

@@ -0,0 +1,191 @@
from flask import Blueprint, render_template, request, redirect, flash
from app import db
from app.models.subcontractor_model import Subcontractor
from app.utils.helpers import login_required
subcontractor_bp = Blueprint("subcontractor", __name__, url_prefix="/subcontractor")
# ---------------- ADD -----------------
@subcontractor_bp.route("/add")
@login_required
def add_subcontractor():
return render_template("subcontractor/add.html")
@subcontractor_bp.route("/save", methods=["POST"])
@login_required
def save_subcontractor():
# 1. Get and clean the name from the form
name = request.form.get("subcontractor_name", "").strip()
# 2. Basic validation: Ensure the name isn't empty
if not name:
flash("Subcontractor name cannot be empty.", "danger")
return redirect("/subcontractor/add")
# 3. Check if a subcontractor with this name already exists
existing_sub = Subcontractor.query.filter_by(subcontractor_name=name).first()
if existing_sub:
flash(f"Subcontractor with name '{name}' already exists!", "danger")
return redirect("/subcontractor/add")
# 4. If no duplicate is found, proceed to save
try:
subcontractor = Subcontractor(
subcontractor_name=name,
contact_person=request.form.get("contact_person"),
mobile_no=request.form.get("mobile_no"),
email_id=request.form.get("email_id"),
gst_no=request.form.get("gst_no")
)
db.session.add(subcontractor)
db.session.commit()
flash("Subcontractor added successfully!", "success")
except Exception as e:
db.session.rollback()
flash("An error occurred while saving. Please try again.", "danger")
return redirect("/subcontractor/list")
# ---------------- LIST -----------------
@subcontractor_bp.route("/list")
@login_required
def subcontractor_list():
subcontractors = Subcontractor.query.all()
return render_template("subcontractor/list.html", subcontractors=subcontractors)
# ---------------- EDIT -----------------
@subcontractor_bp.route("/edit/<int:id>")
@login_required
def edit_subcontractor(id):
subcontractor = Subcontractor.query.get_or_404(id)
return render_template("subcontractor/edit.html", subcontractor=subcontractor)
# ---------------- UPDATE -----------------
@subcontractor_bp.route("/update/<int:id>", methods=["POST"])
@login_required
def update_subcontractor(id):
subcontractor = Subcontractor.query.get_or_404(id)
new_name = request.form.get("subcontractor_name")
# Check if the new name is taken by someone ELSE (not this current ID)
duplicate = Subcontractor.query.filter(
Subcontractor.subcontractor_name == new_name,
Subcontractor.id != id
).first()
if duplicate:
flash("Another subcontractor already uses this name.", "danger")
return redirect(f"/subcontractor/edit/{id}")
subcontractor.subcontractor_name = new_name
db.session.commit()
flash("Subcontractor updated successfully!", "success")
return redirect("/subcontractor/list")
# ---------------- DELETE -----------------
@subcontractor_bp.route("/delete/<int:id>")
@login_required
def delete_subcontractor(id):
subcontractor = Subcontractor.query.get_or_404(id)
db.session.delete(subcontractor)
db.session.commit()
flash("Subcontractor deleted successfully!", "success")
return redirect("/subcontractor/list")
from flask import Blueprint, render_template, request, redirect, flash
from app import db
from app.models.subcontractor_model import Subcontractor
from app.utils.helpers import login_required
subcontractor_bp = Blueprint("subcontractor", __name__, url_prefix="/subcontractor")
# ---------------- ADD -----------------
@subcontractor_bp.route("/add")
@login_required
def add_subcontractor():
return render_template("subcontractor/add.html")
@subcontractor_bp.route("/save", methods=["POST"])
@login_required
def save_subcontractor():
name = request.form.get("subcontractor_name", "").strip()
if not name:
flash("Subcontractor name cannot be empty.", "danger")
return redirect("/subcontractor/add")
existing_sub = Subcontractor.query.filter_by(subcontractor_name=name).first()
if existing_sub:
flash(f"Subcontractor with name '{name}' already exists!", "danger")
return redirect("/subcontractor/add")
try:
subcontractor = Subcontractor(
subcontractor_name=name,
contact_person=request.form.get("contact_person"),
mobile_no=request.form.get("mobile_no"),
email_id=request.form.get("email_id"),
gst_no=request.form.get("gst_no")
)
db.session.add(subcontractor)
db.session.commit()
flash("Subcontractor added successfully!", "success")
except Exception as e:
db.session.rollback()
flash("An error occurred while saving. Please try again.", "danger")
return redirect("/subcontractor/list")
# ---------------- LIST -----------------
@subcontractor_bp.route("/list")
@login_required
def subcontractor_list():
subcontractors = Subcontractor.query.all()
return render_template("subcontractor/list.html", subcontractors=subcontractors)
# ---------------- EDIT -----------------
@subcontractor_bp.route("/edit/<int:id>")
@login_required
def edit_subcontractor(id):
subcontractor = Subcontractor.query.get_or_404(id)
return render_template("subcontractor/edit.html", subcontractor=subcontractor)
# ---------------- UPDATE -----------------
@subcontractor_bp.route("/update/<int:id>", methods=["POST"])
@login_required
def update_subcontractor(id):
subcontractor = Subcontractor.query.get_or_404(id)
new_name = request.form.get("subcontractor_name")
# Check if the new name is taken by someone ELSE (not this current ID)
duplicate = Subcontractor.query.filter(
Subcontractor.subcontractor_name == new_name,
Subcontractor.id != id
).first()
if duplicate:
flash("Another subcontractor already uses this name.", "danger")
return redirect(f"/subcontractor/edit/{id}")
subcontractor.subcontractor_name = new_name
db.session.commit()
flash("Subcontractor updated successfully!", "success")
return redirect("/subcontractor/list")
# ---------------- DELETE -----------------
@subcontractor_bp.route("/delete/<int:id>")
@login_required
def delete_subcontractor(id):
subcontractor = Subcontractor.query.get_or_404(id)
db.session.delete(subcontractor)
db.session.commit()
flash("Subcontractor deleted successfully!", "success")
return redirect("/subcontractor/list")

11
app/routes/user.py Normal file
View File

@@ -0,0 +1,11 @@
from flask import Blueprint, render_template
from app.services.user_service import UserService
from app.utils.helpers import login_required
user_bp = Blueprint("user", __name__, url_prefix="/user")
@user_bp.route("/list")
@login_required
def list_users():
users = UserService.get_all_users()
return render_template("users.html", users=users, title="Users")