commit 37dd118cbdb8dad44688870732fc9538c4e5bf51 Author: anishd100 Date: Mon Feb 2 14:57:20 2026 +0530 Merge branch 'dev-anish' of http://gitea.lcepl.org/pjpatil12/Comparison_Project into dev-anish diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8a75f36 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +.git +.gitignore +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +*.egg-info/ +dist/ +build/ +.vscode/ +.idea/ +*.log +.env +instance/ +.pytest_cache/ +.coverage diff --git a/.env b/.env new file mode 100644 index 0000000..9c1fd29 --- /dev/null +++ b/.env @@ -0,0 +1,26 @@ +# ----------------------------- +# Flask App Configuration +# ----------------------------- +FLASK_ENV=development +FLASK_DEBUG=True +FLASK_HOST=0.0.0.0 +FLASK_PORT=5001 + +# ----------------------------- +# Security +# ----------------------------- +SECRET_KEY=change-this-to-strong-secret-key + +# ----------------------------- +# Database Configuration +# ----------------------------- +DB_DIALECT=mysql +DB_DRIVER=pymysql +DB_HOST=127.0.0.1 +DB_PORT=3307 +DB_NAME=comparisondb +DB_USER=root +DB_PASSWORD=admin + +# DATABASE_URL=mysql+pymysql://root:root@localhost/comparisondb + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b50bb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Python +app/__pycache__/ +*.pyc +*.pyo +*.pyd +*.__pycache__ + +# Ingnor upload files +app/static/uploads/ + +# Ignore env files +venv + +# Ignore Log files ss +logs/ + +# Ignore db folders +instance/ + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..75cb21a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements and install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create necessary directories +RUN mkdir -p app/logs app/static/uploads app/static/downloads + +# Expose port +EXPOSE 5001 + +# Run the application +CMD ["python", "run.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..6eb3acc --- /dev/null +++ b/README.md @@ -0,0 +1,583 @@ +# Comparison Project - Backend Documentation + +A Flask-based web application for comparing and managing construction project data (excavation and manhole work) between subcontractors and clients. The system imports data from Excel files, stores it in a database, and generates comparison reports. + +## Table of Contents + +- [Project Overview](#project-overview) +- [Tech Stack](#tech-stack) +- [Project Structure](#project-structure) +- [Database Schema](#database-schema) +- [Application Flow](#application-flow) +- [API Routes & Endpoints](#api-routes--endpoints) +- [Setup & Installation](#setup--installation) + +--- + +## Project Overview + +The Comparison Project is designed to: +- **Manage subcontractors** and their excavation data (Manhole Excavation, Trench Excavation, Domestic Chambers) +- **Import client-side project data** for comparison +- **Compare subcontractor work** against client specifications +- **Generate detailed reports** highlighting differences between client and subcontractor data +- **User authentication** with login/registration system + +### Key Features + +✅ User Authentication (Register/Login/Logout) +✅ Subcontractor Management (Add/Edit/List/Delete) +✅ File Import (Excel/CSV) for both Client and Subcontractor data +✅ Data validation and duplicate detection +✅ Comparison Reports with Excel export +✅ Dashboard with file management + +--- + +## Tech Stack + +**Backend Framework**: Flask +**Database**: SQL Database (MySQL/PostgreSQL/SQLite configured via environment variables) +**ORM**: SQLAlchemy +**File Processing**: Pandas, OpenPyXL, XlsxWriter +**Authentication**: Werkzeug (Password hashing) +**Configuration**: Python Dotenv + +### Dependencies + +``` +Flask +pandas +openpyxl +xlrd +Werkzeug +python-dotenv +cryptography +xlsxwriter +matplotlib + +``` + +--- + +## Project Structure + +``` +Comparison_Project/ +├── run.py # Application entry point +├── requirements.txt # Python dependencies +├── .env # Environment variables (DB, secrets) +├── README.md # This file +│ +├── app/ +│ ├── __init__.py # Flask app initialization +│ ├── config.py # Configuration settings +│ │ +│ ├── models/ # Database models (SQLAlchemy ORM) +│ │ ├── user_model.py # User table schema +│ │ ├── subcontractor_model.py # Subcontractor table schema +│ │ ├── manhole_excavation_model.py # Subcontractor MH excavation +│ │ ├── trench_excavation_model.py # Subcontractor Trench excavation +│ │ ├── manhole_domestic_chamber_model.py # Subcontractor Domestic chamber +│ │ ├── mh_ex_client_model.py # Client MH excavation +│ │ ├── tr_ex_client_model.py # Client Trench excavation +│ │ └── mh_dc_client_model.py # Client Domestic chamber +│ │ +│ ├── routes/ # Flask Blueprints (API endpoints) +│ │ ├── auth.py # Login, Register, Logout +│ │ ├── dashboard.py # Dashboard page +│ │ ├── file_import.py # File upload endpoints +│ │ ├── file_report.py # Fetch and format data for reports +│ │ ├── generate_comparison_report.py # Comparison logic & Excel export +│ │ ├── subcontractor_routes.py # CRUD for subcontractors +│ │ ├── user.py # User management routes +│ │ └── file_format.py # File format validation +│ │ +│ ├── services/ # Business logic layer +│ │ ├── db_service.py # Database connection & SQLAlchemy init +│ │ ├── file_service.py # File parsing & data insertion +│ │ └── user_service.py # User authentication logic +│ │ +│ ├── utils/ # Helper functions +│ │ ├── file_utils.py # File path utilities +│ │ ├── helpers.py # Decorators, common helpers +│ │ └── __pycache__/ +│ │ +│ ├── static/ # Static assets +│ │ ├── css/ # Stylesheets +│ │ ├── images/ # Images +│ │ ├── uploads/ # Uploaded files directory +│ │ │ ├── Client_Bill_1/ +│ │ │ └── sub_1/ +│ │ └── downloads/ # Generated reports +│ │ +│ ├── templates/ # Jinja2 HTML templates +│ │ ├── base.html # Base template +│ │ ├── login.html +│ │ ├── register.html +│ │ ├── dashboard.html +│ │ ├── file_import.html +│ │ ├── file_import_client.html +│ │ ├── file_format.html +│ │ ├── file_report.html +│ │ ├── generate_comparison_report.html +│ │ ├── generate_comparison_client_vs_subcont.html +│ │ ├── list_user.html +│ │ ├── report.html +│ │ ├── users.htm +│ │ └── subcontractor/ +│ │ ├── add.html +│ │ ├── edit.html +│ │ └── list.html +│ │ +│ └── logs/ # Application logs +│ +├── instance/ # Instance folder (DB, temp files) +└── logs/ # Root level logs +``` + +--- + +## Database Schema + +### 1. **Users Table** (`users`) +Stores user authentication data. + +| Column | Type | Constraints | Purpose | +|--------|------|-------------|---------| +| `id` | Integer | Primary Key | User identifier | +| `name` | String(120) | NOT NULL | User's full name | +| `email` | String(120) | UNIQUE, NOT NULL | User's email | +| `password_hash` | String(255) | NOT NULL | Hashed password (Werkzeug) | + +**Key Methods:** +- `set_password(password)` - Hash and store password +- `check_password(password)` - Verify password + +--- + +### 2. **Subcontractors Table** (`subcontractors`) +Stores subcontractor company information. + +| Column | Type | Constraints | Purpose | +|--------|------|-------------|---------| +| `id` | Integer | Primary Key | Subcontractor ID | +| `subcontractor_name` | String(255) | NOT NULL | Company name | +| `address` | String(500) | - | Company address | +| `gst_no` | String(50) | - | GST number | +| `pan_no` | String(50) | - | PAN number | +| `mobile_no` | String(20) | - | Contact phone | +| `email_id` | String(150) | - | Contact email | +| `contact_person` | String(150) | - | Contact person name | +| `status` | String(20) | Default: "Active" | Active/Inactive status | +| `created_at` | DateTime | Default: today | Record creation date | + +**Relationships:** +- One-to-Many with `manhole_excavation` +- One-to-Many with `trench_excavation` +- One-to-Many with `manhole_domestic_chamber` + +--- + +### 3. **Manhole Excavation (Subcontractor)** (`manhole_excavation`) +Stores manhole excavation work data from subcontractors. + +| Column Type | Purpose | +|-----|---------| +| `id` | Primary Key | +| `subcontractor_id` | Foreign Key → Subcontractor | +| `Location` | Worksite location | +| `MH_NO` | Manhole number | +| `RA_Bill_No` | RA Bill reference (for grouping) | +| `Upto_IL_Depth`, `Cutting_Depth`, `ID_of_MH_m`, `Ex_Dia_of_Manhole`, `Area_of_Manhole` | Dimension fields | +| **Excavation Categories** (by depth ranges): | | +| Soft_Murum_0_to_1_5, Soft_Murum_1_5_to_3_0, etc. | Material type + depth range | +| Hard_Murum_0_to_1_5, Hard_Murum_1_5_to_3_0, etc. | Hard mudstone layers | +| Soft_Rock_0_to_1_5, Soft_Rock_1_5_to_3_0, etc. | Soft rock layers | +| Hard_Rock_0_to_1_5 through Hard_Rock_6_0_to_7_5 | Hard rock layers | +| **Totals** | Sum per category | +| `Total` | Grand total excavation | +| `Remarks`, `created_at` | Notes and timestamp | + +--- + +### 4. **Trench Excavation (Subcontractor)** (`trench_excavation`) +Stores trench/sewer line excavation work data from subcontractors. + +**Similar structure to Manhole Excavation with additional fields:** +- Width categories: `Width_0_to_2_5`, `Width_2_5_to_3_0`, etc. +- `Avg_Depth`, `Actual_Trench_Length`, `Pipe_Dia_mm` + +--- + +### 5. **Manhole Domestic Chamber (Subcontractor)** (`manhole_domestic_chamber`) +Stores domestic chamber construction data. + +| Key Fields | Purpose | +|-----------|---------| +| `id`, `subcontractor_id` | Primary & Foreign Key | +| `Location`, `MH_NO`, `RA_Bill_No` | Identification | +| `Depth_of_MH`, `MH_TOP_LEVEL`, `MH_IL_LEVEL` | Dimensions | +| `d_0_to_1_5` through `d_6_0_to_6_5` | Depth-based excavation categories | +| `Domestic_Chambers` | Count of chambers | +| `DWC_Pipe_Length`, `UPVC_Pipe_Length` | Pipe lengths | + +--- + +### 6-8. **Client Data Tables** (Similar structure to subcontractor models) + +- **`mh_ex_client`** - Manhole Excavation (Client specifications) +- **`tr_ex_client`** - Trench Excavation (Client specifications) +- **`mh_dc_client`** - Manhole Domestic Chamber (Client specifications) + +**Note:** Client tables do NOT have `subcontractor_id` as they represent client baseline data. + +--- + +## Application Flow + +### 1. **User Authentication Flow** +``` +User Access (/) + ↓ +Redirect to /login + ↓ +[Login Page] + ├─ POST /auth/login + │ ↓ + │ UserService.validate_login(email, password) + │ ↓ + │ Query User table, verify password + │ ↓ + │ Set session["user_id"], session["user_name"] + ↓ +Redirect to /dashboard +``` + +### 2. **Subcontractor File Import Flow** +``` +User → /file/import [GET] + ↓ +Display form with subcontractor dropdown + ↓ +User selects subcontractor & uploads Excel file + ↓ +POST /file/import + ↓ +FileService.handle_file_upload() + ├─ Validate file type (xlsx, xls, csv) + ├─ Create upload folder: static/uploads/sub_{id}/ + ├─ Save file + │ + ├─ Read Excel sheets: + │ ├─ "Tr.Ex." (Trench Excavation) + │ ├─ "MH Ex." (Manhole Excavation) + │ └─ "MH & DC" (Manhole & Domestic Chamber) + │ + ├─ Process each sheet: + │ ├─ Normalize column names + │ ├─ Clean data (handle NaN, empty values) + │ ├─ Check for duplicates by (Location, MH_NO, RA_Bill_No, subcontractor_id) + │ ├─ Insert records into respective tables + │ └─ Commit to database + │ + └─ Return success/error message +``` + +### 3. **Client File Import Flow** +``` +User → /file/import_client [GET] + ↓ +Display form for client file upload + ↓ +User uploads Excel file with client specifications + ↓ +POST /file/import_client + ↓ +FileService.handle_client_file_upload() + ├─ Validate & save file + ├─ Read sheets: "Tr.Ex.", "MH Ex.", "MH & DC" + ├─ Process and insert into: + │ ├─ mh_ex_client + │ ├─ tr_ex_client + │ └─ mh_dc_client + └─ Return status +``` + +### 4. **Comparison Report Generation Flow** +``` +User → /report/generate_comparison [GET] + ↓ +Display form to select RA Bill No. + ↓ +User selects bill number + ↓ +POST /report/generate_comparison + ↓ +generate_report_bp routes request + ├─ Fetch client data (mh_ex_client, tr_ex_client, mh_dc_client) + ├─ Fetch subcontractor data (by RA_Bill_No) + ├─ For each record type: + │ ├─ Create lookup table by (Location, MH_NO) + │ ├─ Match client records with subcontractor records + │ ├─ Calculate totals for each category + │ ├─ Compute differences/variances + │ └─ Format for Excel output + │ + ├─ Generate Excel file with comparison results: + │ ├─ Summary sheet + │ ├─ Detailed Trench Excavation comparison + │ ├─ Detailed Manhole Excavation comparison + │ └─ Detailed Domestic Chamber comparison + │ + └─ Send file to client as download +``` + +### 5. **Dashboard & Data Retrieval Flow** +``` +User → /dashboard [GET] + ↓ +Check session["user_id"] (authentication) + ↓ +Render dashboard.html + ├─ Display available reports + ├─ List recent RA Bills + ├─ Show subcontractor list + └─ Provide navigation to: + ├─ File import + ├─ Reports + ├─ Subcontractor management + └─ Logout +``` + +--- + +## API Routes & Endpoints + +### Authentication Routes (`/auth`) +| Method | Endpoint | Purpose | Auth | +|--------|----------|---------|------| +| GET/POST | `/login` | Login form & validation | ❌ | +| GET/POST | `/register` | User registration | ❌ | +| GET | `/logout` | Clear session & logout | ✅ | + +### Dashboard Route (`/dashboard`) +| Method | Endpoint | Purpose | Auth | +|--------|----------|---------|------| +| GET | `/dashboard/` | Main dashboard | ✅ | + +### Subcontractor Routes (`/subcontractor`) +| Method | Endpoint | Purpose | Auth | +|--------|----------|---------|------| +| GET | `/subcontractor/add` | Add form | ✅ | +| POST | `/subcontractor/save` | Save new subcontractor | ✅ | +| GET | `/subcontractor/list` | List all subcontractors | ✅ | +| GET | `/subcontractor/edit/` | Edit form | ✅ | +| POST | `/subcontractor/update/` | Update subcontractor | ✅ | +| GET | `/subcontractor/delete/` | Delete subcontractor | ✅ | + +### File Import Routes (`/file`) +| Method | Endpoint | Purpose | Auth | +|--------|----------|---------|------| +| GET/POST | `/file/import` | Subcontractor file upload | ✅ | +| GET/POST | `/file/import_client` | Client file upload | ✅ | +| GET/POST | `/file/report` | View imported data report | ✅ | + +### Report Routes (`/report`) +| Method | Endpoint | Purpose | Auth | +|--------|----------|---------|------| +| GET/POST | `/report/generate_comparison` | Generate comparison report | ✅ | +| GET/POST | `/report/generate_comparison_client_vs_subcont` | Alternative comparison | ✅ | + +### User Routes (`/user`) +| Method | Endpoint | Purpose | Auth | +|--------|----------|---------|------| +| GET | `/user/list` | List all users | ✅ | +| Other | (Check routes) | User management | ✅ | + +### File Format Route (`/format`) +| Method | Endpoint | Purpose | Auth | +|--------|----------|---------|------| +| GET/POST | `/format/...` | File format reference | ✅ | + +--- + +## Setup & Installation + +### Prerequisites +- Python 3.7+ +- MySQL/PostgreSQL (or SQLite for development) +- pip (Python package manager) + +### 1. Clone & Install Dependencies +```bash +cd d:\New folder\Comparison\Comparison_Project +pip install -r requirements.txt +``` + +### 2. Configure Environment Variables +Create a `.env` file in the project root: +```env +# ----------------------------- +# Flask App Configuration +# ----------------------------- +FLASK_ENV=development +FLASK_DEBUG=True +FLASK_HOST='0.0.0.0' +FLASK_PORT=5001 + +# ----------------------------- +# Security +# ----------------------------- +SECRET_KEY=change-this-to-strong-secret-key + +# ----------------------------- +# Database Configuration +# ----------------------------- +DB_DIALECT=mysql +DB_DRIVER=pymysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_NAME=comparisondb +DB_USER=root +DB_PASSWORD=your_password + + +``` + +### 3. Initialize Database +```bash +python run.py +``` +This will: +- Load environment variables +- Create Flask app with configuration +- Initialize SQLAlchemy +- Create all tables (if not exist) +- Start Flask development server + +### 4. Access Application +Open browser: `http://127.0.0.1:5000/` +- First time: Redirect to `/login` +- Register a new account +- Login and start using the application + +--- + +## Key Services & Utilities + +### FileService (`app/services/file_service.py`) +**Purpose:** Handles file uploads, parsing, and data validation + +**Key Methods:** +- `handle_file_upload(file, subcontractor_id, RA_Bill_No)` - Upload & process subcontractor file +- `handle_client_file_upload(file, RA_Bill_No)` - Upload & process client file +- `process_trench_excavation(df, subcontractor_id, RA_Bill_No)` - Parse & insert trench data +- `process_manhole_excavation(df, subcontractor_id, RA_Bill_No)` - Parse & insert manhole data +- `process_manhole_domestic_chamber(df, subcontractor_id, RA_Bill_No)` - Parse & insert chamber data + +### UserService (`app/services/user_service.py`) +**Purpose:** User authentication & management + +**Key Methods:** +- `register_user(name, email, password)` - Register new user +- `validate_login(email, password)` - Authenticate user +- `get_all_users()` - Fetch all users + +### DBService (`app/services/db_service.py`) +**Purpose:** Database connection & SQLAlchemy initialization + +**Exports:** +- `db` - SQLAlchemy instance for ORM operations +- `migrate` - Flask-Migrate for schema migrations + +--- + +## Authentication & Security + +### Session Management +- Uses Flask `session` object +- Stores `user_id` and `user_name` after successful login +- `@login_required` decorator validates authenticated requests + +### Password Security +- Passwords hashed using Werkzeug's `generate_password_hash()` +- Verification via `check_password_hash()` +- No plaintext passwords stored in database + +### File Security +- File uploads sanitized with `secure_filename()` +- Only allowed extensions: `.xlsx`, `.xls`, `.csv` +- Files stored in user-specific subfolders + +--- + +## Error Handling & Validation + +### File Import Validation +1. **File Type Check** - Only CSV/XLS/XLSX allowed +2. **Sheet Validation** - Required sheets must exist +3. **Column Normalization** - Auto-fixes column name inconsistencies +4. **Data Type Conversion** - Converts to appropriate types +5. **Duplicate Detection** - Prevents duplicate records by (Location, MH_NO, RA_Bill_No) +6. **Null Handling** - Converts empty/NaN values to None + +### Database Constraints +- Foreign key relationships enforced +- Unique email constraint on users +- NOT NULL constraints on critical fields + +--- + +## Example: Typical User Workflow + +``` +1. User registers/logs in + → POST /auth/register (new user) or /auth/login + +2. User adds subcontractors + → GET /subcontractor/add + → POST /subcontractor/save + +3. User uploads subcontractor data (Excel file with 3 sheets) + → GET /file/import + → POST /file/import (file, subcontractor_id, RA_Bill_No) + → Database populated with excavation data + +4. User uploads client specifications (Excel file with same 3 sheets) + → GET /file/import_client + → POST /file/import_client (file, RA_Bill_No) + → Database populated with client data + +5. User generates comparison report + → GET /report/generate_comparison + → POST /report/generate_comparison (RA_Bill_No) + → System compares client vs subcontractor data + → Generates Excel file showing differences + → User downloads report + +6. User logs out + → GET /auth/logout + → Session cleared, redirected to login +``` + +--- + +## Future Enhancements + +- [ ] Email notifications for report generation +- [ ] Data visualization dashboards +- [ ] Role-based access control (Admin, User, Viewer) +- [ ] Bulk report generation +- [ ] Data export to PDF format +- [ ] Audit logs for data modifications +- [ ] API documentation (Swagger/OpenAPI) +- [ ] Unit & integration tests + +--- + +## Support & Contribution + +For issues, feature requests, or contributions, please contact the development team. + +**Last Updated:** January 2026 \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e5f62bb --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,52 @@ +from flask import Flask, redirect, url_for +from app.config import Config +from app.services.db_service import db + +def create_app(): + app = Flask(__name__) + app.config.from_object(Config) + + # Initialize extensions + db.init_app(app) + + # Register blueprints + register_blueprints(app) + # Register error handlers + register_error_handlers(app) + + # ROOT → LOGIN + @app.route("/") + def index(): + return redirect(url_for("auth.login")) + + return app + + +def register_blueprints(app): + from app.routes.auth import auth_bp + from app.routes.user 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 + from app.routes.file_report import file_report_bp + from app.routes.generate_comparison_report import generate_report_bp + from app.routes.file_format import file_format_bp + + app.register_blueprint(auth_bp) + app.register_blueprint(user_bp) + app.register_blueprint(dashboard_bp) + app.register_blueprint(subcontractor_bp) + app.register_blueprint(file_import_bp) + app.register_blueprint(file_report_bp) + app.register_blueprint(generate_report_bp) + app.register_blueprint(file_format_bp ) + + +def register_error_handlers(app): + @app.errorhandler(404) + def page_not_found(e): + return "Page Not Found", 404 + + @app.errorhandler(500) + def internal_error(e): + return "Internal Server Error", 500 diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..2ca03b2 --- /dev/null +++ b/app/config.py @@ -0,0 +1,29 @@ +import os +# project base url +BASE_DIR = os.path.abspath(os.path.dirname(__file__)) + +class Config: + # secret key + SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key") + + # Database varibles + DB_DIALECT = os.getenv("DB_DIALECT") + DB_DRIVER = os.getenv("DB_DRIVER") + DB_USER = os.getenv("DB_USER") + DB_PASSWORD = os.getenv("DB_PASSWORD") + DB_HOST = os.getenv("DB_HOST") + DB_PORT = os.getenv("DB_PORT") + DB_NAME = os.getenv("DB_NAME") + # database connection url + SQLALCHEMY_DATABASE_URI = ( + f"{DB_DIALECT}+{DB_DRIVER}://" + f"{DB_USER}:{DB_PASSWORD}@" + f"{DB_HOST}:{DB_PORT}/" + f"{DB_NAME}" + ) + + SQLALCHEMY_TRACK_MODIFICATIONS = False + # uploads folder path + UPLOAD_FOLDER = os.path.join(BASE_DIR, "static", "uploads") + # file extension + ALLOWED_EXTENSIONS = {"xlsx", "xls", "csv"} diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/laying_client_model.py b/app/models/laying_client_model.py new file mode 100644 index 0000000..e2b35c8 --- /dev/null +++ b/app/models/laying_client_model.py @@ -0,0 +1,58 @@ +from app import db +from datetime import datetime + +class LayingClient(db.Model): + __tablename__ = "laying_client" + + id = db.Column(db.Integer, primary_key=True) + + # Basic Fields + Location = db.Column(db.String(500)) + MH_NO = db.Column(db.String(100)) + CC_length = db.Column(db.Float) + # Bedding Qty. + Outer_dia_of_MH_m = db.Column(db.Float) + Bedding_Length = db.Column(db.Float) + Width = db.Column(db.Float) + Depth = db.Column(db.Float) + Qty = db.Column(db.Float) + + # PIPE LAYING Qty. + Pipe_Dia_mm = db.Column(db.Float) + ID_of_MH_m = db.Column(db.Float) + Laying_Length = db.Column(db.Float) + + pipe_150_mm = db.Column(db.Float) + pipe_200_mm = db.Column(db.Float) + pipe_250_mm = db.Column(db.Float) + pipe_300_mm = db.Column(db.Float) + pipe_350_mm = db.Column(db.Float) + pipe_400_mm = db.Column(db.Float) + pipe_450_mm = db.Column(db.Float) + pipe_500_mm = db.Column(db.Float) + pipe_600_mm = db.Column(db.Float) + pipe_700_mm = db.Column(db.Float) + pipe_900_mm = db.Column(db.Float) + pipe_1200_mm = db.Column(db.Float) + + Total = db.Column(db.Float) + Remarks = db.Column(db.String(500)) + RA_Bill_No=db.Column(db.String(500)) + + created_at = db.Column(db.DateTime, default=datetime.today) + + + def __repr__(self): + return f"" + + def serialize(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + + def sum_laying_fields(): + return [ + "pipe_150_mm", "pipe_200_mm", "pipe_250_mm", + "pipe_300_mm", "pipe_350_mm", "pipe_400_mm", + "pipe_450_mm", "pipe_500_mm", "pipe_600_mm", + "pipe_700_mm", "pipe_900_mm", "pipe_1200_mm" + ] \ No newline at end of file diff --git a/app/models/laying_model.py b/app/models/laying_model.py new file mode 100644 index 0000000..3d49343 --- /dev/null +++ b/app/models/laying_model.py @@ -0,0 +1,54 @@ +from app import db +from datetime import datetime + +class Laying(db.Model): + __tablename__ = "laying" + + id = db.Column(db.Integer, primary_key=True) + # Foreign Key to Subcontractor tables + subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False) + # Relationship for easy access (subcontractor.subcontractor_name) + subcontractor = db.relationship("Subcontractor", backref="laying_records") + + # Pipe Laying Fields + Location = db.Column(db.String(500)) + MH_NO = db.Column(db.String(100)) + CC_length = db.Column(db.Float) + Pipe_Dia_mm = db.Column(db.Float) + ID_of_MH_m = db.Column(db.Float) + Laying_Length = db.Column(db.Float) + + pipe_150_mm = db.Column(db.Float) + pipe_200_mm = db.Column(db.Float) + pipe_250_mm = db.Column(db.Float) + pipe_300_mm = db.Column(db.Float) + pipe_350_mm = db.Column(db.Float) + pipe_400_mm = db.Column(db.Float) + pipe_450_mm = db.Column(db.Float) + pipe_500_mm = db.Column(db.Float) + pipe_600_mm = db.Column(db.Float) + pipe_700_mm = db.Column(db.Float) + pipe_900_mm = db.Column(db.Float) + pipe_1200_mm = db.Column(db.Float) + + Total = db.Column(db.Float) + Remarks = db.Column(db.String(500)) + RA_Bill_No=db.Column(db.String(500)) + + created_at = db.Column(db.DateTime, default=datetime.today) + + + def __repr__(self): + return f"" + + def serialize(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + + def sum_laying_fields(): + return [ + "pipe_150_mm", "pipe_200_mm", "pipe_250_mm", + "pipe_300_mm", "pipe_350_mm", "pipe_400_mm", + "pipe_450_mm", "pipe_500_mm", "pipe_600_mm", + "pipe_700_mm", "pipe_900_mm", "pipe_1200_mm" + ] diff --git a/app/models/manhole_domestic_chamber_model.py b/app/models/manhole_domestic_chamber_model.py new file mode 100644 index 0000000..274198e --- /dev/null +++ b/app/models/manhole_domestic_chamber_model.py @@ -0,0 +1,63 @@ +from app import db +from datetime import datetime + +class ManholeDomesticChamber(db.Model): + __tablename__ = "manhole_domestic_chamber" + + id = db.Column(db.Integer, primary_key=True) + # Foreign Key to Subcontractor table + subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False) + # Relationship for easy access (subcontractor.subcontractor_name) + subcontractor = db.relationship("Subcontractor", backref="manhole_domestic_chamber_records") + + # Basic Fields + Location = db.Column(db.String(500)) + MH_NO = db.Column(db.String(100)) + Depth_of_MH = db.Column(db.Float) + + # Excavation categories + d_0_to_0_75 = db.Column(db.Float) + d_0_76_to_1_05 = db.Column(db.Float) + d_1_06_to_1_65 = db.Column(db.Float) + d_1_66_to_2_15 = db.Column(db.Float) + d_2_16_to_2_65 = db.Column(db.Float) + d_2_66_to_3_15 = db.Column(db.Float) + d_3_16_to_3_65= db.Column(db.Float) + d_3_66_to_4_15 = db.Column(db.Float) + d_4_16_to_4_65 = db.Column(db.Float) + d_4_66_to_5_15 = db.Column(db.Float) + d_5_16_to_5_65 = db.Column(db.Float) + + d_5_66_to_6_15 = db.Column(db.Float) + d_6_16_to_6_65 = db.Column(db.Float) + d_6_66_to_7_15 = db.Column(db.Float) + d_7_16_to_7_65 = db.Column(db.Float) + d_7_66_to_8_15 = db.Column(db.Float) + d_8_16_to_8_65 = db.Column(db.Float) + d_8_66_to_9_15 = db.Column(db.Float) + d_9_16_to_9_65 = db.Column(db.Float) + + Domestic_Chambers = db.Column(db.Float) + DWC_Pipe_Length = db.Column(db.Float) + UPVC_Pipe_Length = db.Column(db.Float) + RA_Bill_No=db.Column(db.String(500)) + + created_at = db.Column(db.DateTime, default=datetime.today) + + def __repr__(self): + return f"" + + def serialize(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + + def sum_mh_dc_fields(): + return [ + "d_0_to_0_75", "d_0_76_to_1_05", "d_1_06_to_1_65", + "d_1_66_to_2_15", "d_2_16_to_2_65", "d_2_66_to_3_15", + "d_3_16_to_3_65", "d_3_66_to_4_15", "d_4_16_to_4_65", + "d_4_66_to_5_15", "d_5_16_to_5_65", "d_5_66_to_6_15", + "d_6_16_to_6_65", "d_6_66_to_7_15", "d_7_16_to_7_65", + "d_7_66_to_8_15", "d_8_16_to_8_65", "d_8_66_to_9_15", + "d_9_16_to_9_65"] + diff --git a/app/models/manhole_excavation_model.py b/app/models/manhole_excavation_model.py new file mode 100644 index 0000000..51e773c --- /dev/null +++ b/app/models/manhole_excavation_model.py @@ -0,0 +1,67 @@ +from app import db +from datetime import datetime + +class ManholeExcavation(db.Model): + __tablename__ = "manhole_excavation" + + id = db.Column(db.Integer, primary_key=True) + # Foreign Key to Subcontractor table + subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False) + # Relationship for easy access (subcontractor.subcontractor_name) + subcontractor = db.relationship("Subcontractor", backref="manhole_records") + + # Basic Fields + Location = db.Column(db.String(500)) + MH_NO = db.Column(db.String(100)) + + Upto_IL_Depth = db.Column(db.Float) + Cutting_Depth = db.Column(db.Float) + ID_of_MH_m = db.Column(db.Float) + Ex_Dia_of_Manhole = db.Column(db.Float) + Area_of_Manhole = db.Column(db.Float) + + # Excavation categories + Soft_Murum_0_to_1_5 = db.Column(db.Float) + Soft_Murum_1_5_to_3_0 = db.Column(db.Float) + Soft_Murum_3_0_to_4_5 = db.Column(db.Float) + + Hard_Murum_0_to_1_5 = db.Column(db.Float) + Hard_Murum_1_5_to_3_0 = db.Column(db.Float) + + Soft_Rock_0_to_1_5 = db.Column(db.Float) + Soft_Rock_1_5_to_3_0 = db.Column(db.Float) + + Hard_Rock_0_to_1_5 = db.Column(db.Float) + Hard_Rock_1_5_to_3_0 = db.Column(db.Float) + Hard_Rock_3_0_to_4_5 = db.Column(db.Float) + Hard_Rock_4_5_to_6_0 = db.Column(db.Float) + Hard_Rock_6_0_to_7_5 = db.Column(db.Float) + + # Totals + Soft_Murum_0_to_1_5_total = db.Column(db.Float) + Soft_Murum_1_5_to_3_0_total = db.Column(db.Float) + Soft_Murum_3_0_to_4_5_total = db.Column(db.Float) + + Hard_Murum_0_to_1_5_total = db.Column(db.Float) + Hard_Murum_1_5_and_above_total = db.Column(db.Float) + + Soft_Rock_0_to_1_5_total = db.Column(db.Float) + Soft_Rock_1_5_and_above_total = db.Column(db.Float) + + Hard_Rock_0_to_1_5_total = db.Column(db.Float) + Hard_Rock_1_5_to_3_0_total = db.Column(db.Float) + Hard_Rock_3_0_to_4_5_total = db.Column(db.Float) + Hard_Rock_4_5_to_6_0_total = db.Column(db.Float) + Hard_Rock_6_0_to_7_5_total = db.Column(db.Float) + + Total = db.Column(db.Float) + Remarks = db.Column(db.String(500)) + RA_Bill_No=db.Column(db.String(500)) + + created_at = db.Column(db.DateTime, default=datetime.today) + + def __repr__(self): + return f"" + + def serialize(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} diff --git a/app/models/mh_dc_client_model.py b/app/models/mh_dc_client_model.py new file mode 100644 index 0000000..ae0d508 --- /dev/null +++ b/app/models/mh_dc_client_model.py @@ -0,0 +1,57 @@ +from app import db +from datetime import datetime + +class ManholeDomesticChamberClient(db.Model): + __tablename__ = "mh_dc_client" + + id = db.Column(db.Integer, primary_key=True) + # Foreign Key to Subcontractor table + # subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False) + # Relationship for easy access (subcontractor.subcontractor_name) + # subcontractor = db.relationship("Subcontractor", backref="mh_dc_records") + + # Basic Fields + 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) + MH_IL_LEVEL = db.Column(db.Float) + Depth_of_MH = db.Column(db.Float) + + + # Excavation categories + d_0_to_1_5 = db.Column(db.Float) + d_1_5_to_2_0 = db.Column(db.Float) + d_2_0_to_2_5 = db.Column(db.Float) + d_2_5_to_3_0 = db.Column(db.Float) + + d_3_0_to_3_5 = db.Column(db.Float) + d_3_5_to_4_0 = db.Column(db.Float) + d_4_0_to_4_5= db.Column(db.Float) + d_4_5_to_5_0 = db.Column(db.Float) + + d_5_0_to_5_5 = db.Column(db.Float) + d_5_5_to_6_0 = db.Column(db.Float) + d_6_0_to_6_5 = db.Column(db.Float) + + Domestic_Chambers = db.Column(db.Float) + + created_at = db.Column(db.DateTime, default=datetime.today) + + def __repr__(self): + return f"" + + + def serialize(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + + def sum_mh_dc_fields(): + return [ + "d_0_to_0_75", "d_0_76_to_1_05", "d_1_06_to_1_65", + "d_1_66_to_2_15", "d_2_16_to_2_65", "d_2_66_to_3_15", + "d_3_16_to_3_65", "d_3_66_to_4_15", "d_4_16_to_4_65", + "d_4_66_to_5_15", "d_5_16_to_5_65", "d_5_66_to_6_15", + "d_6_16_to_6_65", "d_6_66_to_7_15", "d_7_16_to_7_65", + "d_7_66_to_8_15", "d_8_16_to_8_65", "d_8_66_to_9_15", + "d_9_16_to_9_65" ] \ No newline at end of file diff --git a/app/models/mh_ex_client_model.py b/app/models/mh_ex_client_model.py new file mode 100644 index 0000000..c4fd6c8 --- /dev/null +++ b/app/models/mh_ex_client_model.py @@ -0,0 +1,83 @@ +from app import db +from datetime import datetime + +class ManholeExcavationClient(db.Model): + __tablename__ = "mh_ex_client" + + id = db.Column(db.Integer, primary_key=True) + # Foreign Key to Subcontractor table + # subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False) + # Relationship for easy access (subcontractor.subcontractor_name) + # subcontractor = db.relationship("Subcontractor", backref="mh_ex_records") + + # Basic Fields + 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) + MH_Invert_Level = db.Column(db.Float) + MH_Top_Level = db.Column(db.Float) + Ex_Level = db.Column(db.Float) + Cutting_Depth = db.Column(db.Float) + MH_Depth = db.Column(db.Float) + ID_of_MH_m = db.Column(db.Float) + + Dia_of_MH_Cutting = db.Column(db.Float) + Area_of_Manhole = db.Column(db.Float) + + # Excavation categories + Marshi_Muddy_Slushy_0_to_1_5 = db.Column(db.Float) + Marshi_Muddy_Slushy_1_5_to_3_0 = db.Column(db.Float) + Marshi_Muddy_Slushy_3_0_to_4_5 = db.Column(db.Float) + + Soft_Murum_0_to_1_5 = db.Column(db.Float) + Soft_Murum_1_5_to_3_0 = db.Column(db.Float) + Soft_Murum_3_0_to_4_5 = db.Column(db.Float) + + Hard_Murum_0_to_1_5 = db.Column(db.Float) + Hard_Murum_1_5_to_3_0 = db.Column(db.Float) + Hard_Murum_3_0_to_4_5 = db.Column(db.Float) + + Soft_Rock_0_to_1_5 = db.Column(db.Float) + Soft_Rock_1_5_to_3_0 = db.Column(db.Float) + Soft_Murum_3_0_to_4_5 = db.Column(db.Float) + + Hard_Rock_0_to_1_5 = db.Column(db.Float) + Hard_Rock_1_5_to_3_0 = db.Column(db.Float) + Hard_Rock_3_0_to_4_5 = db.Column(db.Float) + Hard_Rock_4_5_to_6_0 = db.Column(db.Float) + Hard_Rock_6_0_to_7_5 = db.Column(db.Float) + + # Totals + Marshi_Muddy_Slushy_0_to_1_5_total = db.Column(db.Float) + Marshi_Muddy_Slushy_1_5_to_3_0_total = db.Column(db.Float) + Marshi_Muddy_Slushy_3_0_to_4_5_total = db.Column(db.Float) + + Soft_Murum_0_to_1_5_total = db.Column(db.Float) + Soft_Murum_1_5_to_3_0_total = db.Column(db.Float) + Soft_Murum_3_0_to_4_5_total = db.Column(db.Float) + + Hard_Murum_0_to_1_5_total = db.Column(db.Float) + Hard_Murum_1_5_to_3_0_total = db.Column(db.Float) + Hard_Murum_3_0_to_4_5_total = db.Column(db.Float) + + Soft_Rock_0_to_1_5_total = db.Column(db.Float) + Soft_Rock_1_5_to_3_0_total = db.Column(db.Float) + Soft_Rock_3_0_to_4_5_total = db.Column(db.Float) + + Hard_Rock_0_to_1_5_total = db.Column(db.Float) + Hard_Rock_1_5_to_3_0_total = db.Column(db.Float) + Hard_Rock_3_0_to_4_5_total = db.Column(db.Float) + Hard_Rock_4_5_to_6_0_total = db.Column(db.Float) + Hard_Rock_6_0_to_7_5_total = db.Column(db.Float) + + Remarks = db.Column(db.String(500)) + Total = db.Column(db.Float) + + created_at = db.Column(db.DateTime, default=datetime.today) + + def __repr__(self): + return f"" + + def serialize(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} diff --git a/app/models/subcontractor_model.py b/app/models/subcontractor_model.py new file mode 100644 index 0000000..22aed97 --- /dev/null +++ b/app/models/subcontractor_model.py @@ -0,0 +1,21 @@ +from app import db +# from app.services.db_service import db +from datetime import datetime + +class Subcontractor(db.Model): + __tablename__ = "subcontractors" + + id = db.Column(db.Integer, primary_key=True) + subcontractor_name = db.Column(db.String(255), nullable=False) + address = db.Column(db.String(500)) + gst_no = db.Column(db.String(50)) + pan_no = db.Column(db.String(50)) + mobile_no = db.Column(db.String(20)) + email_id = db.Column(db.String(150)) + contact_person = db.Column(db.String(150)) + status = db.Column(db.String(20), default="Active") + created_at = db.Column(db.DateTime, default=datetime.today) + + def __repr__(self): + return f"" + diff --git a/app/models/tr_ex_client_model.py b/app/models/tr_ex_client_model.py new file mode 100644 index 0000000..1214de0 --- /dev/null +++ b/app/models/tr_ex_client_model.py @@ -0,0 +1,88 @@ +from app import db +from datetime import datetime + +class TrenchExcavationClient(db.Model): + __tablename__ = "tr_ex_client" + + id = db.Column(db.Integer, primary_key=True) + # Foreign Key to Subcontractor table + # subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False) + # Relationship for easy access (subcontractor.subcontractor_name) + # subcontractor = db.relationship("Subcontractor", backref="tr_ex_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) + Actual_Trench_Length = db.Column(db.Float) + Ground_Level = db.Column(db.Float) + Invert_Level = db.Column(db.Float) + Excavated_level = db.Column(db.Float) + Cutting_Depth = db.Column(db.Float) + Avg_Depth = db.Column(db.Float) + Pipe_Dia_mm = db.Column(db.Float) + + # width + Width_0_to_1_5_m = db.Column(db.Float) + Width_1_5_to_3_0_m = db.Column(db.Float) + Width_3_0_to_4_5_m = db.Column(db.Float) + Width_4_5_to_6_0_m = db.Column(db.Float) + Width_6_0_to_7_5_m = db.Column(db.Float) + + # Excavation categories + Marshi_Muddy_Slushy_0_to_1_5 = db.Column(db.Float) + Marshi_Muddy_Slushy_1_5_to_3_0 = db.Column(db.Float) + Marshi_Muddy_Slushy_3_0_to_4_5 = db.Column(db.Float) + + Soft_Murum_0_to_1_5 = db.Column(db.Float) + Soft_Murum_1_5_to_3_0 = db.Column(db.Float) + Soft_Murum_3_0_to_4_5 = db.Column(db.Float) + + Hard_Murum_0_to_1_5 = db.Column(db.Float) + Hard_Murum_1_5_to_3_0 = db.Column(db.Float) + Hard_Murum_3_0_to_4_5 = db.Column(db.Float) + + Soft_Rock_0_to_1_5 = db.Column(db.Float) + Soft_Rock_1_5_to_3_0 = db.Column(db.Float) + Soft_Rock_3_0_to_4_5 = db.Column(db.Float) + + Hard_Rock_0_to_1_5 = db.Column(db.Float) + Hard_Rock_1_5_to_3_0 = db.Column(db.Float) + Hard_Rock_3_0_to_4_5 = db.Column(db.Float) + Hard_Rock_4_5_to_6_0 = db.Column(db.Float) + Hard_Rock_6_0_to_7_5 = db.Column(db.Float) + + # Totals + Marshi_Muddy_Slushy_0_to_1_5_total = db.Column(db.Float) + Marshi_Muddy_Slushy_1_5_to_3_0_total = db.Column(db.Float) + Marshi_Muddy_Slushy_3_0_to_4_5_total = db.Column(db.Float) + + Soft_Murum_0_to_1_5_total = db.Column(db.Float) + Soft_Murum_1_5_to_3_0_total = db.Column(db.Float) + Soft_Murum_3_0_to_4_5_total = db.Column(db.Float) + + Hard_Murum_0_to_1_5_total = db.Column(db.Float) + Hard_Murum_1_5_to_3_0_total = db.Column(db.Float) + Hard_Murum_3_0_to_4_5_total = db.Column(db.Float) + + Soft_Rock_0_to_1_5_total = db.Column(db.Float) + Soft_Rock_1_5_to_3_0_total = db.Column(db.Float) + Soft_Rock_3_0_to_4_5_total = db.Column(db.Float) + + Hard_Rock_0_to_1_5_total = db.Column(db.Float) + Hard_Rock_1_5_to_3_0_total = db.Column(db.Float) + Hard_Rock_3_0_to_4_5_total = db.Column(db.Float) + Hard_Rock_4_5_to_6_0_total = db.Column(db.Float) + Hard_Rock_6_0_to_7_5_total = db.Column(db.Float) + + Total = db.Column(db.Float) + Remarks = db.Column(db.String(500)) + + created_at = db.Column(db.DateTime, default=datetime.today) + + def __repr__(self): + return f"" + + def serialize(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} diff --git a/app/models/trench_excavation_model.py b/app/models/trench_excavation_model.py new file mode 100644 index 0000000..eacfd0a --- /dev/null +++ b/app/models/trench_excavation_model.py @@ -0,0 +1,110 @@ +from app import db +from datetime import datetime + +class TrenchExcavation(db.Model): + __tablename__ = "trench_excavation" + + id = db.Column(db.Integer, primary_key=True) + # Foreign Key to Subcontractor table + subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False) + # Relationship for easy access (subcontractor.subcontractor_name) + subcontractor = db.relationship("Subcontractor", backref="trench_records") + + # Basic Fields + Location = db.Column(db.String(500)) + MH_NO = db.Column(db.String(100)) + CC_length = db.Column(db.Float) + Invert_Level = db.Column(db.Float) + MH_Top_Level = db.Column(db.Float) + Ground_Level = db.Column(db.Float) + ID_of_MH_m = db.Column(db.Float) + Actual_Trench_Length = db.Column(db.Float) + Pipe_Dia_mm = db.Column(db.Float) + + # width + Width_0_to_2_5 = db.Column(db.Float) + Width_2_5_to_3_0 = db.Column(db.Float) + Width_3_0_to_4_5 = db.Column(db.Float) + Width_4_5_to_6_0 = db.Column(db.Float) + + Upto_IL_Depth = db.Column(db.Float) + Cutting_Depth = db.Column(db.Float) + Avg_Depth = db.Column(db.Float) + + # Excavation categories + Soft_Murum_0_to_1_5 = db.Column(db.Float) + Soft_Murum_1_5_to_3_0 = db.Column(db.Float) + Soft_Murum_3_0_to_4_5 = db.Column(db.Float) + + Hard_Murum_0_to_1_5 = db.Column(db.Float) + Hard_Murum_1_5_to_3_0 = db.Column(db.Float) + + Soft_Rock_0_to_1_5 = db.Column(db.Float) + Soft_Rock_1_5_to_3_0 = db.Column(db.Float) + + Hard_Rock_0_to_1_5 = db.Column(db.Float) + Hard_Rock_1_5_to_3_0 = db.Column(db.Float) + Hard_Rock_3_0_to_4_5 = db.Column(db.Float) + Hard_Rock_4_5_to_6_0 = db.Column(db.Float) + Hard_Rock_6_0_to_7_5 = db.Column(db.Float) + + # Totals + Soft_Murum_0_to_1_5_total = db.Column(db.Float) + Soft_Murum_1_5_to_3_0_total = db.Column(db.Float) + Soft_Murum_3_0_to_4_5_total = db.Column(db.Float) + + Hard_Murum_0_to_1_5_total = db.Column(db.Float) + Hard_Murum_1_5_and_above_total = db.Column(db.Float) + + Soft_Rock_0_to_1_5_total = db.Column(db.Float) + Soft_Rock_1_5_and_above_total = db.Column(db.Float) + + Hard_Rock_0_to_1_5_total = db.Column(db.Float) + Hard_Rock_1_5_to_3_0_total = db.Column(db.Float) + Hard_Rock_3_0_to_4_5_total = db.Column(db.Float) + Hard_Rock_4_5_to_6_0_total = db.Column(db.Float) + Hard_Rock_6_0_to_7_5_total = db.Column(db.Float) + + Total = db.Column(db.Float) + Remarks = db.Column(db.String(500)) + RA_Bill_No=db.Column(db.String(500)) + + created_at = db.Column(db.DateTime, default=datetime.today) + + + def __repr__(self): + return f"" + + def serialize(self): + return {c.name: getattr(self, c.name) for c in self.__table__.columns} + + def excavation_category_sums(self): + + def safe(val): + return val or 0 + + return { + "Soft_Murum_Total": ( + safe(self.Soft_Murum_0_to_1_5) + + safe(self.Soft_Murum_1_5_to_3_0) + + safe(self.Soft_Murum_3_0_to_4_5) + ), + + "Hard_Murum_Total": ( + safe(self.Hard_Murum_0_to_1_5) + + safe(self.Hard_Murum_1_5_to_3_0) + ), + + "Soft_Rock_Total": ( + safe(self.Soft_Rock_0_to_1_5) + + safe(self.Soft_Rock_1_5_to_3_0) + ), + + "Hard_Rock_Total": ( + safe(self.Hard_Rock_0_to_1_5) + + safe(self.Hard_Rock_1_5_to_3_0) + + safe(self.Hard_Rock_3_0_to_4_5) + + safe(self.Hard_Rock_4_5_to_6_0) + + safe(self.Hard_Rock_6_0_to_7_5) + ), + } diff --git a/app/models/user_model.py b/app/models/user_model.py new file mode 100644 index 0000000..4cf7c26 --- /dev/null +++ b/app/models/user_model.py @@ -0,0 +1,16 @@ +from app.services.db_service import db +from werkzeug.security import generate_password_hash, check_password_hash + +class User(db.Model): + __tablename__ = "users" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(120), nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) + password_hash = db.Column(db.String(255), nullable=False) + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/auth.py b/app/routes/auth.py new file mode 100644 index 0000000..9916348 --- /dev/null +++ b/app/routes/auth.py @@ -0,0 +1,48 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash, session +from app.services.user_service import UserService + +auth_bp = Blueprint("auth", __name__) + +@auth_bp.route("/login", methods=["GET", "POST"]) +def login(): + if session.get("user_id"): + return redirect(url_for("dashboard.dashboard")) + + if request.method == "POST": + email = request.form.get("email") + password = request.form.get("password") + + user = UserService.validate_login(email, password) + if user: + session["user_id"] = user.id + session["user_name"] = user.name + flash("Login successful", "success") + return redirect(url_for("dashboard.dashboard")) + + flash("Invalid email or password", "danger") + + return render_template("login.html", title="Login") + + +@auth_bp.route("/logout") +def logout(): + session.clear() + flash("Logged out successfully", "info") + return redirect(url_for("auth.login")) + +@auth_bp.route("/register", methods=["GET", "POST"]) +def register(): + if request.method == "POST": + name = request.form.get("name") + email = request.form.get("email") + password = request.form.get("password") + + user = UserService.register_user(name, email, password) + if not user: + flash("Email already exists", "danger") + return redirect(url_for("auth.register")) + + flash("User registered successfully", "success") + return redirect(url_for("auth.login")) + + return render_template("register.html", title="Register") diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py new file mode 100644 index 0000000..f6d79c3 --- /dev/null +++ b/app/routes/dashboard.py @@ -0,0 +1,136 @@ +# import matplotlib +# matplotlib.use("Agg") + +# from flask import Blueprint, render_template, session, redirect, url_for +# import matplotlib.pyplot as plt +# import io +# import base64 +# from app.utils.plot_utils import plot_to_base64 +# from app.services.dashboard_service import DashboardService + +# dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/dashboard") + +# # dashboard_bp = Blueprint("dashboard", __name__) + +# # charts +# # def plot_to_base64(): +# # img = io.BytesIO() +# # plt.savefig(img, format="png", bbox_inches="tight") +# # plt.close() +# # img.seek(0) +# # return base64.b64encode(img.getvalue()).decode() + +# # bar chart +# def bar_chart(): +# categories = ["Trench", "Manhole", "Pipe Laying", "Restoration"] +# values = [120, 80, 150, 60] + +# plt.figure() +# plt.bar(categories, values) +# plt.title("Work Category Report") +# plt.xlabel("test Category") +# plt.ylabel("test Quantity") + + +# return plot_to_base64(plt) + +# # Pie chart +# def pie_chart(): +# labels = ["Completed", "In Progress", "Pending"] +# sizes = [55, 20, 25] + +# plt.figure() +# plt.pie(sizes, labels=labels, autopct="%1.1f%%", startangle=140) +# plt.title("Project Status") + +# return plot_to_base64(plt) + +# # Histogram chart +# def histogram_chart(): +# daily_work = [5, 10, 15, 20, 20, 25, 30, 35, 40, 45, 50] + +# plt.figure() +# plt.hist(daily_work, bins=5) +# plt.title("Daily Work Distribution") +# plt.xlabel("Work Units") +# plt.ylabel("Frequency") + +# return plot_to_base64(plt) + +# # Dashboaed page +# @dashboard_bp.route("/") +# def dashboard(): +# if not session.get("user_id"): +# return redirect(url_for("auth.login")) + +# return render_template( +# "dashboard.html", +# title="Dashboard", +# bar_chart=bar_chart(), +# pie_chart=pie_chart(), +# histogram=histogram_chart() +# ) + +# # subcontractor dashboard +# @dashboard_bp.route("/subcontractor_dashboard", methods=["GET", "POST"]) +# def subcontractor_dashboard(): +# if not session.get("user_id"): +# return redirect(url_for("auth.login")) + +# tr_dash = DashboardService().bar_chart_of_tr_ex + + +# return render_template( +# "subcontractor_dashboard.html", +# title="Dashboard", +# bar_chart=tr_dash +# ) + +from flask import Blueprint, render_template, session, redirect, url_for, jsonify +from sqlalchemy import func +from app import db +from app.models.trench_excavation_model import TrenchExcavation +from app.models.manhole_excavation_model import ManholeExcavation +from app.models.laying_model import Laying + +dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/dashboard") + +@dashboard_bp.route("/api/live-stats") +def live_stats(): + try: + # 1. Overall Volume + t_count = TrenchExcavation.query.count() + m_count = ManholeExcavation.query.count() + l_count = Laying.query.count() + + # 2. Location Distribution (Business reach) + loc_results = db.session.query( + TrenchExcavation.Location, + func.count(TrenchExcavation.id) + ).group_by(TrenchExcavation.Location).all() + + # 3. Work Timeline (Business productivity trend) + # Assuming your models have a 'created_at' field + timeline_results = db.session.query( + func.date(TrenchExcavation.created_at), + func.count(TrenchExcavation.id) + ).group_by(func.date(TrenchExcavation.created_at)).order_by(func.date(TrenchExcavation.created_at)).all() + + return jsonify({ + "summary": { + "trench": t_count, + "manhole": m_count, + "laying": l_count, + "total": t_count + m_count + l_count + }, + "locations": {row[0]: row[1] for row in loc_results if row[0]}, + "timeline": {str(row[0]): row[1] for row in timeline_results} + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@dashboard_bp.route("/") +def dashboard(): + if not session.get("user_id"): + return redirect(url_for("auth.login")) + return render_template("dashboard.html", title="Business Intelligence Dashboard") \ No newline at end of file diff --git a/app/routes/file_format.py b/app/routes/file_format.py new file mode 100644 index 0000000..54ed193 --- /dev/null +++ b/app/routes/file_format.py @@ -0,0 +1,30 @@ +from flask import Blueprint, render_template, send_from_directory, abort, current_app +from app.utils.helpers import login_required +import os + +file_format_bp = Blueprint("file_format", __name__) + +@file_format_bp .route("/file_format") +@login_required +def download_format(): + return render_template("file_format.html", title="Download File Formats") + + +@file_format_bp .route("/file_format/download/") +@login_required +def download_excel_format(filename): + + download_folder = os.path.join( + current_app.root_path, "static", "downloads/format" + ) + + file_path = os.path.join(download_folder, filename) + + if not os.path.exists(file_path): + abort(404) + + return send_from_directory( + directory=download_folder, + path=filename, + as_attachment=True + ) diff --git a/app/routes/file_import.py b/app/routes/file_import.py new file mode 100644 index 0000000..1dda07d --- /dev/null +++ b/app/routes/file_import.py @@ -0,0 +1,46 @@ +from flask import Blueprint, render_template, request, flash +from app.services.file_service import FileService +from app.models.subcontractor_model import Subcontractor +from app.utils.helpers import login_required + +file_import_bp = Blueprint("file_import", __name__, url_prefix="/file") + +# this is contractractor immport routes +@file_import_bp.route("/import_Subcontractor", methods=["GET", "POST"]) +@login_required +def import_file(): + subcontractors = Subcontractor.query.all() + + if request.method == "POST": + file = request.files.get("file") + subcontractor_id = request.form.get("subcontractor_id") + RA_Bill_No = request.form.get("RA_Bill_No") + + service = FileService() + success, msg = service.handle_file_upload(file, subcontractor_id, RA_Bill_No) + + flash(msg, "success" if success else "danger") + + return render_template( + "file_import_subcontractor.html", + title="Sub-cont. File Import", + subcontractors=subcontractors + ) + + +# this is client import routes +@file_import_bp.route("/import_client", methods=["GET", "POST"]) +@login_required +def client_import_file(): + subcontractors = Subcontractor.query.all() + + if request.method == "POST": + file = request.files.get("file") + RA_Bill_No = request.form.get("RA_Bill_No") + + service = FileService() + success, msg = service.handle_client_file_upload(file, RA_Bill_No) + + flash(msg, "success" if success else "danger") + + return render_template("file_import_client.html", title="Client File Import", subcontractors=subcontractors) \ No newline at end of file diff --git a/app/routes/file_report.py b/app/routes/file_report.py new file mode 100644 index 0000000..0daaade --- /dev/null +++ b/app/routes/file_report.py @@ -0,0 +1,214 @@ +import pandas as pd +import io +from flask import Blueprint, render_template, request, send_file, flash +from app.utils.helpers import login_required + +from app.models.subcontractor_model import Subcontractor + +from app.models.manhole_excavation_model import ManholeExcavation +from app.models.trench_excavation_model import TrenchExcavation +from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber +from app.models.laying_model import Laying + +from app.models.mh_ex_client_model import ManholeExcavationClient +from app.models.tr_ex_client_model import TrenchExcavationClient +from app.models.mh_dc_client_model import ManholeDomesticChamberClient +from app.models.laying_client_model import LayingClient + + +# --- BLUEPRINT DEFINITION --- +file_report_bp = Blueprint("file_report", __name__, url_prefix="/file") + +# --- Client class --- +class ClientBill: + def __init__(self): + self.df_tr = pd.DataFrame() + self.df_mh = pd.DataFrame() + self.df_dc = pd.DataFrame() + self.df_laying = pd.DataFrame() + + def Fetch(self, RA_Bill_No): + trench = TrenchExcavationClient.query.filter_by(RA_Bill_No=RA_Bill_No).all() + mh = ManholeExcavationClient.query.filter_by(RA_Bill_No=RA_Bill_No).all() + dc = ManholeDomesticChamberClient.query.filter_by(RA_Bill_No=RA_Bill_No).all() + lay = LayingClient.query.filter_by(RA_Bill_No=RA_Bill_No).all() + + self.df_tr = pd.DataFrame([c.serialize() for c in trench]) + self.df_mh = pd.DataFrame([c.serialize() for c in mh]) + self.df_dc = pd.DataFrame([c.serialize() for c in dc]) + self.df_laying = pd.DataFrame([c.serialize() for c in lay]) + + drop_cols = ["id", "created_at", "_sa_instance_state"] + for df in [self.df_tr, self.df_mh, self.df_dc, self.df_laying]: + if not df.empty: + df.drop(columns=drop_cols, errors="ignore", inplace=True) + +# --- Subcontractor class --- +class SubcontractorBill: + def __init__(self): + self.df_tr = pd.DataFrame() + self.df_mh = pd.DataFrame() + self.df_dc = pd.DataFrame() + self.df_laying = pd.DataFrame() + + def Fetch(self, RA_Bill_No=None, subcontractor_id=None): + filters = {} + if subcontractor_id: + filters["subcontractor_id"] = subcontractor_id + if RA_Bill_No: + filters["RA_Bill_No"] = RA_Bill_No + + trench = TrenchExcavation.query.filter_by(**filters).all() + mh = ManholeExcavation.query.filter_by(**filters).all() + dc = ManholeDomesticChamber.query.filter_by(**filters).all() + lay = Laying.query.filter_by(**filters).all() + + self.df_tr = pd.DataFrame([c.serialize() for c in trench]) + self.df_mh = pd.DataFrame([c.serialize() for c in mh]) + self.df_dc = pd.DataFrame([c.serialize() for c in dc]) + self.df_laying = pd.DataFrame([c.serialize() for c in lay]) + + drop_cols = ["id", "created_at", "_sa_instance_state"] + for df in [self.df_tr, self.df_mh, self.df_dc, self.df_laying]: + if not df.empty: + df.drop(columns=drop_cols, errors="ignore", inplace=True) + + +# --- subcontractor report only --- +@file_report_bp.route("/Subcontractor_report", methods=["GET", "POST"]) +@login_required +def report_file(): + subcontractors = Subcontractor.query.all() + tables = None + selected_sc_id = None + ra_bill_no = None + download_all = False + + if request.method == "POST": + subcontractor_id = request.form.get("subcontractor_id") + ra_bill_no = request.form.get("ra_bill_no") + download_all = request.form.get("download_all") == "true" + action = request.form.get("action") + + if not subcontractor_id: + flash("Please select a subcontractor.", "danger") + return render_template("subcontractor_report.html", subcontractors=subcontractors) + + subcontractor = Subcontractor.query.get(subcontractor_id) + bill_gen = SubcontractorBill() + + if download_all: + bill_gen.Fetch(subcontractor_id=subcontractor_id) + file_name = f"{subcontractor.subcontractor_name}_ALL_BILLS.xlsx" + else: + if not ra_bill_no: + flash("Please enter an RA Bill Number.", "danger") + return render_template("subcontractor_report.html", subcontractors=subcontractors) + bill_gen.Fetch(RA_Bill_No=ra_bill_no, subcontractor_id=subcontractor_id) + file_name = f"{subcontractor.subcontractor_name}_RA_{ra_bill_no}_Report.xlsx" + + if bill_gen.df_tr.empty and bill_gen.df_mh.empty and bill_gen.df_dc.empty: + flash("No data found for this selection.", "warning") + return render_template("subcontractor_report.html", subcontractors=subcontractors) + + # If download is clicked, return file immediately + if action == "download": + output = io.BytesIO() + with pd.ExcelWriter(output, engine="xlsxwriter") as writer: + bill_gen.df_tr.to_excel(writer, index=False, sheet_name="Tr.Ex.") + bill_gen.df_mh.to_excel(writer, index=False, sheet_name="MH.Ex.") + bill_gen.df_dc.to_excel(writer, index=False, sheet_name="MH & DC") + bill_gen.df_laying.to_excel(writer, index=False, sheet_name="Laying") + output.seek(0) + return send_file(output, download_name=file_name, as_attachment=True) + + # We add bootstrap classes directly to the pandas output + table_classes = "table table-bordered table-striped table-hover table-sm mb-0" + tables = { + "tr": bill_gen.df_tr.to_html(classes=table_classes, index=False), + "mh": bill_gen.df_mh.to_html(classes=table_classes, index=False), + "dc": bill_gen.df_dc.to_html(classes=table_classes, index=False), + "laying": bill_gen.df_laying.to_html(classes=table_classes, index=False) + } + selected_sc_id = subcontractor_id + + return render_template( + "subcontractor_report.html", + subcontractors=subcontractors, + tables=tables, + selected_sc_id=selected_sc_id, + ra_bill_no=ra_bill_no, + download_all=download_all + ) + +# --- client report only --- +@file_report_bp.route("/client_report", methods=["GET", "POST"]) +@login_required +def client_vs_all_subcontractor(): + tables = {"tr": None, "mh": None, "dc": None} + ra_val = "" + + if request.method == "POST": + RA_Bill_No = request.form.get("RA_Bill_No") + ra_val = RA_Bill_No + + if not RA_Bill_No: + flash("Please enter RA Bill No.", "danger") + return render_template("client_report.html", tables=tables, ra_val=ra_val) + + clientBill = ClientBill() + clientBill.Fetch(RA_Bill_No=RA_Bill_No) + contractorBill = SubcontractorBill() + contractorBill.Fetch(RA_Bill_No=RA_Bill_No) + + # --- SAFETY CHECK: Verify data exists before merging --- + if clientBill.df_tr.empty and clientBill.df_mh.empty: + flash(f"No Client records found for RA Bill {RA_Bill_No}", "warning") + return render_template("client_report.html", tables=tables, ra_val=ra_val) + + qty_cols = [...] # (Keep your existing list) + mh_dc_qty_cols = [...] # (Keep your existing list) + mh_lay_qty_cols =[...] + + def aggregate_df(df, group_cols, sum_cols): + if df.empty: + # Create an empty DF with the correct columns to avoid Merge/Key Errors + return pd.DataFrame(columns=group_cols + sum_cols) + existing_cols = [c for c in sum_cols if c in df.columns] + # Ensure group_cols exist in the DF + for col in group_cols: + if col not in df.columns: + df[col] = "N/A" # Fill missing join keys + return df.groupby(group_cols, as_index=False)[existing_cols].sum() + + # Aggregate data + df_sub_tr_grp = aggregate_df(contractorBill.df_tr, ["Location", "MH_NO"], qty_cols) + df_sub_mh_grp = aggregate_df(contractorBill.df_mh, ["Location", "MH_NO"], qty_cols) + df_sub_dc_grp = aggregate_df(contractorBill.df_dc, ["Location", "MH_NO"], mh_dc_qty_cols) + df_sub_lay_grp = aggregate_df(contractorBill.df_dc, ["Location", "MH_NO"], mh_lay_qty_cols) + + # --- FINAL MERGE LOGIC --- + # We check if "Location" exists in the client data. If not, we add it to prevent the KeyError. + for df_client in [clientBill.df_tr, clientBill.df_mh, clientBill.df_dc, clientBill.df_laying ]: + if not df_client.empty and "Location" not in df_client.columns: + df_client["Location"] = "Unknown" + + try: + df_tr_cmp = clientBill.df_tr.merge(df_sub_tr_grp, on=["Location", "MH_NO"], how="left", suffixes=("_Client", "_Sub")) + df_mh_cmp = clientBill.df_mh.merge(df_sub_mh_grp, on=["Location", "MH_NO"], how="left", suffixes=("_Client", "_Sub")) + df_dc_cmp = clientBill.df_dc.merge(df_sub_dc_grp, on=["Location", "MH_NO"], how="left", suffixes=("_Client", "_Sub")) + df_lay_cmp = clientBill.df_laying.merge(df_sub_lay_grp, on=["Location", "MH_NO"], how="left", suffixes=("_Client", "_Sub")) + except KeyError as e: + flash(f"Merge Error: Missing column {str(e)}. Check if 'Location' is defined in your database models.", "danger") + return render_template("client_report.html", tables=tables, ra_val=ra_val) + + + # Convert to HTML for preview + tables["tr"] = df_tr_cmp.to_html(classes='table table-striped table-hover table-sm', index=False) + tables["mh"] = df_mh_cmp.to_html(classes='table table-striped table-hover table-sm', index=False) + tables["dc"] = df_dc_cmp.to_html(classes='table table-striped table-hover table-sm', index=False) + tables["laying"] = df_lay_cmp.to_html(classes='table table-striped table-hover table-sm', index=False) + + + return render_template("client_report.html", tables=tables, ra_val=ra_val) + \ No newline at end of file diff --git a/app/routes/generate_comparison_report.py b/app/routes/generate_comparison_report.py new file mode 100644 index 0000000..64c85db --- /dev/null +++ b/app/routes/generate_comparison_report.py @@ -0,0 +1,352 @@ +from flask import Blueprint, render_template, request, send_file, flash +from collections import defaultdict +import pandas as pd +import io + +from app.models.subcontractor_model import Subcontractor +from app.models.trench_excavation_model import TrenchExcavation +from app.models.manhole_excavation_model import ManholeExcavation +from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber +from app.models.laying_model import Laying + +from app.models.tr_ex_client_model import TrenchExcavationClient +from app.models.mh_ex_client_model import ManholeExcavationClient +from app.models.mh_dc_client_model import ManholeDomesticChamberClient +from app.models.laying_client_model import LayingClient + +from app.utils.helpers import login_required +import re + + +generate_report_bp = Blueprint("generate_report", __name__, url_prefix="/report") + + +# sum field of pipe laying (pipe_150_mm) +PIPE_MM_PATTERN = re.compile(r"^pipe_\d+_mm$") +# sum fields of MH dc (d_0_to_0_75) +D_RANGE_PATTERN = re.compile( r"^d_\d+(?:_\d+)?_to_\d+(?:_\d+)?$") + + +# NORMALIZER +def normalize_key(value): + if value is None: + return None + return str(value).strip().upper() + + +# HEADER FORMATTER +def format_header(header): + if "-" in header: + prefix, rest = header.split("-", 1) + prefix = prefix.title() + else: + prefix, rest = None, header + + parts = rest.split("_") + result = [] + i = 0 + + while i < len(parts): + if i + 1 < len(parts) and parts[i].isdigit() and parts[i + 1].isdigit(): + result.append(f"{parts[i]}.{parts[i + 1]}") + i += 2 + else: + result.append(parts[i].title()) + i += 1 + + final_text = " ".join(result) + return f"{prefix}-{final_text}" if prefix else final_text + + +# LOOKUP CREATOR +def make_lookup(rows, key_field): + lookup = {} + for r in rows: + location = normalize_key(r.get("Location")) + key_val = normalize_key(r.get(key_field)) + + if location and key_val: + lookup.setdefault((location, key_val), []).append(r) + + return lookup + + + +# COMPARISON BUILDER +def build_comparison(client_rows, contractor_rows, key_field): + contractor_lookup = make_lookup(contractor_rows, key_field) + output = [] + + used_index = defaultdict(int) # 🔥 THIS FIXES YOUR ISSUE + + for c in client_rows: + client_location = normalize_key(c.get("Location")) + client_key = normalize_key(c.get(key_field)) + + if not client_location or not client_key: + continue + + subs = contractor_lookup.get((client_location, client_key)) + if not subs: + continue + + idx = used_index[(client_location, client_key)] + + # ❗ If subcontractor rows are exhausted, skip + if idx >= len(subs): + continue + + s = subs[idx] # ✅ take NEXT subcontractor row + used_index[(client_location, client_key)] += 1 + + # ---- totals ---- + client_total = sum( + float(v or 0) + for k, v in c.items() + if k.endswith("_total") + or D_RANGE_PATTERN.match(k) + or PIPE_MM_PATTERN.match(k) + ) + + sub_total = sum( + float(v or 0) + for k, v in s.items() + if k.endswith("_total") + or D_RANGE_PATTERN.match(k) + or PIPE_MM_PATTERN.match(k) + ) + + row = { + "Location": client_location, + key_field.replace("_", " "): client_key + } + + for k, v in c.items(): + if k not in ["id", "created_at"]: + row[f"Client-{k}"] = v + + row["Client-Total"] = round(client_total, 2) + row[" "] = "" + + for k, v in s.items(): + if k not in ["id", "created_at", "subcontractor_id"]: + row[f"Subcontractor-{k}"] = v + + row["Subcontractor-Total"] = round(sub_total, 2) + row["Diff"] = round(client_total - sub_total, 2) + + output.append(row) + + df = pd.DataFrame(output) + df.columns = [format_header(col) for col in df.columns] + return df + + + + +# EXCEL SHEET WRITER +def write_sheet(writer, df, sheet_name, subcontractor_name): + workbook = writer.book + df.to_excel(writer, sheet_name=sheet_name, index=False, startrow=3) + ws = writer.sheets[sheet_name] + + title_fmt = workbook.add_format({"bold": True, "font_size": 14}) + client_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#B6DAED"}) + sub_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F3A081"}) + total_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F7D261"}) + diff_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#82DD49"}) + default_header_fmt = workbook.add_format({"bold": True,"border": 1,"bg_color": "#E7E6E6","align": "center","valign": "vcenter"}) + + + ws.merge_range( + 0, 0, 0, len(df.columns) - 1, + "CLIENT vs SUBCONTRACTOR", + title_fmt + ) + ws.merge_range( + 1, 0, 1, len(df.columns) - 1, + f"Subcontractor Name - {subcontractor_name}", + title_fmt + ) + + + for col_num, col_name in enumerate(df.columns): + if col_name.startswith("Client-"): + ws.write(3, col_num, col_name, client_fmt) + elif col_name.startswith("Subcontractor-"): + ws.write(3, col_num, col_name, sub_fmt) + elif col_name.endswith("_total") or col_name.endswith("_total") : + ws.write(3, col_num, col_name, total_fmt) + elif col_name == "Diff": + ws.write(3, col_num, col_name, diff_fmt) + else: + ws.write(3, col_num, col_name, default_header_fmt) + + ws.set_column(col_num, col_num, 20) + + +# REPORT ROUTE +@generate_report_bp.route("/comparison_report", methods=["GET", "POST"]) +@login_required +def comparison_report(): + subcontractors = Subcontractor.query.all() + + if request.method == "POST": + subcontractor_id = request.form.get("subcontractor_id") + if not subcontractor_id: + flash("Please select subcontractor", "danger") + return render_template("generate_comparison_report.html",subcontractors=subcontractors) + + subcontractor = Subcontractor.query.get_or_404(subcontractor_id) + + # -------- DATA -------- + tr_client = [r.serialize() for r in TrenchExcavationClient.query.all()] + tr_sub = [r.serialize() for r in TrenchExcavation.query.filter_by( + subcontractor_id=subcontractor_id + ).all()] + df_tr = build_comparison(tr_client, tr_sub, "MH_NO") + + mh_client = [r.serialize() for r in ManholeExcavationClient.query.all()] + mh_sub = [r.serialize() for r in ManholeExcavation.query.filter_by( + subcontractor_id=subcontractor_id + ).all()] + df_mh = build_comparison(mh_client, mh_sub, "MH_NO") + + dc_client = [r.serialize() for r in ManholeDomesticChamberClient.query.all()] + dc_sub = [r.serialize() for r in ManholeDomesticChamber.query.filter_by( + subcontractor_id=subcontractor_id + ).all()] + df_dc = build_comparison(dc_client, dc_sub, "MH_NO") + # df_dc = build_comparison_mh_dc(dc_client, dc_sub, "MH_NO") + + lay_client = [r.serialize() for r in LayingClient.query.all()] + lay_sub = [r.serialize() for r in Laying.query.filter_by( + subcontractor_id=subcontractor_id + ).all()] + df_lay = build_comparison(lay_client, lay_sub, "MH_NO") + # df_lay = build_comparison_laying(lay_client, lay_sub, "MH_NO") + + + # -------- EXCEL -------- + output = io.BytesIO() + filename = f"{subcontractor.subcontractor_name}_Comparison_Report.xlsx" + + with pd.ExcelWriter(output, engine="xlsxwriter") as writer: + write_sheet(writer, df_tr, "Tr.Ex", subcontractor.subcontractor_name) + write_sheet(writer, df_mh, "Mh.Ex", subcontractor.subcontractor_name) + write_sheet(writer, df_dc, "MH & DC", subcontractor.subcontractor_name) + write_sheet(writer, df_lay, "Laying", subcontractor.subcontractor_name) + + output.seek(0) + return send_file( + output, + as_attachment=True, + download_name=filename, + mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + + return render_template("generate_comparison_report.html",subcontractors=subcontractors) + + +# def build_comparison_mh_dc(client_rows, contractor_rows, key_field): +# contractor_lookup = make_lookup(contractor_rows, key_field) +# mh_dc_fields = ManholeDomesticChamberClient.sum_mh_dc_fields() + +# output = [] + +# for c in client_rows: +# loc = normalize_key(c.get("Location")) +# key = normalize_key(c.get(key_field)) +# if not loc or not key: +# continue + +# s = contractor_lookup.get((loc, key)) +# if not s: +# continue + +# client_total = sum(float(c.get(f, 0) or 0) for f in mh_dc_fields) +# sub_total = sum(float(s.get(f, 0) or 0) for f in mh_dc_fields) + +# row = { +# "Location": loc, +# key_field.replace("_", " "): key +# } + +# # CLIENT – ALL FIELDS +# for k, v in c.items(): +# if k in ["id", "created_at"]: +# continue +# row[f"Client-{k}"] = v + +# row["Client-Total"] = round(client_total, 2) +# row[" "] = "" + +# # SUBCONTRACTOR – ALL FIELDS +# for k, v in s.items(): +# if k in ["id", "created_at", "subcontractor_id"]: +# continue +# row[f"Subcontractor-{k}"] = v + +# row["Subcontractor-Total"] = round(sub_total, 2) +# row["Diff"] = round(client_total - sub_total, 2) + +# output.append(row) + +# df = pd.DataFrame(output) +# df.columns = [format_header(col) for col in df.columns] +# return df + + +# def build_comparison_laying(client_rows, contractor_rows, key_field): +# contractor_lookup = make_lookup(contractor_rows, key_field) +# laying_fields = Laying.sum_laying_fields() + +# output = [] + +# for c in client_rows: +# loc = normalize_key(c.get("Location")) +# key = normalize_key(c.get(key_field)) +# if not loc or not key: +# continue + +# s = contractor_lookup.get((loc, key)) +# if not s: +# continue + +# client_total = sum(float(c.get(f, 0) or 0) for f in laying_fields) +# sub_total = sum(float(s.get(f, 0) or 0) for f in laying_fields) + +# print("--------------",key,"----------") +# print("sum -client_total ",client_total) +# print("sum -sub_total ",sub_total) +# print("Diff ---- ",client_total - sub_total) +# print("------------------------") +# row = { +# "Location": loc, +# key_field.replace("_", " "): key +# } + +# # CLIENT – ALL FIELDS +# for k, v in c.items(): +# if k in ["id", "created_at"]: +# continue +# row[f"Client-{k}"] = v + +# row["Client-Total"] = round(client_total, 2) +# row[" "] = "" + +# # SUBCONTRACTOR – ALL FIELDS +# for k, v in s.items(): +# if k in ["id", "created_at", "subcontractor_id"]: +# continue +# row[f"Subcontractor-{k}"] = v + +# row["Subcontractor-Total"] = round(sub_total, 2) +# row["Diff"] = round(client_total - sub_total, 2) + +# output.append(row) + +# df = pd.DataFrame(output) +# df.columns = [format_header(col) for col in df.columns] +# return df + \ No newline at end of file diff --git a/app/routes/subcontractor_routes.py b/app/routes/subcontractor_routes.py new file mode 100644 index 0000000..5a6a4b6 --- /dev/null +++ b/app/routes/subcontractor_routes.py @@ -0,0 +1,191 @@ +from flask import Blueprint, render_template, request, redirect, flash +from app import db +from app.models.subcontractor_model import Subcontractor +from app.utils.helpers import login_required + +subcontractor_bp = Blueprint("subcontractor", __name__, url_prefix="/subcontractor") + +# ---------------- ADD ----------------- +@subcontractor_bp.route("/add") +@login_required +def add_subcontractor(): + return render_template("subcontractor/add.html") +@subcontractor_bp.route("/save", methods=["POST"]) +@login_required +def save_subcontractor(): + # 1. Get and clean the name from the form + name = request.form.get("subcontractor_name", "").strip() + + # 2. Basic validation: Ensure the name isn't empty + if not name: + flash("Subcontractor name cannot be empty.", "danger") + return redirect("/subcontractor/add") + + # 3. Check if a subcontractor with this name already exists + existing_sub = Subcontractor.query.filter_by(subcontractor_name=name).first() + + if existing_sub: + flash(f"Subcontractor with name '{name}' already exists!", "danger") + return redirect("/subcontractor/add") + + # 4. If no duplicate is found, proceed to save + try: + subcontractor = Subcontractor( + subcontractor_name=name, + contact_person=request.form.get("contact_person"), + mobile_no=request.form.get("mobile_no"), + email_id=request.form.get("email_id"), + gst_no=request.form.get("gst_no") + ) + + db.session.add(subcontractor) + db.session.commit() + flash("Subcontractor added successfully!", "success") + + except Exception as e: + db.session.rollback() + flash("An error occurred while saving. Please try again.", "danger") + + return redirect("/subcontractor/list") + +# ---------------- LIST ----------------- +@subcontractor_bp.route("/list") +@login_required +def subcontractor_list(): + subcontractors = Subcontractor.query.all() + return render_template("subcontractor/list.html", subcontractors=subcontractors) + +# ---------------- EDIT ----------------- +@subcontractor_bp.route("/edit/") +@login_required +def edit_subcontractor(id): + subcontractor = Subcontractor.query.get_or_404(id) + return render_template("subcontractor/edit.html", subcontractor=subcontractor) + +# ---------------- UPDATE ----------------- +@subcontractor_bp.route("/update/", methods=["POST"]) +@login_required +def update_subcontractor(id): + subcontractor = Subcontractor.query.get_or_404(id) + new_name = request.form.get("subcontractor_name") + + # Check if the new name is taken by someone ELSE (not this current ID) + duplicate = Subcontractor.query.filter( + Subcontractor.subcontractor_name == new_name, + Subcontractor.id != id + ).first() + + if duplicate: + flash("Another subcontractor already uses this name.", "danger") + return redirect(f"/subcontractor/edit/{id}") + + subcontractor.subcontractor_name = new_name + + db.session.commit() + + flash("Subcontractor updated successfully!", "success") + return redirect("/subcontractor/list") + +# ---------------- DELETE ----------------- +@subcontractor_bp.route("/delete/") +@login_required +def delete_subcontractor(id): + subcontractor = Subcontractor.query.get_or_404(id) + + db.session.delete(subcontractor) + db.session.commit() + + flash("Subcontractor deleted successfully!", "success") + return redirect("/subcontractor/list") +from flask import Blueprint, render_template, request, redirect, flash +from app import db +from app.models.subcontractor_model import Subcontractor +from app.utils.helpers import login_required + +subcontractor_bp = Blueprint("subcontractor", __name__, url_prefix="/subcontractor") + +# ---------------- ADD ----------------- +@subcontractor_bp.route("/add") +@login_required +def add_subcontractor(): + return render_template("subcontractor/add.html") + +@subcontractor_bp.route("/save", methods=["POST"]) +@login_required +def save_subcontractor(): + name = request.form.get("subcontractor_name", "").strip() + if not name: + flash("Subcontractor name cannot be empty.", "danger") + return redirect("/subcontractor/add") + existing_sub = Subcontractor.query.filter_by(subcontractor_name=name).first() + + if existing_sub: + flash(f"Subcontractor with name '{name}' already exists!", "danger") + return redirect("/subcontractor/add") + try: + subcontractor = Subcontractor( + subcontractor_name=name, + contact_person=request.form.get("contact_person"), + mobile_no=request.form.get("mobile_no"), + email_id=request.form.get("email_id"), + gst_no=request.form.get("gst_no") + ) + + db.session.add(subcontractor) + db.session.commit() + flash("Subcontractor added successfully!", "success") + + except Exception as e: + db.session.rollback() + flash("An error occurred while saving. Please try again.", "danger") + + return redirect("/subcontractor/list") + +# ---------------- LIST ----------------- +@subcontractor_bp.route("/list") +@login_required +def subcontractor_list(): + subcontractors = Subcontractor.query.all() + return render_template("subcontractor/list.html", subcontractors=subcontractors) + +# ---------------- EDIT ----------------- +@subcontractor_bp.route("/edit/") +@login_required +def edit_subcontractor(id): + subcontractor = Subcontractor.query.get_or_404(id) + return render_template("subcontractor/edit.html", subcontractor=subcontractor) + +# ---------------- UPDATE ----------------- +@subcontractor_bp.route("/update/", methods=["POST"]) +@login_required +def update_subcontractor(id): + subcontractor = Subcontractor.query.get_or_404(id) + new_name = request.form.get("subcontractor_name") + + # Check if the new name is taken by someone ELSE (not this current ID) + duplicate = Subcontractor.query.filter( + Subcontractor.subcontractor_name == new_name, + Subcontractor.id != id + ).first() + + if duplicate: + flash("Another subcontractor already uses this name.", "danger") + return redirect(f"/subcontractor/edit/{id}") + + subcontractor.subcontractor_name = new_name + db.session.commit() + + flash("Subcontractor updated successfully!", "success") + return redirect("/subcontractor/list") + +# ---------------- DELETE ----------------- +@subcontractor_bp.route("/delete/") +@login_required +def delete_subcontractor(id): + subcontractor = Subcontractor.query.get_or_404(id) + + db.session.delete(subcontractor) + db.session.commit() + + flash("Subcontractor deleted successfully!", "success") + return redirect("/subcontractor/list") \ No newline at end of file diff --git a/app/routes/user.py b/app/routes/user.py new file mode 100644 index 0000000..5f16e3c --- /dev/null +++ b/app/routes/user.py @@ -0,0 +1,11 @@ +from flask import Blueprint, render_template +from app.services.user_service import UserService +from app.utils.helpers import login_required + +user_bp = Blueprint("user", __name__, url_prefix="/user") + +@user_bp.route("/list") +@login_required +def list_users(): + users = UserService.get_all_users() + return render_template("users.html", users=users, title="Users") diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/dashboard_service.py b/app/services/dashboard_service.py new file mode 100644 index 0000000..af082ad --- /dev/null +++ b/app/services/dashboard_service.py @@ -0,0 +1,49 @@ +from app.config import Config +from app import db +import matplotlib +matplotlib.use("Agg") +from app.routes.dashboard import plot_to_base64 +import matplotlib.pyplot as plt +import io +import base64 + +from app.utils.plot_utils import plot_to_base64 + +# Subcontractor models import +from app.models.trench_excavation_model import TrenchExcavation + +# Client models import +# from app.models.tr_ex_client_model import TrenchExcavationClient + + +class DashboardService: + + + # bar chart + def bar_chart_of_tr_ex(): + categories = ["Soft Murum", "Hard Murum", "Soft Rock", "Hard Rock"] + values = [120, 80, 150, 60] + + tr = TrenchExcavation() + + record = TrenchExcavation.query.first() + print(" RA_Bill_No::",record["RA_Bill_No"]) + + totals = tr.excavation_category_sums() + + # print(totals["Soft_Murum_Total"]) + # print(totals["Hard_Rock_Total"]) + + + plt.figure() + plt.bar(categories, values) + plt.title("Trench Excavation Work Category Report") + plt.xlabel("Excavation category") + plt.ylabel("Quantity") + + return plot_to_base64(plt) + + + def subcontractor_dash(): + return True + \ No newline at end of file diff --git a/app/services/db_service.py b/app/services/db_service.py new file mode 100644 index 0000000..f8adddc --- /dev/null +++ b/app/services/db_service.py @@ -0,0 +1,19 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate + +db = SQLAlchemy() +migrate = Migrate() + + +# import mysql.connector +# from app.config import Config + +# class DBService: + +# def connect(self): +# return mysql.connector.connect( +# host=Config.DB_HOST, +# user=Config.DB_USER, +# password=Config.DB_PASSWORD, +# database=Config.DB_NAME +# ) diff --git a/app/services/file_service.py b/app/services/file_service.py new file mode 100644 index 0000000..206ec1c --- /dev/null +++ b/app/services/file_service.py @@ -0,0 +1,375 @@ +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 + +# Subcontractor models import +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 + + + +class FileService: + + # ---------------- COMMON HELPERS ---------------- + def allowed_file(self, filename): + return ("." in filename and filename.rsplit(".", 1)[1].lower() in Config.ALLOWED_EXTENSIONS) + + def normalize(self, val): + if val is None or pd.isna(val): + return None + + val = str(val).strip() + if val.lower() in ["", "nan", "none", "-", "—"]: + return None + + return val.upper() + + # ---------------- SUBCONTRACTOR FILE UPLOAD ---------------- + def handle_file_upload(self, file, subcontractor_id, RA_Bill_No): + + if not subcontractor_id: + return False, "Please select subcontractor." + + if not RA_Bill_No: + return False, "Please Enter RA Bill No." + + if not file or file.filename == "": + return False, "No file selected." + + if not self.allowed_file(file.filename): + return False, "Invalid file type! Allowed: CSV, XLSX, XLS" + + ensure_upload_folder() + + folder = os.path.join(Config.UPLOAD_FOLDER, f"sub_{subcontractor_id}") + os.makedirs(folder, exist_ok=True) + + filename = secure_filename(file.filename) + filepath = os.path.join(folder, filename) + file.save(filepath) + + try: + df_tr_ex = pd.read_excel(filepath, sheet_name="Tr.Ex.", header=12, dtype={"MH No": str}) + df_mh_ex = pd.read_excel(filepath, sheet_name="MH Ex.", header=12, dtype={"MH No": str}) + df_mh_dc = pd.read_excel(filepath, sheet_name="MH & DC", header=11, dtype={"MH No": str}) + df_laying = pd.read_excel(filepath, sheet_name="Laying", header=11, dtype={"MH No": str}) + + self.process_trench_excavation(df_tr_ex, subcontractor_id, RA_Bill_No) + self.process_manhole_excavation(df_mh_ex, subcontractor_id, RA_Bill_No) + self.process_manhole_domestic_chamber(df_mh_dc, subcontractor_id, RA_Bill_No) + self.process_laying(df_laying, subcontractor_id, RA_Bill_No) + + return True, "SUBCONTRACTOR File uploaded successfully." + + except Exception as e: + db.session.rollback() + return False, f"Import failed: {e}" + + # ---------------- Trench Excavation (Subcontractor) ---------------- + def process_trench_excavation(self, df, subcontractor_id, RA_Bill_No): + + df.columns = ( + df.columns.astype(str) + .str.strip() + .str.replace(r"[^\w]", "_", regex=True) + .str.replace("__+", "_", regex=True) + .str.strip("_") + ) + + df = df.dropna(how="all") + + if "Location" in df.columns: + df["Location"] = df["Location"].ffill() + + errors = [] + + for idx, row in df.iterrows(): + location = self.normalize(row.get("Location")) + mh_no = self.normalize(row.get("MH_NO")) + + if not location or not mh_no: + continue + + # exists = TrenchExcavation.query.filter_by( + # subcontractor_id=subcontractor_id, + # RA_Bill_No=RA_Bill_No, + # Location=location, + # MH_NO=mh_no, + # ).first() + + # if exists: + # errors.append( + # f"Model-Tr.Ex. (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}" + # ) + # continue + + record_data = {} + for col in df.columns: + if hasattr(TrenchExcavation, col): + val = row[col] + if pd.isna(val) or str(val).strip() in ["", "-", "—", "nan"]: + val = None + record_data[col] = val + + record = TrenchExcavation( + subcontractor_id=subcontractor_id, + RA_Bill_No=RA_Bill_No, + **record_data, + ) + db.session.add(record) + + if errors: + raise Exception(" | ".join(errors)) + + db.session.commit() + + # ---------------- Manhole Excavation (Subcontractor) ---------------- + def process_manhole_excavation(self, df, subcontractor_id, RA_Bill_No): + + df.columns = ( + df.columns.astype(str) + .str.strip() + .str.replace(r"[^\w]", "_", regex=True) + .str.replace("__+", "_", regex=True) + .str.strip("_") + ) + + df = df.dropna(how="all") + + if "Location" in df.columns: + df["Location"] = df["Location"].ffill() + + errors = [] + + for idx, row in df.iterrows(): + location = self.normalize(row.get("Location")) + mh_no = self.normalize(row.get("MH_NO")) + + if not location or not mh_no: + continue + + # exists = ManholeExcavation.query.filter_by( + # subcontractor_id=subcontractor_id, + # RA_Bill_No=RA_Bill_No, + # Location=location, + # MH_NO=mh_no, + # ).first() + + # if exists: + # errors.append( + # f"Model-MH Ex. (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}" + # ) + # continue + + record_data = {} + for col in df.columns: + if hasattr(ManholeExcavation, col): + val = row[col] + if pd.isna(val) or str(val).strip() in ["", "-", "—", "nan"]: + val = None + record_data[col] = val + + record = ManholeExcavation( + subcontractor_id=subcontractor_id, + RA_Bill_No=RA_Bill_No, + **record_data, + ) + db.session.add(record) + + if errors: + raise Exception(" | ".join(errors)) + + db.session.commit() + + # ---------------- Manhole & Domestic Chamber (Subcontractor) ---------------- + def process_manhole_domestic_chamber(self, df, subcontractor_id, RA_Bill_No): + + df.columns = ( + df.columns.astype(str) + .str.strip() + .str.replace(r"[^\w]", "_", regex=True) + .str.replace("__+", "_", regex=True) + .str.strip("_") + ) + + df = df.dropna(how="all") + + if "Location" in df.columns: + df["Location"] = df["Location"].ffill() + + errors = [] + + for idx, row in df.iterrows(): + location = self.normalize(row.get("Location")) + mh_no = self.normalize(row.get("MH_NO")) + + if not location or not mh_no: + continue + + # exists = ManholeDomesticChamber.query.filter_by( + # subcontractor_id=subcontractor_id, + # RA_Bill_No=RA_Bill_No, + # Location=location, + # MH_NO=mh_no, + # ).first() + + # if exists: + # errors.append( + # f"Model-MH & DC (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}" + # ) + # continue + + record_data = {} + for col in df.columns: + if hasattr(ManholeDomesticChamber, col): + val = row[col] + if pd.isna(val) or str(val).strip() in ["", "-", "—", "nan"]: + val = None + record_data[col] = val + + record = ManholeDomesticChamber( + subcontractor_id=subcontractor_id, + RA_Bill_No=RA_Bill_No, + **record_data, + ) + db.session.add(record) + + if errors: + raise Exception(" | ".join(errors)) + + db.session.commit() + + # ---------------- Laying (Subcontractor) ---------------- + def process_laying(self, df, subcontractor_id, RA_Bill_No): + + df.columns = ( + df.columns.astype(str) + .str.strip() + .str.replace(r"[^\w]", "_", regex=True) + .str.replace("__+", "_", regex=True) + .str.strip("_") + ) + + df = df.dropna(how="all") + + if "Location" in df.columns: + df["Location"] = df["Location"].ffill() + + errors = [] + + for idx, row in df.iterrows(): + location = self.normalize(row.get("Location")) + mh_no = self.normalize(row.get("MH_NO")) + + if not location or not mh_no: + continue + + # exists = ManholeDomesticChamber.query.filter_by( + # subcontractor_id=subcontractor_id, + # RA_Bill_No=RA_Bill_No, + # Location=location, + # MH_NO=mh_no, + # ).first() + + # if exists: + # errors.append( + # f"Model-MH & DC (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}" + # ) + # continue + + record_data = {} + for col in df.columns: + if hasattr(Laying, col): + val = row[col] + if pd.isna(val) or str(val).strip() in ["", "-", "—", "nan"]: + val = None + record_data[col] = val + + record = Laying( + subcontractor_id=subcontractor_id, + RA_Bill_No=RA_Bill_No, + **record_data, + ) + db.session.add(record) + + if errors: + raise Exception(" | ".join(errors)) + + db.session.commit() + + + + # ---------------- CLIENT FILE UPLOAD ---------------- + def handle_client_file_upload(self, file, RA_Bill_No): + + if not RA_Bill_No: + return False, "Please Enter RA Bill No." + + if not file or file.filename == "": + return False, "No file selected." + + if not self.allowed_file(file.filename): + return False, "Invalid file type! Allowed: CSV, XLSX, XLS" + + ensure_upload_folder() + + folder = os.path.join(Config.UPLOAD_FOLDER, f"Client_Bill_{RA_Bill_No}") + os.makedirs(folder, exist_ok=True) + + filename = secure_filename(file.filename) + filepath = os.path.join(folder, filename) + file.save(filepath) + + try: + df_tr_ex = pd.read_excel(filepath, sheet_name="Tr.Ex.", header=4) + df_mh_ex = pd.read_excel(filepath, sheet_name="MH Ex.", header=4) + df_mh_dc = pd.read_excel(filepath, sheet_name="MH & DC", header=3) + df_lay = pd.read_excel(filepath, sheet_name="Laying & Bedding", header=3) + + self.save_client_data(df_tr_ex, TrenchExcavationClient, RA_Bill_No) + self.save_client_data(df_mh_ex, ManholeExcavationClient, RA_Bill_No) + self.save_client_data(df_mh_dc, ManholeDomesticChamberClient, RA_Bill_No) + self.save_client_data(df_lay, LayingClient, RA_Bill_No) + + db.session.commit() + return True, "Client file uploaded successfully." + + except Exception as e: + db.session.rollback() + return False, f"Client import failed: {e}" + + # ---------------- CLIENT SAVE METHOD ---------------- + def save_client_data(self, df, model, RA_Bill_No): + + df.columns = [str(c).strip() for c in df.columns] + + if "Location" in df.columns: + df["Location"] = df["Location"].ffill() + + df = df.dropna(how="all") + + for idx, row in df.iterrows(): + record_data = {} + + for col in df.columns: + if hasattr(model, col): + val = row[col] + if pd.isna(val) or str(val).strip() in ["", "-", "—", "nan"]: + val = None + record_data[col] = val + + record = model(RA_Bill_No=RA_Bill_No, **record_data) + db.session.add(record) + diff --git a/app/services/user_service.py b/app/services/user_service.py new file mode 100644 index 0000000..59f787b --- /dev/null +++ b/app/services/user_service.py @@ -0,0 +1,27 @@ +from app.models.user_model import User +from app.services.db_service import db + +class UserService: + + @staticmethod + def register_user(name, email, password): + if User.query.filter_by(email=email).first(): + return None + + user = User(name=name, email=email) + user.set_password(password) + + db.session.add(user) + db.session.commit() + return user + + @staticmethod + def validate_login(email, password): + user = User.query.filter_by(email=email).first() + if user and user.check_password(password): + return user + return None + + @staticmethod + def get_all_users(): + return User.query.all() diff --git a/app/static/downloads/format/client_format.xlsx b/app/static/downloads/format/client_format.xlsx new file mode 100644 index 0000000..8dc754e Binary files /dev/null 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 new file mode 100644 index 0000000..6991030 Binary files /dev/null and b/app/static/downloads/format/subcontractor_format.xlsx differ diff --git a/app/static/images/lcepl.png b/app/static/images/lcepl.png new file mode 100644 index 0000000..b4aa03b Binary files /dev/null and b/app/static/images/lcepl.png differ diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..62effa3 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,217 @@ + + + + + + + {{ title if title else "Comparison Software" }} + + + + + + + + + + + + + + + + +
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} + +
+ {% endfor %} + {% endif %} + {% endwith %} +
+ + +
+
+ {% block content %}{% endblock %} +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/app/templates/client_report.html b/app/templates/client_report.html new file mode 100644 index 0000000..ab95e43 --- /dev/null +++ b/app/templates/client_report.html @@ -0,0 +1,74 @@ +{% extends "base.html" %} + +{% block content %} +
+

Client File Reports

+ +
+
+ + + +
+
+ +
+
+ +
+
+
+
+ + {% if tables.tr or tables.mh or tables.dc or tables.laying %} +
+

Comparison Preview

+ + + +
+
+
+ {{ tables.tr|safe }} +
+
+
+
+ {{ tables.mh|safe }} +
+
+
+
+ {{ tables.dc|safe }} +
+
+
+
+ {{ tables.laying|safe }} +
+ +
+
+ {% endif %} + +
+ {% endblock %} \ No newline at end of file diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html new file mode 100644 index 0000000..b6a6de3 --- /dev/null +++ b/app/templates/dashboard.html @@ -0,0 +1,118 @@ +{% extends "base.html" %} + +{% block content %} +
+

Comparison Software Solapur (UGD) - Live Dashboard

+ +
+
+
+
+
Trenching Units
+

0

+
+
+
+
+
+
+
Manhole Units
+

0

+
+
+
+
+
+
+
Laying Units
+

0

+
+
+
+
+ +
+
+
+
Live Category Bar Chart
+
+ +
+
+
+ +
+
+
Location Distribution Pie Chart
+
+ +
+
+
+
+
+ + + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/file_format.html b/app/templates/file_format.html new file mode 100644 index 0000000..366b85b --- /dev/null +++ b/app/templates/file_format.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} + +{% block content %} +

Download File Formats

+ +
+ + +
+
+
+
+ ⬇️ +
+
Subcontractor Upload Format
+

Excel (.xlsx)

+

File size: 245 KB

+ + + Download + +
+
+
+ + +
+
+
+
+ ⬇️ +
+
Client Upload Format
+

Excel (.xlsx)

+

File size: 310 KB

+ + + Download + +
+
+
+ + + + +
+{% endblock %} \ No newline at end of file diff --git a/app/templates/file_import_client.html b/app/templates/file_import_client.html new file mode 100644 index 0000000..e44ffd9 --- /dev/null +++ b/app/templates/file_import_client.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{% block content %} +

Client File Import

+ +
+ +
+ + + + + + + + + + + + + + +
+ +
+{% endblock %} \ No newline at end of file diff --git a/app/templates/file_import_subcontractor.html b/app/templates/file_import_subcontractor.html new file mode 100644 index 0000000..9b5bb45 --- /dev/null +++ b/app/templates/file_import_subcontractor.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} + +{% block content %} +

Sub-Contractor File Import

+ +
+ +
+ + + + + + + + + + + + + + +
+ +
+{% endblock %} \ No newline at end of file diff --git a/app/templates/generate_comparison_report.html b/app/templates/generate_comparison_report.html new file mode 100644 index 0000000..073f963 --- /dev/null +++ b/app/templates/generate_comparison_report.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block content %} +
+ +

Subcontractor vs Client Comparison

+ + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + +
+ +
+ + + + + + +
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/list_user.html b/app/templates/list_user.html new file mode 100644 index 0000000..ae534d6 --- /dev/null +++ b/app/templates/list_user.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} + +{% block content %} +

Users List

+ +
+ + + + + + + + + + + {% for user in users %} + + + + + + {% endfor %} + +
IDNameEmail
{{ user.id }}{{ user.name }}{{ user.email }}
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..5fffe67 --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,82 @@ + + + + + + LCEPL | Login + + + + + + + + +
+
+ + +
+ +
+ +
+ + +
+ LCEPL Logo + +

+ Laxmi Civil Engineering Services Pvt Ltd +

+

+ Data Comparison Software Solapur(UGD) +

+
+ + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+
+ +
+ +
+
+ + + + + + \ No newline at end of file diff --git a/app/templates/register.html b/app/templates/register.html new file mode 100644 index 0000000..80090fd --- /dev/null +++ b/app/templates/register.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% block content %} +

Register User

+ +
+
+ + + + + + + + + + +
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/subcontractor/add.html b/app/templates/subcontractor/add.html new file mode 100644 index 0000000..ef73ac1 --- /dev/null +++ b/app/templates/subcontractor/add.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% block content %} + +
+ +

Add New Subcontractor

+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/subcontractor/edit.html b/app/templates/subcontractor/edit.html new file mode 100644 index 0000000..4c4c037 --- /dev/null +++ b/app/templates/subcontractor/edit.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% block content %} + +
+

Edit Subcontractor

+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + Back + +
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/subcontractor/list.html b/app/templates/subcontractor/list.html new file mode 100644 index 0000000..2acd851 --- /dev/null +++ b/app/templates/subcontractor/list.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} +{% block content %} + +
+ + +
+
+

+ Subcontractor List +

+
+ + +
+ + +
+ + + + + + + + + + + + + + {% for s in subcontractors %} + + + + + + + + + + + + + + + {% endfor %} + + + + + + + + + + + + + + + + +
IDNameMobileEmailGST NoAction
{{ s.id }} + {{ s.subcontractor_name }} + + {{ s.mobile_no }} + + {{ s.email_id }} + + {{ s.gst_no }} + + +
Total Subcontractors---{{ subcontractors|length }}
+
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/subcontractor_dashboard.html b/app/templates/subcontractor_dashboard.html new file mode 100644 index 0000000..f1aa700 --- /dev/null +++ b/app/templates/subcontractor_dashboard.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block content %} + +
+ +

Subcontractor Dashboard

+ + +
+ + +
+
+
+ Work Category Bar Chart +
+
+ +
+
+
+ + +
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/subcontractor_report.html b/app/templates/subcontractor_report.html new file mode 100644 index 0000000..47768f4 --- /dev/null +++ b/app/templates/subcontractor_report.html @@ -0,0 +1,142 @@ +{% extends "base.html" %} + +{% block content %} +
+ +

Generate Subcontractor Report

+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+ + {% if tables %} + +
+ +

Report Preview

+ +
+ + +
+ +
+ + +
+ +
+
+ {{ tables.tr | safe }} +
+
+ +
+
+ {{ tables.mh | safe }} +
+
+ +
+
+ {{ tables.dc | safe }} +
+
+ +
+
+ {{ tables.laying | safe }} +
+
+ +
+
+
+ {% endif %} + +
+ + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/users.htm b/app/templates/users.htm new file mode 100644 index 0000000..ae534d6 --- /dev/null +++ b/app/templates/users.htm @@ -0,0 +1,28 @@ +{% extends "base.html" %} + +{% block content %} +

Users List

+ +
+ + + + + + + + + + + {% for user in users %} + + + + + + {% endfor %} + +
IDNameEmail
{{ user.id }}{{ user.name }}{{ user.email }}
+
+ +{% endblock %} \ No newline at end of file diff --git a/app/utils/file_utils.py b/app/utils/file_utils.py new file mode 100644 index 0000000..8a79d69 --- /dev/null +++ b/app/utils/file_utils.py @@ -0,0 +1,6 @@ +import os +from app.config import Config + +def ensure_upload_folder(): + if not os.path.exists(Config.UPLOAD_FOLDER): + os.makedirs(Config.UPLOAD_FOLDER) diff --git a/app/utils/helpers.py b/app/utils/helpers.py new file mode 100644 index 0000000..ba87d21 --- /dev/null +++ b/app/utils/helpers.py @@ -0,0 +1,14 @@ +# def is_logged_in(session): +# return session.get("user_id") is not None + +from functools import wraps +from flask import session, redirect, url_for, flash + +def login_required(view): + @wraps(view) + def wrapped_view(*args, **kwargs): + if "user_id" not in session: + flash("Please login first", "warning") + return redirect(url_for("auth.login")) + return view(*args, **kwargs) + return wrapped_view diff --git a/app/utils/plot_utils.py b/app/utils/plot_utils.py new file mode 100644 index 0000000..5bed985 --- /dev/null +++ b/app/utils/plot_utils.py @@ -0,0 +1,10 @@ +import base64 +from io import BytesIO +import matplotlib +matplotlib.use("Agg") + +def plot_to_base64(plt): + img = BytesIO() + plt.savefig(img, format="png", bbox_inches="tight") + img.seek(0) + return base64.b64encode(img.read()).decode("utf-8") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..69a2fbe --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,46 @@ +version: '3.8' + +services: + db: + image: mysql:8.0 + container_name: comparison_db + restart: always + environment: + MYSQL_ROOT_PASSWORD: admin + MYSQL_DATABASE: comparisondb + ports: + - "3307:3306" + volumes: + - mysql_data:/var/lib/mysql + + app: + build: . + container_name: comparison_app + restart: always + environment: + FLASK_ENV: development + FLASK_DEBUG: "True" + FLASK_HOST: "0.0.0.0" + FLASK_PORT: "5001" + + DB_DIALECT: mysql + DB_DRIVER: pymysql + DB_HOST: db + DB_PORT: 3306 + DB_NAME: comparisondb + DB_USER: root + DB_PASSWORD: admin + + ports: + - "5001:5001" + + depends_on: + - db + + volumes: + - ./app/logs:/app/app/logs + - ./app/static/uploads:/app/app/static/uploads + - ./app/static/downloads:/app/app/static/downloads + +volumes: + mysql_data: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7b7ceb7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +Flask +pandas +openpyxl +xlrd +Werkzeug +python-dotenv +pymysql +sqlalchemy +flask + +cryptography +xlsxwriter +matplotlib +flask_sqlalchemy +flask_migrate \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..bdd7cf6 --- /dev/null +++ b/run.py @@ -0,0 +1,17 @@ +from dotenv import load_dotenv +load_dotenv() +from app import create_app +from app.services.db_service import db +import os + +app = create_app() + +if __name__ == "__main__": + with app.app_context(): + db.create_all() + + app.run( + host=os.getenv("FLASK_HOST"), + port=int(os.getenv("FLASK_PORT")), + debug=os.getenv("FLASK_DEBUG") == "True" + )