From 844dcbee814b3afbf32feeff9e263ac2c4c260b9 Mon Sep 17 00:00:00 2001 From: anishd100 Date: Tue, 13 Jan 2026 15:58:52 +0530 Subject: [PATCH] added client RA bill wise download report --- .env | 31 + .gitignore | 20 + README.md | 570 ++++++++++++++++++ app/__init__.py | 83 +++ app/config.py | 29 + app/models/__init__.py | 0 app/models/manhole_domestic_chamber_model.py | 51 ++ app/models/manhole_excavation_model.py | 67 ++ app/models/mh_dc_client_model.py | 46 ++ app/models/mh_ex_client_model.py | 83 +++ app/models/subcontractor_model.py | 21 + app/models/tr_ex_client_model.py | 88 +++ app/models/trench_excavation_model.py | 79 +++ app/models/user_model.py | 16 + app/routes/__init__.py | 0 app/routes/auth.py | 48 ++ app/routes/dashboard.py | 11 + app/routes/file_format.py | 30 + app/routes/file_import.py | 45 ++ app/routes/file_report.py | 222 +++++++ app/routes/generate_comparison_report.py | 213 +++++++ app/routes/subcontractor_routes.py | 71 +++ app/routes/user.py | 11 + app/services/__init__.py | 0 app/services/db_service.py | 19 + app/services/file_service.py | 306 ++++++++++ app/services/user_service.py | 27 + app/static/css/subcontractor.css | 35 ++ .../downloads/format/client_format.xlsx | Bin 0 -> 37799 bytes .../format/subcontractor_format.xlsx | Bin 0 -> 41731 bytes app/static/images/lcepl.png | Bin 0 -> 4224 bytes app/templates/base.html | 181 ++++++ app/templates/dashboard.html | 10 + app/templates/file_format.html | 66 ++ app/templates/file_import.html | 32 + app/templates/file_import_client.html | 41 ++ ...generate_comparison_client_vs_subcont.html | 43 ++ app/templates/generate_comparison_report.html | 39 ++ app/templates/list_user.html | 28 + app/templates/login.html | 82 +++ app/templates/register.html | 19 + app/templates/report.html | 40 ++ app/templates/subcontractor/add.html | 40 ++ app/templates/subcontractor/edit.html | 41 ++ app/templates/subcontractor/list.html | 91 +++ app/templates/users.htm | 28 + app/utils/file_utils.py | 6 + app/utils/helpers.py | 14 + requirements.txt | 8 + run.py | 17 + 50 files changed, 3048 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/config.py create mode 100644 app/models/__init__.py create mode 100644 app/models/manhole_domestic_chamber_model.py create mode 100644 app/models/manhole_excavation_model.py create mode 100644 app/models/mh_dc_client_model.py create mode 100644 app/models/mh_ex_client_model.py create mode 100644 app/models/subcontractor_model.py create mode 100644 app/models/tr_ex_client_model.py create mode 100644 app/models/trench_excavation_model.py create mode 100644 app/models/user_model.py create mode 100644 app/routes/__init__.py create mode 100644 app/routes/auth.py create mode 100644 app/routes/dashboard.py create mode 100644 app/routes/file_format.py create mode 100644 app/routes/file_import.py create mode 100644 app/routes/file_report.py create mode 100644 app/routes/generate_comparison_report.py create mode 100644 app/routes/subcontractor_routes.py create mode 100644 app/routes/user.py create mode 100644 app/services/__init__.py create mode 100644 app/services/db_service.py create mode 100644 app/services/file_service.py create mode 100644 app/services/user_service.py create mode 100644 app/static/css/subcontractor.css create mode 100644 app/static/downloads/format/client_format.xlsx create mode 100644 app/static/downloads/format/subcontractor_format.xlsx create mode 100644 app/static/images/lcepl.png create mode 100644 app/templates/base.html create mode 100644 app/templates/dashboard.html create mode 100644 app/templates/file_format.html create mode 100644 app/templates/file_import.html create mode 100644 app/templates/file_import_client.html create mode 100644 app/templates/generate_comparison_client_vs_subcont.html create mode 100644 app/templates/generate_comparison_report.html create mode 100644 app/templates/list_user.html create mode 100644 app/templates/login.html create mode 100644 app/templates/register.html create mode 100644 app/templates/report.html create mode 100644 app/templates/subcontractor/add.html create mode 100644 app/templates/subcontractor/edit.html create mode 100644 app/templates/subcontractor/list.html create mode 100644 app/templates/users.htm create mode 100644 app/utils/file_utils.py create mode 100644 app/utils/helpers.py create mode 100644 requirements.txt create mode 100644 run.py 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 0000000000000000000000000000000000000000..ac6eab605c0c743b96b6848658c82bb5f9606f73 GIT binary patch literal 37799 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 Z5;(w{6_`{RfJgN6GWY^hqRRvj4*+U?OIQE^ literal 0 HcmV?d00001 diff --git a/app/static/downloads/format/subcontractor_format.xlsx b/app/static/downloads/format/subcontractor_format.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1a24522d3a410cb0d752fa93360a660bb0a41360 GIT binary patch literal 41731 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

p)f%jg%Z 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 Vf{-czVW5u#P(Ud0Q|hmG{{v_L%JTpK literal 0 HcmV?d00001 diff --git a/app/static/images/lcepl.png b/app/static/images/lcepl.png new file mode 100644 index 0000000000000000000000000000000000000000..b4aa03bff450b7193b60254b5241ea16f2d70ad0 GIT binary patch literal 4224 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 Z8Om(5KF4*Z{QB3xU~FKaU!~_B`9C~Uaz+3E literal 0 HcmV?d00001 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" + )