added client RA bill wise download report
This commit is contained in:
31
.env
Normal file
31
.env
Normal file
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@@ -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/
|
||||
|
||||
|
||||
570
README.md
Normal file
570
README.md
Normal file
@@ -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/<id>` | Edit form | ✅ |
|
||||
| POST | `/subcontractor/update/<id>` | Update subcontractor | ✅ |
|
||||
| GET | `/subcontractor/delete/<id>` | 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
|
||||
83
app/__init__.py
Normal file
83
app/__init__.py
Normal file
@@ -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
|
||||
29
app/config.py
Normal file
29
app/config.py
Normal file
@@ -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"}
|
||||
0
app/models/__init__.py
Normal file
0
app/models/__init__.py
Normal file
51
app/models/manhole_domestic_chamber_model.py
Normal file
51
app/models/manhole_domestic_chamber_model.py
Normal file
@@ -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"<HanholeDomesticChamberConstruction {self.Location}>"
|
||||
|
||||
def serialize(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
67
app/models/manhole_excavation_model.py
Normal file
67
app/models/manhole_excavation_model.py
Normal file
@@ -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"<HanholeExcavation {self.Location}>"
|
||||
|
||||
def serialize(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
46
app/models/mh_dc_client_model.py
Normal file
46
app/models/mh_dc_client_model.py
Normal file
@@ -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"<HanholeDomesticChamberConstruction {self.Location}>"
|
||||
|
||||
|
||||
def serialize(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
83
app/models/mh_ex_client_model.py
Normal file
83
app/models/mh_ex_client_model.py
Normal file
@@ -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"<HanholeExcavation {self.Location}>"
|
||||
|
||||
def serialize(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
21
app/models/subcontractor_model.py
Normal file
21
app/models/subcontractor_model.py
Normal file
@@ -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"<Subcontractor {self.subcontractor_name}>"
|
||||
|
||||
88
app/models/tr_ex_client_model.py
Normal file
88
app/models/tr_ex_client_model.py
Normal file
@@ -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"<TrenchExcavation {self.Location}>"
|
||||
|
||||
def serialize(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
79
app/models/trench_excavation_model.py
Normal file
79
app/models/trench_excavation_model.py
Normal file
@@ -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"<TrenchExcavation {self.Location}>"
|
||||
|
||||
def serialize(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
16
app/models/user_model.py
Normal file
16
app/models/user_model.py
Normal file
@@ -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)
|
||||
0
app/routes/__init__.py
Normal file
0
app/routes/__init__.py
Normal file
48
app/routes/auth.py
Normal file
48
app/routes/auth.py
Normal file
@@ -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")
|
||||
11
app/routes/dashboard.py
Normal file
11
app/routes/dashboard.py
Normal file
@@ -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")
|
||||
30
app/routes/file_format.py
Normal file
30
app/routes/file_format.py
Normal file
@@ -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/<filename>")
|
||||
@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
|
||||
)
|
||||
45
app/routes/file_import.py
Normal file
45
app/routes/file_import.py
Normal file
@@ -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)
|
||||
222
app/routes/file_report.py
Normal file
222
app/routes/file_report.py
Normal file
@@ -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")
|
||||
213
app/routes/generate_comparison_report.py
Normal file
213
app/routes/generate_comparison_report.py
Normal file
@@ -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)
|
||||
|
||||
|
||||
71
app/routes/subcontractor_routes.py
Normal file
71
app/routes/subcontractor_routes.py
Normal file
@@ -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/<int:id>")
|
||||
@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/<int:id>", 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/<int:id>")
|
||||
@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")
|
||||
11
app/routes/user.py
Normal file
11
app/routes/user.py
Normal file
@@ -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")
|
||||
0
app/services/__init__.py
Normal file
0
app/services/__init__.py
Normal file
19
app/services/db_service.py
Normal file
19
app/services/db_service.py
Normal file
@@ -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
|
||||
# )
|
||||
306
app/services/file_service.py
Normal file
306
app/services/file_service.py
Normal file
@@ -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)
|
||||
27
app/services/user_service.py
Normal file
27
app/services/user_service.py
Normal file
@@ -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()
|
||||
35
app/static/css/subcontractor.css
Normal file
35
app/static/css/subcontractor.css
Normal file
@@ -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;
|
||||
}
|
||||
BIN
app/static/downloads/format/client_format.xlsx
Normal file
BIN
app/static/downloads/format/client_format.xlsx
Normal file
Binary file not shown.
BIN
app/static/downloads/format/subcontractor_format.xlsx
Normal file
BIN
app/static/downloads/format/subcontractor_format.xlsx
Normal file
Binary file not shown.
BIN
app/static/images/lcepl.png
Normal file
BIN
app/static/images/lcepl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
181
app/templates/base.html
Normal file
181
app/templates/base.html
Normal file
@@ -0,0 +1,181 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ title if title else "Comparison Software" }}</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap Icons -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
|
||||
<!-- NAVBAR -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark shadow-sm">
|
||||
<div class="container-fluid">
|
||||
|
||||
<!-- Brand -->
|
||||
<a class="navbar-brand fw-bold" href="/">
|
||||
<i class="bi bi-buildings me-1"></i> LCEPL
|
||||
</a>
|
||||
|
||||
<!-- Mobile toggle -->
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMenu">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<!-- Menu -->
|
||||
<div class="collapse navbar-collapse" id="navMenu">
|
||||
<ul class="navbar-nav ms-auto align-items-lg-center">
|
||||
|
||||
<!-- Dashboard -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/dashboard">
|
||||
<i class="bi bi-speedometer2 me-1"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Subcontractor -->
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
|
||||
<i class="bi bi-people-fill me-1"></i> Subcontractor
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li>
|
||||
<a class="dropdown-item" href="/subcontractor/add">
|
||||
<i class="bi bi-plus-circle me-2"></i> Add Subcontractor
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="/subcontractor/list">
|
||||
<i class="bi bi-list-ul me-2"></i> Subcontractor List
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- File System -->
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
|
||||
<i class="bi bi-folder-fill me-1"></i> File System
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li>
|
||||
<a class="dropdown-item" href="/file/import">
|
||||
<i class="bi bi-upload me-2"></i> Import File
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="/file/report">
|
||||
<i class="bi bi-download me-2"></i> Download Reports
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- Client System -->
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
|
||||
<i class="bi bi-building me-1"></i> Client System
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li>
|
||||
<a class="dropdown-item" href="/file/import_client">
|
||||
<i class="bi bi-upload me-2"></i> Import Client File
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item" href="/report/comparison_report">
|
||||
<i class="bi bi-arrow-left-right me-2"></i> client vs sub-cont. Comparison Report
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="/file/client_vs_subcont">
|
||||
<i class="bi bi-arrow-left-right me-2"></i> Comparison Report
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- Formats -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/file_format">
|
||||
<i class="bi bi-file-earmark-text me-1"></i> Formats
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- USER DROPDOWN -->
|
||||
{% if session.get("user_id") %}
|
||||
<li class="nav-item dropdown ms-lg-3">
|
||||
|
||||
<a class="nav-link dropdown-toggle d-flex align-items-center gap-2" href="#"
|
||||
data-bs-toggle="dropdown">
|
||||
<i class="bi bi-person-circle fs-5"></i>
|
||||
<span class="d-none d-lg-inline">
|
||||
{{ session.get("user_name") }}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark shadow">
|
||||
|
||||
<!-- User card -->
|
||||
<li class="px-3 py-3 text-center border-bottom">
|
||||
<i class="bi bi-person-circle fs-1"></i>
|
||||
<div class="fw-semibold mt-1">
|
||||
{{ session.get("user_name") }}
|
||||
</div>
|
||||
<small class="text-muted">Logged in user</small>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item" href="/dashboard">
|
||||
<i class="bi bi-speedometer2 me-2"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item text-warning" href="/logout">
|
||||
<i class="bi bi-box-arrow-right me-2"></i> Logout
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- FLASH MESSAGES -->
|
||||
<div class="container mt-3">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show">
|
||||
{{ message }}
|
||||
<button class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<!-- PAGE CONTENT -->
|
||||
<div class="container mt-4">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
10
app/templates/dashboard.html
Normal file
10
app/templates/dashboard.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4">Dashboard</h2>
|
||||
|
||||
<div class="card p-4 shadow-sm">
|
||||
<h5>Welcome to Comparison Project</h5>
|
||||
<p>This is dashboard panel.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
66
app/templates/file_format.html
Normal file
66
app/templates/file_format.html
Normal file
@@ -0,0 +1,66 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4">Download File Formats</h2>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Subcontractor Format -->
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center shadow-sm h-100">
|
||||
<div class="card-body d-flex flex-column justify-content-center">
|
||||
<div class="mb-3 fs-1 text-primary">
|
||||
⬇️
|
||||
</div>
|
||||
<h5 class="card-title">Subcontractor Upload Format</h5>
|
||||
<p class="text-muted mb-2">Excel (.xlsx)</p>
|
||||
<p class="small text-secondary">File size: 245 KB</p>
|
||||
|
||||
<a href="{{ url_for('file_format.download_excel_format', filename='subcontractor_format.xlsx') }}"
|
||||
class="btn btn-outline-primary mt-auto">
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client Format -->
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center shadow-sm h-100">
|
||||
<div class="card-body d-flex flex-column justify-content-center">
|
||||
<div class="mb-3 fs-1 text-success">
|
||||
⬇️
|
||||
</div>
|
||||
<h5 class="card-title">Client Upload Format</h5>
|
||||
<p class="text-muted mb-2">Excel (.xlsx)</p>
|
||||
<p class="small text-secondary">File size: 310 KB</p>
|
||||
|
||||
<a href="{{ url_for('file_format.download_excel_format', filename='client_format.xlsx') }}"
|
||||
class="btn btn-outline-success mt-auto">
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MH & DC Format -->
|
||||
<!-- <div class="col-md-4">
|
||||
<div class="card text-center shadow-sm h-100">
|
||||
<div class="card-body d-flex flex-column justify-content-center">
|
||||
<div class="mb-3 fs-1 text-warning">
|
||||
⬇️
|
||||
</div>
|
||||
<h5 class="card-title">Manhole & DC Format</h5>
|
||||
<p class="text-muted mb-2">Excel (.xlsx)</p>
|
||||
<p class="small text-secondary">File size: 190 KB</p>
|
||||
|
||||
<a href="{{ url_for('file_format.download_excel_format', filename='mh_dc_format.xlsx') }}"
|
||||
class="btn btn-outline-warning mt-auto">
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
32
app/templates/file_import.html
Normal file
32
app/templates/file_import.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4">Sub-Contractor File Import</h2>
|
||||
|
||||
<div class="card p-4 shadow-sm">
|
||||
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
|
||||
<!-- 1. SELECT SUBCONTRACTOR -->
|
||||
<label class="form-label">Select Subcontractor</label>
|
||||
<select name="subcontractor_id" id="subcontractor_id" class="form-select mb-3" required>
|
||||
<option value="">-- Select Subcontractor --</option>
|
||||
|
||||
{% for sc in subcontractors %}
|
||||
<option value="{{ sc.id }}">{{ sc.subcontractor_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<!-- 2. RA bill no -->
|
||||
<label class="form-label">RA Bill No</label>
|
||||
<input type="text" name="RA_Bill_No" class="form-control mb-3" required>
|
||||
|
||||
<!-- 3. FILE UPLOAD -->
|
||||
<label class="form-label">Choose File</label>
|
||||
<input type="file" name="file" class="form-control mb-3" required>
|
||||
|
||||
<button class="btn btn-primary w-100">Upload</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
41
app/templates/file_import_client.html
Normal file
41
app/templates/file_import_client.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4">Client File Import</h2>
|
||||
|
||||
<div class="card p-4 shadow-sm">
|
||||
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
|
||||
<!-- 1. SELECT SUBCONTRACTOR -->
|
||||
<!-- <label class="form-label">Select Subcontractor vs Client</label>
|
||||
<select name="subcontractor_id" id="subcontractor_id" class="form-select mb-3" required>
|
||||
<option value="">-- Select Subcontractor --</option>
|
||||
|
||||
{% for sc in subcontractors %}
|
||||
<option value="{{ sc.id }}">{{ sc.subcontractor_name }}</option>
|
||||
{% endfor %}
|
||||
</select> -->
|
||||
|
||||
<!-- 2. FILE TYPE (MODEL NAME) -->
|
||||
<!-- <label class="form-label">Select File Type</label>
|
||||
<select name="file_type" id="file_type" class="form-select mb-3" required>
|
||||
<option value="">-- Select File Type --</option>
|
||||
<option value="">Sheet</option>
|
||||
<option value="tr_ex_client">Tr. Ex</option>
|
||||
<option value="mh_ex_client">Mh. Ex </option>
|
||||
<option value="mh_dc_client">MH & DC </option>
|
||||
<option value="">Laying Sheet</option>
|
||||
</select> -->
|
||||
|
||||
<label class="form-label">RA Bill No</label>
|
||||
<input type="text" name="RA_Bill_No" class="form-control mb-3" required>
|
||||
<!-- 3. FILE UPLOAD -->
|
||||
<label class="form-label">Choose File</label>
|
||||
<input type="file" name="file" class="form-control mb-3" required>
|
||||
|
||||
<button class="btn btn-primary w-100">Upload</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
43
app/templates/generate_comparison_client_vs_subcont.html
Normal file
43
app/templates/generate_comparison_client_vs_subcont.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
|
||||
<h2 class="mb-4">File Comparison </h2>
|
||||
|
||||
<!-- FLASH MESSAGES -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="card p-4 shadow-sm">
|
||||
|
||||
<form method="POST">
|
||||
|
||||
<!-- SELECT SUBCONTRACTOR -->
|
||||
<!-- <label class="form-label fw-semibold">Select Subcontractor</label>
|
||||
<select name="subcontractor_id" id="subcontractor_id" class="form-select mb-3" required>
|
||||
<option value="">-- Select Subcontractor --</option>
|
||||
|
||||
{% for sc in subcontractors %}
|
||||
<option value="{{ sc.id }}">{{ sc.subcontractor_name }}</option>
|
||||
{% endfor %}
|
||||
</select> -->
|
||||
|
||||
<label class="form-label">RA Bill No</label>
|
||||
<input type="text" name="RA_Bill_No" class="form-control mb-3" required>
|
||||
|
||||
|
||||
<button class="btn btn-primary w-100">Generate Report</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
39
app/templates/generate_comparison_report.html
Normal file
39
app/templates/generate_comparison_report.html
Normal file
@@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
|
||||
<h2 class="mb-4">Subcontractor vs Client Comparison</h2>
|
||||
|
||||
<!-- FLASH MESSAGES -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="card p-4 shadow-sm">
|
||||
|
||||
<form method="POST">
|
||||
|
||||
<!-- SELECT SUBCONTRACTOR -->
|
||||
<label class="form-label fw-semibold">Select Subcontractor</label>
|
||||
<select name="subcontractor_id" id="subcontractor_id" class="form-select mb-3" required>
|
||||
<option value="">-- Select Subcontractor --</option>
|
||||
|
||||
{% for sc in subcontractors %}
|
||||
<option value="{{ sc.id }}">{{ sc.subcontractor_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<button class="btn btn-primary w-100">Generate Report</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
28
app/templates/list_user.html
Normal file
28
app/templates/list_user.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4">Users List</h2>
|
||||
|
||||
<div class="card p-4 shadow-sm">
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.name }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
82
app/templates/login.html
Normal file
82
app/templates/login.html
Normal file
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>LCEPL | Login</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container-fluid vh-100">
|
||||
<div class="row h-100 justify-content-center align-items-center">
|
||||
|
||||
<!-- Increased column width -->
|
||||
<div class="col-12 col-sm-10 col-md-8 col-lg-5 col-xl-4">
|
||||
|
||||
<div class="card shadow-lg border-0">
|
||||
<!-- Increased padding -->
|
||||
<div class="card-body p-5">
|
||||
|
||||
<!-- Branding -->
|
||||
<div class="text-center mb-4">
|
||||
<img src="{{ url_for('static', filename='images/lcepl.png') }}" alt="LCEPL Logo"
|
||||
class="img-fluid mb-3" style="max-height:80px;">
|
||||
|
||||
<h4 class="fw-bold mb-1">
|
||||
Laxmi Civil Engineering Services Pvt Ltd
|
||||
</h4>
|
||||
<p class="text-muted mb-0">
|
||||
Data Comparison System
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Flash messages -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Login Form -->
|
||||
<form method="POST">
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">User Name</label>
|
||||
<input type="email" name="email" class="form-control " placeholder="Enter email"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">Password</label>
|
||||
<input type="password" name="password" class="form-control" placeholder="Enter password"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success btn-lg w-100">
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
19
app/templates/register.html
Normal file
19
app/templates/register.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h2>Register User</h2>
|
||||
|
||||
<div class="card p-4 shadow-sm">
|
||||
<form method="POST">
|
||||
<label>Name</label>
|
||||
<input class="form-control mb-3" name="name" required>
|
||||
|
||||
<label>Email</label>
|
||||
<input type="email" class="form-control mb-3" name="email" required>
|
||||
|
||||
<label>Password</label>
|
||||
<input type="password" class="form-control mb-3" name="password" required>
|
||||
|
||||
<button class="btn btn-primary">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
40
app/templates/report.html
Normal file
40
app/templates/report.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h2 class="mb-4">Generate Subcontractor Report</h2>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
)
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="card p-4 shadow-sm">
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Select Subcontractor</label>
|
||||
<select name="subcontractor_id" class="form-select" required>
|
||||
<option value="">-- Select Subcontractor --</option>
|
||||
{% for sc in subcontractors %}
|
||||
<option value="{{ sc.id }}">{{ sc.subcontractor_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">RA Bill Number</label>
|
||||
<input type="text" name="ra_bill_no" class="form-control" placeholder="e.g. 01" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">Generate Excel Report</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
40
app/templates/subcontractor/add.html
Normal file
40
app/templates/subcontractor/add.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="card shadow-sm p-4">
|
||||
|
||||
<h4 class="mb-3">Add New Subcontractor</h4>
|
||||
|
||||
<form action="/subcontractor/save" method="POST">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Subcontractor Name</label>
|
||||
<input type="text" class="form-control" name="subcontractor_name" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Contact Person</label>
|
||||
<input type="text" class="form-control" name="contact_person">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Mobile</label>
|
||||
<input type="text" class="form-control" name="mobile_no">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" class="form-control" name="email_id">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">GST No</label>
|
||||
<input type="text" class="form-control" name="gst_no">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success">Save</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
41
app/templates/subcontractor/edit.html
Normal file
41
app/templates/subcontractor/edit.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="card shadow-sm p-4">
|
||||
<h4 class="mb-3">Edit Subcontractor</h4>
|
||||
|
||||
<form action="/subcontractor/update/{{ subcontractor.id }}" method="POST">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Subcontractor Name</label>
|
||||
<input type="text" class="form-control" name="subcontractor_name"
|
||||
value="{{ subcontractor.subcontractor_name }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Contact Person</label>
|
||||
<input type="text" class="form-control" name="contact_person" value="{{ subcontractor.contact_person }}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Mobile</label>
|
||||
<input type="text" class="form-control" name="mobile_no" value="{{ subcontractor.mobile_no }}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" class="form-control" name="email_id" value="{{ subcontractor.email_id }}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">GST No</label>
|
||||
<input type="text" class="form-control" name="gst_no" value="{{ subcontractor.gst_no }}">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success">Update</button>
|
||||
<a href="/subcontractor/list" class="btn btn-secondary">Back</a>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
91
app/templates/subcontractor/list.html
Normal file
91
app/templates/subcontractor/list.html
Normal file
@@ -0,0 +1,91 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="card shadow-sm p-3 p-md-4">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="row mb-3 align-items-center">
|
||||
<div class="col-12 col-md-6">
|
||||
<h4 class="mb-2 mb-md-0 text-center text-md-start">
|
||||
Subcontractor List
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 text-center text-md-end">
|
||||
<a href="/subcontractor/add" class="btn btn-success w-100 w-md-auto">
|
||||
➕ Add Subcontractor
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped align-middle text-nowrap">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th class="d-none d-md-table-cell">Mobile</th>
|
||||
<th class="d-none d-lg-table-cell">Email</th>
|
||||
<th class="d-none d-lg-table-cell">GST No</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for s in subcontractors %}
|
||||
<tr>
|
||||
<td>{{ s.id }}</td>
|
||||
|
||||
<td class="fw-semibold text-wrap">
|
||||
{{ s.subcontractor_name }}
|
||||
</td>
|
||||
|
||||
<td class="d-none d-md-table-cell">
|
||||
{{ s.mobile_no }}
|
||||
</td>
|
||||
|
||||
<td class="d-none d-lg-table-cell">
|
||||
{{ s.email_id }}
|
||||
</td>
|
||||
|
||||
<td class="d-none d-lg-table-cell">
|
||||
{{ s.gst_no }}
|
||||
</td>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<td class="text-center">
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<a href="/subcontractor/edit/{{ s.id }}" class="btn btn-sm btn-warning">
|
||||
Edit
|
||||
</a>
|
||||
<a href="/subcontractor/delete/{{ s.id }}" class="btn btn-sm btn-danger"
|
||||
onclick="return confirm('Are you sure?')">
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
/tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
<!-- TOTAL ROW -->
|
||||
<tfoot>
|
||||
<tr class="table-secondary fw-bold">
|
||||
<td colspan="2" class="text-end"> Total Subcontractors</td>
|
||||
|
||||
<td class="d-none d-lg-table-cell">-</td>
|
||||
<td class="d-none d-lg-table-cell">-</td>
|
||||
<td class="d-none d-lg-table-cell">-</td>
|
||||
|
||||
<td class="text-center">{{ subcontractors|length }}</td>
|
||||
|
||||
</tr>
|
||||
</tfoot>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
28
app/templates/users.htm
Normal file
28
app/templates/users.htm
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4">Users List</h2>
|
||||
|
||||
<div class="card p-4 shadow-sm">
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.name }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
6
app/utils/file_utils.py
Normal file
6
app/utils/file_utils.py
Normal file
@@ -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)
|
||||
14
app/utils/helpers.py
Normal file
14
app/utils/helpers.py
Normal file
@@ -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
|
||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Flask
|
||||
pandas
|
||||
openpyxl
|
||||
xlrd
|
||||
Werkzeug
|
||||
python-dotenv
|
||||
cryptography
|
||||
xlsxwriter
|
||||
17
run.py
Normal file
17
run.py
Normal file
@@ -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"
|
||||
)
|
||||
Reference in New Issue
Block a user