Compare commits
16 Commits
ac9d964ef4
...
71ce8ca819
| Author | SHA1 | Date | |
|---|---|---|---|
| 71ce8ca819 | |||
| 68a694d2c7 | |||
| 90c18383da | |||
| f22dc0cccd | |||
| 0007d6e87d | |||
| 8cba86323e | |||
| e4837b203a | |||
| 4fa35fe05a | |||
| 15b07b3d55 | |||
| ff5da926f3 | |||
| 8f817c94fd | |||
| ed85313d42 | |||
| a853992e64 | |||
| 677df59863 | |||
| de074c68ae | |||
| ae5fee8f46 |
5
.env
5
.env
@@ -24,8 +24,3 @@ DB_PASSWORD=root
|
||||
|
||||
# DATABASE_URL=mysql+pymysql://root:root@localhost/comparisondb
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
584
README.md
584
README.md
@@ -1,3 +1,583 @@
|
||||
# Comparison_Project
|
||||
# Comparison Project - Backend Documentation
|
||||
|
||||
Comparison Project
|
||||
A Flask-based web application for comparing and managing construction project data (excavation and manhole work) between subcontractors and clients. The system imports data from Excel files, stores it in a database, and generates comparison reports.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Project Overview](#project-overview)
|
||||
- [Tech Stack](#tech-stack)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Database Schema](#database-schema)
|
||||
- [Application Flow](#application-flow)
|
||||
- [API Routes & Endpoints](#api-routes--endpoints)
|
||||
- [Setup & Installation](#setup--installation)
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
|
||||
The Comparison Project is designed to:
|
||||
- **Manage subcontractors** and their excavation data (Manhole Excavation, Trench Excavation, Domestic Chambers)
|
||||
- **Import client-side project data** for comparison
|
||||
- **Compare subcontractor work** against client specifications
|
||||
- **Generate detailed reports** highlighting differences between client and subcontractor data
|
||||
- **User authentication** with login/registration system
|
||||
|
||||
### Key Features
|
||||
|
||||
✅ User Authentication (Register/Login/Logout)
|
||||
✅ Subcontractor Management (Add/Edit/List/Delete)
|
||||
✅ File Import (Excel/CSV) for both Client and Subcontractor data
|
||||
✅ Data validation and duplicate detection
|
||||
✅ Comparison Reports with Excel export
|
||||
✅ Dashboard with file management
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
**Backend Framework**: Flask
|
||||
**Database**: SQL Database (MySQL/PostgreSQL/SQLite configured via environment variables)
|
||||
**ORM**: SQLAlchemy
|
||||
**File Processing**: Pandas, OpenPyXL, XlsxWriter
|
||||
**Authentication**: Werkzeug (Password hashing)
|
||||
**Configuration**: Python Dotenv
|
||||
|
||||
### Dependencies
|
||||
|
||||
```
|
||||
Flask
|
||||
pandas
|
||||
openpyxl
|
||||
xlrd
|
||||
Werkzeug
|
||||
python-dotenv
|
||||
cryptography
|
||||
xlsxwriter
|
||||
matplotlib
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
Comparison_Project/
|
||||
├── run.py # Application entry point
|
||||
├── requirements.txt # Python dependencies
|
||||
├── .env # Environment variables (DB, secrets)
|
||||
├── README.md # This file
|
||||
│
|
||||
├── app/
|
||||
│ ├── __init__.py # Flask app initialization
|
||||
│ ├── config.py # Configuration settings
|
||||
│ │
|
||||
│ ├── models/ # Database models (SQLAlchemy ORM)
|
||||
│ │ ├── user_model.py # User table schema
|
||||
│ │ ├── subcontractor_model.py # Subcontractor table schema
|
||||
│ │ ├── manhole_excavation_model.py # Subcontractor MH excavation
|
||||
│ │ ├── trench_excavation_model.py # Subcontractor Trench excavation
|
||||
│ │ ├── manhole_domestic_chamber_model.py # Subcontractor Domestic chamber
|
||||
│ │ ├── mh_ex_client_model.py # Client MH excavation
|
||||
│ │ ├── tr_ex_client_model.py # Client Trench excavation
|
||||
│ │ └── mh_dc_client_model.py # Client Domestic chamber
|
||||
│ │
|
||||
│ ├── routes/ # Flask Blueprints (API endpoints)
|
||||
│ │ ├── auth.py # Login, Register, Logout
|
||||
│ │ ├── dashboard.py # Dashboard page
|
||||
│ │ ├── file_import.py # File upload endpoints
|
||||
│ │ ├── file_report.py # Fetch and format data for reports
|
||||
│ │ ├── generate_comparison_report.py # Comparison logic & Excel export
|
||||
│ │ ├── subcontractor_routes.py # CRUD for subcontractors
|
||||
│ │ ├── user.py # User management routes
|
||||
│ │ └── file_format.py # File format validation
|
||||
│ │
|
||||
│ ├── services/ # Business logic layer
|
||||
│ │ ├── db_service.py # Database connection & SQLAlchemy init
|
||||
│ │ ├── file_service.py # File parsing & data insertion
|
||||
│ │ └── user_service.py # User authentication logic
|
||||
│ │
|
||||
│ ├── utils/ # Helper functions
|
||||
│ │ ├── file_utils.py # File path utilities
|
||||
│ │ ├── helpers.py # Decorators, common helpers
|
||||
│ │ └── __pycache__/
|
||||
│ │
|
||||
│ ├── static/ # Static assets
|
||||
│ │ ├── css/ # Stylesheets
|
||||
│ │ ├── images/ # Images
|
||||
│ │ ├── uploads/ # Uploaded files directory
|
||||
│ │ │ ├── Client_Bill_1/
|
||||
│ │ │ └── sub_1/
|
||||
│ │ └── downloads/ # Generated reports
|
||||
│ │
|
||||
│ ├── templates/ # Jinja2 HTML templates
|
||||
│ │ ├── base.html # Base template
|
||||
│ │ ├── login.html
|
||||
│ │ ├── register.html
|
||||
│ │ ├── dashboard.html
|
||||
│ │ ├── file_import.html
|
||||
│ │ ├── file_import_client.html
|
||||
│ │ ├── file_format.html
|
||||
│ │ ├── file_report.html
|
||||
│ │ ├── generate_comparison_report.html
|
||||
│ │ ├── generate_comparison_client_vs_subcont.html
|
||||
│ │ ├── list_user.html
|
||||
│ │ ├── report.html
|
||||
│ │ ├── users.htm
|
||||
│ │ └── subcontractor/
|
||||
│ │ ├── add.html
|
||||
│ │ ├── edit.html
|
||||
│ │ └── list.html
|
||||
│ │
|
||||
│ └── logs/ # Application logs
|
||||
│
|
||||
├── instance/ # Instance folder (DB, temp files)
|
||||
└── logs/ # Root level logs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### 1. **Users Table** (`users`)
|
||||
Stores user authentication data.
|
||||
|
||||
| Column | Type | Constraints | Purpose |
|
||||
|--------|------|-------------|---------|
|
||||
| `id` | Integer | Primary Key | User identifier |
|
||||
| `name` | String(120) | NOT NULL | User's full name |
|
||||
| `email` | String(120) | UNIQUE, NOT NULL | User's email |
|
||||
| `password_hash` | String(255) | NOT NULL | Hashed password (Werkzeug) |
|
||||
|
||||
**Key Methods:**
|
||||
- `set_password(password)` - Hash and store password
|
||||
- `check_password(password)` - Verify password
|
||||
|
||||
---
|
||||
|
||||
### 2. **Subcontractors Table** (`subcontractors`)
|
||||
Stores subcontractor company information.
|
||||
|
||||
| Column | Type | Constraints | Purpose |
|
||||
|--------|------|-------------|---------|
|
||||
| `id` | Integer | Primary Key | Subcontractor ID |
|
||||
| `subcontractor_name` | String(255) | NOT NULL | Company name |
|
||||
| `address` | String(500) | - | Company address |
|
||||
| `gst_no` | String(50) | - | GST number |
|
||||
| `pan_no` | String(50) | - | PAN number |
|
||||
| `mobile_no` | String(20) | - | Contact phone |
|
||||
| `email_id` | String(150) | - | Contact email |
|
||||
| `contact_person` | String(150) | - | Contact person name |
|
||||
| `status` | String(20) | Default: "Active" | Active/Inactive status |
|
||||
| `created_at` | DateTime | Default: today | Record creation date |
|
||||
|
||||
**Relationships:**
|
||||
- One-to-Many with `manhole_excavation`
|
||||
- One-to-Many with `trench_excavation`
|
||||
- One-to-Many with `manhole_domestic_chamber`
|
||||
|
||||
---
|
||||
|
||||
### 3. **Manhole Excavation (Subcontractor)** (`manhole_excavation`)
|
||||
Stores manhole excavation work data from subcontractors.
|
||||
|
||||
| Column Type | Purpose |
|
||||
|-----|---------|
|
||||
| `id` | Primary Key |
|
||||
| `subcontractor_id` | Foreign Key → Subcontractor |
|
||||
| `Location` | Worksite location |
|
||||
| `MH_NO` | Manhole number |
|
||||
| `RA_Bill_No` | RA Bill reference (for grouping) |
|
||||
| `Upto_IL_Depth`, `Cutting_Depth`, `ID_of_MH_m`, `Ex_Dia_of_Manhole`, `Area_of_Manhole` | Dimension fields |
|
||||
| **Excavation Categories** (by depth ranges): | |
|
||||
| Soft_Murum_0_to_1_5, Soft_Murum_1_5_to_3_0, etc. | Material type + depth range |
|
||||
| Hard_Murum_0_to_1_5, Hard_Murum_1_5_to_3_0, etc. | Hard mudstone layers |
|
||||
| Soft_Rock_0_to_1_5, Soft_Rock_1_5_to_3_0, etc. | Soft rock layers |
|
||||
| Hard_Rock_0_to_1_5 through Hard_Rock_6_0_to_7_5 | Hard rock layers |
|
||||
| **Totals** | Sum per category |
|
||||
| `Total` | Grand total excavation |
|
||||
| `Remarks`, `created_at` | Notes and timestamp |
|
||||
|
||||
---
|
||||
|
||||
### 4. **Trench Excavation (Subcontractor)** (`trench_excavation`)
|
||||
Stores trench/sewer line excavation work data from subcontractors.
|
||||
|
||||
**Similar structure to Manhole Excavation with additional fields:**
|
||||
- Width categories: `Width_0_to_2_5`, `Width_2_5_to_3_0`, etc.
|
||||
- `Avg_Depth`, `Actual_Trench_Length`, `Pipe_Dia_mm`
|
||||
|
||||
---
|
||||
|
||||
### 5. **Manhole Domestic Chamber (Subcontractor)** (`manhole_domestic_chamber`)
|
||||
Stores domestic chamber construction data.
|
||||
|
||||
| Key Fields | Purpose |
|
||||
|-----------|---------|
|
||||
| `id`, `subcontractor_id` | Primary & Foreign Key |
|
||||
| `Location`, `MH_NO`, `RA_Bill_No` | Identification |
|
||||
| `Depth_of_MH`, `MH_TOP_LEVEL`, `MH_IL_LEVEL` | Dimensions |
|
||||
| `d_0_to_1_5` through `d_6_0_to_6_5` | Depth-based excavation categories |
|
||||
| `Domestic_Chambers` | Count of chambers |
|
||||
| `DWC_Pipe_Length`, `UPVC_Pipe_Length` | Pipe lengths |
|
||||
|
||||
---
|
||||
|
||||
### 6-8. **Client Data Tables** (Similar structure to subcontractor models)
|
||||
|
||||
- **`mh_ex_client`** - Manhole Excavation (Client specifications)
|
||||
- **`tr_ex_client`** - Trench Excavation (Client specifications)
|
||||
- **`mh_dc_client`** - Manhole Domestic Chamber (Client specifications)
|
||||
|
||||
**Note:** Client tables do NOT have `subcontractor_id` as they represent client baseline data.
|
||||
|
||||
---
|
||||
|
||||
## Application Flow
|
||||
|
||||
### 1. **User Authentication Flow**
|
||||
```
|
||||
User Access (/)
|
||||
↓
|
||||
Redirect to /login
|
||||
↓
|
||||
[Login Page]
|
||||
├─ POST /auth/login
|
||||
│ ↓
|
||||
│ UserService.validate_login(email, password)
|
||||
│ ↓
|
||||
│ Query User table, verify password
|
||||
│ ↓
|
||||
│ Set session["user_id"], session["user_name"]
|
||||
↓
|
||||
Redirect to /dashboard
|
||||
```
|
||||
|
||||
### 2. **Subcontractor File Import Flow**
|
||||
```
|
||||
User → /file/import [GET]
|
||||
↓
|
||||
Display form with subcontractor dropdown
|
||||
↓
|
||||
User selects subcontractor & uploads Excel file
|
||||
↓
|
||||
POST /file/import
|
||||
↓
|
||||
FileService.handle_file_upload()
|
||||
├─ Validate file type (xlsx, xls, csv)
|
||||
├─ Create upload folder: static/uploads/sub_{id}/
|
||||
├─ Save file
|
||||
│
|
||||
├─ Read Excel sheets:
|
||||
│ ├─ "Tr.Ex." (Trench Excavation)
|
||||
│ ├─ "MH Ex." (Manhole Excavation)
|
||||
│ └─ "MH & DC" (Manhole & Domestic Chamber)
|
||||
│
|
||||
├─ Process each sheet:
|
||||
│ ├─ Normalize column names
|
||||
│ ├─ Clean data (handle NaN, empty values)
|
||||
│ ├─ Check for duplicates by (Location, MH_NO, RA_Bill_No, subcontractor_id)
|
||||
│ ├─ Insert records into respective tables
|
||||
│ └─ Commit to database
|
||||
│
|
||||
└─ Return success/error message
|
||||
```
|
||||
|
||||
### 3. **Client File Import Flow**
|
||||
```
|
||||
User → /file/import_client [GET]
|
||||
↓
|
||||
Display form for client file upload
|
||||
↓
|
||||
User uploads Excel file with client specifications
|
||||
↓
|
||||
POST /file/import_client
|
||||
↓
|
||||
FileService.handle_client_file_upload()
|
||||
├─ Validate & save file
|
||||
├─ Read sheets: "Tr.Ex.", "MH Ex.", "MH & DC"
|
||||
├─ Process and insert into:
|
||||
│ ├─ mh_ex_client
|
||||
│ ├─ tr_ex_client
|
||||
│ └─ mh_dc_client
|
||||
└─ Return status
|
||||
```
|
||||
|
||||
### 4. **Comparison Report Generation Flow**
|
||||
```
|
||||
User → /report/generate_comparison [GET]
|
||||
↓
|
||||
Display form to select RA Bill No.
|
||||
↓
|
||||
User selects bill number
|
||||
↓
|
||||
POST /report/generate_comparison
|
||||
↓
|
||||
generate_report_bp routes request
|
||||
├─ Fetch client data (mh_ex_client, tr_ex_client, mh_dc_client)
|
||||
├─ Fetch subcontractor data (by RA_Bill_No)
|
||||
├─ For each record type:
|
||||
│ ├─ Create lookup table by (Location, MH_NO)
|
||||
│ ├─ Match client records with subcontractor records
|
||||
│ ├─ Calculate totals for each category
|
||||
│ ├─ Compute differences/variances
|
||||
│ └─ Format for Excel output
|
||||
│
|
||||
├─ Generate Excel file with comparison results:
|
||||
│ ├─ Summary sheet
|
||||
│ ├─ Detailed Trench Excavation comparison
|
||||
│ ├─ Detailed Manhole Excavation comparison
|
||||
│ └─ Detailed Domestic Chamber comparison
|
||||
│
|
||||
└─ Send file to client as download
|
||||
```
|
||||
|
||||
### 5. **Dashboard & Data Retrieval Flow**
|
||||
```
|
||||
User → /dashboard [GET]
|
||||
↓
|
||||
Check session["user_id"] (authentication)
|
||||
↓
|
||||
Render dashboard.html
|
||||
├─ Display available reports
|
||||
├─ List recent RA Bills
|
||||
├─ Show subcontractor list
|
||||
└─ Provide navigation to:
|
||||
├─ File import
|
||||
├─ Reports
|
||||
├─ Subcontractor management
|
||||
└─ Logout
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Routes & Endpoints
|
||||
|
||||
### Authentication Routes (`/auth`)
|
||||
| Method | Endpoint | Purpose | Auth |
|
||||
|--------|----------|---------|------|
|
||||
| GET/POST | `/login` | Login form & validation | ❌ |
|
||||
| GET/POST | `/register` | User registration | ❌ |
|
||||
| GET | `/logout` | Clear session & logout | ✅ |
|
||||
|
||||
### Dashboard Route (`/dashboard`)
|
||||
| Method | Endpoint | Purpose | Auth |
|
||||
|--------|----------|---------|------|
|
||||
| GET | `/dashboard/` | Main dashboard | ✅ |
|
||||
|
||||
### Subcontractor Routes (`/subcontractor`)
|
||||
| Method | Endpoint | Purpose | Auth |
|
||||
|--------|----------|---------|------|
|
||||
| GET | `/subcontractor/add` | Add form | ✅ |
|
||||
| POST | `/subcontractor/save` | Save new subcontractor | ✅ |
|
||||
| GET | `/subcontractor/list` | List all subcontractors | ✅ |
|
||||
| GET | `/subcontractor/edit/<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 App Configuration
|
||||
# -----------------------------
|
||||
FLASK_ENV=development
|
||||
FLASK_DEBUG=True
|
||||
FLASK_HOST='0.0.0.0'
|
||||
FLASK_PORT=5001
|
||||
|
||||
# -----------------------------
|
||||
# Security
|
||||
# -----------------------------
|
||||
SECRET_KEY=change-this-to-strong-secret-key
|
||||
|
||||
# -----------------------------
|
||||
# Database Configuration
|
||||
# -----------------------------
|
||||
DB_DIALECT=mysql
|
||||
DB_DRIVER=pymysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_NAME=comparisondb
|
||||
DB_USER=root
|
||||
DB_PASSWORD=your_password
|
||||
|
||||
|
||||
```
|
||||
|
||||
### 3. Initialize Database
|
||||
```bash
|
||||
python run.py
|
||||
```
|
||||
This will:
|
||||
- Load environment variables
|
||||
- Create Flask app with configuration
|
||||
- Initialize SQLAlchemy
|
||||
- Create all tables (if not exist)
|
||||
- Start Flask development server
|
||||
|
||||
### 4. Access Application
|
||||
Open browser: `http://127.0.0.1:5000/`
|
||||
- First time: Redirect to `/login`
|
||||
- Register a new account
|
||||
- Login and start using the application
|
||||
|
||||
---
|
||||
|
||||
## Key Services & Utilities
|
||||
|
||||
### FileService (`app/services/file_service.py`)
|
||||
**Purpose:** Handles file uploads, parsing, and data validation
|
||||
|
||||
**Key Methods:**
|
||||
- `handle_file_upload(file, subcontractor_id, RA_Bill_No)` - Upload & process subcontractor file
|
||||
- `handle_client_file_upload(file, RA_Bill_No)` - Upload & process client file
|
||||
- `process_trench_excavation(df, subcontractor_id, RA_Bill_No)` - Parse & insert trench data
|
||||
- `process_manhole_excavation(df, subcontractor_id, RA_Bill_No)` - Parse & insert manhole data
|
||||
- `process_manhole_domestic_chamber(df, subcontractor_id, RA_Bill_No)` - Parse & insert chamber data
|
||||
|
||||
### UserService (`app/services/user_service.py`)
|
||||
**Purpose:** User authentication & management
|
||||
|
||||
**Key Methods:**
|
||||
- `register_user(name, email, password)` - Register new user
|
||||
- `validate_login(email, password)` - Authenticate user
|
||||
- `get_all_users()` - Fetch all users
|
||||
|
||||
### DBService (`app/services/db_service.py`)
|
||||
**Purpose:** Database connection & SQLAlchemy initialization
|
||||
|
||||
**Exports:**
|
||||
- `db` - SQLAlchemy instance for ORM operations
|
||||
- `migrate` - Flask-Migrate for schema migrations
|
||||
|
||||
---
|
||||
|
||||
## Authentication & Security
|
||||
|
||||
### Session Management
|
||||
- Uses Flask `session` object
|
||||
- Stores `user_id` and `user_name` after successful login
|
||||
- `@login_required` decorator validates authenticated requests
|
||||
|
||||
### Password Security
|
||||
- Passwords hashed using Werkzeug's `generate_password_hash()`
|
||||
- Verification via `check_password_hash()`
|
||||
- No plaintext passwords stored in database
|
||||
|
||||
### File Security
|
||||
- File uploads sanitized with `secure_filename()`
|
||||
- Only allowed extensions: `.xlsx`, `.xls`, `.csv`
|
||||
- Files stored in user-specific subfolders
|
||||
|
||||
---
|
||||
|
||||
## Error Handling & Validation
|
||||
|
||||
### File Import Validation
|
||||
1. **File Type Check** - Only CSV/XLS/XLSX allowed
|
||||
2. **Sheet Validation** - Required sheets must exist
|
||||
3. **Column Normalization** - Auto-fixes column name inconsistencies
|
||||
4. **Data Type Conversion** - Converts to appropriate types
|
||||
5. **Duplicate Detection** - Prevents duplicate records by (Location, MH_NO, RA_Bill_No)
|
||||
6. **Null Handling** - Converts empty/NaN values to None
|
||||
|
||||
### Database Constraints
|
||||
- Foreign key relationships enforced
|
||||
- Unique email constraint on users
|
||||
- NOT NULL constraints on critical fields
|
||||
|
||||
---
|
||||
|
||||
## Example: Typical User Workflow
|
||||
|
||||
```
|
||||
1. User registers/logs in
|
||||
→ POST /auth/register (new user) or /auth/login
|
||||
|
||||
2. User adds subcontractors
|
||||
→ GET /subcontractor/add
|
||||
→ POST /subcontractor/save
|
||||
|
||||
3. User uploads subcontractor data (Excel file with 3 sheets)
|
||||
→ GET /file/import
|
||||
→ POST /file/import (file, subcontractor_id, RA_Bill_No)
|
||||
→ Database populated with excavation data
|
||||
|
||||
4. User uploads client specifications (Excel file with same 3 sheets)
|
||||
→ GET /file/import_client
|
||||
→ POST /file/import_client (file, RA_Bill_No)
|
||||
→ Database populated with client data
|
||||
|
||||
5. User generates comparison report
|
||||
→ GET /report/generate_comparison
|
||||
→ POST /report/generate_comparison (RA_Bill_No)
|
||||
→ System compares client vs subcontractor data
|
||||
→ Generates Excel file showing differences
|
||||
→ User downloads report
|
||||
|
||||
6. User logs out
|
||||
→ GET /auth/logout
|
||||
→ Session cleared, redirected to login
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Email notifications for report generation
|
||||
- [ ] Data visualization dashboards
|
||||
- [ ] Role-based access control (Admin, User, Viewer)
|
||||
- [ ] Bulk report generation
|
||||
- [ ] Data export to PDF format
|
||||
- [ ] Audit logs for data modifications
|
||||
- [ ] API documentation (Swagger/OpenAPI)
|
||||
- [ ] Unit & integration tests
|
||||
|
||||
---
|
||||
|
||||
## Support & Contribution
|
||||
|
||||
For issues, feature requests, or contributions, please contact the development team.
|
||||
|
||||
**Last Updated:** January 2026
|
||||
@@ -32,7 +32,7 @@ def register_blueprints(app):
|
||||
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(auth_bp)
|
||||
app.register_blueprint(user_bp)
|
||||
app.register_blueprint(dashboard_bp)
|
||||
app.register_blueprint(subcontractor_bp)
|
||||
|
||||
@@ -5,16 +5,19 @@ class LayingClient(db.Model):
|
||||
__tablename__ = "laying_client"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
# Foreign Key to Subcontractor tables
|
||||
# subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False)
|
||||
# Relationship for easy access (subcontractor.subcontractor_name)
|
||||
# subcontractor = db.relationship("Subcontractor", backref="laying_records")
|
||||
|
||||
# Basic Fields
|
||||
Location = db.Column(db.String(500))
|
||||
MH_NO = db.Column(db.String(100))
|
||||
CC_length = db.Column(db.Float)
|
||||
# Bedding Qty.
|
||||
Outer_dia_of_MH_m = db.Column(db.Float)
|
||||
Bedding_Length = db.Column(db.Float)
|
||||
Width = db.Column(db.Float)
|
||||
Depth = db.Column(db.Float)
|
||||
Qty = db.Column(db.Float)
|
||||
|
||||
# PIPE LAYING Qty.
|
||||
Pipe_Dia_mm = db.Column(db.Float)
|
||||
ID_of_MH_m = db.Column(db.Float)
|
||||
Laying_Length = db.Column(db.Float)
|
||||
@@ -32,7 +35,6 @@ class LayingClient(db.Model):
|
||||
pipe_900_mm = db.Column(db.Float)
|
||||
pipe_1200_mm = db.Column(db.Float)
|
||||
|
||||
|
||||
Total = db.Column(db.Float)
|
||||
Remarks = db.Column(db.String(500))
|
||||
RA_Bill_No=db.Column(db.String(500))
|
||||
@@ -45,3 +47,12 @@ class LayingClient(db.Model):
|
||||
|
||||
def serialize(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
|
||||
def sum_laying_fields():
|
||||
return [
|
||||
"pipe_150_mm", "pipe_200_mm", "pipe_250_mm",
|
||||
"pipe_300_mm", "pipe_350_mm", "pipe_400_mm",
|
||||
"pipe_450_mm", "pipe_500_mm", "pipe_600_mm",
|
||||
"pipe_700_mm", "pipe_900_mm", "pipe_1200_mm"
|
||||
]
|
||||
@@ -10,11 +10,10 @@ class Laying(db.Model):
|
||||
# Relationship for easy access (subcontractor.subcontractor_name)
|
||||
subcontractor = db.relationship("Subcontractor", backref="laying_records")
|
||||
|
||||
# Basic Fields
|
||||
# Pipe Laying Fields
|
||||
Location = db.Column(db.String(500))
|
||||
MH_NO = db.Column(db.String(100))
|
||||
CC_length = db.Column(db.Float)
|
||||
|
||||
Pipe_Dia_mm = db.Column(db.Float)
|
||||
ID_of_MH_m = db.Column(db.Float)
|
||||
Laying_Length = db.Column(db.Float)
|
||||
@@ -32,7 +31,6 @@ class Laying(db.Model):
|
||||
pipe_900_mm = db.Column(db.Float)
|
||||
pipe_1200_mm = db.Column(db.Float)
|
||||
|
||||
|
||||
Total = db.Column(db.Float)
|
||||
Remarks = db.Column(db.String(500))
|
||||
RA_Bill_No=db.Column(db.String(500))
|
||||
@@ -45,3 +43,12 @@ class Laying(db.Model):
|
||||
|
||||
def serialize(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
|
||||
def sum_laying_fields():
|
||||
return [
|
||||
"pipe_150_mm", "pipe_200_mm", "pipe_250_mm",
|
||||
"pipe_300_mm", "pipe_350_mm", "pipe_400_mm",
|
||||
"pipe_450_mm", "pipe_500_mm", "pipe_600_mm",
|
||||
"pipe_700_mm", "pipe_900_mm", "pipe_1200_mm"
|
||||
]
|
||||
|
||||
@@ -49,4 +49,15 @@ class ManholeDomesticChamber(db.Model):
|
||||
|
||||
def serialize(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
|
||||
def sum_mh_dc_fields():
|
||||
return [
|
||||
"d_0_to_0_75", "d_0_76_to_1_05", "d_1_06_to_1_65",
|
||||
"d_1_66_to_2_15", "d_2_16_to_2_65", "d_2_66_to_3_15",
|
||||
"d_3_16_to_3_65", "d_3_66_to_4_15", "d_4_16_to_4_65",
|
||||
"d_4_66_to_5_15", "d_5_16_to_5_65", "d_5_66_to_6_15",
|
||||
"d_6_16_to_6_65", "d_6_66_to_7_15", "d_7_16_to_7_65",
|
||||
"d_7_66_to_8_15", "d_8_16_to_8_65", "d_8_66_to_9_15",
|
||||
"d_9_16_to_9_65"]
|
||||
|
||||
|
||||
@@ -44,3 +44,14 @@ class ManholeDomesticChamberClient(db.Model):
|
||||
|
||||
def serialize(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
|
||||
def sum_mh_dc_fields():
|
||||
return [
|
||||
"d_0_to_0_75", "d_0_76_to_1_05", "d_1_06_to_1_65",
|
||||
"d_1_66_to_2_15", "d_2_16_to_2_65", "d_2_66_to_3_15",
|
||||
"d_3_16_to_3_65", "d_3_66_to_4_15", "d_4_16_to_4_65",
|
||||
"d_4_66_to_5_15", "d_5_16_to_5_65", "d_5_66_to_6_15",
|
||||
"d_6_16_to_6_65", "d_6_66_to_7_15", "d_7_16_to_7_65",
|
||||
"d_7_66_to_8_15", "d_8_16_to_8_65", "d_8_66_to_9_15",
|
||||
"d_9_16_to_9_65" ]
|
||||
@@ -77,3 +77,34 @@ class TrenchExcavation(db.Model):
|
||||
|
||||
def serialize(self):
|
||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||
|
||||
def excavation_category_sums(self):
|
||||
|
||||
def safe(val):
|
||||
return val or 0
|
||||
|
||||
return {
|
||||
"Soft_Murum_Total": (
|
||||
safe(self.Soft_Murum_0_to_1_5)
|
||||
+ safe(self.Soft_Murum_1_5_to_3_0)
|
||||
+ safe(self.Soft_Murum_3_0_to_4_5)
|
||||
),
|
||||
|
||||
"Hard_Murum_Total": (
|
||||
safe(self.Hard_Murum_0_to_1_5)
|
||||
+ safe(self.Hard_Murum_1_5_to_3_0)
|
||||
),
|
||||
|
||||
"Soft_Rock_Total": (
|
||||
safe(self.Soft_Rock_0_to_1_5)
|
||||
+ safe(self.Soft_Rock_1_5_to_3_0)
|
||||
),
|
||||
|
||||
"Hard_Rock_Total": (
|
||||
safe(self.Hard_Rock_0_to_1_5)
|
||||
+ safe(self.Hard_Rock_1_5_to_3_0)
|
||||
+ safe(self.Hard_Rock_3_0_to_4_5)
|
||||
+ safe(self.Hard_Rock_4_5_to_6_0)
|
||||
+ safe(self.Hard_Rock_6_0_to_7_5)
|
||||
),
|
||||
}
|
||||
|
||||
@@ -5,16 +5,20 @@ from flask import Blueprint, render_template, session, redirect, url_for
|
||||
import matplotlib.pyplot as plt
|
||||
import io
|
||||
import base64
|
||||
from app.utils.plot_utils import plot_to_base64
|
||||
from app.services.dashboard_service import DashboardService
|
||||
|
||||
dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/dashboard")
|
||||
|
||||
# dashboard_bp = Blueprint("dashboard", __name__)
|
||||
|
||||
# charts
|
||||
def plot_to_base64():
|
||||
img = io.BytesIO()
|
||||
plt.savefig(img, format="png", bbox_inches="tight")
|
||||
plt.close()
|
||||
img.seek(0)
|
||||
return base64.b64encode(img.getvalue()).decode()
|
||||
# def plot_to_base64():
|
||||
# img = io.BytesIO()
|
||||
# plt.savefig(img, format="png", bbox_inches="tight")
|
||||
# plt.close()
|
||||
# img.seek(0)
|
||||
# return base64.b64encode(img.getvalue()).decode()
|
||||
|
||||
# bar chart
|
||||
def bar_chart():
|
||||
@@ -28,7 +32,7 @@ def bar_chart():
|
||||
plt.ylabel("Count")
|
||||
|
||||
|
||||
return plot_to_base64()
|
||||
return plot_to_base64(plt)
|
||||
|
||||
# Pie chart
|
||||
def pie_chart():
|
||||
@@ -39,7 +43,7 @@ def pie_chart():
|
||||
plt.pie(sizes, labels=labels, autopct="%1.1f%%", startangle=140)
|
||||
plt.title("Project Status")
|
||||
|
||||
return plot_to_base64()
|
||||
return plot_to_base64(plt)
|
||||
|
||||
# Histogram chart
|
||||
def histogram_chart():
|
||||
@@ -51,7 +55,7 @@ def histogram_chart():
|
||||
plt.xlabel("Work Units")
|
||||
plt.ylabel("Frequency")
|
||||
|
||||
return plot_to_base64()
|
||||
return plot_to_base64(plt)
|
||||
|
||||
# Dashboaed page
|
||||
@dashboard_bp.route("/")
|
||||
@@ -68,15 +72,16 @@ def dashboard():
|
||||
)
|
||||
|
||||
# subcontractor dashboard
|
||||
@dashboard_bp.route("/subcontractor_dashboard")
|
||||
@dashboard_bp.route("/subcontractor_dashboard", methods=["GET", "POST"])
|
||||
def subcontractor_dashboard():
|
||||
if not session.get("user_id"):
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
tr_dash = DashboardService().bar_chart_of_tr_ex
|
||||
|
||||
|
||||
return render_template(
|
||||
"subcontractor_dashboard.html",
|
||||
title="Dashboard",
|
||||
bar_chart=bar_chart(),
|
||||
pie_chart=pie_chart(),
|
||||
histogram=histogram_chart()
|
||||
)
|
||||
bar_chart=tr_dash
|
||||
)
|
||||
@@ -43,4 +43,4 @@ def client_import_file():
|
||||
|
||||
flash(msg, "success" if success else "danger")
|
||||
|
||||
return render_template("file_import_client.html", title="Client File Import", subcontractors=subcontractors)
|
||||
return render_template("file_import_client.html", title="Client File Import", subcontractors=subcontractors)
|
||||
@@ -72,7 +72,12 @@ class SubcontractorBill:
|
||||
for df in [self.df_tr, self.df_mh, self.df_dc, self.df_laying]:
|
||||
if not df.empty:
|
||||
df.drop(columns=drop_cols, errors="ignore", inplace=True)
|
||||
|
||||
|
||||
print(":::-- TR. EX --:::",self.df_tr,"\n")
|
||||
print(":::--MH EX--:::",self.df_mh,"\n")
|
||||
print(":::--MH DC--:::",self.df_dc,"\n")
|
||||
print(":::--Laying--:::",self.df_laying,"\n")
|
||||
|
||||
# --- subcontractor report only ---
|
||||
@file_report_bp.route("/Subcontractor_report", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@@ -153,7 +158,7 @@ def client_vs_all_subcontractor():
|
||||
|
||||
if not RA_Bill_No:
|
||||
flash("Please enter RA Bill No.", "danger")
|
||||
return render_template("generate_comparison_client_vs_subcont.html", tables=tables, ra_val=ra_val)
|
||||
return render_template("client_report.html", tables=tables, ra_val=ra_val)
|
||||
|
||||
clientBill = ClientBill()
|
||||
clientBill.Fetch(RA_Bill_No=RA_Bill_No)
|
||||
@@ -163,11 +168,12 @@ def client_vs_all_subcontractor():
|
||||
# --- SAFETY CHECK: Verify data exists before merging ---
|
||||
if clientBill.df_tr.empty and clientBill.df_mh.empty:
|
||||
flash(f"No Client records found for RA Bill {RA_Bill_No}", "warning")
|
||||
return render_template("generate_comparison_client_vs_subcont.html", tables=tables, ra_val=ra_val)
|
||||
return render_template("client_report.html", tables=tables, ra_val=ra_val)
|
||||
|
||||
qty_cols = [...] # (Keep your existing list)
|
||||
mh_dc_qty_cols = [...] # (Keep your existing list)
|
||||
|
||||
mh_lay_qty_cols =[...]
|
||||
|
||||
def aggregate_df(df, group_cols, sum_cols):
|
||||
if df.empty:
|
||||
# Create an empty DF with the correct columns to avoid Merge/Key Errors
|
||||
@@ -183,10 +189,11 @@ def client_vs_all_subcontractor():
|
||||
df_sub_tr_grp = aggregate_df(contractorBill.df_tr, ["Location", "MH_NO"], qty_cols)
|
||||
df_sub_mh_grp = aggregate_df(contractorBill.df_mh, ["Location", "MH_NO"], qty_cols)
|
||||
df_sub_dc_grp = aggregate_df(contractorBill.df_dc, ["Location", "MH_NO"], mh_dc_qty_cols)
|
||||
df_sub_lay_grp = aggregate_df(contractorBill.df_dc, ["Location", "MH_NO"], mh_lay_qty_cols)
|
||||
|
||||
# --- FINAL MERGE LOGIC ---
|
||||
# We check if "Location" exists in the client data. If not, we add it to prevent the KeyError.
|
||||
for df_client in [clientBill.df_tr, clientBill.df_mh, clientBill.df_dc]:
|
||||
for df_client in [clientBill.df_tr, clientBill.df_mh, clientBill.df_dc, clientBill.df_laying ]:
|
||||
if not df_client.empty and "Location" not in df_client.columns:
|
||||
df_client["Location"] = "Unknown"
|
||||
|
||||
@@ -194,15 +201,17 @@ def client_vs_all_subcontractor():
|
||||
df_tr_cmp = clientBill.df_tr.merge(df_sub_tr_grp, on=["Location", "MH_NO"], how="left", suffixes=("_Client", "_Sub"))
|
||||
df_mh_cmp = clientBill.df_mh.merge(df_sub_mh_grp, on=["Location", "MH_NO"], how="left", suffixes=("_Client", "_Sub"))
|
||||
df_dc_cmp = clientBill.df_dc.merge(df_sub_dc_grp, on=["Location", "MH_NO"], how="left", suffixes=("_Client", "_Sub"))
|
||||
df_lay_cmp = clientBill.df_laying.merge(df_sub_lay_grp, on=["Location", "MH_NO"], how="left", suffixes=("_Client", "_Sub"))
|
||||
except KeyError as e:
|
||||
flash(f"Merge Error: Missing column {str(e)}. Check if 'Location' is defined in your database models.", "danger")
|
||||
return render_template("generate_comparison_client_vs_subcont.html", tables=tables, ra_val=ra_val)
|
||||
return render_template("client_report.html", tables=tables, ra_val=ra_val)
|
||||
|
||||
|
||||
# Convert to HTML for preview
|
||||
tables["tr"] = df_tr_cmp.to_html(classes='table table-striped table-hover table-sm', index=False)
|
||||
tables["mh"] = df_mh_cmp.to_html(classes='table table-striped table-hover table-sm', index=False)
|
||||
tables["dc"] = df_dc_cmp.to_html(classes='table table-striped table-hover table-sm', index=False)
|
||||
tables["laying"] = df_lay_cmp.to_html(classes='table table-striped table-hover table-sm', index=False)
|
||||
|
||||
|
||||
return render_template("client_report.html", tables=tables, ra_val=ra_val)
|
||||
|
||||
@@ -14,10 +14,18 @@ from app.models.mh_dc_client_model import ManholeDomesticChamberClient
|
||||
from app.models.laying_client_model import LayingClient
|
||||
|
||||
from app.utils.helpers import login_required
|
||||
import re
|
||||
|
||||
|
||||
generate_report_bp = Blueprint("generate_report", __name__, url_prefix="/report")
|
||||
|
||||
|
||||
# sum field of pipe laying (pipe_150_mm)
|
||||
PIPE_MM_PATTERN = re.compile(r"^pipe_\d+_mm$")
|
||||
# sum fields of MH dc (d_0_to_0_75)
|
||||
D_RANGE_PATTERN = re.compile( r"^d_\d+(?:_\d+)?_to_\d+(?:_\d+)?$")
|
||||
|
||||
|
||||
# NORMALIZER
|
||||
def normalize_key(value):
|
||||
if value is None:
|
||||
@@ -81,13 +89,13 @@ def build_comparison(client_rows, contractor_rows, key_field):
|
||||
client_total = sum(
|
||||
float(v or 0)
|
||||
for k, v in c.items()
|
||||
if k.endswith("_total")
|
||||
if k.endswith("_total") or D_RANGE_PATTERN.match(k) or PIPE_MM_PATTERN.match(k)
|
||||
)
|
||||
|
||||
sub_total = sum(
|
||||
float(v or 0)
|
||||
for k, v in s.items()
|
||||
if k.endswith("_total")
|
||||
if k.endswith("_total") or D_RANGE_PATTERN.match(k) or PIPE_MM_PATTERN.match(k)
|
||||
)
|
||||
|
||||
diff = client_total - sub_total
|
||||
@@ -129,10 +137,10 @@ def write_sheet(writer, df, sheet_name, subcontractor_name):
|
||||
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"})
|
||||
client_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#B6DAED"})
|
||||
sub_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F3A081"})
|
||||
total_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F7D261"})
|
||||
diff_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#82DD49"})
|
||||
default_header_fmt = workbook.add_format({"bold": True,"border": 1,"bg_color": "#E7E6E6","align": "center","valign": "vcenter"})
|
||||
|
||||
|
||||
@@ -147,12 +155,13 @@ def write_sheet(writer, df, sheet_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"):
|
||||
elif col_name.endswith("_total") or col_name.endswith("_total") :
|
||||
ws.write(3, col_num, col_name, total_fmt)
|
||||
elif col_name == "Diff":
|
||||
ws.write(3, col_num, col_name, diff_fmt)
|
||||
@@ -194,12 +203,14 @@ def comparison_report():
|
||||
subcontractor_id=subcontractor_id
|
||||
).all()]
|
||||
df_dc = build_comparison(dc_client, dc_sub, "MH_NO")
|
||||
# df_dc = build_comparison_mh_dc(dc_client, dc_sub, "MH_NO")
|
||||
|
||||
lay_client = [r.serialize() for r in LayingClient.query.all()]
|
||||
lay_sub = [r.serialize() for r in Laying.query.filter_by(
|
||||
subcontractor_id=subcontractor_id
|
||||
).all()]
|
||||
df_lay = build_comparison(lay_client, lay_sub, "MH_NO")
|
||||
# df_lay = build_comparison_laying(lay_client, lay_sub, "MH_NO")
|
||||
|
||||
|
||||
# -------- EXCEL --------
|
||||
@@ -223,3 +234,104 @@ def comparison_report():
|
||||
return render_template("generate_comparison_report.html",subcontractors=subcontractors)
|
||||
|
||||
|
||||
# def build_comparison_mh_dc(client_rows, contractor_rows, key_field):
|
||||
# contractor_lookup = make_lookup(contractor_rows, key_field)
|
||||
# mh_dc_fields = ManholeDomesticChamberClient.sum_mh_dc_fields()
|
||||
|
||||
# output = []
|
||||
|
||||
# for c in client_rows:
|
||||
# loc = normalize_key(c.get("Location"))
|
||||
# key = normalize_key(c.get(key_field))
|
||||
# if not loc or not key:
|
||||
# continue
|
||||
|
||||
# s = contractor_lookup.get((loc, key))
|
||||
# if not s:
|
||||
# continue
|
||||
|
||||
# client_total = sum(float(c.get(f, 0) or 0) for f in mh_dc_fields)
|
||||
# sub_total = sum(float(s.get(f, 0) or 0) for f in mh_dc_fields)
|
||||
|
||||
# row = {
|
||||
# "Location": loc,
|
||||
# key_field.replace("_", " "): key
|
||||
# }
|
||||
|
||||
# # CLIENT – ALL FIELDS
|
||||
# for k, v in c.items():
|
||||
# if k in ["id", "created_at"]:
|
||||
# continue
|
||||
# row[f"Client-{k}"] = v
|
||||
|
||||
# row["Client-Total"] = round(client_total, 2)
|
||||
# row[" "] = ""
|
||||
|
||||
# # SUBCONTRACTOR – ALL FIELDS
|
||||
# for k, v in s.items():
|
||||
# if k in ["id", "created_at", "subcontractor_id"]:
|
||||
# continue
|
||||
# row[f"Subcontractor-{k}"] = v
|
||||
|
||||
# row["Subcontractor-Total"] = round(sub_total, 2)
|
||||
# row["Diff"] = round(client_total - sub_total, 2)
|
||||
|
||||
# output.append(row)
|
||||
|
||||
# df = pd.DataFrame(output)
|
||||
# df.columns = [format_header(col) for col in df.columns]
|
||||
# return df
|
||||
|
||||
|
||||
# def build_comparison_laying(client_rows, contractor_rows, key_field):
|
||||
# contractor_lookup = make_lookup(contractor_rows, key_field)
|
||||
# laying_fields = Laying.sum_laying_fields()
|
||||
|
||||
# output = []
|
||||
|
||||
# for c in client_rows:
|
||||
# loc = normalize_key(c.get("Location"))
|
||||
# key = normalize_key(c.get(key_field))
|
||||
# if not loc or not key:
|
||||
# continue
|
||||
|
||||
# s = contractor_lookup.get((loc, key))
|
||||
# if not s:
|
||||
# continue
|
||||
|
||||
# client_total = sum(float(c.get(f, 0) or 0) for f in laying_fields)
|
||||
# sub_total = sum(float(s.get(f, 0) or 0) for f in laying_fields)
|
||||
|
||||
# print("--------------",key,"----------")
|
||||
# print("sum -client_total ",client_total)
|
||||
# print("sum -sub_total ",sub_total)
|
||||
# print("Diff ---- ",client_total - sub_total)
|
||||
# print("------------------------")
|
||||
# row = {
|
||||
# "Location": loc,
|
||||
# key_field.replace("_", " "): key
|
||||
# }
|
||||
|
||||
# # CLIENT – ALL FIELDS
|
||||
# for k, v in c.items():
|
||||
# if k in ["id", "created_at"]:
|
||||
# continue
|
||||
# row[f"Client-{k}"] = v
|
||||
|
||||
# row["Client-Total"] = round(client_total, 2)
|
||||
# row[" "] = ""
|
||||
|
||||
# # SUBCONTRACTOR – ALL FIELDS
|
||||
# for k, v in s.items():
|
||||
# if k in ["id", "created_at", "subcontractor_id"]:
|
||||
# continue
|
||||
# row[f"Subcontractor-{k}"] = v
|
||||
|
||||
# row["Subcontractor-Total"] = round(sub_total, 2)
|
||||
# row["Diff"] = round(client_total - sub_total, 2)
|
||||
|
||||
# output.append(row)
|
||||
|
||||
# df = pd.DataFrame(output)
|
||||
# df.columns = [format_header(col) for col in df.columns]
|
||||
# return df
|
||||
|
||||
@@ -2,9 +2,108 @@ from flask import Blueprint, render_template, request, redirect, flash
|
||||
from app import db
|
||||
from app.models.subcontractor_model import Subcontractor
|
||||
from app.utils.helpers import login_required
|
||||
|
||||
|
||||
subcontractor_bp = Blueprint("subcontractor", __name__, url_prefix="/subcontractor")
|
||||
|
||||
|
||||
# ---------------- ADD -----------------
|
||||
@subcontractor_bp.route("/add")
|
||||
@login_required
|
||||
def add_subcontractor():
|
||||
return render_template("subcontractor/add.html")
|
||||
@subcontractor_bp.route("/save", methods=["POST"])
|
||||
@login_required
|
||||
def save_subcontractor():
|
||||
# 1. Get and clean the name from the form
|
||||
name = request.form.get("subcontractor_name", "").strip()
|
||||
|
||||
# 2. Basic validation: Ensure the name isn't empty
|
||||
if not name:
|
||||
flash("Subcontractor name cannot be empty.", "danger")
|
||||
return redirect("/subcontractor/add")
|
||||
|
||||
# 3. Check if a subcontractor with this name already exists
|
||||
existing_sub = Subcontractor.query.filter_by(subcontractor_name=name).first()
|
||||
|
||||
if existing_sub:
|
||||
flash(f"Subcontractor with name '{name}' already exists!", "danger")
|
||||
return redirect("/subcontractor/add")
|
||||
|
||||
# 4. If no duplicate is found, proceed to save
|
||||
try:
|
||||
subcontractor = Subcontractor(
|
||||
subcontractor_name=name,
|
||||
contact_person=request.form.get("contact_person"),
|
||||
mobile_no=request.form.get("mobile_no"),
|
||||
email_id=request.form.get("email_id"),
|
||||
gst_no=request.form.get("gst_no")
|
||||
)
|
||||
|
||||
db.session.add(subcontractor)
|
||||
db.session.commit()
|
||||
flash("Subcontractor added successfully!", "success")
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash("An error occurred while saving. Please try again.", "danger")
|
||||
|
||||
return redirect("/subcontractor/list")
|
||||
|
||||
# ---------------- LIST -----------------
|
||||
@subcontractor_bp.route("/list")
|
||||
@login_required
|
||||
def subcontractor_list():
|
||||
subcontractors = Subcontractor.query.all()
|
||||
return render_template("subcontractor/list.html", subcontractors=subcontractors)
|
||||
|
||||
# ---------------- EDIT -----------------
|
||||
@subcontractor_bp.route("/edit/<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)
|
||||
new_name = request.form.get("subcontractor_name")
|
||||
|
||||
# Check if the new name is taken by someone ELSE (not this current ID)
|
||||
duplicate = Subcontractor.query.filter(
|
||||
Subcontractor.subcontractor_name == new_name,
|
||||
Subcontractor.id != id
|
||||
).first()
|
||||
|
||||
if duplicate:
|
||||
flash("Another subcontractor already uses this name.", "danger")
|
||||
return redirect(f"/subcontractor/edit/{id}")
|
||||
|
||||
subcontractor.subcontractor_name = new_name
|
||||
|
||||
db.session.commit()
|
||||
|
||||
flash("Subcontractor updated successfully!", "success")
|
||||
return redirect("/subcontractor/list")
|
||||
|
||||
# ---------------- DELETE -----------------
|
||||
@subcontractor_bp.route("/delete/<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")
|
||||
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
|
||||
@@ -14,50 +113,71 @@ def add_subcontractor():
|
||||
@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")
|
||||
name = request.form.get("subcontractor_name", "").strip()
|
||||
if not name:
|
||||
flash("Subcontractor name cannot be empty.", "danger")
|
||||
return redirect("/subcontractor/add")
|
||||
existing_sub = Subcontractor.query.filter_by(subcontractor_name=name).first()
|
||||
|
||||
if existing_sub:
|
||||
flash(f"Subcontractor with name '{name}' already exists!", "danger")
|
||||
return redirect("/subcontractor/add")
|
||||
try:
|
||||
subcontractor = Subcontractor(
|
||||
subcontractor_name=name,
|
||||
contact_person=request.form.get("contact_person"),
|
||||
mobile_no=request.form.get("mobile_no"),
|
||||
email_id=request.form.get("email_id"),
|
||||
gst_no=request.form.get("gst_no")
|
||||
)
|
||||
|
||||
db.session.add(subcontractor)
|
||||
db.session.commit()
|
||||
flash("Subcontractor added successfully!", "success")
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash("An error occurred while saving. Please try again.", "danger")
|
||||
|
||||
return redirect("/subcontractor/list")
|
||||
|
||||
|
||||
# ---------------- LIST -----------------
|
||||
@subcontractor_bp.route("/list")
|
||||
@login_required
|
||||
def subcontractor_list():
|
||||
subcontractors = Subcontractor.query.all()
|
||||
return render_template("subcontractor/list.html", subcontractors=subcontractors)
|
||||
|
||||
|
||||
# ---------------- EDIT -----------------
|
||||
@subcontractor_bp.route("/edit/<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")
|
||||
|
||||
new_name = request.form.get("subcontractor_name")
|
||||
|
||||
# Check if the new name is taken by someone ELSE (not this current ID)
|
||||
duplicate = Subcontractor.query.filter(
|
||||
Subcontractor.subcontractor_name == new_name,
|
||||
Subcontractor.id != id
|
||||
).first()
|
||||
|
||||
if duplicate:
|
||||
flash("Another subcontractor already uses this name.", "danger")
|
||||
return redirect(f"/subcontractor/edit/{id}")
|
||||
|
||||
subcontractor.subcontractor_name = new_name
|
||||
db.session.commit()
|
||||
|
||||
|
||||
flash("Subcontractor updated successfully!", "success")
|
||||
return redirect("/subcontractor/list")
|
||||
|
||||
|
||||
# ---------------- DELETE -----------------
|
||||
@subcontractor_bp.route("/delete/<int:id>")
|
||||
@login_required
|
||||
@@ -68,4 +188,4 @@ def delete_subcontractor(id):
|
||||
db.session.commit()
|
||||
|
||||
flash("Subcontractor deleted successfully!", "success")
|
||||
return redirect("/subcontractor/list")
|
||||
return redirect("/subcontractor/list")
|
||||
49
app/services/dashboard_service.py
Normal file
49
app/services/dashboard_service.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from app.config import Config
|
||||
from app import db
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
from app.routes.dashboard import plot_to_base64
|
||||
import matplotlib.pyplot as plt
|
||||
import io
|
||||
import base64
|
||||
|
||||
from app.utils.plot_utils import plot_to_base64
|
||||
|
||||
# Subcontractor models import
|
||||
from app.models.trench_excavation_model import TrenchExcavation
|
||||
|
||||
# Client models import
|
||||
# from app.models.tr_ex_client_model import TrenchExcavationClient
|
||||
|
||||
|
||||
class DashboardService:
|
||||
|
||||
|
||||
# bar chart
|
||||
def bar_chart_of_tr_ex():
|
||||
categories = ["Soft Murum", "Hard Murum", "Soft Rock", "Hard Rock"]
|
||||
values = [120, 80, 150, 60]
|
||||
|
||||
tr = TrenchExcavation()
|
||||
|
||||
record = TrenchExcavation.query.first()
|
||||
print(" RA_Bill_No::",record["RA_Bill_No"])
|
||||
|
||||
totals = tr.excavation_category_sums()
|
||||
|
||||
# print(totals["Soft_Murum_Total"])
|
||||
# print(totals["Hard_Rock_Total"])
|
||||
|
||||
|
||||
plt.figure()
|
||||
plt.bar(categories, values)
|
||||
plt.title("Trench Excavation Work Category Report")
|
||||
plt.xlabel("Excavation category")
|
||||
plt.ylabel("Quantity")
|
||||
|
||||
return plot_to_base64(plt)
|
||||
|
||||
|
||||
def subcontractor_dash():
|
||||
return True
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -33,7 +33,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if tables.tr or tables.mh or tables.dc %}
|
||||
{% if tables.tr or tables.mh or tables.dc or tables.laying %}
|
||||
<div class="card shadow-sm p-3">
|
||||
<h4 class="mb-3">Comparison Preview</h4>
|
||||
|
||||
@@ -50,6 +50,9 @@
|
||||
<button class="nav-link" id="dc-tab" data-bs-toggle="tab" data-bs-target="#dc" type="button">MH & DC
|
||||
Comparison</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" id="laying-tab" data-bs-toggle="tab" data-bs-target="#laying" type="button">Laying
|
||||
& Bedding Comparison</button>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content mt-3" id="reportTabsContent">
|
||||
@@ -68,6 +71,11 @@
|
||||
{{ tables.dc|safe }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="laying" role="tabpanel">
|
||||
<div class="table-responsive" style="max-height: 500px;">
|
||||
{{ tables.laying|safe }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
10
app/utils/plot_utils.py
Normal file
10
app/utils/plot_utils.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import base64
|
||||
from io import BytesIO
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
|
||||
def plot_to_base64(plt):
|
||||
img = BytesIO()
|
||||
plt.savefig(img, format="png", bbox_inches="tight")
|
||||
img.seek(0)
|
||||
return base64.b64encode(img.read()).decode("utf-8")
|
||||
Reference in New Issue
Block a user