diff --git a/.env b/.env index 2477ef9..86c7bd2 100644 --- a/.env +++ b/.env @@ -4,7 +4,7 @@ FLASK_ENV=development FLASK_DEBUG=True FLASK_HOST=0.0.0.0 -FLASK_PORT=5011 +FLASK_PORT=5015 # ----------------------------- # Security @@ -24,3 +24,14 @@ DB_PASSWORD=root # DATABASE_URL=mysql+pymysql://root:root@localhost/comparisondb + +# ----------------------------- +# LDAP Configuration new +# ----------------------------- +LDAP_SERVER=ldap://host.docker.internal +LDAP_PORT=389 +LDAP_USE_SSL=False + +LDAP_DOMAIN=lcepl.org +LDAP_BASE_DN=DC=lcepl,DC=org +LDAP_SEARCH_BASE=OU=Users,DC=lcepl,DC=org diff --git a/app/__init__.py b/app/__init__.py index 52606e3..8838957 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -28,7 +28,7 @@ def create_app(): def register_blueprints(app): from app.routes.auth import auth_bp - from app.routes.user import user_bp + from app.routes.user_routes import user_bp from app.routes.dashboard import dashboard_bp from app.routes.subcontractor_routes import subcontractor_bp from app.routes.file_import import file_import_bp @@ -36,6 +36,10 @@ def register_blueprints(app): from app.routes.generate_comparison_report import generate_report_bp from app.routes.file_format import file_format_bp + # new + from app.routes.activity_routes import activity_bp + from app.routes.engineering_master_routes import engi_bp + app.register_blueprint(auth_bp) app.register_blueprint(user_bp) app.register_blueprint(dashboard_bp) @@ -43,8 +47,12 @@ def register_blueprints(app): app.register_blueprint(file_import_bp) app.register_blueprint(file_report_bp) app.register_blueprint(generate_report_bp) - app.register_blueprint(file_format_bp ) + app.register_blueprint(file_format_bp) + # new + app.register_blueprint(activity_bp) + app.register_blueprint(engi_bp) + def register_error_handlers(app): diff --git a/app/config.py b/app/config.py index ece5bed..54b2182 100644 --- a/app/config.py +++ b/app/config.py @@ -1,6 +1,4 @@ import os -# project base url -BASE_DIR = os.path.abspath(os.path.dirname(__file__)) class Config: # secret key @@ -23,7 +21,14 @@ class Config: ) SQLALCHEMY_TRACK_MODIFICATIONS = False - # uploads folder path - UPLOAD_FOLDER = os.path.join(BASE_DIR, "static", "uploads") - # file extension - ALLOWED_EXTENSIONS = {"xlsx", "xls", "csv"} + + + # LDAP Configuration New + LDAP_SERVER = os.getenv("LDAP_SERVER") + LDAP_PORT = int(os.getenv("LDAP_PORT", 389)) + LDAP_USE_SSL = os.getenv("LDAP_USE_SSL", "False").lower() == "true" + + LDAP_BASE_DN = os.getenv("LDAP_BASE_DN") + LDAP_DOMAIN = os.getenv("LDAP_DOMAIN") + + LDAP_SEARCH_BASE = os.getenv("LDAP_SEARCH_BASE") \ No newline at end of file diff --git a/app/constants/http_status.py b/app/constants/http_status.py index 9859f7d..e0e259c 100644 --- a/app/constants/http_status.py +++ b/app/constants/http_status.py @@ -1,11 +1,29 @@ class HTTPStatus: - OK = 200 - CREATED = 201 - BAD_REQUEST = 400 - UNAUTHORIZED = 401 - FORBIDDEN = 403 - NOT_FOUND = 404 - METHOD_NOT_ALLOWED = 405 - CONFLICT = 409 - UNPROCESSABLE_ENTITY = 422 - INTERNAL_SERVER_ERROR = 500 \ No newline at end of file + # ========================== + # 2xx Success + # ========================== + OK = 200 # Request successful + CREATED = 201 # Resource created successfully + ACCEPTED = 202 # Request accepted for processing + NO_CONTENT = 204 # Success, no response body + + # ========================== + # 4xx Client Errors + # ========================== + BAD_REQUEST = 400 # Invalid request + UNAUTHORIZED = 401 # Authentication required + FORBIDDEN = 403 # Access denied + NOT_FOUND = 404 # Resource not found + METHOD_NOT_ALLOWED = 405 # HTTP method not allowed + CONFLICT = 409 # Resource conflict (e.g., duplicate) + UNPROCESSABLE_ENTITY = 422 # Validation error + TOO_MANY_REQUESTS = 429 # Rate limit exceeded + + # ========================== + # 5xx Server Errors + # ========================== + INTERNAL_SERVER_ERROR = 500 # Internal server error + NOT_IMPLEMENTED = 501 # Feature not implemented + BAD_GATEWAY = 502 # Invalid response from upstream server + SERVICE_UNAVAILABLE = 503 # Server temporarily unavailable + GATEWAY_TIMEOUT = 504 # Upstream server timeout \ No newline at end of file diff --git a/app/constants/messages.py b/app/constants/messages.py index ada80e3..769da3b 100644 --- a/app/constants/messages.py +++ b/app/constants/messages.py @@ -1,17 +1,65 @@ class SuccessMessage: - FETCHED = "Data fetched successfully" - CREATED = "Resource created successfully" - UPDATED = "Resource updated successfully" - DELETED = "Resource deleted successfully" - LOGIN = "Login successful" - LOGOUT = "Logout successful" + FETCHED = "Data fetched successfully." + CREATED = "Resource created successfully." + UPDATED = "Resource updated successfully." + DELETED = "Resource deleted successfully." + + SAVED = "Data saved successfully." + IMPORTED = "Data imported successfully." + EXPORTED = "Data exported successfully." + UPLOADED = "File uploaded successfully." + DOWNLOADED = "File downloaded successfully." + + LOGIN = "Login successful." + LOGOUT = "Logout successful." + PASSWORD_CHANGED = "Password changed successfully." + PASSWORD_RESET = "Password reset successfully." + + EMAIL_SENT = "Email sent successfully." + STATUS_UPDATED = "Status updated successfully." class ErrorMessage: - INVALID_REQUEST = "Invalid request data" - UNAUTHORIZED = "Unauthorized access" - FORBIDDEN = "Access forbidden" - NOT_FOUND = "Resource not found" - VALIDATION_FAILED = "Validation failed" - INTERNAL_ERROR = "Internal server error" - DUPLICATE_ENTRY = "Duplicate record found" + INVALID_REQUEST = "Invalid request." + INVALID_DATA = "Invalid data provided." + VALIDATION_FAILED = "Validation failed." + + UNAUTHORIZED = "Unauthorized access." + FORBIDDEN = "Access denied." + NOT_FOUND = "Resource not found." + METHOD_NOT_ALLOWED = "Method not allowed." + + DUPLICATE_ENTRY = "Duplicate record found." + RECORD_EXISTS = "Record already exists." + RECORD_NOT_FOUND = "Record does not exist." + + FILE_NOT_FOUND = "File not found." + FILE_UPLOAD_FAILED = "File upload failed." + FILE_IMPORT_FAILED = "File import failed." + + DATABASE_ERROR = "Database operation failed." + INTERNAL_SERVER_ERROR = "Internal server error." + SERVICE_UNAVAILABLE = "Service temporarily unavailable." + + LOGIN_FAILED = "Invalid username or password." + SESSION_EXPIRED = "Session expired. Please login again." + + PASSWORD_MISMATCH = "Passwords do not match." + INVALID_TOKEN = "Invalid or expired token." + + +class WarningMessage: + NO_DATA_FOUND = "No data found." + ALREADY_EXISTS = "Record already exists." + UNSAVED_CHANGES = "You have unsaved changes." + DELETE_CONFIRMATION = "Are you sure you want to delete the selected record(s)?" + INVALID_FILTER = "No records match the selected filters." + + +class InfoMessage: + PROCESSING = "Request is being processed." + LOADING = "Loading data..." + SAVING = "Saving data..." + DELETING = "Deleting record..." + IMPORTING = "Importing data..." + EXPORTING = "Preparing export..." \ No newline at end of file diff --git a/app/models/laying_client_model.py b/app/models/laying_client_model.py index 1cfa15b..86f0ce1 100644 --- a/app/models/laying_client_model.py +++ b/app/models/laying_client_model.py @@ -2,6 +2,7 @@ from app import db from datetime import datetime from sqlalchemy import event from app.utils.regex_utils import RegularExpression +from decimal import Decimal class LayingClient(db.Model): __tablename__ = "laying_client" @@ -9,39 +10,47 @@ class LayingClient(db.Model): id = db.Column(db.Integer, primary_key=True) # Basic Fields + RA_Bill_No = db.Column(db.String(500)) Location = db.Column(db.String(500)) MH_NO = db.Column(db.String(100)) - CC_length = db.Column(db.Float, default=0) + CC_length = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) # Bedding Qty. - Outer_dia_of_MH_m = db.Column(db.Float, default=0) - Bedding_Length = db.Column(db.Float, default=0) - Width = db.Column(db.Float, default=0) - Depth = db.Column(db.Float, default=0) - Qty = db.Column(db.Float, default=0) + Outer_dia_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Bedding_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Width = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Qty = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) # PIPE LAYING Qty. - Pipe_Dia_mm = db.Column(db.Float, default=0) - ID_of_MH_m = db.Column(db.Float, default=0) - Laying_Length = db.Column(db.Float, default=0) + Pipe_Dia_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Laying_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - pipe_150_mm = db.Column(db.Float, default=0) - pipe_200_mm = db.Column(db.Float, default=0) - pipe_250_mm = db.Column(db.Float, default=0) - pipe_300_mm = db.Column(db.Float, default=0) - pipe_350_mm = db.Column(db.Float, default=0) - pipe_400_mm = db.Column(db.Float, default=0) - pipe_450_mm = db.Column(db.Float, default=0) - pipe_500_mm = db.Column(db.Float, default=0) - pipe_600_mm = db.Column(db.Float, default=0) - pipe_700_mm = db.Column(db.Float, default=0) - pipe_900_mm = db.Column(db.Float, default=0) - pipe_1200_mm = db.Column(db.Float, default=0) + pipe_150_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_250_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_300_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_350_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_400_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_450_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_500_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_600_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_700_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_900_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_1200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Total = db.Column(db.Float, default=0) - Remarks = db.Column(db.String(500)) - RA_Bill_No=db.Column(db.String(500)) + np4_pipe_200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + np4_pipe_250_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + np4_pipe_300_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + np4_pipe_350_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + np4_pipe_400_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + np4_pipe_450_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + np4_pipe_500_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + np4_pipe_600_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - created_at = db.Column(db.DateTime, default=datetime.today) + Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00")) + Remarks = db.Column(db.String(500), default="Import File") + created_at = db.Column(db.DateTime,default=datetime.now,nullable=False) def __repr__(self): @@ -53,11 +62,13 @@ class LayingClient(db.Model): # AUTO TOTAL USING REGEX def calculate_laying_total(mapper, connection, target): - total = 0 + total = Decimal("0.00") for column in target.__table__.columns: if RegularExpression.PIPE_MM_PATTERN.match(column.name): - total += getattr(target, column.name) or 0 - target.Total = total - + value = getattr(target, column.name) + if value is not None: + total += Decimal(value) + target.Total = total.quantize(Decimal("0.01")) + event.listen(LayingClient, "before_insert", calculate_laying_total) event.listen(LayingClient, "before_update", calculate_laying_total) \ No newline at end of file diff --git a/app/models/laying_model.py b/app/models/laying_model.py index e159c43..acaf033 100644 --- a/app/models/laying_model.py +++ b/app/models/laying_model.py @@ -2,6 +2,7 @@ from app import db from datetime import datetime from sqlalchemy import event from app.utils.regex_utils import RegularExpression +from decimal import Decimal class Laying(db.Model): __tablename__ = "laying" @@ -13,31 +14,30 @@ class Laying(db.Model): subcontractor = db.relationship("Subcontractor", backref="laying_records") # Pipe Laying Fields + RA_Bill_No=db.Column(db.String(500)) Location = db.Column(db.String(500)) MH_NO = db.Column(db.String(100)) - CC_length = db.Column(db.Float, default=0) - Pipe_Dia_mm = db.Column(db.Float, default=0) - ID_of_MH_m = db.Column(db.Float, default=0) - Laying_Length = db.Column(db.Float, default=0) + CC_length = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Pipe_Dia_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Laying_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - pipe_150_mm = db.Column(db.Float, default=0) - pipe_200_mm = db.Column(db.Float, default=0) - pipe_250_mm = db.Column(db.Float, default=0) - pipe_300_mm = db.Column(db.Float, default=0) - pipe_350_mm = db.Column(db.Float, default=0) - pipe_400_mm = db.Column(db.Float, default=0) - pipe_450_mm = db.Column(db.Float, default=0) - pipe_500_mm = db.Column(db.Float, default=0) - pipe_600_mm = db.Column(db.Float, default=0) - pipe_700_mm = db.Column(db.Float, default=0) - pipe_900_mm = db.Column(db.Float, default=0) - pipe_1200_mm = db.Column(db.Float, default=0) + pipe_150_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_250_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_300_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_350_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_400_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_450_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_500_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_600_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_700_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_900_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + pipe_1200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Total = db.Column(db.Float, default=0) - Remarks = db.Column(db.String(500)) - RA_Bill_No=db.Column(db.String(500)) - - created_at = db.Column(db.DateTime, default=datetime.today) + Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00")) + Remarks = db.Column(db.String(500), default="Import File") + created_at = db.Column(db.DateTime,default=datetime.now,nullable=False) def __repr__(self): @@ -49,11 +49,13 @@ class Laying(db.Model): # AUTO TOTAL USING REGEX def calculate_laying_total(mapper, connection, target): - total = 0 + total = Decimal("0.00") for column in target.__table__.columns: if RegularExpression.PIPE_MM_PATTERN.match(column.name): - total += getattr(target, column.name) or 0 - target.Total = total + value = getattr(target, column.name) + if value is not None: + total += Decimal(value) + target.Total = total.quantize(Decimal("0.01")) event.listen(Laying, "before_insert", calculate_laying_total) event.listen(Laying, "before_update", calculate_laying_total) \ No newline at end of file diff --git a/app/models/manhole_domestic_chamber_model.py b/app/models/manhole_domestic_chamber_model.py index 5303420..a27a0e8 100644 --- a/app/models/manhole_domestic_chamber_model.py +++ b/app/models/manhole_domestic_chamber_model.py @@ -2,6 +2,7 @@ from app import db from datetime import datetime from sqlalchemy import event from app.utils.regex_utils import RegularExpression +from decimal import Decimal class ManholeDomesticChamber(db.Model): __tablename__ = "manhole_domestic_chamber" @@ -13,38 +14,40 @@ class ManholeDomesticChamber(db.Model): subcontractor = db.relationship("Subcontractor", backref="manhole_domestic_chamber_records") # Basic Fields + RA_Bill_No=db.Column(db.String(500)) Location = db.Column(db.String(500)) MH_NO = db.Column(db.String(100)) - Depth_of_MH = db.Column(db.Float, default=0) - - # Excavation categories - d_0_to_0_75 = db.Column(db.Float, default=0) - d_0_76_to_1_05 = db.Column(db.Float, default=0) - d_1_06_to_1_65 = db.Column(db.Float, default=0) - d_1_66_to_2_15 = db.Column(db.Float, default=0) - d_2_16_to_2_65 = db.Column(db.Float, default=0) - d_2_66_to_3_15 = db.Column(db.Float, default=0) - d_3_16_to_3_65= db.Column(db.Float, default=0) - d_3_66_to_4_15 = db.Column(db.Float, default=0) - d_4_16_to_4_65 = db.Column(db.Float, default=0) - d_4_66_to_5_15 = db.Column(db.Float, default=0) - d_5_16_to_5_65 = db.Column(db.Float, default=0) - d_5_66_to_6_15 = db.Column(db.Float, default=0) - d_6_16_to_6_65 = db.Column(db.Float, default=0) - d_6_66_to_7_15 = db.Column(db.Float, default=0) - d_7_16_to_7_65 = db.Column(db.Float, default=0) - d_7_66_to_8_15 = db.Column(db.Float, default=0) - d_8_16_to_8_65 = db.Column(db.Float, default=0) - d_8_66_to_9_15 = db.Column(db.Float, default=0) - d_9_16_to_9_65 = db.Column(db.Float, default=0) + Depth_of_MH = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Domestic_Chambers = db.Column(db.Float, default=0) - DWC_Pipe_Length = db.Column(db.Float, default=0) - UPVC_Pipe_Length = db.Column(db.Float, default=0) - RA_Bill_No=db.Column(db.String(500)) + # Excavation categories + d_0_to_0_75 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_0_76_to_1_05 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_1_06_to_1_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_1_66_to_2_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_2_16_to_2_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_2_66_to_3_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_3_16_to_3_65= db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_3_66_to_4_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_4_16_to_4_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_4_66_to_5_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_5_16_to_5_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_5_66_to_6_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_6_16_to_6_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_6_66_to_7_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_7_16_to_7_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_7_66_to_8_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_8_16_to_8_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_8_66_to_9_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_9_16_to_9_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Total = db.Column(db.Float, default=0) - created_at = db.Column(db.DateTime, default=datetime.today) + Domestic_Chambers = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + DWC_Pipe_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + UPVC_Pipe_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + + + Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00")) + Remarks = db.Column(db.String(500), default="Import File") + created_at = db.Column(db.DateTime,default=datetime.now,nullable=False) def __repr__(self): return f"" @@ -55,11 +58,13 @@ class ManholeDomesticChamber(db.Model): # AUTO TOTAL USING REGEX def calculate_mh_dc_total(mapper, connection, target): - total = 0 + total = Decimal("0.00") for column in target.__table__.columns: if RegularExpression.D_RANGE_PATTERN.match(column.name): - total += getattr(target, column.name) or 0 - target.Total = total + value = getattr(target, column.name) + if value is not None: + total += Decimal(value) + target.Total = total.quantize(Decimal("0.01")) event.listen(ManholeDomesticChamber, "before_insert", calculate_mh_dc_total) event.listen(ManholeDomesticChamber, "before_update", calculate_mh_dc_total) \ No newline at end of file diff --git a/app/models/manhole_excavation_model.py b/app/models/manhole_excavation_model.py index 15839a2..1ff78bd 100644 --- a/app/models/manhole_excavation_model.py +++ b/app/models/manhole_excavation_model.py @@ -2,6 +2,7 @@ from app import db from datetime import datetime from sqlalchemy import event from app.utils.regex_utils import RegularExpression +from decimal import Decimal class ManholeExcavation(db.Model): __tablename__ = "manhole_excavation" @@ -13,54 +14,53 @@ class ManholeExcavation(db.Model): subcontractor = db.relationship("Subcontractor", backref="manhole_records") # Basic Fields + RA_Bill_No=db.Column(db.String(500)) Location = db.Column(db.String(500)) MH_NO = db.Column(db.String(100)) - Upto_IL_Depth = db.Column(db.Float, default=0) - Cutting_Depth = db.Column(db.Float, default=0) - ID_of_MH_m = db.Column(db.Float, default=0) - Ex_Dia_of_Manhole = db.Column(db.Float, default=0) - Area_of_Manhole = db.Column(db.Float, default=0) + Upto_IL_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Cutting_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Ex_Dia_of_Manhole = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Area_of_Manhole = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) # Excavation categories - Soft_Murum_0_to_1_5 = db.Column(db.Float, default=0) - Soft_Murum_1_5_to_3_0 = db.Column(db.Float, default=0) - Soft_Murum_3_0_to_4_5 = db.Column(db.Float, default=0) + Soft_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Hard_Murum_0_to_1_5 = db.Column(db.Float, default=0) - Hard_Murum_1_5_to_3_0 = db.Column(db.Float, default=0) + Hard_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Soft_Rock_0_to_1_5 = db.Column(db.Float, default=0) - Soft_Rock_1_5_to_3_0 = db.Column(db.Float, default=0) + Soft_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Hard_Rock_0_to_1_5 = db.Column(db.Float, default=0) - Hard_Rock_1_5_to_3_0 = db.Column(db.Float, default=0) - Hard_Rock_3_0_to_4_5 = db.Column(db.Float, default=0) - Hard_Rock_4_5_to_6_0 = db.Column(db.Float, default=0) - Hard_Rock_6_0_to_7_5 = db.Column(db.Float, default=0) + Hard_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_4_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) # Totals - Soft_Murum_0_to_1_5_total = db.Column(db.Float, default=0) - Soft_Murum_1_5_to_3_0_total = db.Column(db.Float, default=0) - Soft_Murum_3_0_to_4_5_total = db.Column(db.Float, default=0) + Soft_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Hard_Murum_0_to_1_5_total = db.Column(db.Float, default=0) - Hard_Murum_1_5_and_above_total = db.Column(db.Float, default=0) + Hard_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Murum_1_5_and_above_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Soft_Rock_0_to_1_5_total = db.Column(db.Float, default=0) - Soft_Rock_1_5_and_above_total = db.Column(db.Float, default=0) + Soft_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Rock_1_5_and_above_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Hard_Rock_0_to_1_5_total = db.Column(db.Float, default=0) - Hard_Rock_1_5_to_3_0_total = db.Column(db.Float, default=0) - Hard_Rock_3_0_to_4_5_total = db.Column(db.Float, default=0) - Hard_Rock_4_5_to_6_0_total = db.Column(db.Float, default=0) - Hard_Rock_6_0_to_7_5_total = db.Column(db.Float, default=0) + Hard_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_4_5_to_6_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Total = db.Column(db.Float, default=0) - Remarks = db.Column(db.String(500)) - RA_Bill_No=db.Column(db.String(500)) - - created_at = db.Column(db.DateTime, default=datetime.today) + Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00")) + Remarks = db.Column(db.String(500), default="Import File") + created_at = db.Column(db.DateTime,default=datetime.now,nullable=False) def __repr__(self): return f"" @@ -70,11 +70,13 @@ class ManholeExcavation(db.Model): # AUTO TOTAL USING REGEX def calculate_Manhole_total(mapper, connection, target): - total = 0 + total = Decimal("0.00") for column in target.__table__.columns: if RegularExpression.STR_TOTAL_PATTERN.match(column.name): - total += getattr(target, column.name) or 0 - target.Total = total + value = getattr(target, column.name) + if value is not None: + total += Decimal(value) + target.Total = total.quantize(Decimal("0.01")) event.listen(ManholeExcavation, "before_insert", calculate_Manhole_total) diff --git a/app/models/mh_dc_client_model.py b/app/models/mh_dc_client_model.py index 0fb90ee..fc70297 100644 --- a/app/models/mh_dc_client_model.py +++ b/app/models/mh_dc_client_model.py @@ -2,6 +2,7 @@ from app import db from datetime import datetime from sqlalchemy import event from app.utils.regex_utils import RegularExpression +from decimal import Decimal class ManholeDomesticChamberClient(db.Model): __tablename__ = "mh_dc_client" @@ -9,30 +10,31 @@ class ManholeDomesticChamberClient(db.Model): id = db.Column(db.Integer, primary_key=True) # Basic Fields - RA_Bill_No=db.Column(db.String(500)) + RA_Bill_No = db.Column(db.String(500)) Location = db.Column(db.String(500)) MH_NO = db.Column(db.String(100)) - MH_TOP_LEVEL = db.Column(db.Float, default=0) - MH_IL_LEVEL = db.Column(db.Float, default=0) - Depth_of_MH = db.Column(db.Float, default=0) + MH_TOP_LEVEL = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + MH_IL_LEVEL = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Depth_of_MH = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) # Excavation categories - d_0_to_1_5 = db.Column(db.Float, default=0) - d_1_5_to_2_0 = db.Column(db.Float, default=0) - d_2_0_to_2_5 = db.Column(db.Float, default=0) - d_2_5_to_3_0 = db.Column(db.Float, default=0) - d_3_0_to_3_5 = db.Column(db.Float, default=0) - d_3_5_to_4_0 = db.Column(db.Float, default=0) - d_4_0_to_4_5= db.Column(db.Float, default=0) - d_4_5_to_5_0 = db.Column(db.Float, default=0) - d_5_0_to_5_5 = db.Column(db.Float, default=0) - d_5_5_to_6_0 = db.Column(db.Float, default=0) - d_6_0_to_6_5 = db.Column(db.Float, default=0) + d_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_1_5_to_2_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_2_0_to_2_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_2_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_3_0_to_3_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_3_5_to_4_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_4_0_to_4_5= db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_4_5_to_5_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_5_0_to_5_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_5_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + d_6_0_to_6_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Domestic_Chambers = db.Column(db.Float, default=0) + Domestic_Chambers = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Total = db.Column(db.Float, default=0) - created_at = db.Column(db.DateTime, default=datetime.today) + Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00")) + Remarks = db.Column(db.String(500), default="Import File") + created_at = db.Column(db.DateTime,default=datetime.now,nullable=False) def __repr__(self): return f"" @@ -44,12 +46,13 @@ class ManholeDomesticChamberClient(db.Model): # AUTO TOTAL USING REGEX def calculate_mh_dc_total(mapper, connection, target): - total = 0 + total = Decimal("0.00") for column in target.__table__.columns: if RegularExpression.D_RANGE_PATTERN.match(column.name): - total += getattr(target, column.name) or 0 - target.Total = total - + value = getattr(target, column.name) + if value is not None: + total += Decimal(value) + target.Total = total.quantize(Decimal("0.01")) event.listen(ManholeDomesticChamberClient, "before_insert", calculate_mh_dc_total) event.listen(ManholeDomesticChamberClient, "before_update", calculate_mh_dc_total) \ No newline at end of file diff --git a/app/models/mh_ex_client_model.py b/app/models/mh_ex_client_model.py index d7707c7..271d653 100644 --- a/app/models/mh_ex_client_model.py +++ b/app/models/mh_ex_client_model.py @@ -2,6 +2,7 @@ from app import db from datetime import datetime from sqlalchemy import event from app.utils.regex_utils import RegularExpression +from decimal import Decimal class ManholeExcavationClient(db.Model): __tablename__ = "mh_ex_client" @@ -9,70 +10,70 @@ class ManholeExcavationClient(db.Model): id = db.Column(db.Integer, primary_key=True) # Basic Fields - RA_Bill_No=db.Column(db.String(500)) + RA_Bill_No = db.Column(db.String(500)) Location = db.Column(db.String(500)) MH_NO = db.Column(db.String(100)) - Ground_Level = db.Column(db.Float, default=0) - MH_Invert_Level = db.Column(db.Float, default=0) - MH_Top_Level = db.Column(db.Float, default=0) - Ex_Level = db.Column(db.Float, default=0) - Cutting_Depth = db.Column(db.Float, default=0) - MH_Depth = db.Column(db.Float, default=0) - ID_of_MH_m = db.Column(db.Float, default=0) + Ground_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + MH_Invert_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + MH_Top_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Ex_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Cutting_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + MH_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Dia_of_MH_Cutting = db.Column(db.Float, default=0) - Area_of_Manhole = db.Column(db.Float, default=0) + Dia_of_MH_Cutting = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Area_of_Manhole = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) # Excavation categories - Marshi_Muddy_Slushy_0_to_1_5 = db.Column(db.Float, default=0) - Marshi_Muddy_Slushy_1_5_to_3_0 = db.Column(db.Float, default=0) - Marshi_Muddy_Slushy_3_0_to_4_5 = db.Column(db.Float, default=0) + Marshi_Muddy_Slushy_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Marshi_Muddy_Slushy_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Marshi_Muddy_Slushy_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Soft_Murum_0_to_1_5 = db.Column(db.Float, default=0) - Soft_Murum_1_5_to_3_0 = db.Column(db.Float, default=0) - Soft_Murum_3_0_to_4_5 = db.Column(db.Float, default=0) + Soft_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Hard_Murum_0_to_1_5 = db.Column(db.Float, default=0) - Hard_Murum_1_5_to_3_0 = db.Column(db.Float, default=0) - Hard_Murum_3_0_to_4_5 = db.Column(db.Float, default=0) + Hard_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Soft_Rock_0_to_1_5 = db.Column(db.Float, default=0) - Soft_Rock_1_5_to_3_0 = db.Column(db.Float, default=0) - Soft_Murum_3_0_to_4_5 = db.Column(db.Float, default=0) + Soft_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Hard_Rock_0_to_1_5 = db.Column(db.Float, default=0) - Hard_Rock_1_5_to_3_0 = db.Column(db.Float, default=0) - Hard_Rock_3_0_to_4_5 = db.Column(db.Float, default=0) - Hard_Rock_4_5_to_6_0 = db.Column(db.Float, default=0) - Hard_Rock_6_0_to_7_5 = db.Column(db.Float, default=0) + Hard_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_4_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) # Totals - Marshi_Muddy_Slushy_0_to_1_5_total = db.Column(db.Float, default=0) - Marshi_Muddy_Slushy_1_5_to_3_0_total = db.Column(db.Float, default=0) - Marshi_Muddy_Slushy_3_0_to_4_5_total = db.Column(db.Float, default=0) + Marshi_Muddy_Slushy_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Marshi_Muddy_Slushy_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Marshi_Muddy_Slushy_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Soft_Murum_0_to_1_5_total = db.Column(db.Float, default=0) - Soft_Murum_1_5_to_3_0_total = db.Column(db.Float, default=0) - Soft_Murum_3_0_to_4_5_total = db.Column(db.Float, default=0) + Soft_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Hard_Murum_0_to_1_5_total = db.Column(db.Float, default=0) - Hard_Murum_1_5_to_3_0_total = db.Column(db.Float, default=0) - Hard_Murum_3_0_to_4_5_total = db.Column(db.Float, default=0) + Hard_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Soft_Rock_0_to_1_5_total = db.Column(db.Float, default=0) - Soft_Rock_1_5_to_3_0_total = db.Column(db.Float, default=0) - Soft_Rock_3_0_to_4_5_total = db.Column(db.Float, default=0) + Soft_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Hard_Rock_0_to_1_5_total = db.Column(db.Float, default=0) - Hard_Rock_1_5_to_3_0_total = db.Column(db.Float, default=0) - Hard_Rock_3_0_to_4_5_total = db.Column(db.Float, default=0) - Hard_Rock_4_5_to_6_0_total = db.Column(db.Float, default=0) - Hard_Rock_6_0_to_7_5_total = db.Column(db.Float, default=0) + Hard_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_4_5_to_6_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Remarks = db.Column(db.String(500)) - Total = db.Column(db.Float, default=0) - - created_at = db.Column(db.DateTime, default=datetime.today) + + Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00")) + Remarks = db.Column(db.String(500), default="Import File") + created_at = db.Column(db.DateTime,default=datetime.now,nullable=False) def __repr__(self): return f"" @@ -81,14 +82,15 @@ class ManholeExcavationClient(db.Model): return {c.name: getattr(self, c.name) for c in self.__table__.columns} -# AUTO TOTAL USING REGEX +# AUTO TOTAL USING REGEX def calculate_Manhole_total(mapper, connection, target): - total = 0 + total = Decimal("0.00") for column in target.__table__.columns: if RegularExpression.STR_TOTAL_PATTERN.match(column.name): - total += getattr(target, column.name) or 0 - target.Total = total - + value = getattr(target, column.name) + if value is not None: + total += Decimal(value) + target.Total = total.quantize(Decimal("0.01")) event.listen(ManholeExcavationClient, "before_insert", calculate_Manhole_total) event.listen(ManholeExcavationClient, "before_update", calculate_Manhole_total) \ No newline at end of file diff --git a/app/logs/app.log b/app/models/rate_model.py similarity index 100% rename from app/logs/app.log rename to app/models/rate_model.py diff --git a/app/models/subcontractor_rate_model.py b/app/models/subcontractor_rate_model.py new file mode 100644 index 0000000..5a8f8fb --- /dev/null +++ b/app/models/subcontractor_rate_model.py @@ -0,0 +1,23 @@ +from app import db +from datetime import datetime + +class SubcontractorRate(db.Model): + __tablename__ = "subcontractor_rates" + + id = db.Column(db.Integer, primary_key=True) + subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False) + subcontractor = db.relationship("Subcontractor", backref="rate_master") + + category = db.Column(db.String(50), nullable=False) + item_code = db.Column(db.String(50), nullable=False) + item_name = db.Column(db.String(200), nullable=False) + unit = db.Column(db.String(20)) + rate = db.Column(db.Numeric(12,2), nullable=False) + effective_from = db.Column(db.Date, nullable=False) + effective_to = db.Column(db.Date) + status = db.Column(db.String(20), default="Active") + created_at = db.Column(db.DateTime, default=datetime.now) + + def __repr__(self): + return f"" + \ No newline at end of file diff --git a/app/models/tr_ex_client_model.py b/app/models/tr_ex_client_model.py index 4f943d0..6cb0346 100644 --- a/app/models/tr_ex_client_model.py +++ b/app/models/tr_ex_client_model.py @@ -2,6 +2,8 @@ from app import db from datetime import datetime from sqlalchemy import event from app.utils.regex_utils import RegularExpression +from decimal import Decimal + class TrenchExcavationClient(db.Model): __tablename__ = "tr_ex_client" @@ -12,72 +14,71 @@ class TrenchExcavationClient(db.Model): RA_Bill_No=db.Column(db.String(500)) Location = db.Column(db.String(500)) MH_NO = db.Column(db.String(100)) - CC_length = db.Column(db.Float, default=0) - Actual_Trench_Length = db.Column(db.Float, default=0) - Ground_Level = db.Column(db.Float, default=0) - Invert_Level = db.Column(db.Float, default=0) - Excavated_level = db.Column(db.Float, default=0) - Cutting_Depth = db.Column(db.Float, default=0) - Avg_Depth = db.Column(db.Float, default=0) - Pipe_Dia_mm = db.Column(db.Float, default=0) + CC_length = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Actual_Trench_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Ground_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Invert_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Excavated_level = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Cutting_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Avg_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Pipe_Dia_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) # width - Width_0_to_1_5_m = db.Column(db.Float, default=0) - Width_1_5_to_3_0_m = db.Column(db.Float, default=0) - Width_3_0_to_4_5_m = db.Column(db.Float, default=0) - Width_4_5_to_6_0_m = db.Column(db.Float, default=0) - Width_6_0_to_7_5_m = db.Column(db.Float, default=0) + Width_0_to_1_5_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Width_1_5_to_3_0_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Width_3_0_to_4_5_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Width_4_5_to_6_0_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Width_6_0_to_7_5_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) # Excavation categories - Marshi_Muddy_Slushy_0_to_1_5 = db.Column(db.Float, default=0) - Marshi_Muddy_Slushy_1_5_to_3_0 = db.Column(db.Float, default=0) - Marshi_Muddy_Slushy_3_0_to_4_5 = db.Column(db.Float, default=0) + Marshi_Muddy_Slushy_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Marshi_Muddy_Slushy_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Marshi_Muddy_Slushy_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Soft_Murum_0_to_1_5 = db.Column(db.Float, default=0) - Soft_Murum_1_5_to_3_0 = db.Column(db.Float, default=0) - Soft_Murum_3_0_to_4_5 = db.Column(db.Float, default=0) + Soft_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Hard_Murum_0_to_1_5 = db.Column(db.Float, default=0) - Hard_Murum_1_5_to_3_0 = db.Column(db.Float, default=0) - Hard_Murum_3_0_to_4_5 = db.Column(db.Float, default=0) + Hard_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Soft_Rock_0_to_1_5 = db.Column(db.Float, default=0) - Soft_Rock_1_5_to_3_0 = db.Column(db.Float, default=0) - Soft_Rock_3_0_to_4_5 = db.Column(db.Float, default=0) + Soft_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Hard_Rock_0_to_1_5 = db.Column(db.Float, default=0) - Hard_Rock_1_5_to_3_0 = db.Column(db.Float, default=0) - Hard_Rock_3_0_to_4_5 = db.Column(db.Float, default=0) - Hard_Rock_4_5_to_6_0 = db.Column(db.Float, default=0) - Hard_Rock_6_0_to_7_5 = db.Column(db.Float, default=0) + Hard_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_4_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) # Totals - Marshi_Muddy_Slushy_0_to_1_5_total = db.Column(db.Float, default=0) - Marshi_Muddy_Slushy_1_5_to_3_0_total = db.Column(db.Float, default=0) - Marshi_Muddy_Slushy_3_0_to_4_5_total = db.Column(db.Float, default=0) + Marshi_Muddy_Slushy_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Marshi_Muddy_Slushy_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Marshi_Muddy_Slushy_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Soft_Murum_0_to_1_5_total = db.Column(db.Float, default=0) - Soft_Murum_1_5_to_3_0_total = db.Column(db.Float, default=0) - Soft_Murum_3_0_to_4_5_total = db.Column(db.Float, default=0) + Soft_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Hard_Murum_0_to_1_5_total = db.Column(db.Float, default=0) - Hard_Murum_1_5_to_3_0_total = db.Column(db.Float, default=0) - Hard_Murum_3_0_to_4_5_total = db.Column(db.Float, default=0) + Hard_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Soft_Rock_0_to_1_5_total = db.Column(db.Float, default=0) - Soft_Rock_1_5_to_3_0_total = db.Column(db.Float, default=0) - Soft_Rock_3_0_to_4_5_total = db.Column(db.Float, default=0) + Soft_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Hard_Rock_0_to_1_5_total = db.Column(db.Float, default=0) - Hard_Rock_1_5_to_3_0_total = db.Column(db.Float, default=0) - Hard_Rock_3_0_to_4_5_total = db.Column(db.Float, default=0) - Hard_Rock_4_5_to_6_0_total = db.Column(db.Float, default=0) - Hard_Rock_6_0_to_7_5_total = db.Column(db.Float, default=0) + Hard_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_4_5_to_6_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Total = db.Column(db.Float, default=0) - Remarks = db.Column(db.String(500)) - - created_at = db.Column(db.DateTime, default=datetime.today) + Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00")) + Remarks = db.Column(db.String(500), default="Import File") + created_at = db.Column(db.DateTime,default=datetime.now,nullable=False) def __repr__(self): return f"" @@ -88,11 +89,13 @@ class TrenchExcavationClient(db.Model): # AUTO TOTAL USING REGEX def calculate_trench_client_total(mapper, connection, target): - total = 0 + total = Decimal("0.00") for column in target.__table__.columns: if RegularExpression.STR_TOTAL_PATTERN.match(column.name): - total += getattr(target, column.name) or 0 - target.Total = total + value = getattr(target, column.name) + if value is not None: + total += Decimal(value) + target.Total = total.quantize(Decimal("0.01")) event.listen(TrenchExcavationClient, "before_insert", calculate_trench_client_total) event.listen(TrenchExcavationClient, "before_update", calculate_trench_client_total) \ No newline at end of file diff --git a/app/models/trench_excavation_model.py b/app/models/trench_excavation_model.py index b3959ef..5b7f198 100644 --- a/app/models/trench_excavation_model.py +++ b/app/models/trench_excavation_model.py @@ -2,6 +2,7 @@ from app import db from datetime import datetime from sqlalchemy import event from app.utils.regex_utils import RegularExpression +from decimal import Decimal class TrenchExcavation(db.Model): __tablename__ = "trench_excavation" @@ -13,65 +14,64 @@ class TrenchExcavation(db.Model): subcontractor = db.relationship("Subcontractor", backref="trench_records") # Basic Fields + RA_Bill_No=db.Column(db.String(500)) Location = db.Column(db.String(500)) MH_NO = db.Column(db.String(100)) - CC_length = db.Column(db.Float, default=0) - Invert_Level = db.Column(db.Float, default=0) - MH_Top_Level = db.Column(db.Float, default=0) - Ground_Level = db.Column(db.Float, default=0) - ID_of_MH_m = db.Column(db.Float, default=0) - Actual_Trench_Length = db.Column(db.Float, default=0) - Pipe_Dia_mm = db.Column(db.Float, default=0) + CC_length = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Invert_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + MH_Top_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Ground_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Actual_Trench_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Pipe_Dia_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) # width - Width_0_to_2_5 = db.Column(db.Float, default=0) - Width_2_5_to_3_0 = db.Column(db.Float, default=0) - Width_3_0_to_4_5 = db.Column(db.Float, default=0) - Width_4_5_to_6_0 = db.Column(db.Float, default=0) + Width_0_to_2_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Width_2_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Width_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Width_4_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Upto_IL_Depth = db.Column(db.Float, default=0) - Cutting_Depth = db.Column(db.Float, default=0) - Avg_Depth = db.Column(db.Float, default=0) + Upto_IL_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Cutting_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Avg_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) # Excavation categories - Soft_Murum_0_to_1_5 = db.Column(db.Float, default=0) - Soft_Murum_1_5_to_3_0 = db.Column(db.Float, default=0) - Soft_Murum_3_0_to_4_5 = db.Column(db.Float, default=0) + Soft_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Hard_Murum_0_to_1_5 = db.Column(db.Float, default=0) - Hard_Murum_1_5_to_3_0 = db.Column(db.Float, default=0) + Hard_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Soft_Rock_0_to_1_5 = db.Column(db.Float, default=0) - Soft_Rock_1_5_to_3_0 = db.Column(db.Float, default=0) + Soft_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Hard_Rock_0_to_1_5 = db.Column(db.Float, default=0) - Hard_Rock_1_5_to_3_0 = db.Column(db.Float, default=0) - Hard_Rock_3_0_to_4_5 = db.Column(db.Float, default=0) - Hard_Rock_4_5_to_6_0 = db.Column(db.Float, default=0) - Hard_Rock_6_0_to_7_5 = db.Column(db.Float, default=0) + Hard_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_4_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) # Totals - Soft_Murum_0_to_1_5_total = db.Column(db.Float, default=0) - Soft_Murum_1_5_to_3_0_total = db.Column(db.Float, default=0) - Soft_Murum_3_0_to_4_5_total = db.Column(db.Float, default=0) + Soft_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Hard_Murum_0_to_1_5_total = db.Column(db.Float, default=0) - Hard_Murum_1_5_and_above_total = db.Column(db.Float, default=0) + Hard_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Murum_1_5_and_above_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Soft_Rock_0_to_1_5_total = db.Column(db.Float, default=0) - Soft_Rock_1_5_and_above_total = db.Column(db.Float, default=0) + Soft_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Soft_Rock_1_5_and_above_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Hard_Rock_0_to_1_5_total = db.Column(db.Float, default=0) - Hard_Rock_1_5_to_3_0_total = db.Column(db.Float, default=0) - Hard_Rock_3_0_to_4_5_total = db.Column(db.Float, default=0) - Hard_Rock_4_5_to_6_0_total = db.Column(db.Float, default=0) - Hard_Rock_6_0_to_7_5_total = db.Column(db.Float, default=0) + Hard_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_4_5_to_6_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) + Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00")) - Total = db.Column(db.Float, default=0) - Remarks = db.Column(db.String(500)) - RA_Bill_No=db.Column(db.String(500)) - - created_at = db.Column(db.DateTime, default=datetime.today) + Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00")) + Remarks = db.Column(db.String(500), default="Import File") + created_at = db.Column(db.DateTime,default=datetime.now,nullable=False) def __repr__(self): @@ -83,12 +83,13 @@ class TrenchExcavation(db.Model): # AUTO TOTAL USING REGEX def calculate_trench_total(mapper, connection, target): - total = 0 + total = Decimal("0.00") for column in target.__table__.columns: if RegularExpression.STR_TOTAL_PATTERN.match(column.name): - total += getattr(target, column.name) or 0 - target.Total = total - + value = getattr(target, column.name) + if value is not None: + total += Decimal(value) + target.Total = total.quantize(Decimal("0.01")) event.listen(TrenchExcavation, "before_insert", calculate_trench_total) event.listen(TrenchExcavation, "before_update", calculate_trench_total) \ No newline at end of file diff --git a/app/models/width_model.py b/app/models/width_model.py new file mode 100644 index 0000000..6eb1533 --- /dev/null +++ b/app/models/width_model.py @@ -0,0 +1,21 @@ +from app import db + +class Width(db.Model): + __tablename__ = "width" + + id = db.Column(db.Integer, primary_key=True) + Dia_mm = db.Column(db.Numeric(10, 2), default=0) + + pipe_150_mm = db.Column(db.Numeric(10, 2), default=0) + pipe_200_mm = db.Column(db.Numeric(10, 2), default=0) + pipe_250_mm = db.Column(db.Numeric(10, 2), default=0) + pipe_300_mm = db.Column(db.Numeric(10, 2), default=0) + pipe_350_mm = db.Column(db.Numeric(10, 2), default=0) + pipe_400_mm = db.Column(db.Numeric(10, 2), default=0) + pipe_450_mm = db.Column(db.Numeric(10, 2), default=0) + pipe_500_mm = db.Column(db.Numeric(10, 2), default=0) + pipe_600_mm = db.Column(db.Numeric(10, 2), default=0) + pipe_700_mm = db.Column(db.Numeric(10, 2), default=0) + pipe_900_mm = db.Column(db.Numeric(10, 2), default=0) + pipe_1200_mm = db.Column(db.Numeric(10, 2), default=0) + diff --git a/app/routes/activity_routes.py b/app/routes/activity_routes.py new file mode 100644 index 0000000..afd1906 --- /dev/null +++ b/app/routes/activity_routes.py @@ -0,0 +1,59 @@ +from flask import Blueprint, render_template, request, send_file, abort +import os + +from app.utils.helpers import login_required +from app.utils.file_utils import get_logs_folder , ALLOWED_LOG_FILE +from app.services.activity_service import ActivityService + +activity_bp = Blueprint("activity", __name__, url_prefix="/activity") + +# call activity_log page +@activity_bp.route("/") +@login_required +def activity(): + file_name = request.args.get("file", "app.log") + search = request.args.get("search", "") + level = request.args.get("level", "").upper() + user = request.args.get("user", "") + from_date = request.args.get("from_date", "") + to_date = request.args.get("to_date", "") + + logs = ActivityService.read_logs( + file_name=file_name, + search=search, + level=level, + user=user, + from_date=from_date, + to_date=to_date + ) + + return render_template( + "activity/activity_log.html", + logs=logs, + file_name=file_name, + search=search, + level=level, + user=user, + from_date=from_date, + to_date=to_date + ) + +# Download activity_log files +@activity_bp.route("/logs") +@login_required +def download_log(): + + filename = request.args.get("file", "app.log") + if filename not in ALLOWED_LOG_FILE: + abort(400, "Invalid log file.") + + file_path = os.path.join(get_logs_folder(), filename) + if not os.path.isfile(file_path): + abort(404, "Log file not found.") + + return send_file( + file_path, + as_attachment=True, + download_name=filename, + mimetype="text/plain" + ) \ No newline at end of file diff --git a/app/routes/auth.py b/app/routes/auth.py index 9916348..88d6855 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -1,48 +1,98 @@ -from flask import Blueprint, render_template, request, redirect, url_for, flash, session +from flask import (Blueprint, render_template, request, redirect, url_for, flash, session, current_app) + from app.services.user_service import UserService +from app.constants.messages import SuccessMessage, ErrorMessage +from app.constants.http_status import HTTPStatus auth_bp = Blueprint("auth", __name__) + +# ========================== +# LOGIN +# ========================== @auth_bp.route("/login", methods=["GET", "POST"]) def login(): + if session.get("user_id"): + current_app.logger.info("User already logged in.") 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")) + try: + email = request.form.get("email", "").strip() + password = request.form.get("password", "") - flash("Invalid email or password", "danger") + if not email or not password: + flash(ErrorMessage.INVALID_REQUEST, "danger") + current_app.logger.warning("Login failed. Email or password missing.") + return render_template("login.html", title="Login") + + user = UserService.validate_login(email, password) + + if user: + session.clear() + session["user_id"] = user.id + session["user_name"] = user.name + session["email"] = user.email + session.permanent = True + + current_app.logger.info(f"Login successful. User={user.name}") + flash(SuccessMessage.LOGIN, "success") + return redirect(url_for("dashboard.dashboard")) + + current_app.logger.warning(f"Invalid login attempt. Email={email}") + flash(ErrorMessage.LOGIN_FAILED,"danger") + + except Exception as e: + current_app.logger.exception("Login Error" ) + flash(ErrorMessage.INTERNAL_SERVER_ERROR,"danger") return render_template("login.html", title="Login") +# ========================== +# LOGOUT +# ========================== @auth_bp.route("/logout") def logout(): + username = session.get("user_name", "Unknown") session.clear() - flash("Logged out successfully", "info") + current_app.logger.info(f"Logout successful. User={username}") + flash(SuccessMessage.LOGOUT,"info") + return redirect(url_for("auth.login")) + +# ========================== +# REGISTER +# ========================== @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") + try: + name = request.form.get("name", "").strip() + email = request.form.get("email", "").strip() + 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")) + if not name or not email or not password: + flash(ErrorMessage.INVALID_REQUEST,"danger") + return redirect(url_for("auth.register")) - flash("User registered successfully", "success") - return redirect(url_for("auth.login")) + user = UserService.register_user(name, email, password) - return render_template("register.html", title="Register") + if not user: + current_app.logger.warning(f"Duplicate Registration: {email}") + flash(ErrorMessage.DUPLICATE_ENTRY,"danger") + return redirect(url_for("auth.register")) + + current_app.logger.info(f"New user registered: {email}") + flash(SuccessMessage.CREATED,"success") + return redirect(url_for("auth.login")) + + except Exception: + current_app.logger.exception("User Registration Failed" ) + flash(ErrorMessage.INTERNAL_SERVER_ERROR,"danger") + + return render_template("register.html",title="Register") \ No newline at end of file diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py index af6f6d7..20953f1 100644 --- a/app/routes/dashboard.py +++ b/app/routes/dashboard.py @@ -1,3 +1,4 @@ + import matplotlib matplotlib.use("Agg") @@ -6,14 +7,25 @@ import matplotlib.pyplot as plt import io import base64 from app.utils.plot_utils import plot_to_base64 +from app.utils.helpers import login_required from app.services.dashboard_service import DashboardService from sqlalchemy import func from app import db +from sqlalchemy import tuple_ + +# Subcontractor models import +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.subcontractor_model import Subcontractor +# client models import +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 + dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/dashboard") @@ -26,6 +38,7 @@ def dashboard(): @dashboard_bp.route("/api/live-stats") +@login_required def live_stats(): try: # 1. Overall Volume @@ -61,9 +74,9 @@ def live_stats(): - # subcontractor dashboard @dashboard_bp.route("/subcontractor_dashboard") +@login_required def subcontractor_dashboard(): if not session.get("user_id"): @@ -76,39 +89,526 @@ def subcontractor_dashboard(): subcontractors=subcontractors ) - -# API FOR CHART DATA -@dashboard_bp.route("/api/subcontractor-chart") -def subcontractor_chart(): +# API: Get Unique RA Bills +@dashboard_bp.route("/api/get-ra-bills") +@login_required +def get_ra_bills(): subcontractor_id = request.args.get("subcontractor") category = request.args.get("category") - ra_bill = request.args.get("ra_bill") - query = db.session.query( - TrenchExcavation.excavation_category, - func.sum(TrenchExcavation.Total) - ) + if not subcontractor_id or not category: + return {"ra_bills": []} - if subcontractor_id: - query = query.filter(TrenchExcavation.subcontractor_id == subcontractor_id) + match category: - if category: - query = query.filter(TrenchExcavation.subcontractor_id == subcontractor_id) + case "trench_excavation": + results = db.session.query( + TrenchExcavation.RA_Bill_No + ).filter( + TrenchExcavation.subcontractor_id == subcontractor_id + ).distinct().order_by(TrenchExcavation.RA_Bill_No).all() + # (Add others same pattern later) + case "manhole_excavation": + results = db.session.query( + ManholeExcavation.RA_Bill_No + ).filter( + ManholeExcavation.subcontractor_id == subcontractor_id + ).distinct().order_by(ManholeExcavation.RA_Bill_No).all() + + case "Manhole_Domestic_Chamber": + results = db.session.query( + ManholeDomesticChamber.RA_Bill_No + ).filter( + ManholeDomesticChamber.subcontractor_id == subcontractor_id + ).distinct().order_by(ManholeDomesticChamber.RA_Bill_No).all() + + case "Laying": + results = db.session.query( + Laying.RA_Bill_No + ).filter( + Laying.subcontractor_id == subcontractor_id + ).distinct().order_by(Laying.RA_Bill_No).all() + + ra_bills = [r[0] for r in results if r[0]] + + return {"ra_bills": ra_bills} + + +def total(records, field): + return float(sum(getattr(r, field) or 0 for r in records)) + +# category= trench_excavation +@dashboard_bp.route("/api/tr-analysis") +def trench_analysis(): + + subcontractor_id = request.args.get("subcontractor", "").strip() + ra_bill = request.args.get("ra_bill", "").strip() + + # Convert "1,2,3" -> ["1", "2", "3"] + ra_bill_list = [] if ra_bill: - query = query.filter(TrenchExcavation.RA_Bill_No == ra_bill) + ra_bill_list = [x.strip() for x in ra_bill.split(",") if x.strip()] - results = query.group_by(TrenchExcavation.excavation_category).all() + # Subcontractor Query + sub_query = TrenchExcavation.query + if subcontractor_id: + sub_query = sub_query.filter( + TrenchExcavation.subcontractor_id == int(subcontractor_id) + ) - labels = [] - values = [] + if ra_bill_list: + sub_query = sub_query.filter( + TrenchExcavation.RA_Bill_No.in_(ra_bill_list) + ) - for r in results: - labels.append(r[0]) - values.append(float(r[1] or 0)) + sub_records = sub_query.all() + + sub_keys = [ + ( + (r.MH_NO or "").strip().upper(), + (r.Location or "").strip().upper() + ) + for r in sub_records + ] + + client_query = TrenchExcavationClient.query + + if sub_keys: + + client_query = client_query.filter( + tuple_( + func.upper(func.trim(TrenchExcavationClient.MH_NO)), + func.upper(func.trim(TrenchExcavationClient.Location)) + ).in_(sub_keys) + + ) + + client_records = client_query.all() + + chart_data = [ + { + "label": "Marshi 0 to 1.5", + "client": total(client_records, "Marshi_Muddy_Slushy_0_to_1_5_total"), + "sub": 0 + }, + { + "label": "Marshi 1.5 to 3.0", + "client": total(client_records, "Marshi_Muddy_Slushy_1_5_to_3_0_total"), + "sub": 0 + }, + { + "label": "Marshi 3.0 to 4.5", + "client": total(client_records, "Marshi_Muddy_Slushy_3_0_to_4_5_total"), + "sub": 0 + }, + + { + "label": "Soft Murum 0 to 1.5", + "client": total(client_records, "Soft_Murum_0_to_1_5_total"), + "sub": total(sub_records, "Soft_Murum_0_to_1_5_total") + }, + { + "label": "Soft Murum 1.5 to 3.0", + "client": total(client_records, "Soft_Murum_1_5_to_3_0_total"), + "sub": total(sub_records, "Soft_Murum_1_5_to_3_0_total") + }, + { + "label": "Soft Murum 3.0 to 4.5", + "client": total(client_records, "Soft_Murum_3_0_to_4_5_total"), + "sub": total(sub_records, "Soft_Murum_3_0_to_4_5_total") + }, + + { + "label": "Hard Murum 0 to 1.5", + "client": total(client_records, "Hard_Murum_0_to_1_5_total"), + "sub": total(sub_records, "Hard_Murum_0_to_1_5_total") + }, + { + "label": "Hard Murum 1.5 to 3.0", + "client": total(client_records, "Hard_Murum_1_5_to_3_0_total"), + "sub": total(sub_records, "Hard_Murum_1_5_and_above_total") + }, + + { + "label": "Soft Rock 0 to 1.5", + "client": total(client_records, "Soft_Rock_0_to_1_5_total"), + "sub": total(sub_records, "Soft_Rock_0_to_1_5_total") + }, + { + "label": "Soft Rock 1.5 to 3.0", + "client": total(client_records, "Soft_Rock_1_5_to_3_0_total"), + "sub": total(sub_records, "Soft_Rock_1_5_and_above_total") + }, + + { + "label": "Hard Rock 0 to 1.5", + "client": total(client_records, "Hard_Rock_0_to_1_5_total"), + "sub": total(sub_records, "Hard_Rock_0_to_1_5_total") + }, + { + "label": "Hard Rock 1.5 to 3.0", + "client": total(client_records, "Hard_Rock_1_5_to_3_0_total"), + "sub": total(sub_records, "Hard_Rock_1_5_to_3_0_total") + }, + { + "label": "Hard Rock 3.0 to 4.5", + "client": total(client_records, "Hard_Rock_3_0_to_4_5_total"), + "sub": total(sub_records, "Hard_Rock_3_0_to_4_5_total") + }, + { + "label": "Hard Rock 4.5 to 6.0", + "client": total(client_records, "Hard_Rock_4_5_to_6_0_total"), + "sub": total(sub_records, "Hard_Rock_4_5_to_6_0_total") + }, + { + "label": "Hard Rock 6.0 to 7.5", + "client": total(client_records, "Hard_Rock_6_0_to_7_5_total"), + "sub": total(sub_records, "Hard_Rock_6_0_to_7_5_total") + } + ] + + return jsonify({ + "title": "Trench Excavation Comparison", + "y_title": "Excavation Qty (Cum)", + "labels": [x["label"] for x in chart_data], + "client_qty": [x["client"] for x in chart_data], + "sub_qty": [x["sub"] for x in chart_data] + }) + +# category = manhole_excavation +@dashboard_bp.route("/api/mh-analysis") +def manhole_analysis(): + + subcontractor_id = request.args.get("subcontractor", "").strip() + ra_bill = request.args.get("ra_bill", "").strip() + + # Convert "1,2,3" -> ["1", "2", "3"] + ra_bill_list = [] + if ra_bill: + ra_bill_list = [x.strip() for x in ra_bill.split(",") if x.strip()] + + # Subcontractor Query + sub_query = ManholeExcavation.query + if subcontractor_id: + sub_query = sub_query.filter( + ManholeExcavation.subcontractor_id == int(subcontractor_id) + ) + + if ra_bill_list: + sub_query = sub_query.filter( + ManholeExcavation.RA_Bill_No.in_(ra_bill_list) + ) + + sub_records = sub_query.all() + + sub_keys = [ + ( + (r.MH_NO or "").strip().upper(), + (r.Location or "").strip().upper() + ) + for r in sub_records + ] + + client_query = ManholeExcavationClient.query + + if sub_keys: + + client_query = client_query.filter( + tuple_( + func.upper(func.trim(ManholeExcavationClient.MH_NO)), + func.upper(func.trim(ManholeExcavationClient.Location)) + ).in_(sub_keys) + + ) + + client_records = client_query.all() + + chart_data = [ + { + "label": "Marshi 0 to 1.5", + "client": total(client_records, "Marshi_Muddy_Slushy_0_to_1_5_total"), + "sub": 0 + }, + { + "label": "Marshi 1.5 to 3.0", + "client": total(client_records, "Marshi_Muddy_Slushy_1_5_to_3_0_total"), + "sub": 0 + }, + { + "label": "Marshi 3.0 to 4.5", + "client": total(client_records, "Marshi_Muddy_Slushy_3_0_to_4_5_total"), + "sub": 0 + }, + + { + "label": "Soft Murum 0 to 1.5", + "client": total(client_records, "Soft_Murum_0_to_1_5_total"), + "sub": total(sub_records, "Soft_Murum_0_to_1_5_total") + }, + { + "label": "Soft Murum 1.5 to 3.0", + "client": total(client_records, "Soft_Murum_1_5_to_3_0_total"), + "sub": total(sub_records, "Soft_Murum_1_5_to_3_0_total") + }, + { + "label": "Soft Murum 3.0 to 4.5", + "client": total(client_records, "Soft_Murum_3_0_to_4_5_total"), + "sub": total(sub_records, "Soft_Murum_3_0_to_4_5_total") + }, + + { + "label": "Hard Murum 0 to 1.5", + "client": total(client_records, "Hard_Murum_0_to_1_5_total"), + "sub": total(sub_records, "Hard_Murum_0_to_1_5_total") + }, + { + "label": "Hard Murum 1.5 to 3.0", + "client": total(client_records, "Hard_Murum_1_5_to_3_0_total"), + "sub": total(sub_records, "Hard_Murum_1_5_and_above_total") + }, + + { + "label": "Soft Rock 0 to 1.5", + "client": total(client_records, "Soft_Rock_0_to_1_5_total"), + "sub": total(sub_records, "Soft_Rock_0_to_1_5_total") + }, + { + "label": "Soft Rock 1.5 to 3.0", + "client": total(client_records, "Soft_Rock_1_5_to_3_0_total"), + "sub": total(sub_records, "Soft_Rock_1_5_and_above_total") + }, + + { + "label": "Hard Rock 0 to 1.5", + "client": total(client_records, "Hard_Rock_0_to_1_5_total"), + "sub": total(sub_records, "Hard_Rock_0_to_1_5_total") + }, + { + "label": "Hard Rock 1.5 to 3.0", + "client": total(client_records, "Hard_Rock_1_5_to_3_0_total"), + "sub": total(sub_records, "Hard_Rock_1_5_to_3_0_total") + }, + { + "label": "Hard Rock 3.0 to 4.5", + "client": total(client_records, "Hard_Rock_3_0_to_4_5_total"), + "sub": total(sub_records, "Hard_Rock_3_0_to_4_5_total") + }, + { + "label": "Hard Rock 4.5 to 6.0", + "client": total(client_records, "Hard_Rock_4_5_to_6_0_total"), + "sub": total(sub_records, "Hard_Rock_4_5_to_6_0_total") + }, + { + "label": "Hard Rock 6.0 to 7.5", + "client": total(client_records, "Hard_Rock_6_0_to_7_5_total"), + "sub": total(sub_records, "Hard_Rock_6_0_to_7_5_total") + } + ] return jsonify({ - "labels": labels, - "values": values - }) \ No newline at end of file + "title": "Manhole Excavation Comparison", + "y_title": "Manhole Qty (Nos)", + "labels": [x["label"] for x in chart_data], + "client_qty": [x["client"] for x in chart_data], + "sub_qty": [x["sub"] for x in chart_data] + }) + + + +# category = Manhole_Domestic_Chamber +@dashboard_bp.route("/api/mdc-analysis") +def Manhole_Domestic_Chamber_analysis(): + + subcontractor_id = request.args.get("subcontractor", "").strip() + ra_bill = request.args.get("ra_bill", "").strip() + + # Convert "1,2,3" -> ["1", "2", "3"] + ra_bill_list = [] + if ra_bill: + ra_bill_list = [x.strip() for x in ra_bill.split(",") if x.strip()] + + # Subcontractor Query + sub_query = ManholeDomesticChamber.query + if subcontractor_id: + sub_query = sub_query.filter( + ManholeDomesticChamber.subcontractor_id == int(subcontractor_id) + ) + + if ra_bill_list: + sub_query = sub_query.filter( + ManholeDomesticChamber.RA_Bill_No.in_(ra_bill_list) + ) + + sub_records = sub_query.all() + + sub_keys = [ + ( + (r.MH_NO or "").strip().upper(), + (r.Location or "").strip().upper() + ) + for r in sub_records + ] + + client_query = ManholeDomesticChamberClient.query + + if sub_keys: + + client_query = client_query.filter( + tuple_( + func.upper(func.trim(ManholeDomesticChamberClient.MH_NO)), + func.upper(func.trim(ManholeDomesticChamberClient.Location)) + ).in_(sub_keys) + + ) + + client_records = client_query.all() + + chart_data = [ + { + "label": "Depth of MH", + "client": total(client_records, "Depth_of_MH"), + "sub": total(sub_records, "Depth_of_MH") + }, + { + "label": "Domestic_Chambers Total", + "client": total(client_records, "Total"), + "sub": total(sub_records, "Total") + } + ] + + return jsonify({ + "title": "Domestic Chamber Comparison", + "y_title": "Quantity (Nos)", + "labels": [x["label"] for x in chart_data], + "client_qty": [x["client"] for x in chart_data], + "sub_qty": [x["sub"] for x in chart_data] + }) + + + +# category = Laying +@dashboard_bp.route("/api/laying-analysis") +def laying_analysis(): + + subcontractor_id = request.args.get("subcontractor", "").strip() + ra_bill = request.args.get("ra_bill", "").strip() + + # Convert "1,2,3" -> ["1", "2", "3"] + ra_bill_list = [] + if ra_bill: + ra_bill_list = [x.strip() for x in ra_bill.split(",") if x.strip()] + + # Subcontractor Query + sub_query = Laying.query + if subcontractor_id: + sub_query = sub_query.filter( + Laying.subcontractor_id == int(subcontractor_id) + ) + + if ra_bill_list: + sub_query = sub_query.filter( + Laying.RA_Bill_No.in_(ra_bill_list) + ) + + sub_records = sub_query.all() + + sub_keys = [ + ( + (r.MH_NO or "").strip().upper(), + (r.Location or "").strip().upper() + ) + for r in sub_records + ] + + client_query = LayingClient.query + + if sub_keys: + + client_query = client_query.filter( + tuple_( + func.upper(func.trim(LayingClient.MH_NO)), + func.upper(func.trim(LayingClient.Location)) + ).in_(sub_keys) + + ) + + client_records = client_query.all() + + chart_data = [ + { + "label": "150 mm", + "client": total(client_records, "pipe_150_mm"), + "sub": total(sub_records, "pipe_150_mm") + }, + { + "label": "200 mm", + "client": total(client_records, "pipe_200_mm"), + "sub": total(sub_records, "pipe_200_mm") + }, + { + "label": "250 mm", + "client": total(client_records, "pipe_250_mm"), + "sub": total(sub_records, "pipe_250_mm") + }, + + { + "label": "300 mm", + "client": total(client_records, "pipe_300_mm"), + "sub": total(sub_records, "pipe_300_mm") + }, + { + "label": "350 mm", + "client": total(client_records, "pipe_350_mm"), + "sub": total(sub_records, "pipe_350_mm") + }, + + { + "label": "400 mm", + "client": total(client_records, "pipe_400_mm"), + "sub": total(sub_records, "pipe_400_mm") + }, + { + "label": "450 mm", + "client": total(client_records, "pipe_450_mm"), + "sub": total(sub_records, "pipe_450_mm") + }, + + { + "label": "500 mm", + "client": total(client_records, "pipe_500_mm"), + "sub": total(sub_records, "pipe_500_mm") + }, + { + "label": "600 mm", + "client": total(client_records, "pipe_600_mm"), + "sub": total(sub_records, "pipe_600_mm") + }, + { + "label": "700 mm", + "client": total(client_records, "pipe_700_mm"), + "sub": total(sub_records, "pipe_700_mm") + }, + { + "label": "900 mm", + "client": total(client_records, "pipe_900_mm"), + "sub": total(sub_records, "pipe_900_mm") + }, + { + "label": "1200 mm", + "client": total(client_records, "pipe_1200_mm"), + "sub": total(sub_records, "pipe_1200_mm") + } + ] + + return jsonify({ + "title": "Pipe Laying Comparison", + "y_title": "Pipe Length (Mtr)", + "labels": [x["label"] for x in chart_data], + "client_qty": [x["client"] for x in chart_data], + "sub_qty": [x["sub"] for x in chart_data] + }) + + + \ No newline at end of file diff --git a/app/routes/engineering_master_routes.py b/app/routes/engineering_master_routes.py new file mode 100644 index 0000000..71cb99f --- /dev/null +++ b/app/routes/engineering_master_routes.py @@ -0,0 +1,56 @@ +from flask import ( + Blueprint, + render_template, + request, + redirect, + url_for, + flash +) + +from app.models.subcontractor_model import Subcontractor +from app.services.subcontractor_rate_service import SubcontractorRateService +from app.constants.messages import SuccessMessage, ErrorMessage + +engi_bp = Blueprint( + "engineering", + __name__, + url_prefix="/engi" +) + + + +@engi_bp.route("/") +def engineering_master(): + + return render_template( + "engineering/index.html", + title="Engineering Masters" + ) + + +@engi_bp.route("/subcontractor-rate") +def add_subcontractor_rates(): + subcontractors = Subcontractor.query.filter_by(status="Active").all() + + if request.method == "POST": + try: + SubcontractorRateService.save_rate(request.form) + flash(SuccessMessage.SAVE, "success") + return redirect(url_for("engineering.add_subcontractor_rates")) + except Exception as e: + flash(ErrorMessage.INTERNAL_SERVER_ERROR, "danger") + + return render_template( + "engineering/add_rate.html", + title="Subcontractor Rate Master", + subcontractors=subcontractors + ) + +@engi_bp.route("/client-rate") +def client_rates(): + + return render_template( + "engineering/client_rate.html", + title="Client Rate Master" + ) + diff --git a/app/routes/file_report.py b/app/routes/file_report.py index c16415a..c5bd57c 100644 --- a/app/routes/file_report.py +++ b/app/routes/file_report.py @@ -1,10 +1,12 @@ import pandas as pd import io -from flask import Blueprint, render_template, request, send_file, flash +from flask import Blueprint, render_template, request, send_file, flash, jsonify,redirect, url_for from app.utils.helpers import login_required +from app.utils.regex_utils import RegularExpression +from app import db +import re 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 @@ -15,10 +17,320 @@ 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 +from app.services.abstract_service import AbstractReportService + # --- BLUEPRINT DEFINITION --- file_report_bp = Blueprint("file_report", __name__, url_prefix="/file") + +# ---------------- ACTION COLUMN ---------------- +def add_action_columns(df, model_key): + if df.empty: + return df + + df.insert(0, "Select", df["Id"].apply( + lambda x: f'' + )) + + df["Update"] = df["Id"].apply( + lambda x: f' Edit' + ) + + df["Delete"] = df["Id"].apply( + lambda x: f'' + ) + + return df + + + + + +# ---------------- FETCH ---------------- +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() + # self.df_abstract = pd.DataFrame() # NEW + + def Fetch(self, RA_Bill_No=None, subcontractor_id=None, location=None): + + filters = {} + if subcontractor_id: + filters["subcontractor_id"] = subcontractor_id + if RA_Bill_No: + filters["RA_Bill_No"] = RA_Bill_No + + # Fetch data in database + 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() + + # LOCATION FILTER + if location: + search = location.strip().lower() + print("location::",search) + trench = [ + t for t in trench + if search in (t.Location or "").strip().lower() + ] + + mh = [ + t for t in mh + if search in (t.Location or "").strip().lower() + ] + + dc = [ + t for t in dc + if search in (t.Location or "").strip().lower() + ] + + lay = [ + t for t in lay + if search in (t.Location or "").strip().lower() + ] + + # Set dataframe + 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 = ["11", "_sa_instance_state", "subcontractor_id" , "created_at"] + + 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) + format_column_names(df) + + name = "" + if subcontractor_id: + sc = Subcontractor.query.get(subcontractor_id) + if sc: + name = sc.subcontractor_name + + +# ---------------- DELETE ---------------- +@file_report_bp.route("/delete_records", methods=["POST"]) +@login_required +def delete_records(): + + data = request.json or {} + model = data.get("model") + ids = data.get("ids", []) + + model_map = { + "tr": TrenchExcavation, + "mh": ManholeExcavation, + "dc": ManholeDomesticChamber, + "laying": Laying + } + + ModelClass = model_map.get(model) + + # validate model BEFORE using it + if not ModelClass: + return jsonify({"status": "error", "message": f"Invalid model '{model}'"}), 400 + + if not ids: + return jsonify({"status": "error", "message": "No IDs provided"}), 400 + + try: + for record_id in ids: + obj = ModelClass.query.get(record_id) + if obj: + db.session.delete(obj) + + db.session.commit() + return jsonify({"status": "success"}) + except Exception as e: + db.session.rollback() + return jsonify({"status": "error", "message": str(e)}), 500 + + +@file_report_bp.route("/edit//", methods=["GET", "POST"]) +@login_required +def edit_record(model, record_id): + + model_map = { + "tr": TrenchExcavation, + "mh": ManholeExcavation, + "dc": ManholeDomesticChamber, + "laying": Laying + } + + ModelClass = model_map.get(model) + + if not ModelClass: + flash("Invalid Model.", "danger") + return redirect(url_for("file_report.report_file")) + + record = ModelClass.query.get_or_404(record_id) + + if request.method == "POST": + + # Update all fields except id + for column in record.__table__.columns: + + if column.name == "id": + continue + + if column.name in request.form: + setattr(record, column.name, request.form.get(column.name)) + + try: + db.session.commit() + flash("Record updated successfully.", "success") + # ✅ fixed: correct blueprint name + return redirect(url_for("file_report.report_file")) + + except Exception as e: + db.session.rollback() + flash(str(e), "danger") + + return render_template( + "edit_record.html", + record=record, + model=model + ) + + +@file_report_bp.route("/Subcontractor_report", methods=["GET", "POST"]) +@login_required +def report_file(): + # get all subcontractor data + subcontractors = Subcontractor.query.all() + + tables = None + abstract_html = "" + selected_sc_id = None + ra_bill_no = "" + location = "" + category = "" + + # Search or load data + if request.method == "POST": + # get from data + subcontractor_id = request.form.get("subcontractor_id") + ra_bill_no = request.form.get("ra_bill_no", "").strip() + location = request.form.get("location", "").strip() + category = request.form.get("category", "") + action = request.form.get("action", "preview") + + if not subcontractor_id: + flash("Select Subcontractor", "danger") + return render_template( + "subcontractor_report.html", + subcontractors=subcontractors + ) + + selected_sc_id = subcontractor_id + bill = SubcontractorBill() + + if action == "excel_all": + bill.Fetch(subcontractor_id=subcontractor_id) + else: + bill.Fetch(ra_bill_no,subcontractor_id,location) + + # ----------------------------------------- + # Generate Abstract Report for Web + # ----------------------------------------- + abstract_service = AbstractReportService( + subcontractor_id=subcontractor_id, + ra_bill_no=ra_bill_no + ) + abstract_html = abstract_service.generate_html() + + # ---------------- CATEGORY FILTER ---------------- + if category == "tr": + bill.df_mh = bill.df_dc = bill.df_laying = pd.DataFrame() + elif category == "mh": + bill.df_tr = bill.df_dc = bill.df_laying = pd.DataFrame() + elif category == "dc": + bill.df_tr = bill.df_mh = bill.df_laying = pd.DataFrame() + elif category == "laying": + bill.df_tr = bill.df_mh = bill.df_dc = pd.DataFrame() + + + # =================================================== + # DOWNLOAD EXCEL + # =================================================== + if action in ["excel", "excel_all"]: + output = io.BytesIO() + + with pd.ExcelWriter(output,engine="xlsxwriter") as writer: + workbook = writer.book + abstract = AbstractReportService(subcontractor_id=subcontractor_id,ra_bill_no=ra_bill_no) + abstract.generate(workbook) + + bill.df_tr.to_excel(writer,sheet_name="Tr.Ex",index=False) + bill.df_mh.to_excel(writer,sheet_name="Mh.Ex",index=False) + bill.df_dc.to_excel(writer,sheet_name="MH & DC",index=False) + bill.df_laying.to_excel(writer,sheet_name="Pipe Laying",index=False) + writer.close() + output.seek(0) + + return send_file( + output, + download_name= "subcontractor_Report.xlsx", + as_attachment=True + ) + + # =================================================== + # PDF + # =================================================== + if action == "pdf": + flash( + "PDF Export Coming Soon.", + "info" + ) + + # =================================================== + # ADD ACTIONS + # =================================================== + bill.df_tr = add_action_columns(bill.df_tr, "tr") + bill.df_mh = add_action_columns(bill.df_mh, "mh") + bill.df_dc = add_action_columns(bill.df_dc, "dc") + bill.df_laying = add_action_columns(bill.df_laying, "laying") + + # this are html classes + # table_class = ( "table " "table-bordered" "table-hover " "table-striped " "table-sm " "align-middle " "datatable " "mb-0") + table_class = ( + "table " + "table-bordered " + "table-hover " + "table-striped " + "table-sm " + "align-middle " + "datatable " + "text-nowrap " + "mb-0" + ) + + # This are showing on web tables + tables = { + "tr": bill.df_tr.to_html(classes=table_class, index=False, escape=False), + "mh": bill.df_mh.to_html(classes=table_class, index=False, escape=False), + "dc": bill.df_dc.to_html(classes=table_class, index=False, escape=False ), + "laying": bill.df_laying.to_html(classes=table_class, index=False, escape=False) + } + + return render_template( + "subcontractor_report.html", + subcontractors=subcontractors, + selected_sc_id=selected_sc_id, + selected_ra_bill=ra_bill_no, + selected_location=location, + selected_category=category, + tables=tables, + abstract_html=abstract_html + ) + + # --- Client class --- class ClientBill: def __init__(self): @@ -43,105 +355,8 @@ class ClientBill: 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 (PREVIEW + DOWNLOAD) --- @file_report_bp.route("/client_report", methods=["GET", "POST"]) @login_required @@ -204,6 +419,7 @@ def client_vs_all_subcontractor(): 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) +<<<<<<< HEAD # Convert to HTML for preview @@ -214,4 +430,124 @@ def client_vs_all_subcontractor(): return render_template("client_report.html", tables=tables, ra_val=ra_val) - \ No newline at end of file + +======= + + # -------- DOWNLOAD -------- + 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="Trench") + bill_gen.df_mh.to_excel(writer, index=False, sheet_name="MH") + 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=f"Client_RA_{RA_Bill_No}_Report.xlsx", + as_attachment=True + ) + + # -------- PREVIEW -------- + table_class = "table table-bordered table-striped table-hover table-sm" + + tables["tr"] = bill_gen.df_tr.to_html(classes=table_class, index=False) + tables["mh"] = bill_gen.df_mh.to_html(classes=table_class, index=False) + tables["dc"] = bill_gen.df_dc.to_html(classes=table_class, index=False) + tables["laying"] = bill_gen.df_laying.to_html(classes=table_class, index=False) + + return render_template("client_report.html", tables=tables, ra_val=ra_val) + + +def format_column_names(df): + if df.empty: + return df + + new_columns = [] + + for col in df.columns: + + # ---------------------------------------- + # Pipe columns + # pipe_150_mm -> Pipe 150 MM + # ---------------------------------------- + if RegularExpression.PIPE_MM_PATTERN.match(col): + m = re.match(r"pipe_(\d+)_mm", col) + new_columns.append(f"Pipe {m.group(1)} MM") + continue + + # ---------------------------------------- + # Domestic Chamber + # d_0_to_0_75 -> 0.00 To 0.75 + # d_1_5_to_3_0 -> 1.50 To 3.00 + # ---------------------------------------- + if RegularExpression.D_RANGE_PATTERN.match(col): + + value = col[2:] # remove d_ + + value = re.sub( + r'(\d+)_(\d+)', + lambda m: f"{m.group(1)}.{m.group(2)}", + value + ) + + value = value.replace("_to_", " To ") + + new_columns.append(value) + continue + + # ---------------------------------------- + # Total columns + # Soft_Murum_0_to_1_5_total + # -> + # Soft Murum 0 To 1.5 Total + # ---------------------------------------- + if RegularExpression.STR_TOTAL_PATTERN.match(col): + + value = col[:-6] # remove _total + + value = re.sub( + r'(\d+)_(\d+)', + lambda m: f"{m.group(1)}.{m.group(2)}", + value + ) + + value = value.replace("_to_", " To ") + value = value.replace("_", " ") + + new_columns.append(value.title() + " Total") + continue + + # ---------------------------------------- + # General columns + # ---------------------------------------- + value = col.replace("_", " ").title() + + replacements = { + "Mh No": "MH No", + "Ra Bill No": "RA Bill No", + "Cc Length": "CC Length", + "Id Of Mh M": "ID of MH (m)", + "Pipe Dia Mm": "Pipe Dia (MM)", + "Mh Top Level": "MH Top Level", + "Upto Il Depth": "Upto IL Depth", + "Actual Trench Length": "Actual Trench Length", + "Ground Level": "Ground Level", + "Invert Level": "Invert Level", + "Ex Dia Of Manhole": "External Dia of Manhole", + "Area Of Manhole": "Area of Manhole", + "Depth Of Mh": "Depth of MH", + } + + value = replacements.get(value, value) + + new_columns.append(value) + + df.columns = new_columns + + return df +>>>>>>> pankaj-dev diff --git a/app/routes/generate_comparison_report.py b/app/routes/generate_comparison_report.py index 6624792..18922ba 100644 --- a/app/routes/generate_comparison_report.py +++ b/app/routes/generate_comparison_report.py @@ -2,27 +2,27 @@ from flask import Blueprint, render_template, request, send_file, flash from collections import defaultdict import pandas as pd import io +from app.utils.helpers import login_required +from app.utils.regex_utils import RegularExpression +# Contractor models import 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 +# Client models import 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 -from app.utils.regex_utils import RegularExpression - - + + generate_report_bp = Blueprint("generate_report", __name__, url_prefix="/report") - - # NORMALIZER def normalize_key(value): if value is None: @@ -241,7 +241,7 @@ def comparison_report(): write_sheet(writer, df_dc, "MH & DC", subcontractor.subcontractor_name) write_sheet(writer, df_lay, "Laying", subcontractor.subcontractor_name) - output.seek(0) + output.seek(0) return send_file( output, as_attachment=True, diff --git a/app/routes/user.py b/app/routes/user.py deleted file mode 100644 index afef697..0000000 --- a/app/routes/user.py +++ /dev/null @@ -1,13 +0,0 @@ -from flask import Blueprint, render_template -from app.services.user_service import UserService -from app.utils.helpers import login_required -from flask import current_app - -user_bp = Blueprint("user", __name__, url_prefix="/user") - -@user_bp.route("/list") -@login_required -def list_users(): - current_app.logger.info("User list viewed") - users = UserService.get_all_users() - return render_template("users.html", users=users, title="Users") \ No newline at end of file diff --git a/app/routes/user_routes.py b/app/routes/user_routes.py new file mode 100644 index 0000000..3d3eddc --- /dev/null +++ b/app/routes/user_routes.py @@ -0,0 +1,50 @@ +# from flask import Blueprint, render_template +# from app.services.user_service import UserService +# from app.utils.helpers import login_required +# from flask import current_app + +# user_bp = Blueprint("user", __name__, url_prefix="/user") + +# @user_bp.route("/list") +# @login_required +# def list_users(): +# current_app.logger.info("User list viewed") +# users = UserService.get_all_users() +# return render_template("/user/users.html", users=users, title="Users | List") + + + + +from flask import (Blueprint,render_template,current_app,flash) +from app.services.user_service import UserService +from app.utils.helpers import login_required +from app.constants.messages import SuccessMessage, ErrorMessage +from app.constants.http_status import HTTPStatus + + +user_bp = Blueprint("user", __name__, url_prefix="/user") + + +# ================================================== +# User List +# ================================================== +@user_bp.route("/list", methods=["GET"]) +@login_required +def list_users(): + + try: + + current_app.logger.info("Fetching user list.") + users = UserService.get_all_users() + current_app.logger.info(f"User list loaded successfully. Total Users: {len(users)}") + + return render_template("user/users.html", users=users, title="Users | List") + + except Exception as e: + + current_app.logger.exception("Failed to load user list.") + + flash(ErrorMessage.INTERNAL_SERVER_ERROR,"danger") + + return render_template("user/users.html",users=[],title="Users | List"), HTTPStatus.INTERNAL_SERVER_ERROR + \ No newline at end of file diff --git a/app/services/abstract_service.py b/app/services/abstract_service.py new file mode 100644 index 0000000..62e55d8 --- /dev/null +++ b/app/services/abstract_service.py @@ -0,0 +1,359 @@ +from sqlalchemy import func +from app import db + +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 + + +class AbstractReportService: + + def __init__(self, subcontractor_id=None, ra_bill_no=None): + + self.subcontractor_id = subcontractor_id + self.ra_bill_no = ra_bill_no + + # --------------------------------------------------------- + # FILTER + # --------------------------------------------------------- + def filters(self): + filters = {} + + if self.subcontractor_id: + filters["subcontractor_id"] = self.subcontractor_id + + if self.ra_bill_no: + filters["RA_Bill_No"] = self.ra_bill_no + + return filters + + # --------------------------------------------------------- + # CONTRACTOR + # --------------------------------------------------------- + def contractor_name(self): + + if not self.subcontractor_id: + return "" + + contractor = Subcontractor.query.get(self.subcontractor_id) + + if contractor: + return contractor.subcontractor_name + + return "" + + # --------------------------------------------------------- + # WRITE ABSTRACT SHEET + # --------------------------------------------------------- + def generate(self, workbook): + + worksheet = workbook.add_worksheet("Abstract") + + title = workbook.add_format({ + "bold": True, + "font_size": 16, + "align": "center", + "valign": "vcenter", + "border": 1 + }) + + heading = workbook.add_format({ + "bold": True, + "bg_color": "#D9EAD3", + "border": 1, + "align": "center" + }) + + cell = workbook.add_format({"border": 1}) + number = workbook.add_format({"border": 1,"num_format": "#,##0.00"}) + + # ----------------------------------------------------- + # HEADER + # ----------------------------------------------------- + worksheet.merge_range( + "A1:D1", + "ABSTRACT OF QUANTITY", + title + ) + + worksheet.write("A3", "Contractor", heading) + worksheet.write("B3", self.contractor_name(), cell) + + worksheet.write("C3", "RA Bill", heading) + worksheet.write("D3", self.ra_bill_no or "", cell) + + worksheet.write_row( + "A5", + [ + "Sr", + "Description", + "UOM", + "Qty" + ], + heading + ) + + row = 5 + sr = 1 + + # ----------------------------------------------------- + # TRENCH + # ----------------------------------------------------- + worksheet.write(row, 0, "") + worksheet.write(row, 1, "TRENCH EXCAVATION", heading) + + row += 1 + + for item in self.trench_summary(): + worksheet.write(row, 0, sr, cell) + worksheet.write(row, 1, item["Description"], cell) + worksheet.write(row, 2, item["UOM"], cell) + worksheet.write(row, 3, item["Qty"], number) + + sr += 1 + row += 1 + + # ----------------------------------------------------- + # MANHOLE + # ----------------------------------------------------- + worksheet.write(row, 1, "MANHOLE EXCAVATION", heading) + row += 1 + + for item in self.manhole_summary(): + worksheet.write(row, 0, sr, cell) + worksheet.write(row, 1, item["Description"], cell) + worksheet.write(row, 2, item["UOM"], cell) + worksheet.write(row, 3, item["Qty"], number) + + sr += 1 + row += 1 + + # ----------------------------------------------------- + # DOMESTIC CHAMBER + # ----------------------------------------------------- + worksheet.write(row, 1, "DOMESTIC CHAMBER", heading) + + row += 1 + + for item in self.domestic_summary(): + worksheet.write(row, 0, sr, cell) + worksheet.write(row, 1, item["Description"], cell) + worksheet.write(row, 2, item["UOM"], cell) + worksheet.write(row, 3, item["Qty"], number) + + sr += 1 + row += 1 + + # ----------------------------------------------------- + # PIPE LAYING + # ----------------------------------------------------- + worksheet.write(row, 1, "PIPE LAYING", heading) + row += 1 + + for item in self.laying_summary(): + worksheet.write(row, 0, sr, cell) + worksheet.write(row, 1, item["Description"], cell) + worksheet.write(row, 2, item["UOM"], cell) + worksheet.write(row, 3, item["Qty"], number) + + sr += 1 + row += 1 + + worksheet.set_column("A:A", 8) + worksheet.set_column("B:B", 55) + worksheet.set_column("C:C", 10) + worksheet.set_column("D:D", 18) + + # ============================================================ + # TRENCH + # ============================================================ + def trench_summary(self): + + f = self.filters() + + data = [ + ("Soft Murum 0-1.5 mm","Cum",TrenchExcavation.Soft_Murum_0_to_1_5_total), + ("Soft Murum 1.5-3.0 mm","Cum",TrenchExcavation.Soft_Murum_1_5_to_3_0_total), + ("Soft Murum 3.0-4.5 mm","Cum",TrenchExcavation.Soft_Murum_3_0_to_4_5_total), + + ("Hard Murum 0-1.5 mm","Cum",TrenchExcavation.Hard_Murum_0_to_1_5_total), + ("Hard Murum Above 1.5 mm","Cum",TrenchExcavation.Hard_Murum_1_5_and_above_total), + + ("Soft Rock 0-1.5 mm","Cum",TrenchExcavation.Soft_Rock_0_to_1_5_total), + ("Soft Rock Above 1.5 mm","Cum",TrenchExcavation.Soft_Rock_1_5_and_above_total), + + ("Hard Rock 0-1.5 mm","Cum",TrenchExcavation.Hard_Rock_0_to_1_5_total), + ("Hard Rock 1.5-3.0 mm","Cum",TrenchExcavation.Hard_Rock_1_5_to_3_0_total), + ("Hard Rock 3.0-4.5 mm","Cum",TrenchExcavation.Hard_Rock_3_0_to_4_5_total), + ("Hard Rock 4.5-6.0 mm","Cum",TrenchExcavation.Hard_Rock_4_5_to_6_0_total), + ("Hard Rock 6.0-7.5 mm","Cum",TrenchExcavation.Hard_Rock_6_0_to_7_5_total), + ] + + return self.make_summary(data, TrenchExcavation, f) + + # ============================================================ + # MANHOLE + # ============================================================ + def manhole_summary(self): + + f = self.filters() + + data = [ + + ("Soft Murum 0-1.5 mm","Cum",ManholeExcavation.Soft_Murum_0_to_1_5_total), + ("Soft Murum 1.5-3.0 mm","Cum",ManholeExcavation.Soft_Murum_1_5_to_3_0_total), + ("Soft Murum 3.0-4.5 mm","Cum",ManholeExcavation.Soft_Murum_3_0_to_4_5_total), + + ("Hard Murum 0-1.5 mm","Cum",ManholeExcavation.Hard_Murum_0_to_1_5_total), + ("Hard Murum Above 1.5 mm","Cum",ManholeExcavation.Hard_Murum_1_5_and_above_total), + + ("Soft Rock 0-1.5 mm","Cum",ManholeExcavation.Soft_Rock_0_to_1_5_total), + ("Soft Rock Above 1.5 mm","Cum",ManholeExcavation.Soft_Rock_1_5_and_above_total), + + ("Hard Rock 0-1.5 mm","Cum",ManholeExcavation.Hard_Rock_0_to_1_5_total), + ("Hard Rock 1.5-3.0 mm","Cum",ManholeExcavation.Hard_Rock_1_5_to_3_0_total), + ("Hard Rock 3.0-4.5 mm","Cum",ManholeExcavation.Hard_Rock_3_0_to_4_5_total), + ("Hard Rock 4.5-6.0 mm","Cum",ManholeExcavation.Hard_Rock_4_5_to_6_0_total), + ("Hard Rock 6.0-7.5 mm","Cum",ManholeExcavation.Hard_Rock_6_0_to_7_5_total), + + ] + + return self.make_summary(data, ManholeExcavation, f) + + # ============================================================ + # DOMESTIC CHAMBER + # ============================================================ + def domestic_summary(self): + f = self.filters() + data = [ + ("0-0.75 mm","Nos",ManholeDomesticChamber.d_0_to_0_75), + ("0.76-1.05 mm","Nos",ManholeDomesticChamber.d_0_76_to_1_05), + ("1.06-1.65 mm","Nos",ManholeDomesticChamber.d_1_06_to_1_65), + ("1.66-2.15 mm","Nos",ManholeDomesticChamber.d_1_66_to_2_15), + ("2.16-2.65 mm","Nos",ManholeDomesticChamber.d_2_16_to_2_65), + ("2.66-3.15 mm","Nos",ManholeDomesticChamber.d_2_66_to_3_15), + ("3.16-3.65 mm","Nos",ManholeDomesticChamber.d_3_16_to_3_65), + ("3.66-4.15 mm","Nos",ManholeDomesticChamber.d_3_66_to_4_15), + ("4.16-4.65 mm","Nos",ManholeDomesticChamber.d_4_16_to_4_65), + ("4.66-5.15 mm","Nos",ManholeDomesticChamber.d_4_66_to_5_15), + ("5.16-5.65 mm","Nos",ManholeDomesticChamber.d_5_16_to_5_65), + ("5.66-6.15 mm","Nos",ManholeDomesticChamber.d_5_66_to_6_15), + ("6.16-6.65 mm","Nos",ManholeDomesticChamber.d_6_16_to_6_65), + ("6.66-7.15 mm","Nos",ManholeDomesticChamber.d_6_66_to_7_15), + ("7.16-7.65 mm","Nos",ManholeDomesticChamber.d_7_16_to_7_65), + ("7.66-8.15 mm","Nos",ManholeDomesticChamber.d_7_66_to_8_15), + ("8.16-8.65 mm","Nos",ManholeDomesticChamber.d_8_16_to_8_65), + ("8.66-9.15 mm","Nos",ManholeDomesticChamber.d_8_66_to_9_15), + ("9.16-9.65 mm","Nos",ManholeDomesticChamber.d_9_16_to_9_65), + ] + + return self.make_summary(data, ManholeDomesticChamber, f) + + # ============================================================ + # PIPE LAYING + # ============================================================ + def laying_summary(self): + f = self.filters() + data = [ + ("150 mm Dia","RM",Laying.pipe_150_mm), + ("200 mm Dia","RM",Laying.pipe_200_mm), + ("250 mm Dia","RM",Laying.pipe_250_mm), + ("300 mm Dia","RM",Laying.pipe_300_mm), + ("350 mm Dia","RM",Laying.pipe_350_mm), + ("400 mm Dia","RM",Laying.pipe_400_mm), + ("450 mm Dia","RM",Laying.pipe_450_mm), + ("500 mm Dia","RM",Laying.pipe_500_mm), + ("600 mm Dia","RM",Laying.pipe_600_mm), + ("700 mm Dia","RM",Laying.pipe_700_mm), + ("900 mm Dia","RM",Laying.pipe_900_mm), + ("1200 mm Dia","RM",Laying.pipe_1200_mm), + ] + return self.make_summary(data, Laying, f) + + # ============================================================ + # COMMON SUMMARY + # ============================================================ + def make_summary(self, items, model, filters): + summary = [] + for desc, uom, column in items: + qty = (db.session.query(func.sum(column)).filter_by(**filters).scalar()) + summary.append({ + "Description": desc, + "UOM": uom, + "Qty": float(qty or 0) + }) + return summary + + + def generate_html(self): + + html = """ +
+ + + + + + + + + + + + + + + + + + + + + + + + + """.format( + self.contractor_name(), + self.ra_bill_no or "" + ) + + sr = 1 + sections = [ + ("TRENCH EXCAVATION", self.trench_summary()), + ("MANHOLE EXCAVATION", self.manhole_summary()), + ("DOMESTIC CHAMBER", self.domestic_summary()), + ("PIPE LAYING", self.laying_summary()) + ] + + for title, rows in sections: + html += f""" + + + + """ + + for item in rows: + html += f""" + + + + + + + """ + sr += 1 + + html += """ + +
+ ABSTRACT OF QUANTITY +
Contractor{}RA Bill NO{}
SrDescriptionUOMQty
{title}
{sr}{item['Description']}{item['UOM']} + {item['Qty']:.2f} +
+
+ """ + + return html \ No newline at end of file diff --git a/app/services/activity_service.py b/app/services/activity_service.py new file mode 100644 index 0000000..a44bb4f --- /dev/null +++ b/app/services/activity_service.py @@ -0,0 +1,56 @@ +import re +from pathlib import Path + +# Activity log Service +class ActivityService: + + @staticmethod + def read_logs(file_name="", search="", level="", user="", from_date="", to_date=""): + + file = Path("logs") / file_name + + if not file.exists(): + return [] + + logs = [] + + with open(file, "r", encoding="utf-8") as f: + + for line in reversed(f.readlines()): + # Example: + # 2026-08-01 10:15:55 | INFO | admin | Dashboard | Dashboard Opened + parts = [x.strip() for x in line.split("|")] + + if len(parts) < 5: + continue + + record = { + "date": parts[0], + "level": parts[1], + "user": parts[2], + "module": parts[3], + "message": "|".join(parts[4:]) + } + + # Search Filter + if search and search.lower() not in line.lower(): + continue + + # Level Filter + if level and record["level"] != level: + continue + + # User Filter + if user and user.lower() not in record["user"].lower(): + continue + + # Date Filter + if from_date and record["date"][:10] < from_date: + continue + + if to_date and record["date"][:10] > to_date: + continue + + logs.append(record) + + return logs \ No newline at end of file diff --git a/app/services/comparison_service.py b/app/services/comparison_service.py index e69de29..afb7064 100644 --- a/app/services/comparison_service.py +++ b/app/services/comparison_service.py @@ -0,0 +1,204 @@ +# from collections import defaultdict +# import pandas as pd +# from app.utils.regex_utils import RegularExpression + + +# class ComparisonService: + +# TRENCH_MAPPING = [ +# { +# "label": "Marshi 0 to 1.5", +# "client": "Client_Marshi_Muddy_Slushy_0_to_1_5_total", +# "sub": None +# }, +# { +# "label": "Marshi 1.5 to 3.0", +# "client": "Client_Marshi_Muddy_Slushy_1_5_to_3_0_total", +# "sub": None +# }, +# { +# "label": "Marshi 3.0 to 4.5", +# "client": "Client_Marshi_Muddy_Slushy_3_0_to_4_5_total", +# "sub": None +# }, +# { +# "label": "Soft Murum 0 to 1.5", +# "client": "Client_Soft_Murum_0_to_1_5_total", +# "sub": "Sub_Soft_Murum_0_to_1_5_total" +# }, +# { +# "label": "Soft Murum 1.5 to 3.0", +# "client": "Client_Soft_Murum_1_5_to_3_0_total", +# "sub": "Sub_Soft_Murum_1_5_to_3_0_total" +# }, +# { +# "label": "Soft Murum 3.0 to 4.5", +# "client": "Client_Soft_Murum_3_0_to_4_5_total", +# "sub": "Sub_Soft_Murum_3_0_to_4_5_total" +# }, +# { +# "label": "Hard Murum 0 to 1.5", +# "client": "Client_Hard_Murum_0_to_1_5_total", +# "sub": "Sub_Hard_Murum_0_to_1_5_total" +# }, +# { +# "label": "Hard Murum 1.5+", +# "client": "Client_Hard_Murum_1_5_to_3_0_total", +# "sub": "Sub_Hard_Murum_1_5_and_above_total" +# }, +# { +# "label": "Soft Rock 0 to 1.5", +# "client": "Client_Soft_Rock_0_to_1_5_total", +# "sub": "Sub_Soft_Rock_0_to_1_5_total" +# }, +# { +# "label": "Soft Rock 1.5+", +# "client": "Client_Soft_Rock_1_5_to_3_0_total", +# "sub": "Sub_Soft_Rock_1_5_and_above_total" +# }, +# { +# "label": "Hard Rock 0 to 1.5", +# "client": "Client_Hard_Rock_0_to_1_5_total", +# "sub": "Sub_Hard_Rock_0_to_1_5_total" +# }, +# { +# "label": "Hard Rock 1.5 to 3.0", +# "client": "Client_Hard_Rock_1_5_to_3_0_total", +# "sub": "Sub_Hard_Rock_1_5_to_3_0_total" +# }, +# { +# "label": "Hard Rock 3.0 to 4.5", +# "client": "Client_Hard_Rock_3_0_to_4_5_total", +# "sub": "Sub_Hard_Rock_3_0_to_4_5_total" +# }, +# { +# "label": "Hard Rock 4.5 to 6.0", +# "client": "Client_Hard_Rock_4_5_to_6_0_total", +# "sub": "Sub_Hard_Rock_4_5_to_6_0_total" +# }, +# { +# "label": "Hard Rock 6.0 to 7.5", +# "client": "Client_Hard_Rock_6_0_to_7_5_total", +# "sub": "Sub_Hard_Rock_6_0_to_7_5_total" +# } +# ] + + + +# @staticmethod +# def normalize_key(value): +# if value is None: +# return "" +# return str(value).strip().upper() + +# @classmethod +# def make_lookup(cls, rows, key_field): +# """ +# Create lookup dictionary using: +# (Location, MH_NO) +# """ + +# lookup = defaultdict(list) + +# for row in rows: + +# location = cls.normalize_key(row.get("Location")) +# key = cls.normalize_key(row.get(key_field)) + +# if location and key: +# lookup[(location, key)].append(row) + +# return lookup + +# @classmethod +# def build_comparison(cls, client_rows, subcontractor_rows, key_field="MH_NO"): + +# subcontractor_lookup = cls.make_lookup( +# subcontractor_rows, +# key_field +# ) + +# used = defaultdict(int) + +# output = [] + +# for client in client_rows: + +# location = cls.normalize_key(client.get("Location")) +# key = cls.normalize_key(client.get(key_field)) + +# if not location or not key: +# continue + +# rows = subcontractor_lookup.get((location, key)) + +# if not rows: +# continue + +# index = used[(location, key)] + +# if index >= len(rows): +# continue + +# subcontractor = rows[index] + +# used[(location, key)] += 1 + +# client_total = sum( +# float(v or 0) +# for k, v in client.items() +# if k.endswith("_total") +# or RegularExpression.D_RANGE_PATTERN.match(k) +# or RegularExpression.PIPE_MM_PATTERN.match(k) +# ) + +# subcontractor_total = sum( +# float(v or 0) +# for k, v in subcontractor.items() +# if k.endswith("_total") +# or RegularExpression.D_RANGE_PATTERN.match(k) +# or RegularExpression.PIPE_MM_PATTERN.match(k) +# ) + +# row = { + +# "Location": location, + +# key_field: key, + +# "Client_Total": round(client_total, 2), + +# "Subcontractor_Total": round(subcontractor_total, 2), + +# "Difference": round( +# client_total - subcontractor_total, +# 2 +# ) +# } + +# # Client Columns +# for column, value in client.items(): + +# if column in [ +# "id", +# "created_at" +# ]: +# continue + +# row[f"Client_{column}"] = value + +# # Subcontractor Columns +# for column, value in subcontractor.items(): + +# if column in [ +# "id", +# "created_at", +# "subcontractor_id" +# ]: +# continue + +# row[f"Sub_{column}"] = value + +# output.append(row) + +# return pd.DataFrame(output) \ No newline at end of file diff --git a/app/services/file_service.py b/app/services/file_service.py index 206ec1c..340e07a 100644 --- a/app/services/file_service.py +++ b/app/services/file_service.py @@ -1,10 +1,8 @@ +from app import db import os import pandas as pd from werkzeug.utils import secure_filename -from app.utils.file_utils import ensure_upload_folder - -from app.config import Config -from app import db +from app.utils.file_utils import ensure_upload_folder, get_uploads_folder, ALLOWED_EXTENSIONS # Subcontractor models import from app.models.trench_excavation_model import TrenchExcavation @@ -24,8 +22,9 @@ class FileService: # ---------------- COMMON HELPERS ---------------- def allowed_file(self, filename): - return ("." in filename and filename.rsplit(".", 1)[1].lower() in Config.ALLOWED_EXTENSIONS) - + return ("." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS) + + # data normalizations def normalize(self, val): if val is None or pd.isna(val): return None @@ -35,7 +34,8 @@ class FileService: return None return val.upper() - + + # --------------- Sub-contractor service -------------- # ---------------- SUBCONTRACTOR FILE UPLOAD ---------------- def handle_file_upload(self, file, subcontractor_id, RA_Bill_No): @@ -52,8 +52,9 @@ class FileService: return False, "Invalid file type! Allowed: CSV, XLSX, XLS" ensure_upload_folder() + path = get_uploads_folder() - folder = os.path.join(Config.UPLOAD_FOLDER, f"sub_{subcontractor_id}") + folder = os.path.join(path, f"sub_{subcontractor_id}") os.makedirs(folder, exist_ok=True) filename = secure_filename(file.filename) @@ -75,7 +76,7 @@ class FileService: except Exception as e: db.session.rollback() - return False, f"Import failed: {e}" + return False, f"Import failed: {e}" # ---------------- Trench Excavation (Subcontractor) ---------------- def process_trench_excavation(self, df, subcontractor_id, RA_Bill_No): @@ -309,8 +310,7 @@ class FileService: db.session.commit() - - + # --------------- Client service -------------- # ---------------- CLIENT FILE UPLOAD ---------------- def handle_client_file_upload(self, file, RA_Bill_No): @@ -324,8 +324,9 @@ class FileService: return False, "Invalid file type! Allowed: CSV, XLSX, XLS" ensure_upload_folder() + path = get_uploads_folder() - folder = os.path.join(Config.UPLOAD_FOLDER, f"Client_Bill_{RA_Bill_No}") + folder = os.path.join(path, f"Client_Bill_{RA_Bill_No}") os.makedirs(folder, exist_ok=True) filename = secure_filename(file.filename) diff --git a/app/services/ldap_service.py b/app/services/ldap_service.py new file mode 100644 index 0000000..128bfcf --- /dev/null +++ b/app/services/ldap_service.py @@ -0,0 +1,130 @@ +from ldap3 import ( + Server, + Connection, + ALL, + NTLM, + SIMPLE, + SUBTREE +) + +from flask import current_app +from app.config import Config + + +class LDAPService: + """ + LDAP / Active Directory Authentication Service + """ + + @staticmethod + def authenticate(username, password): + """ + Authenticate LDAP User + + Returns: + { + "success": True, + "user": { + "username": "...", + "name": "...", + "email": "..." + } + } + + OR + + { + "success": False, + "message": "Invalid username or password" + } + """ + + if not username or not password: + return { + "success": False, + "message": "Username and Password are required." + } + + try: + + # ----------------------------------- + # LDAP SERVER + # ----------------------------------- + server = Server( + Config.LDAP_SERVER, + port=Config.LDAP_PORT, + use_ssl=Config.LDAP_USE_SSL, + get_info=ALL + ) + + # ----------------------------------- + # Login Format + # + # username@domain.com + # ----------------------------------- + user_dn = f"{username}@{Config.LDAP_DOMAIN}" + + conn = Connection( + server, + user=user_dn, + password=password, + authentication=SIMPLE, + auto_bind=True + ) + + # ----------------------------------- + # Search User + # ----------------------------------- + search_filter = f"(sAMAccountName={username})" + + conn.search( + search_base=Config.LDAP_SEARCH_BASE, + search_filter=search_filter, + search_scope=SUBTREE, + attributes=[ + "displayName", + "mail", + "givenName", + "sn", + "cn" + ] + ) + + display_name = username + email = "" + + if conn.entries: + + entry = conn.entries[0] + + if "displayName" in entry: + display_name = str(entry.displayName) + + if "mail" in entry: + email = str(entry.mail) + + conn.unbind() + + current_app.logger.info( + f"LDAP Login Success : {username}" + ) + + return { + "success": True, + "user": { + "username": username, + "name": display_name, + "email": email + } + } + + except Exception as ex: + + current_app.logger.warning( + f"LDAP Login Failed : {username} : {str(ex)}" + ) + + return { + "success": False, + "message": "Invalid Username or Password." + } \ No newline at end of file diff --git a/app/services/logger_service.py b/app/services/logger_service.py index 5b7da1b..bb44370 100644 --- a/app/services/logger_service.py +++ b/app/services/logger_service.py @@ -5,75 +5,145 @@ from flask import request, session, has_request_context class RequestContextFilter(logging.Filter): + """Adds request information to every log record.""" def filter(self, record): if has_request_context(): - record.user = session.get("user_email", "Anonymous") - record.ip = request.remote_addr + record.user = session.get("user_name", "Anonymous") + record.ip = request.remote_addr or "Unknown" record.method = request.method record.url = request.url else: record.user = "System" - record.ip = "N/A" - record.method = "N/A" - record.url = "N/A" + record.ip = "-" + record.method = "-" + record.url = "-" return True class LoggerService: - @staticmethod - def init_app(app): + LOG_DIR = "logs" - if not os.path.exists("logs"): - os.makedirs("logs") + APP_LOG = "app.log" + ERROR_LOG = "error.log" + DEBUG_LOG = "debug.log" + + MAX_BYTES = 10 * 1024 * 1024 # 10 MB + BACKUP_COUNT = 10 + + @classmethod + def init_app(cls, app): + + os.makedirs(cls.LOG_DIR, exist_ok=True) formatter = logging.Formatter( - "%(asctime)s | %(levelname)s | " - "User:%(user)s | IP:%(ip)s | " - "Method:%(method)s | URL:%(url)s | " - "%(message)s" + fmt=( + "%(asctime)s | " + "%(levelname)-8s | " + "User=%(user)s | " + "IP=%(ip)s | " + "%(method)s | " + "%(url)s | " + "%(message)s" + ), + datefmt="%Y-%m-%d %H:%M:%S" ) - # INFO LOG - info_handler = RotatingFileHandler( - "logs/app.log", - maxBytes=5 * 1024 * 1024, - backupCount=5 - ) - info_handler.setLevel(logging.INFO) - info_handler.setFormatter(formatter) - - # ERROR LOG - error_handler = RotatingFileHandler( - "logs/error.log", - maxBytes=5 * 1024 * 1024, - backupCount=5 - ) - error_handler.setLevel(logging.ERROR) - error_handler.setFormatter(formatter) - - # CONSOLE - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) - console_handler.setFormatter(formatter) - - # 🔹 ADD FILTER (important) context_filter = RequestContextFilter() - info_handler.addFilter(context_filter) + # Remove default handlers + app.logger.handlers.clear() + + # ========================== + # Application Log + # ========================== + app_handler = RotatingFileHandler( + os.path.join(cls.LOG_DIR, cls.APP_LOG), + maxBytes=cls.MAX_BYTES, + backupCount=cls.BACKUP_COUNT, + encoding="utf-8" + ) + + app_handler.setLevel(logging.INFO) + app_handler.setFormatter(formatter) + app_handler.addFilter(context_filter) + + # ========================== + # Error Log + # ========================== + error_handler = RotatingFileHandler( + os.path.join(cls.LOG_DIR, cls.ERROR_LOG), + maxBytes=cls.MAX_BYTES, + backupCount=cls.BACKUP_COUNT, + encoding="utf-8" + ) + + error_handler.setLevel(logging.ERROR) + error_handler.setFormatter(formatter) error_handler.addFilter(context_filter) + + # ========================== + # Debug Log + # ========================== + debug_handler = RotatingFileHandler( + os.path.join(cls.LOG_DIR, cls.DEBUG_LOG), + maxBytes=cls.MAX_BYTES, + backupCount=cls.BACKUP_COUNT, + encoding="utf-8" + ) + + debug_handler.setLevel(logging.DEBUG) + debug_handler.setFormatter(formatter) + debug_handler.addFilter(context_filter) + + # ========================== + # Console Log + # ========================== + console_handler = logging.StreamHandler() + + console_handler.setLevel(logging.INFO) + console_handler.setFormatter(formatter) console_handler.addFilter(context_filter) + # ========================== + # Logger Configuration + # ========================== app.logger.setLevel(logging.DEBUG) - app.logger.addHandler(info_handler) + app.logger.addHandler(app_handler) app.logger.addHandler(error_handler) + app.logger.addHandler(debug_handler) app.logger.addHandler(console_handler) - # Log every request automatically + # ========================== + # Automatic Request Logging + # ========================== @app.before_request - def log_request(): - app.logger.info("Request Started") \ No newline at end of file + def before_request(): + app.logger.info("Request Started") + + @app.after_request + def after_request(response): + app.logger.info( + f"Request Completed | Status={response.status_code}" + ) + return response + + # ========================== + # Exception Logging + # ========================== + @app.errorhandler(Exception) + def log_exception(error): + + app.logger.exception( + f"Unhandled Exception: {str(error)}" + ) + + raise error + + app.logger.info("=" * 70) + app.logger.info("Application Started Successfully") + app.logger.info("=" * 70) \ No newline at end of file diff --git a/app/services/subcontractor_rate_service.py b/app/services/subcontractor_rate_service.py new file mode 100644 index 0000000..951c378 --- /dev/null +++ b/app/services/subcontractor_rate_service.py @@ -0,0 +1,65 @@ +from app.services.db_service import db +from app.models.subcontractor_rate_model import SubcontractorRate + + +class SubcontractorRateService: + + @staticmethod + def save_rate(form): + + rate = SubcontractorRate( + subcontractor_id=form.get("subcontractor_id"), + category=form.get("category"), + item_code=form.get("item_code"), + item_name=form.get("item_name"), + unit=form.get("unit"), + rate=form.get("rate"), + effective_from=form.get("effective_from"), + effective_to=form.get("effective_to") or None, + status=form.get("status") + ) + + db.session.add(rate) + db.session.commit() + return rate + + + @staticmethod + def get_all_rates(): + + return ( + SubcontractorRate.query + .order_by(SubcontractorRate.created_at.desc()) + .all() + ) + @staticmethod + def get_rate(rate_id): + + return SubcontractorRate.query.get_or_404(rate_id) + + @staticmethod + def update_rate(rate_id, form): + + rate = SubcontractorRate.query.get_or_404(rate_id) + + rate.subcontractor_id = form.get("subcontractor_id") + rate.category = form.get("category") + rate.item_code = form.get("item_code") + rate.item_name = form.get("item_name") + rate.unit = form.get("unit") + rate.rate = form.get("rate") + rate.effective_from = form.get("effective_from") + rate.effective_to = form.get("effective_to") or None + rate.status = form.get("status") + + db.session.commit() + return rate + + @staticmethod + def delete_rate(rate_id): + + rate = SubcontractorRate.query.get_or_404(rate_id) + + db.session.delete(rate) + + db.session.commit() \ No newline at end of file diff --git a/app/services/user_service.py b/app/services/user_service.py index 59f787b..eca3dba 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -1,5 +1,6 @@ from app.models.user_model import User from app.services.db_service import db +from flask import current_app class UserService: @@ -13,6 +14,7 @@ class UserService: db.session.add(user) db.session.commit() + current_app.logger.info("User list viewed") return user @staticmethod diff --git a/app/static/downloads/format/client_format.xlsx b/app/static/downloads/format/client_format.xlsx index 8dc754e..f472b2b 100644 Binary files a/app/static/downloads/format/client_format.xlsx and b/app/static/downloads/format/client_format.xlsx differ diff --git a/app/static/downloads/format/subcontractor_format.xlsx b/app/static/downloads/format/subcontractor_format.xlsx index 6991030..28f94d7 100644 Binary files a/app/static/downloads/format/subcontractor_format.xlsx and b/app/static/downloads/format/subcontractor_format.xlsx differ diff --git a/app/templates/activity/activity_log.html b/app/templates/activity/activity_log.html new file mode 100644 index 0000000..14451c4 --- /dev/null +++ b/app/templates/activity/activity_log.html @@ -0,0 +1,208 @@ +{% extends "base.html" %} +{% block content %} + +
+ + +
+
+
+
+

Activity Logs

+ View application logs, search records and download log files. +
+ + + +
+
+
+ + +
+ +
+
+ Filters +
+
+ +
+ +
+
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ +
+ +
+ + +
+ + + + + +
+ +
+
+ +
+ + +
+ +
+
+
+ + Log Entries +
+ + + {{ logs|length }} Records + +
+
+ + +
+
+ + + + + + + + + + + + + + + + {% for log in logs %} + + + + + + + + + + {% endfor %} + + +
Sr NoDate & TimeUserLevelModuleActivity
{{ loop.index }}{{ log.date }}{{ log.user }} + {% if log.level=="INFO" %} + INFO + {% elif log.level=="WARNING" %} + WARNING + {% else %} + ERROR + {% endif %} + {{ log.module }}{{ log.message }}
+ +
+
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 1472d63..cca715e 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -5,18 +5,23 @@ {{ title if title else "Comparison Software" }} + - + - + + + + + + -