Client billing code push
This commit is contained in:
68
app/routes/__init__.py
Normal file
68
app/routes/__init__.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# from flask import Flask
|
||||
# from flask_sqlalchemy import SQLAlchemy
|
||||
# from flask_migrate import Migrate
|
||||
# from flask_login import LoginManager
|
||||
# from flask_ldap3_login import LDAP3LoginManager
|
||||
# import os
|
||||
|
||||
# db = SQLAlchemy()
|
||||
# migrate = Migrate()
|
||||
# login_manager = LoginManager()
|
||||
# ldap_manager = LDAP3LoginManager()
|
||||
|
||||
# from app.models import User
|
||||
# # Flask-Login user loader
|
||||
# @login_manager.user_loader
|
||||
# def load_user(user_id):
|
||||
# return User.query.get(int(user_id))
|
||||
|
||||
|
||||
# def create_app():
|
||||
# app = Flask(__name__)
|
||||
|
||||
# # --------------------
|
||||
# # App config
|
||||
# # --------------------
|
||||
# app.config['SECRET_KEY'] = 'dev-secret-key' # 🔐 change this in production
|
||||
# app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:admin@localhost/excel_data7'
|
||||
# # 🔽 Add upload folder config
|
||||
# app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'upload')
|
||||
|
||||
# # Make sure folder exists
|
||||
# os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||
# # --------------------
|
||||
# # LDAP config
|
||||
# # --------------------
|
||||
# app.config['LDAP_HOST'] = 'openldap'
|
||||
# app.config['LDAP_PORT'] = 389
|
||||
# app.config['LDAP_BASE_DN'] = 'dc=lcepl,dc=org'
|
||||
# app.config['LDAP_BIND_USER_DN'] = 'cn=admin,dc=lcepl,dc=org'
|
||||
# app.config['LDAP_BIND_USER_PASSWORD'] = 'admin123'
|
||||
# app.config['LDAP_USER_DN'] = 'ou=users'
|
||||
# app.config['LDAP_GROUP_DN'] = 'ou=groups'
|
||||
# app.config['LDAP_USER_RDN_ATTR'] = 'uid'
|
||||
# app.config['LDAP_USER_LOGIN_ATTR'] = 'uid'
|
||||
|
||||
# # Extra to avoid BasicAuth popup
|
||||
# app.config['USE_LDAP_AUTH'] = True
|
||||
# app.config['LDAP_REQUIRE_CERT'] = False
|
||||
# app.config['LDAP_LOGIN_VIEW'] = 'main.login' # your login route
|
||||
|
||||
# # --------------------
|
||||
# # Init extensions
|
||||
# # --------------------
|
||||
# db.init_app(app)
|
||||
# migrate.init_app(app, db)
|
||||
# login_manager.init_app(app)
|
||||
# ldap_manager.init_app(app)
|
||||
|
||||
# # Redirect to login if not authenticated
|
||||
# login_manager.login_view = "main.login"
|
||||
|
||||
# # --------------------
|
||||
# # Register blueprints
|
||||
# # --------------------
|
||||
# from app.routes.main import main
|
||||
# app.register_blueprint(main)
|
||||
|
||||
# return app
|
||||
BIN
app/routes/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
app/routes/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/routes/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
app/routes/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/routes/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
app/routes/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
app/routes/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/ldap_user.cpython-313.pyc
Normal file
BIN
app/routes/__pycache__/ldap_user.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/main.cpython-310.pyc
Normal file
BIN
app/routes/__pycache__/main.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/main.cpython-311.pyc
Normal file
BIN
app/routes/__pycache__/main.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/main.cpython-312.pyc
Normal file
BIN
app/routes/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/main.cpython-313.pyc
Normal file
BIN
app/routes/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/main.cpython-314.pyc
Normal file
BIN
app/routes/__pycache__/main.cpython-314.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/main.cpython-39.pyc
Normal file
BIN
app/routes/__pycache__/main.cpython-39.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/reports.cpython-310.pyc
Normal file
BIN
app/routes/__pycache__/reports.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/reports.cpython-311.pyc
Normal file
BIN
app/routes/__pycache__/reports.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/reports.cpython-312.pyc
Normal file
BIN
app/routes/__pycache__/reports.cpython-312.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/reports.cpython-313.pyc
Normal file
BIN
app/routes/__pycache__/reports.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/reports.cpython-314.pyc
Normal file
BIN
app/routes/__pycache__/reports.cpython-314.pyc
Normal file
Binary file not shown.
BIN
app/routes/__pycache__/reports.cpython-39.pyc
Normal file
BIN
app/routes/__pycache__/reports.cpython-39.pyc
Normal file
Binary file not shown.
10
app/routes/ldap_user.py
Normal file
10
app/routes/ldap_user.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# from flask_login import UserMixin
|
||||
# # --------------------
|
||||
# # LDAP User class (not stored in DB)
|
||||
# # --------------------
|
||||
# class LDAPUser(UserMixin):
|
||||
# """Represents an authenticated LDAP user (kept in session)."""
|
||||
# def __init__(self, dn, username):
|
||||
# self.dn = dn
|
||||
# self.username = username
|
||||
# self.id = username # 🔑 Use username for session id (stable)
|
||||
837
app/routes/main.py
Normal file
837
app/routes/main.py
Normal file
@@ -0,0 +1,837 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash,jsonify
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from sqlalchemy import func, cast, Float
|
||||
from app.models import Task,WorkDetail
|
||||
import pandas as pd
|
||||
from werkzeug.security import generate_password_hash
|
||||
import os
|
||||
from app import LDAPUser
|
||||
import numpy as np
|
||||
|
||||
from flask import current_app
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
from app.models import User
|
||||
from ldap3 import Server, Connection, ALL # ✅ make sure this is at the top
|
||||
|
||||
main = Blueprint("main", __name__)
|
||||
# @main.route("/login", methods=["GET", "POST"])
|
||||
# def login():
|
||||
# # Redirect if already logged in
|
||||
# if current_user.is_authenticated:
|
||||
# return redirect(url_for("main.dashboard"))
|
||||
|
||||
# if request.method == "POST":
|
||||
# username = request.form.get("username", "").strip()
|
||||
# password = request.form.get("password", "")
|
||||
|
||||
# if not username or not password:
|
||||
# flash("Username and password are required.", "danger")
|
||||
# return render_template("login.html")
|
||||
|
||||
# ldap_user_dn = f"uid={username},ou=users,dc=lcepl,dc=org"
|
||||
|
||||
# try:
|
||||
# # Connect to LDAP server
|
||||
# # server = Server("openldap", port=389, get_info=ALL)
|
||||
# server = Server("localhost", port=389, get_info=ALL)
|
||||
# conn = Connection(server, user=ldap_user_dn, password=password)
|
||||
|
||||
# if conn.bind():
|
||||
# # Pass the required 'data' argument
|
||||
# user = LDAPUser(dn=ldap_user_dn, username=username, data={})
|
||||
# login_user(user)
|
||||
# flash(f"Welcome, {username}!", "success")
|
||||
# return redirect(url_for("main.dashboard"))
|
||||
# else:
|
||||
# flash("Invalid LDAP credentials", "danger")
|
||||
|
||||
# except Exception as e:
|
||||
# flash(f"LDAP connection error: {e}", "danger")
|
||||
|
||||
# # GET request or failed login
|
||||
# return render_template("login.html")
|
||||
|
||||
@main.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
# Redirect if already logged in
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for("main.dashboard"))
|
||||
|
||||
if request.method == "POST":
|
||||
username = request.form.get("username", "").strip()
|
||||
password = request.form.get("password", "")
|
||||
|
||||
if not username or not password:
|
||||
flash("Username and password are required.", "danger")
|
||||
return render_template("login.html")
|
||||
|
||||
ldap_user_dn = f"uid={username},ou=users,dc=lcepl,dc=org"
|
||||
|
||||
try:
|
||||
# Try LDAP authentication first
|
||||
server = Server("localhost", port=389, get_info=ALL)
|
||||
conn = Connection(server, user=ldap_user_dn, password=password)
|
||||
|
||||
if conn.bind():
|
||||
user = LDAPUser(dn=ldap_user_dn, username=username, data={})
|
||||
login_user(user)
|
||||
flash(f"Welcome, {username}! (LDAP)", "success")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
else:
|
||||
flash("Invalid LDAP credentials", "danger")
|
||||
|
||||
except Exception as e:
|
||||
# Fallback to LOCAL login if LDAP not available
|
||||
if username == "admin" and password == "admin":
|
||||
user = LDAPUser(dn=None, username=username, data={})
|
||||
login_user(user)
|
||||
flash(f"Welcome, {username}! (Local Login)", "success")
|
||||
return redirect(url_for("main.dashboard"))
|
||||
else:
|
||||
flash("LDAP unavailable and local login failed", "danger")
|
||||
|
||||
return render_template("login.html")
|
||||
|
||||
@main.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
# flash("You have been logged out.", "info")
|
||||
return redirect(url_for("main.login"))
|
||||
|
||||
|
||||
# Home route
|
||||
@main.route('/upload_excel')
|
||||
@login_required
|
||||
def upload_excel():
|
||||
log_activity(current_user.username, "Page Load", "Upload Excel page accessed")
|
||||
return render_template('upload.html')
|
||||
|
||||
|
||||
# @main.route('/dashboard')
|
||||
# def upload_file():
|
||||
# return render_template('dashboard.html')
|
||||
|
||||
|
||||
# # File upload route
|
||||
# @main.route('/upload', methods=['POST'])
|
||||
# @login_required
|
||||
# def upload():
|
||||
# if 'file' not in request.files:
|
||||
# return "No file part"
|
||||
# file = request.files['file']
|
||||
# if file.filename == '':
|
||||
# return "No selected file"
|
||||
# if file:
|
||||
# filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], file.filename)
|
||||
# file.save(filepath)
|
||||
# log_activity(current_user.username, "File Upload", f"Uploaded file: {file.filename}")
|
||||
|
||||
# # Read work details (first 11 rows)
|
||||
# work_details_data = pd.read_excel(filepath, nrows=11, header=None)
|
||||
# work_details_dict = {
|
||||
|
||||
# "name_of_work": work_details_data.iloc[0, 1],
|
||||
# "cover_agreement_no": work_details_data.iloc[1, 1],
|
||||
# "name_of_contractor": work_details_data.iloc[2, 1],
|
||||
# "name_of_tpi_agency": work_details_data.iloc[3, 1],
|
||||
# "name_of_division": work_details_data.iloc[4, 1],
|
||||
# "name_of_village": work_details_data.iloc[5, 1],
|
||||
# "block": work_details_data.iloc[6, 1],
|
||||
# "scheme_id": work_details_data.iloc[7, 1],
|
||||
# "date_of_billing": work_details_data.iloc[8, 1],
|
||||
# "measurement_book": work_details_data.iloc[9, 1],
|
||||
# "district": work_details_data.iloc[10, 1] # Example: row 11 (index 10), column 2 (index 1)
|
||||
|
||||
# }
|
||||
|
||||
# work_details_dict = {key: (None if pd.isna(value) else value) for key, value in work_details_dict.items()}
|
||||
# work_detail = WorkDetail(**work_details_dict)
|
||||
# db.session.add(work_detail)
|
||||
|
||||
# # Read task data starting from row 12
|
||||
# data = pd.read_excel(filepath, skiprows=10)
|
||||
# data = data.astype(str).replace({"nan": None, "NaT": None, "None": None})
|
||||
|
||||
# expected_columns = [
|
||||
# "serial_number", "task_name", "unit", "qty", "rate", "boq_amount",
|
||||
# "previous_billed_qty", "previous_billing_amount",
|
||||
# "in_this_ra_bill_qty", "in_this_ra_billing_amount",
|
||||
# "cumulative_billed_qty", "cumulative_billed_amount",
|
||||
# "variation_qty", "variation_amount", "remark"
|
||||
# ]
|
||||
|
||||
# if data.shape[1] == len(expected_columns):
|
||||
# data.columns = expected_columns
|
||||
# else:
|
||||
# data.columns = expected_columns[:data.shape[1]] # Truncate
|
||||
|
||||
# current_main_task_serial = None
|
||||
# current_main_task_name = None
|
||||
|
||||
# for _, row in data.iterrows():
|
||||
# task_name = str(row["task_name"]) if row["task_name"] else ""
|
||||
# serial_number = row["serial_number"]
|
||||
|
||||
# if serial_number: # Main task
|
||||
# current_main_task_serial = serial_number
|
||||
# current_main_task_name = task_name
|
||||
# parent_id = None
|
||||
# else: # Subtask
|
||||
# parent_id = current_main_task_serial
|
||||
|
||||
# task = Task(
|
||||
# district=work_details_dict.get("district"),
|
||||
# block_name=work_details_dict["block"],
|
||||
# village_name=work_details_dict["name_of_village"],
|
||||
# serial_number=serial_number,
|
||||
# task_name=task_name,
|
||||
# unit=row["unit"],
|
||||
# qty=row["qty"],
|
||||
# rate=row["rate"],
|
||||
# boq_amount=row["boq_amount"],
|
||||
# previous_billed_qty=row["previous_billed_qty"],
|
||||
# previous_billing_amount=row["previous_billing_amount"],
|
||||
# in_this_ra_bill_qty=row["in_this_ra_bill_qty"],
|
||||
# in_this_ra_billing_amount=row["in_this_ra_billing_amount"],
|
||||
# cumulative_billed_qty=row["cumulative_billed_qty"],
|
||||
# cumulative_billed_amount=row["cumulative_billed_amount"],
|
||||
# variation_qty=row["variation_qty"],
|
||||
# variation_amount=row["variation_amount"],
|
||||
# parent_id=parent_id,
|
||||
# parent_task_name=current_main_task_name if not serial_number else None,
|
||||
# remark=row["remark"]
|
||||
# )
|
||||
# db.session.add(task)
|
||||
|
||||
# db.session.commit()
|
||||
# log_activity(current_user.username, "Database Insert", f"Inserted work details and tasks from {file.filename}")
|
||||
# return redirect(url_for('main.display_tasks'))
|
||||
def to_2_decimal(value):
|
||||
try:
|
||||
if value is None or value == "":
|
||||
return None
|
||||
return round(float(value), 2)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
@main.route('/upload', methods=['POST'])
|
||||
@login_required
|
||||
def upload():
|
||||
if 'file' not in request.files:
|
||||
return "No file part"
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
return "No selected file"
|
||||
|
||||
if file:
|
||||
filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], file.filename)
|
||||
file.save(filepath)
|
||||
log_activity(current_user.username, "File Upload", f"Uploaded file: {file.filename}")
|
||||
|
||||
# Read work details (first 11 rows)
|
||||
# work_details_data = pd.read_excel(filepath, nrows=11, header=None)
|
||||
work_details_data = pd.read_excel(filepath, nrows=11, header=None, dtype=str)
|
||||
work_details_dict = {
|
||||
"name_of_work": work_details_data.iloc[0, 1],
|
||||
"cover_agreement_no": work_details_data.iloc[1, 1],
|
||||
"name_of_contractor": work_details_data.iloc[2, 1],
|
||||
"name_of_tpi_agency": work_details_data.iloc[3, 1],
|
||||
"name_of_division": work_details_data.iloc[4, 1],
|
||||
"name_of_village": work_details_data.iloc[5, 1],
|
||||
"block": work_details_data.iloc[6, 1],
|
||||
"scheme_id": work_details_data.iloc[7, 1],
|
||||
"date_of_billing": work_details_data.iloc[8, 1],
|
||||
"measurement_book": work_details_data.iloc[9, 1],
|
||||
"district": work_details_data.iloc[10, 1]
|
||||
}
|
||||
|
||||
# Clean work details NaN
|
||||
work_details_dict = {k: (None if pd.isna(v) else v) for k, v in work_details_dict.items()}
|
||||
|
||||
work_detail = WorkDetail(**work_details_dict)
|
||||
db.session.add(work_detail)
|
||||
|
||||
# Read task data
|
||||
# data = pd.read_excel(filepath, skiprows=10)
|
||||
data = pd.read_excel(filepath, skiprows=10)
|
||||
|
||||
# ✅ Convert all NaN → None (critical for MySQL)
|
||||
data = data.astype(object).where(pd.notna(data), None)
|
||||
|
||||
|
||||
expected_columns = [
|
||||
"serial_number", "task_name", "unit", "qty", "rate", "boq_amount",
|
||||
"previous_billed_qty", "previous_billing_amount",
|
||||
"in_this_ra_bill_qty", "in_this_ra_billing_amount",
|
||||
"cumulative_billed_qty", "cumulative_billed_amount",
|
||||
"variation_qty", "variation_amount", "remark"
|
||||
]
|
||||
|
||||
if data.shape[1] == len(expected_columns):
|
||||
data.columns = expected_columns
|
||||
else:
|
||||
data.columns = expected_columns[:data.shape[1]]
|
||||
|
||||
current_main_task_serial = None
|
||||
current_main_task_name = None
|
||||
|
||||
for _, row in data.iterrows():
|
||||
|
||||
task_name = str(row["task_name"]) if row["task_name"] else ""
|
||||
serial_number = str(row["serial_number"]) if row["serial_number"] else None
|
||||
|
||||
if serial_number:
|
||||
current_main_task_serial = serial_number
|
||||
current_main_task_name = task_name
|
||||
parent_id = None
|
||||
else:
|
||||
parent_id = current_main_task_serial
|
||||
|
||||
task = Task(
|
||||
district=work_details_dict.get("district"),
|
||||
block_name=work_details_dict["block"],
|
||||
village_name=work_details_dict["name_of_village"],
|
||||
serial_number=serial_number,
|
||||
task_name=task_name,
|
||||
unit=row["unit"],
|
||||
qty=to_2_decimal(row["qty"]),
|
||||
rate=to_2_decimal(row["rate"]),
|
||||
boq_amount=to_2_decimal(row["boq_amount"]),
|
||||
previous_billed_qty=to_2_decimal(row["previous_billed_qty"]),
|
||||
previous_billing_amount=to_2_decimal(row["previous_billing_amount"]),
|
||||
in_this_ra_bill_qty=to_2_decimal(row["in_this_ra_bill_qty"]),
|
||||
in_this_ra_billing_amount=to_2_decimal(row["in_this_ra_billing_amount"]),
|
||||
cumulative_billed_qty=to_2_decimal(row["cumulative_billed_qty"]),
|
||||
cumulative_billed_amount=to_2_decimal(row["cumulative_billed_amount"]),
|
||||
variation_qty=to_2_decimal(row["variation_qty"]),
|
||||
variation_amount=to_2_decimal(row["variation_amount"]),
|
||||
parent_id=parent_id,
|
||||
parent_task_name=current_main_task_name if not serial_number else None,
|
||||
remark=row["remark"]
|
||||
)
|
||||
db.session.add(task)
|
||||
|
||||
db.session.commit()
|
||||
log_activity(current_user.username, "Database Insert", f"Inserted work details and tasks from {file.filename}")
|
||||
|
||||
return redirect(url_for('main.display_tasks'))
|
||||
|
||||
# # Update tasks route
|
||||
# @main.route('/update_tasks', methods=['POST'])
|
||||
# @login_required
|
||||
# def update_tasks():
|
||||
# try:
|
||||
# updates = request.get_json()
|
||||
# update_count = 0
|
||||
|
||||
# for key, new_value in updates.items():
|
||||
# if '_' not in key:
|
||||
# continue
|
||||
|
||||
# field_name, task_id_str = key.rsplit('_', 1)
|
||||
# if not task_id_str.isdigit():
|
||||
# continue
|
||||
|
||||
# task_id = int(task_id_str)
|
||||
# task = db.session.query(Task).filter_by(id=task_id).first()
|
||||
|
||||
# if task:
|
||||
# current_value = getattr(task, field_name, None)
|
||||
# if current_value != new_value:
|
||||
# setattr(task, field_name, new_value)
|
||||
# update_count += 1
|
||||
# log_activity(current_user.username, "Task Update", f"Task ID {task.id} - {field_name} changed to {new_value}")
|
||||
|
||||
# if update_count > 0:
|
||||
# db.session.commit()
|
||||
# log_activity(current_user.username, "Database Commit", f"{update_count} task field(s) updated")
|
||||
# return jsonify({'message': f'count: {update_count} field(s) updated.'})
|
||||
# else:
|
||||
# return jsonify({'message': 'No fields were updated.'})
|
||||
|
||||
# except Exception as e:
|
||||
# log_activity(current_user.username, "Error", f"Update tasks error: {str(e)}")
|
||||
# return jsonify({'error': 'An error occurred while updating tasks.'}), 500
|
||||
def recalc_task(task):
|
||||
rate = float(task.rate or 0)
|
||||
qty = float(task.qty or 0)
|
||||
|
||||
prev_qty = float(task.previous_billed_qty or 0)
|
||||
ra_qty = float(task.in_this_ra_bill_qty or 0)
|
||||
|
||||
# H = G * E
|
||||
task.previous_billing_amount = round(prev_qty * rate, 2)
|
||||
|
||||
# J = I * E
|
||||
task.in_this_ra_billing_amount = round(ra_qty * rate, 2)
|
||||
|
||||
# K = I + G
|
||||
task.cumulative_billed_qty = round(prev_qty + ra_qty, 2)
|
||||
|
||||
# L = K * E
|
||||
task.cumulative_billed_amount = round(task.cumulative_billed_qty * rate, 2)
|
||||
|
||||
# M = IF(K > D , K - D , 0)
|
||||
if task.cumulative_billed_qty > qty:
|
||||
task.variation_qty = round(task.cumulative_billed_qty - qty, 2)
|
||||
else:
|
||||
task.variation_qty = 0
|
||||
|
||||
# N = M * E
|
||||
task.variation_amount = round(task.variation_qty * rate, 2)
|
||||
|
||||
@main.route('/update_tasks', methods=['POST'])
|
||||
@login_required
|
||||
def update_tasks():
|
||||
try:
|
||||
updates = request.get_json()
|
||||
update_count = 0
|
||||
|
||||
# fields that should NOT be manually edited
|
||||
formula_fields = [
|
||||
"previous_billing_amount",
|
||||
"in_this_ra_billing_amount",
|
||||
"cumulative_billed_qty",
|
||||
"cumulative_billed_amount",
|
||||
"variation_qty",
|
||||
"variation_amount"
|
||||
]
|
||||
|
||||
for key, new_value in updates.items():
|
||||
if '_' not in key:
|
||||
continue
|
||||
|
||||
field_name, task_id_str = key.rsplit('_', 1)
|
||||
if not task_id_str.isdigit():
|
||||
continue
|
||||
|
||||
task_id = int(task_id_str)
|
||||
task = db.session.query(Task).filter_by(id=task_id).first()
|
||||
|
||||
if task:
|
||||
|
||||
# ❌ Skip manual update for formula fields
|
||||
if field_name in formula_fields:
|
||||
continue
|
||||
|
||||
current_value = getattr(task, field_name, None)
|
||||
|
||||
if str(current_value) != str(new_value):
|
||||
setattr(task, field_name, new_value)
|
||||
|
||||
# 🔥 auto recalc after any change
|
||||
recalc_task(task)
|
||||
|
||||
update_count += 1
|
||||
log_activity(
|
||||
current_user.username,
|
||||
"Task Update",
|
||||
f"Task ID {task.id} - {field_name} changed to {new_value}"
|
||||
)
|
||||
|
||||
if update_count > 0:
|
||||
db.session.commit()
|
||||
log_activity(
|
||||
current_user.username,
|
||||
"Database Commit",
|
||||
f"{update_count} task field(s) updated"
|
||||
)
|
||||
return jsonify({'message': f'count: {update_count} field(s) updated.'})
|
||||
else:
|
||||
return jsonify({'message': 'No fields were updated.'})
|
||||
|
||||
except Exception as e:
|
||||
log_activity(current_user.username, "Error", f"Update tasks error: {str(e)}")
|
||||
return jsonify({'error': 'An error occurred while updating tasks.'}), 500
|
||||
|
||||
# Display tasks route
|
||||
@main.route('/tasks')
|
||||
@login_required
|
||||
def display_tasks():
|
||||
work_details = WorkDetail.query.order_by(WorkDetail.uploaded_at.desc()).first()
|
||||
|
||||
if not work_details:
|
||||
log_activity(current_user.username, "Tasks View", "No work details available")
|
||||
return "No work details available.", 404
|
||||
|
||||
tasks = Task.query.filter_by(
|
||||
district=work_details.district,
|
||||
village_name=work_details.name_of_village,
|
||||
block_name=work_details.block
|
||||
).order_by(Task.uploaded_at.desc()).all()
|
||||
|
||||
grouped_tasks = []
|
||||
current_main_task = None
|
||||
|
||||
for task in tasks:
|
||||
task_data = {
|
||||
"id": task.id,
|
||||
"task_name": task.task_name,
|
||||
"unit": task.unit,
|
||||
"qty": task.qty,
|
||||
"rate": task.rate,
|
||||
"boq_amount": task.boq_amount,
|
||||
"previous_billed_qty": task.previous_billed_qty,
|
||||
"previous_billing_amount": task.previous_billing_amount,
|
||||
"in_this_ra_bill_qty": task.in_this_ra_bill_qty,
|
||||
"in_this_ra_billing_amount": task.in_this_ra_billing_amount,
|
||||
"cumulative_billed_qty": task.cumulative_billed_qty,
|
||||
"cumulative_billed_amount": task.cumulative_billed_amount,
|
||||
"variation_qty": task.variation_qty,
|
||||
"variation_amount": task.variation_amount,
|
||||
"remark": task.remark,
|
||||
"district": task.district
|
||||
}
|
||||
|
||||
if task.serial_number:
|
||||
task_data["subtasks"] = []
|
||||
grouped_tasks.append(task_data)
|
||||
current_main_task = task_data
|
||||
elif current_main_task:
|
||||
current_main_task["subtasks"].append(task_data)
|
||||
|
||||
log_activity(current_user.username, "Tasks View", f"Displayed tasks for {work_details.name_of_village}, {work_details.block}")
|
||||
return render_template('tasks_display.html', work_details=work_details, grouped_tasks=grouped_tasks)
|
||||
|
||||
|
||||
@main.route('/')
|
||||
|
||||
@login_required
|
||||
def dashboard():
|
||||
selected_block = request.args.getlist('block[]', None)
|
||||
|
||||
rate_col = cast(Task.rate, Float)
|
||||
qty_col = cast(Task.in_this_ra_bill_qty, Float)
|
||||
|
||||
query = db.session.query(
|
||||
Task.block_name.label("block_name"),
|
||||
Task.village_name.label("village_name"),
|
||||
func.sum(cast(Task.boq_amount, Float)).label("total_boq_amount"),
|
||||
func.sum(cast(Task.previous_billing_amount, Float)).label("prev_billed_amount"),
|
||||
func.sum(cast(Task.variation_amount, Float)).label("total_variation_amount"),
|
||||
func.sum(cast(Task.cumulative_billed_qty, Float)).label("cumulative_billed_qty"),
|
||||
func.sum(cast(Task.cumulative_billed_qty, Float) * cast(Task.rate, Float)).label("cumulative_billed_amount"),
|
||||
func.sum(qty_col).label("in_this_ra_bill_qty"),
|
||||
func.sum(rate_col * qty_col).label("to_be_claimed_amount")
|
||||
)
|
||||
|
||||
if selected_block and "All" not in selected_block:
|
||||
query = query.filter(Task.block_name.in_(selected_block))
|
||||
|
||||
query = query.group_by(Task.block_name, Task.village_name)
|
||||
villages = query.all()
|
||||
|
||||
village_data = []
|
||||
for village in villages:
|
||||
village_data.append({
|
||||
"block_name": village.block_name,
|
||||
"village_name": village.village_name,
|
||||
"total_boq_amount": village.total_boq_amount or 0,
|
||||
"rate": "-",
|
||||
"prev_billed_amount": village.prev_billed_amount or 0,
|
||||
"total_variation_amount": village.total_variation_amount or 0,
|
||||
"cumulative_billed_qty": village.cumulative_billed_qty or 0,
|
||||
"cumulative_billed_amount": village.cumulative_billed_amount or 0,
|
||||
"in_this_ra_bill_qty": village.in_this_ra_bill_qty or 0,
|
||||
"to_be_claimed_amount": round(village.to_be_claimed_amount or 0, 2)
|
||||
})
|
||||
|
||||
blocks = db.session.query(Task.block_name).distinct().all()
|
||||
block_list = ["All"] + [block[0] for block in blocks]
|
||||
|
||||
# log_activity(current_user.username, "Dashboard View", "Dashboard accessed")
|
||||
return render_template('index.html', villages=village_data, blocks=block_list, selected_block=selected_block)
|
||||
|
||||
|
||||
# @main.route('/generate_report_page')
|
||||
# @login_required
|
||||
# def generate_report_page():
|
||||
# blocks = db.session.query(Task.block_name).distinct().all()
|
||||
# blocks = [block.block_name for block in blocks]
|
||||
# selected_block = request.args.get('block')
|
||||
|
||||
# if selected_block:
|
||||
# main_tasks = db.session.query(Task.task_name).filter(
|
||||
# Task.serial_number.isnot(None),
|
||||
# Task.block_name == selected_block
|
||||
# ).distinct().all()
|
||||
# main_tasks = [task.task_name.strip().replace(",", "").replace("(", "").replace(")", "").replace(".", "").replace("&", "").replace("\n", "") for task in main_tasks]
|
||||
# else:
|
||||
# main_tasks = db.session.query(Task.task_name).filter(Task.serial_number.isnot(None)).distinct().all()
|
||||
# main_tasks = [task.task_name.strip().replace(",", "").replace("(", "").replace(")", "").replace(".", "").replace("&", "").replace("\n", "") for task in main_tasks]
|
||||
|
||||
# log_activity(current_user.username, "Report Page", f"Report generation page accessed (block={selected_block})")
|
||||
# return render_template('task_report.html', main_tasks=main_tasks, blocks=blocks)
|
||||
|
||||
|
||||
@main.route('/get_blocks_by_district')
|
||||
@login_required
|
||||
def get_blocks_by_district():
|
||||
district = request.args.get('district')
|
||||
|
||||
if not district:
|
||||
return jsonify({'blocks': []})
|
||||
|
||||
blocks = db.session.query(Task.block_name)\
|
||||
.filter(Task.district == district)\
|
||||
.distinct().all()
|
||||
|
||||
return jsonify({'blocks': [b[0] for b in blocks]})
|
||||
|
||||
@main.route('/generate_report_page')
|
||||
@login_required
|
||||
def generate_report_page():
|
||||
selected_district = request.args.get('district')
|
||||
selected_block = request.args.get('block')
|
||||
|
||||
# ✅ Get all districts
|
||||
districts = [d[0] for d in db.session.query(Task.district).distinct().all()]
|
||||
|
||||
# ✅ Get blocks based on district
|
||||
if selected_district:
|
||||
blocks = [b[0] for b in db.session.query(Task.block_name)
|
||||
.filter(Task.district == selected_district)
|
||||
.distinct().all()]
|
||||
else:
|
||||
blocks = []
|
||||
|
||||
# ✅ Get main tasks based on block
|
||||
if selected_block:
|
||||
main_tasks = db.session.query(Task.task_name).filter(
|
||||
Task.serial_number.isnot(None),
|
||||
Task.block_name == selected_block
|
||||
).distinct().all()
|
||||
|
||||
main_tasks = [
|
||||
task[0].strip()
|
||||
.replace(",", "")
|
||||
.replace("(", "")
|
||||
.replace(")", "")
|
||||
.replace(".", "")
|
||||
.replace("&", "")
|
||||
.replace("\n", "")
|
||||
for task in main_tasks
|
||||
]
|
||||
else:
|
||||
main_tasks = []
|
||||
|
||||
log_activity(
|
||||
current_user.username,
|
||||
"Report Page",
|
||||
f"Report page accessed district={selected_district}, block={selected_block}"
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'task_report.html',
|
||||
districts=districts,
|
||||
blocks=blocks,
|
||||
main_tasks=main_tasks,
|
||||
selected_district=selected_district,
|
||||
selected_block=selected_block
|
||||
)
|
||||
@main.route('/get_tasks_by_block')
|
||||
@login_required
|
||||
def get_tasks_by_block():
|
||||
block = request.args.get('block')
|
||||
if not block:
|
||||
return jsonify({'tasks': []})
|
||||
|
||||
tasks = db.session.query(Task.task_name)\
|
||||
.filter(Task.block_name == block)\
|
||||
.distinct()\
|
||||
.all()
|
||||
|
||||
task_list = [task[0].strip()
|
||||
.replace(",", "")
|
||||
.replace("(", "")
|
||||
.replace(")", "")
|
||||
.replace(".", "")
|
||||
.replace("&", "")
|
||||
.replace("\n", "") for task in tasks]
|
||||
|
||||
log_activity(current_user.username, "Fetch Tasks", f"Fetched tasks for block {block}")
|
||||
return jsonify({'tasks': task_list})
|
||||
|
||||
|
||||
def get_villages_for_block(block_name):
|
||||
villages = (
|
||||
db.session.query(WorkDetail.name_of_village)
|
||||
.filter(WorkDetail.block == block_name)
|
||||
.distinct()
|
||||
.order_by(WorkDetail.name_of_village)
|
||||
.all()
|
||||
)
|
||||
return [v[0] for v in villages if v[0]]
|
||||
|
||||
|
||||
@main.route('/get_villages_by_block', methods=['GET'])
|
||||
@login_required
|
||||
def get_villages_by_block():
|
||||
block = request.args.get('block')
|
||||
villages = get_villages_for_block(block)
|
||||
log_activity(current_user.username, "Fetch Villages", f"Fetched villages for block {block}")
|
||||
return jsonify({'villages': villages})
|
||||
|
||||
@main.route('/filter_tasks', methods=['GET'])
|
||||
@login_required
|
||||
def filter_tasks():
|
||||
district = request.args.get('district')
|
||||
block = request.args.get('block')
|
||||
village = request.args.get('village')
|
||||
|
||||
# ✅ Fetch distinct districts
|
||||
districts = [d[0] for d in db.session.query(WorkDetail.district).distinct()]
|
||||
|
||||
# ✅ Fetch blocks filtered by district
|
||||
if district:
|
||||
blocks = [b[0] for b in db.session.query(WorkDetail.block)
|
||||
.filter(WorkDetail.district == district).distinct()]
|
||||
else:
|
||||
blocks = []
|
||||
|
||||
# ✅ Fetch villages filtered by district + block
|
||||
if district and block:
|
||||
villages = [v[0] for v in db.session.query(WorkDetail.name_of_village)
|
||||
.filter(WorkDetail.district == district,
|
||||
WorkDetail.block == block)
|
||||
.distinct()]
|
||||
else:
|
||||
villages = []
|
||||
|
||||
grouped_tasks = []
|
||||
|
||||
# ✅ Only fetch tasks if all three (district, block, village) are selected
|
||||
if district and block and village:
|
||||
query = (db.session.query(Task)
|
||||
.join(WorkDetail, Task.village_name == WorkDetail.name_of_village)
|
||||
.filter(WorkDetail.district == district,
|
||||
WorkDetail.block == block,
|
||||
WorkDetail.name_of_village == village))
|
||||
|
||||
tasks = query.order_by(Task.uploaded_at.desc()).all()
|
||||
|
||||
current_main_task = None
|
||||
for task in tasks:
|
||||
task_data = {
|
||||
"id": task.id,
|
||||
"task_name": task.task_name,
|
||||
"unit": task.unit,
|
||||
"qty": task.qty,
|
||||
"rate": task.rate,
|
||||
"boq_amount": task.boq_amount,
|
||||
"previous_billed_qty": task.previous_billed_qty,
|
||||
"previous_billing_amount": task.previous_billing_amount,
|
||||
"in_this_ra_bill_qty": task.in_this_ra_bill_qty,
|
||||
"in_this_ra_billing_amount": task.in_this_ra_billing_amount,
|
||||
"cumulative_billed_qty": task.cumulative_billed_qty,
|
||||
"cumulative_billed_amount": task.cumulative_billed_amount,
|
||||
"variation_qty": task.variation_qty,
|
||||
"variation_amount": task.variation_amount,
|
||||
"remark": task.remark
|
||||
}
|
||||
|
||||
# ✅ Group main tasks (with serial_number) and subtasks
|
||||
if task.serial_number:
|
||||
task_data["subtasks"] = []
|
||||
grouped_tasks.append(task_data)
|
||||
current_main_task = task_data
|
||||
elif current_main_task:
|
||||
current_main_task["subtasks"].append(task_data)
|
||||
|
||||
log_activity(current_user.username, "Filter Tasks",
|
||||
f"Filtered tasks for district={district}, block={block}, village={village}")
|
||||
|
||||
# ✅ Render with both filtering + grouped tasks
|
||||
return render_template(
|
||||
'filter_tasks.html',
|
||||
grouped_tasks=grouped_tasks,
|
||||
districts=districts,
|
||||
blocks=blocks,
|
||||
villages=villages,
|
||||
selected_district=district,
|
||||
selected_block=block,
|
||||
selected_village=village
|
||||
)
|
||||
|
||||
|
||||
|
||||
# ✅ Helper function for logging user activity
|
||||
def log_activity(user, action, details=""):
|
||||
try:
|
||||
log_file = os.path.join(current_app.root_path, "activity.log")
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
with open(log_file, "a") as f:
|
||||
f.write(f"Timestamp: {timestamp} | User: {user} | Action: {action} | Details: {details}\n")
|
||||
except Exception as e:
|
||||
print(f"Logging failed: {e}")
|
||||
|
||||
from flask import request
|
||||
from datetime import datetime
|
||||
|
||||
@main.route('/activity_log', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def activity_log():
|
||||
logs = []
|
||||
log_file = os.path.join(current_app.root_path, 'activity.log')
|
||||
|
||||
if os.path.exists(log_file):
|
||||
with open(log_file, 'r') as f:
|
||||
for line in f:
|
||||
parts = line.strip().split(" | ")
|
||||
if len(parts) == 4:
|
||||
|
||||
logs.append({
|
||||
"timestamp": parts[0].replace("Timestamp:", "").strip(),
|
||||
"user": parts[1].replace("User:", "").strip(),
|
||||
"action": parts[2].replace("Action:", "").strip(),
|
||||
"details": parts[3].replace("Details:", "").strip()
|
||||
})
|
||||
|
||||
# Filters
|
||||
start_date = request.args.get("start_date")
|
||||
end_date = request.args.get("end_date")
|
||||
username = request.args.get("username")
|
||||
|
||||
filtered_logs = logs
|
||||
|
||||
# Date filter (inclusive)
|
||||
if start_date or end_date:
|
||||
try:
|
||||
if start_date:
|
||||
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
else:
|
||||
start_dt = datetime.min
|
||||
|
||||
if end_date:
|
||||
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
|
||||
else:
|
||||
end_dt = datetime.max
|
||||
|
||||
filtered_logs = [
|
||||
log for log in filtered_logs
|
||||
if start_dt <= datetime.strptime(log["timestamp"], "%Y-%m-%d %H:%M:%S") <= end_dt.replace(hour=23, minute=59, second=59)
|
||||
]
|
||||
except Exception as e:
|
||||
print("Date filter error:", e)
|
||||
|
||||
# Username filter
|
||||
if username:
|
||||
filtered_logs = [log for log in filtered_logs if log["user"].lower() == username.lower()]
|
||||
|
||||
return render_template(
|
||||
"activity_log.html",
|
||||
logs=filtered_logs,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
username=username
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
210
app/routes/reports.py
Normal file
210
app/routes/reports.py
Normal file
@@ -0,0 +1,210 @@
|
||||
from flask import Blueprint, request, render_template, send_from_directory, redirect, url_for, current_app, flash
|
||||
from app.__init__ import db
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from app.models import Task
|
||||
|
||||
reports = Blueprint('reports', __name__)
|
||||
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
def clean_text(text):
|
||||
if not isinstance(text, str):
|
||||
return ""
|
||||
return text.strip().replace(",", "").replace("(", "").replace(")", "")\
|
||||
.replace(".", "").replace("&", "").replace("\n", "").lower()
|
||||
|
||||
@reports.route('/report_excel', methods=['GET'])
|
||||
def generate_report():
|
||||
main_task_rexp = r'[\\/*?:"<>|]'
|
||||
block = request.args.get('block', '')
|
||||
main_task = request.args.get('main_task', '')
|
||||
|
||||
block_clean = clean_text(block)
|
||||
main_task_clean = clean_text(main_task)
|
||||
|
||||
if not block_clean:
|
||||
return "Please select a Block.", 400
|
||||
if not main_task_clean:
|
||||
return "Please select a Main Task.", 400
|
||||
|
||||
print(f"Block selected: {block}")
|
||||
print(f"Main task selected: {main_task}")
|
||||
|
||||
# Filter main task records based on cleaned block and task name
|
||||
main_task_records = [task for task in Task.query.filter_by(block_name=block).all()
|
||||
if clean_text(task.task_name) == main_task_clean]
|
||||
|
||||
print(f"Found {len(main_task_records)} main task records")
|
||||
|
||||
# if not main_task_records:
|
||||
# return f"Main Task '{main_task}' not found in the selected block '{block}'.", 404
|
||||
if not main_task_records:
|
||||
flash("No data found for selected Block and Main Task", "error")
|
||||
# return redirect(url_for('generate_report_page'))
|
||||
return redirect(url_for('main.generate_report_page'))
|
||||
|
||||
report_data = {}
|
||||
|
||||
def safe_float(value):
|
||||
try:
|
||||
return round(float(value), 2)
|
||||
except (ValueError, TypeError):
|
||||
return 0.0
|
||||
|
||||
# Process subtasks for each main task
|
||||
for main_task_record in main_task_records:
|
||||
main_task_serial_number = main_task_record.serial_number
|
||||
|
||||
|
||||
# Get all subtasks under the selected main task (match cleaned names)
|
||||
subtasks_query = [
|
||||
task for task in Task.query.filter(Task.block_name == block).all()
|
||||
if clean_text(task.parent_task_name) == main_task_clean
|
||||
]
|
||||
|
||||
print(f"Found {len(subtasks_query)} subtasks for main task '{main_task}'")
|
||||
|
||||
for task in subtasks_query:
|
||||
key = task.village_name.strip() if task.village_name else ""
|
||||
totalElemList = 26
|
||||
if key not in report_data:
|
||||
report_data[key] = [None] * totalElemList
|
||||
report_data[key][0] = task.id
|
||||
report_data[key][1] = task.village_name
|
||||
|
||||
boq_amount = safe_float(task.boq_amount)
|
||||
previous_billing_amount = safe_float(task.previous_billing_amount)
|
||||
remaining_amount = safe_float(boq_amount - previous_billing_amount)
|
||||
tender_amount = safe_float(task.qty) * safe_float(task.rate)
|
||||
values = [
|
||||
safe_float(task.qty),
|
||||
safe_float(task.rate),
|
||||
tender_amount,
|
||||
safe_float(task.previous_billed_qty),
|
||||
previous_billing_amount,
|
||||
remaining_amount
|
||||
]
|
||||
|
||||
# Determine task type section
|
||||
task_name_clean = task.task_name.lower() if task.task_name else ""
|
||||
start, end = (
|
||||
(2, 8) if "supply" in task_name_clean else
|
||||
(8, 14) if "erection" in task_name_clean else
|
||||
(14, 20) if "testing" in task_name_clean else
|
||||
(20, 26) if "commissioning" in task_name_clean else
|
||||
(None, None)
|
||||
)
|
||||
|
||||
if start is not None and end is not None:
|
||||
for i in range(start, end):
|
||||
current_value = safe_float(report_data[key][i]) if report_data[key][i] is not None else 0
|
||||
report_data[key][i] = current_value + values[i - start]
|
||||
|
||||
print(f"Number of villages in report data: {len(report_data)}")
|
||||
|
||||
# if not report_data:
|
||||
# return f"No matching data found for the selected block '{block}' and main task '{main_task}'.", 404
|
||||
if not report_data:
|
||||
flash("Sub task data not found", "error")
|
||||
# return redirect(url_for('generate_report_page'))
|
||||
return redirect(url_for('main.generate_report_page'))
|
||||
|
||||
# Generate Excel report
|
||||
sanitized_main_task = re.sub(main_task_rexp, "", main_task)
|
||||
|
||||
max_length = 30
|
||||
|
||||
if len(sanitized_main_task) > max_length:
|
||||
sanitized_main_task = sanitized_main_task[:max_length]
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
# file_name = f"{sanitized_main_task}_Report.xlsx"
|
||||
file_name = f"{sanitized_main_task}_{timestamp}.xlsx"
|
||||
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], file_name)
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "Subtask Report"
|
||||
|
||||
# Excel formatting
|
||||
thin_border = Border(left=Side(style="thin"), right=Side(style="thin"),
|
||||
top=Side(style="thin"), bottom=Side(style="thin"))
|
||||
header_fill = PatternFill(start_color="FFC000", end_color="FFC000", fill_type="solid")
|
||||
subheader_fill = PatternFill(start_color="92D050", end_color="92D050", fill_type="solid")
|
||||
data_row_fill1 = PatternFill(start_color="FFFFFF", end_color="FFFFFF", fill_type="solid")
|
||||
data_row_fill2 = PatternFill(start_color="D9EAD3", end_color="D9EAD3", fill_type="solid")
|
||||
total_row_fill = PatternFill(start_color="FFF2CC", end_color="FFF2CC", fill_type="solid")
|
||||
|
||||
ws.merge_cells(start_row=1, start_column=3, end_row=1, end_column=8)
|
||||
ws["A1"] = "Task ID"
|
||||
ws["B1"] = "Village Name"
|
||||
ws["C1"] = "Supply (70%)"
|
||||
ws.merge_cells(start_row=1, start_column=9, end_row=1, end_column=14)
|
||||
ws["I1"] = "Erection (20%)"
|
||||
ws.merge_cells(start_row=1, start_column=15, end_row=1, end_column=20)
|
||||
ws["O1"] = "Testing (5%)"
|
||||
ws.merge_cells(start_row=1, start_column=21, end_row=1, end_column=26)
|
||||
ws["U1"] = "Commissioning (5%)"
|
||||
|
||||
for start_col in [3, 9, 15, 21]:
|
||||
ws.cell(row=2, column=start_col).value = "Tender Qty"
|
||||
ws.cell(row=2, column=start_col + 1).value = "Tender Rate"
|
||||
ws.cell(row=2, column=start_col + 2).value = "Tender Amount"
|
||||
ws.cell(row=2, column=start_col + 3).value = "Previous Bill QTY"
|
||||
ws.cell(row=2, column=start_col + 4).value = "Previous Bill Amount"
|
||||
ws.cell(row=2, column=start_col + 5).value = "Remaining Amount"
|
||||
|
||||
# Style header and subheaders
|
||||
for col in range(1, 27):
|
||||
col_letter = ws.cell(row=2, column=col).column_letter
|
||||
ws[f"{col_letter}1"].font = Font(bold=True, size=12)
|
||||
ws[f"{col_letter}1"].alignment = Alignment(horizontal="center", vertical="center")
|
||||
ws[f"{col_letter}1"].fill = header_fill
|
||||
ws[f"{col_letter}1"].border = thin_border
|
||||
|
||||
ws[f"{col_letter}2"].font = Font(bold=True, size=11)
|
||||
ws[f"{col_letter}2"].alignment = Alignment(horizontal="center", vertical="center")
|
||||
ws[f"{col_letter}2"].fill = subheader_fill
|
||||
ws[f"{col_letter}2"].border = thin_border
|
||||
|
||||
# Fill data
|
||||
row_index = 3
|
||||
totals = ["Total", ""] + [0] * 24
|
||||
sum_columns = [4, 6, 7, 10, 12, 13, 16, 18, 19, 22, 24, 25]
|
||||
|
||||
for row_data in report_data.values():
|
||||
ws.append(row_data)
|
||||
fill = data_row_fill1 if row_index % 2 != 0 else data_row_fill2
|
||||
for col in range(1, 27):
|
||||
ws.cell(row=row_index, column=col).fill = fill
|
||||
ws.cell(row=row_index, column=col).border = thin_border
|
||||
for i in sum_columns:
|
||||
totals[i] += safe_float(row_data[i])
|
||||
row_index += 1
|
||||
|
||||
# Add totals
|
||||
ws.append(totals)
|
||||
for col in range(1, 27):
|
||||
ws.cell(row=row_index, column=col).font = Font(bold=True)
|
||||
ws.cell(row=row_index, column=col).fill = total_row_fill
|
||||
ws.cell(row=row_index, column=col).alignment = Alignment(horizontal="center")
|
||||
ws.cell(row=row_index, column=col).border = thin_border
|
||||
|
||||
for i in range(1, 27):
|
||||
ws.column_dimensions[ws.cell(row=2, column=i).column_letter].width = 20
|
||||
|
||||
wb.save(file_path)
|
||||
print(f"Report generated: {file_name}")
|
||||
return redirect(url_for('reports.download_report', filename=file_name))
|
||||
|
||||
|
||||
@reports.route('/download/<filename>')
|
||||
def download_report(filename):
|
||||
return send_from_directory(current_app.config['UPLOAD_FOLDER'], filename, as_attachment=True)
|
||||
Reference in New Issue
Block a user