commit 844dcbee814b3afbf32feeff9e263ac2c4c260b9 Author: anishd100 Date: Tue Jan 13 15:58:52 2026 +0530 added client RA bill wise download report diff --git a/.env b/.env new file mode 100644 index 0000000..f910974 --- /dev/null +++ b/.env @@ -0,0 +1,31 @@ +# ----------------------------- +# 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=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/README.md b/README.md new file mode 100644 index 0000000..af05bfa --- /dev/null +++ b/README.md @@ -0,0 +1,570 @@ +# 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 +``` + +--- + +## 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 Configuration +FLASK_HOST=127.0.0.1 +FLASK_PORT=5000 +FLASK_DEBUG=True +SECRET_KEY=your-secret-key-here + +# Database Configuration +DB_DIALECT=mysql # or postgresql, sqlite +DB_DRIVER=pymysql # or psycopg2, etc. +DB_USER=root +DB_PASSWORD=your_password +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=comparison_project +``` + +### 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..3eb635b --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,83 @@ +# from flask import Flask +# from app.config import Config +# from app.services.db_service import db + +# def create_app(): +# app = Flask(__name__) +# app.config.from_object(Config) + +# db.init_app(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 + +# 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) + +# return app + + +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/manhole_domestic_chamber_model.py b/app/models/manhole_domestic_chamber_model.py new file mode 100644 index 0000000..98d4881 --- /dev/null +++ b/app/models/manhole_domestic_chamber_model.py @@ -0,0 +1,51 @@ +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} 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..6ea30bc --- /dev/null +++ b/app/models/mh_dc_client_model.py @@ -0,0 +1,46 @@ +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} 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..446bf55 --- /dev/null +++ b/app/models/trench_excavation_model.py @@ -0,0 +1,79 @@ +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} 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..04a4544 --- /dev/null +++ b/app/routes/dashboard.py @@ -0,0 +1,11 @@ + +from flask import Blueprint, render_template, session, redirect, url_for + +dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/dashboard") + +@dashboard_bp.route("/") +def dashboard(): + if not session.get("user_id"): + return redirect(url_for("auth.login")) + + return render_template("dashboard.html", title="Dashboard") 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..9983308 --- /dev/null +++ b/app/routes/file_import.py @@ -0,0 +1,45 @@ +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") + +@file_import_bp.route("/import", 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.html", + title="Sub-cont. File Import", + subcontractors=subcontractors + ) + + +# this route import client files +@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) diff --git a/app/routes/file_report.py b/app/routes/file_report.py new file mode 100644 index 0000000..5128b29 --- /dev/null +++ b/app/routes/file_report.py @@ -0,0 +1,222 @@ +from flask import Blueprint, render_template, request, send_file, flash +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.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.utils.helpers import login_required + +import pandas as pd +import io +from enum import Enum + +# --- 1. DEFINE BLUEPRINT FIRST (Prevents NameError) --- +file_report_bp = Blueprint("file_report", __name__, url_prefix="/file") + +class BillType(Enum): + Client = 1 + Subcontractor = 2 + +# --- 2. DEFINE CLASSES --- +class SubcontractorBill: + def __init__(self): + # Initialize as empty DataFrames so .to_excel() always exists + self.df_tr = pd.DataFrame() + self.df_mh = pd.DataFrame() + self.df_dc = pd.DataFrame() + + def Fetch(self, RA_Bill_No, subcontractor_id): + # Query data filtered by both Bill No and Subcontractor ID + trench = TrenchExcavation.query.filter_by(RA_Bill_No=RA_Bill_No, subcontractor_id=subcontractor_id).all() + mh = ManholeExcavation.query.filter_by(RA_Bill_No=RA_Bill_No, subcontractor_id=subcontractor_id).all() + dc = ManholeDomesticChamber.query.filter_by(RA_Bill_No=RA_Bill_No, subcontractor_id=subcontractor_id).all() + + # Convert SQL objects to DataFrames + self.df_tr = pd.DataFrame([c.__dict__ for c in trench]) + self.df_mh = pd.DataFrame([c.__dict__ for c in mh]) + self.df_dc = pd.DataFrame([c.__dict__ for c in dc]) + + # Clean Columns (remove SQLAlchemy internal state) + drop_cols = ["id", "created_at", "_sa_instance_state"] + + for df in [self.df_tr, self.df_mh, self.df_dc]: + if not df.empty: + df.drop(columns=drop_cols, errors="ignore", inplace=True) + +# --- 3. DEFINE ROUTES --- +@file_report_bp.route("/report", methods=["GET", "POST"]) +@login_required +def report_file(): + subcontractors = Subcontractor.query.all() + + if request.method == "POST": + subcontractor_id = request.form.get("subcontractor_id") + ra_bill_no = request.form.get("ra_bill_no") # Collected from the updated HTML + + if not subcontractor_id or not ra_bill_no: + flash("Please select a subcontractor and enter an RA Bill Number.", "danger") + return render_template("report.html", subcontractors=subcontractors) + + subcontractor = Subcontractor.query.get(subcontractor_id) + + # Instantiate and Fetch Data + bill_gen = SubcontractorBill() + bill_gen.Fetch(RA_Bill_No=ra_bill_no, subcontractor_id=subcontractor_id) + + # Check if any data was found + if bill_gen.df_tr.empty and bill_gen.df_mh.empty and bill_gen.df_dc.empty: + flash(f"No data found for {subcontractor.subcontractor_name} in RA Bill {ra_bill_no}", "warning") + return render_template("report.html", subcontractors=subcontractors) + + # WRITE EXCEL FILE + output = io.BytesIO() + file_name = f"{subcontractor.subcontractor_name}_RA_{ra_bill_no}_Report.xlsx" + + 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") + + output.seek(0) + return send_file( + output, + download_name=file_name, + as_attachment=True, + mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + + return render_template("report.html", subcontractors=subcontractors) + +# (ClientBill class and client_vs_all_subcontractor route would follow here...) + +import pandas as pd +import io +from flask import Blueprint, render_template, request, send_file, flash +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.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.utils.helpers import login_required + +# --- BLUEPRINT DEFINITION --- +# Ensure this is unique to avoid conflicts +file_report_bp = Blueprint("file_report", __name__, url_prefix="/file") + +class ClientBill: + def __init__(self): + self.df_tr = pd.DataFrame() + self.df_mh = pd.DataFrame() + self.df_dc = 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() + + 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]) + + # Standardize columns for merging + if not self.df_dc.empty and "MH_NO" in self.df_dc.columns: + self.df_dc.rename(columns={"MH_NO": "Node_No"}, inplace=True) + + drop_cols = ["id", "created_at", "_sa_instance_state"] + for df in [self.df_tr, self.df_mh, self.df_dc]: + if not df.empty: + df.drop(columns=drop_cols, errors="ignore", inplace=True) + +class SubcontractorBill: + def __init__(self): + self.df_tr = pd.DataFrame() + self.df_mh = pd.DataFrame() + self.df_dc = pd.DataFrame() + + def Fetch(self, RA_Bill_No): + trench = TrenchExcavation.query.filter_by(RA_Bill_No=RA_Bill_No).all() + mh = ManholeExcavation.query.filter_by(RA_Bill_No=RA_Bill_No).all() + dc = ManholeDomesticChamber.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]) + + if not self.df_dc.empty and "MH_NO" in self.df_dc.columns: + self.df_dc.rename(columns={"MH_NO": "Node_No"}, inplace=True) + + drop_cols = ["id", "created_at", "_sa_instance_state"] + for df in [self.df_tr, self.df_mh, self.df_dc]: + if not df.empty: + df.drop(columns=drop_cols, errors="ignore", inplace=True) + +@file_report_bp.route("/client_vs_subcont", methods=["GET", "POST"]) +@login_required +def client_vs_all_subcontractor(): + if request.method == "POST": + RA_Bill_No = request.form.get("RA_Bill_No") + + if not RA_Bill_No: + flash("Please enter RA Bill No.", "danger") + return render_template("generate_comparison_client_vs_subcont.html") + + clientBill = ClientBill() + clientBill.Fetch(RA_Bill_No=RA_Bill_No) + contractorBill = SubcontractorBill() + contractorBill.Fetch(RA_Bill_No=RA_Bill_No) + + # Updated QTY lists to match model fields exactly + qty_cols = [ + "Soft_Murum_0_to_1_5_total", "Soft_Murum_1_5_to_3_0_total", "Soft_Murum_3_0_to_4_5_total", + "Hard_Murum_0_to_1_5_total", "Hard_Murum_1_5_to_3_0_total", "Hard_Murum_3_0_to_4_5_total", + "Soft_Rock_0_to_1_5_total", "Soft_Rock_1_5_to_3_0_total", "Soft_Rock_3_0_to_4_5_total", + "Hard_Rock_0_to_1_5_total", "Hard_Rock_1_5_to_3_0_total", "Hard_Rock_3_0_to_4_5_total", + "Hard_Rock_4_5_to_6_0_total", "Hard_Rock_6_0_to_7_5_total" + ] + + mh_dc_qty_cols = [ + "d_0_to_1_5", "d_1_5_to_2_0", "d_2_0_to_2_5", "d_2_5_to_3_0", + "d_3_0_to_3_5", "d_3_5_to_4_0", "d_4_0_to_4_5", "d_4_5_to_5_0", + "d_5_0_to_5_5", "d_5_5_to_6_0", "d_6_0_to_6_5", "Domestic_Chambers" + ] + + # Aggregate Subcontractor Data safely + def aggregate_df(df, group_cols, sum_cols): + if df.empty: return pd.DataFrame() + existing_cols = [c for c in sum_cols if c in df.columns] + return df.groupby(group_cols, as_index=False)[existing_cols].sum() + + 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", "Node_No"], mh_dc_qty_cols) + + # Merge and Calculate Difference + 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", "Node_No"], how="left", suffixes=("_Client", "_Sub")) + + # Calculate Diffs + for df in [df_tr_cmp, df_mh_cmp]: + for col in qty_cols: + if f"{col}_Client" in df.columns: + df[f"{col}_Diff"] = df[f"{col}_Client"].fillna(0) - df[f"{col}_Sub"].fillna(0) + + for col in mh_dc_qty_cols: + if f"{col}_Client" in df_dc_cmp.columns: + df_dc_cmp[f"{col}_Diff"] = df_dc_cmp[f"{col}_Client"].fillna(0) - df_dc_cmp[f"{col}_Sub"].fillna(0) + + output = io.BytesIO() + file_name = f"Comparison_RA_Bill_{RA_Bill_No}.xlsx" + with pd.ExcelWriter(output, engine="xlsxwriter") as writer: + df_tr_cmp.to_excel(writer, sheet_name="Tr.Ex Comparison", index=False) + df_mh_cmp.to_excel(writer, sheet_name="Mh.Ex Comparison", index=False) + df_dc_cmp.to_excel(writer, sheet_name="MH & DC Comparison", index=False) + + output.seek(0) + return send_file(output, download_name=file_name, as_attachment=True, mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + + return render_template("generate_comparison_client_vs_subcont.html") \ 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..a2ed92d --- /dev/null +++ b/app/routes/generate_comparison_report.py @@ -0,0 +1,213 @@ +from flask import Blueprint, render_template, request, send_file, flash +import pandas as pd +import io + +from app.models.subcontractor_model import Subcontractor +from app.models.trench_excavation_model import TrenchExcavation +from app.models.tr_ex_client_model import TrenchExcavationClient +from app.models.manhole_excavation_model import ManholeExcavation +from app.models.mh_ex_client_model import ManholeExcavationClient +from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber +from app.models.mh_dc_client_model import ManholeDomesticChamberClient +from app.utils.helpers import login_required + +generate_report_bp = Blueprint("generate_report", __name__, url_prefix="/report") + + +# 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[(location, key_val)] = r + + return lookup + + +# COMPARISON BUILDER +def build_comparison(client_rows, contractor_rows, key_field): + contractor_lookup = make_lookup(contractor_rows, key_field) + output = [] + + 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 + + s = contractor_lookup.get((client_location, client_key)) + if not s: + continue + + client_total = sum( + float(v or 0) + for k, v in c.items() + if k.endswith("_total") + ) + + sub_total = sum( + float(v or 0) + for k, v in s.items() + if k.endswith("_total") + ) + + diff = client_total - sub_total + + row = { + "Location": client_location, + key_field.replace("_", " "): client_key + } + + # CLIENT DATA + for k, v in c.items(): + if k in ["id", "created_at"]: + continue + row[f"Client-{k}"] = v + + row["Client-Total"] = round(client_total, 2) + row[" "] = "" + + # SUBCONTRACTOR DATA + 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(diff, 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": "#D9EDF7"}) + sub_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F7E1D9"}) + total_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#FFF2CC"}) + diff_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#E2EFDA"}) + 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"): + 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") + + # -------- 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) + + 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) + + diff --git a/app/routes/subcontractor_routes.py b/app/routes/subcontractor_routes.py new file mode 100644 index 0000000..d1242cb --- /dev/null +++ b/app/routes/subcontractor_routes.py @@ -0,0 +1,71 @@ +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(): + subcontractor = Subcontractor( + subcontractor_name=request.form.get("subcontractor_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") + 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) + + subcontractor.subcontractor_name = request.form.get("subcontractor_name") + subcontractor.contact_person = request.form.get("contact_person") + subcontractor.mobile_no = request.form.get("mobile_no") + subcontractor.email_id = request.form.get("email_id") + subcontractor.gst_no = request.form.get("gst_no") + + 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") 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/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..e98e3f7 --- /dev/null +++ b/app/services/file_service.py @@ -0,0 +1,306 @@ +import os +import pandas as pd +from werkzeug.utils import secure_filename + +from app.config import Config +from app import db + +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.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.utils.file_utils import ensure_upload_folder + + +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}) + + 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) + + 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() + + # ---------------- 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) + + 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) + + 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/css/subcontractor.css b/app/static/css/subcontractor.css new file mode 100644 index 0000000..26f4913 --- /dev/null +++ b/app/static/css/subcontractor.css @@ -0,0 +1,35 @@ +body { + background: #f5f7fa; + font-family: Arial; + padding: 40px; +} + +form { + width: 420px; + padding: 20px; + background: #fff; + border-radius: 8px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +label { + margin-top: 10px; + display: block; + font-weight: bold; +} + +input, +textarea { + width: 100%; + padding: 8px; + margin-top: 4px; +} + +button { + margin-top: 20px; + padding: 10px 20px; + background: #0055ff; + color: #fff; + border: 0; + cursor: pointer; +} \ No newline at end of file diff --git a/app/static/downloads/format/client_format.xlsx b/app/static/downloads/format/client_format.xlsx new file mode 100644 index 0000000..ac6eab6 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..1a24522 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..3ea90da --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,181 @@ + + + + + + + {{ 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/dashboard.html b/app/templates/dashboard.html new file mode 100644 index 0000000..a14ea00 --- /dev/null +++ b/app/templates/dashboard.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} +

Dashboard

+ +
+
Welcome to Comparison Project
+

This is dashboard panel.

+
+{% 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.html b/app/templates/file_import.html new file mode 100644 index 0000000..9b5bb45 --- /dev/null +++ b/app/templates/file_import.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/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/generate_comparison_client_vs_subcont.html b/app/templates/generate_comparison_client_vs_subcont.html new file mode 100644 index 0000000..9a3e62b --- /dev/null +++ b/app/templates/generate_comparison_client_vs_subcont.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} + +{% block content %} +
+ +

File 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/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..f704ef7 --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,82 @@ + + + + + + LCEPL | Login + + + + + + + + +
+
+ + +
+ +
+ +
+ + +
+ LCEPL Logo + +

+ Laxmi Civil Engineering Services Pvt Ltd +

+

+ Data Comparison System +

+
+ + + {% 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/report.html b/app/templates/report.html new file mode 100644 index 0000000..43f04cb --- /dev/null +++ b/app/templates/report.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{% block content %} +
+

Generate Subcontractor Report

+ + {% 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/subcontractor/add.html b/app/templates/subcontractor/add.html new file mode 100644 index 0000000..61d15fa --- /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..a26a70f --- /dev/null +++ b/app/templates/subcontractor/list.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} +{% block content %} + +
+ + +
+
+

+ Subcontractor List +

+
+ + +
+ + +
+ + + + + + + + + + + + + + {% for s in subcontractors %} + + + + + + + + + + + + + + /tr> + {% 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/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/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cc2e645 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +Flask +pandas +openpyxl +xlrd +Werkzeug +python-dotenv +cryptography +xlsxwriter \ 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" + )