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
|
# 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
|
||||||
@@ -5,16 +5,19 @@ class LayingClient(db.Model):
|
|||||||
__tablename__ = "laying_client"
|
__tablename__ = "laying_client"
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
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
|
# Basic Fields
|
||||||
Location = db.Column(db.String(500))
|
Location = db.Column(db.String(500))
|
||||||
MH_NO = db.Column(db.String(100))
|
MH_NO = db.Column(db.String(100))
|
||||||
CC_length = db.Column(db.Float)
|
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)
|
Pipe_Dia_mm = db.Column(db.Float)
|
||||||
ID_of_MH_m = db.Column(db.Float)
|
ID_of_MH_m = db.Column(db.Float)
|
||||||
Laying_Length = 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_900_mm = db.Column(db.Float)
|
||||||
pipe_1200_mm = db.Column(db.Float)
|
pipe_1200_mm = db.Column(db.Float)
|
||||||
|
|
||||||
|
|
||||||
Total = db.Column(db.Float)
|
Total = db.Column(db.Float)
|
||||||
Remarks = db.Column(db.String(500))
|
Remarks = db.Column(db.String(500))
|
||||||
RA_Bill_No=db.Column(db.String(500))
|
RA_Bill_No=db.Column(db.String(500))
|
||||||
@@ -45,3 +47,12 @@ class LayingClient(db.Model):
|
|||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
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)
|
# Relationship for easy access (subcontractor.subcontractor_name)
|
||||||
subcontractor = db.relationship("Subcontractor", backref="laying_records")
|
subcontractor = db.relationship("Subcontractor", backref="laying_records")
|
||||||
|
|
||||||
# Basic Fields
|
# Pipe Laying Fields
|
||||||
Location = db.Column(db.String(500))
|
Location = db.Column(db.String(500))
|
||||||
MH_NO = db.Column(db.String(100))
|
MH_NO = db.Column(db.String(100))
|
||||||
CC_length = db.Column(db.Float)
|
CC_length = db.Column(db.Float)
|
||||||
|
|
||||||
Pipe_Dia_mm = db.Column(db.Float)
|
Pipe_Dia_mm = db.Column(db.Float)
|
||||||
ID_of_MH_m = db.Column(db.Float)
|
ID_of_MH_m = db.Column(db.Float)
|
||||||
Laying_Length = 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_900_mm = db.Column(db.Float)
|
||||||
pipe_1200_mm = db.Column(db.Float)
|
pipe_1200_mm = db.Column(db.Float)
|
||||||
|
|
||||||
|
|
||||||
Total = db.Column(db.Float)
|
Total = db.Column(db.Float)
|
||||||
Remarks = db.Column(db.String(500))
|
Remarks = db.Column(db.String(500))
|
||||||
RA_Bill_No=db.Column(db.String(500))
|
RA_Bill_No=db.Column(db.String(500))
|
||||||
@@ -45,3 +43,12 @@ class Laying(db.Model):
|
|||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
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"
|
||||||
|
]
|
||||||
|
|||||||
@@ -50,3 +50,14 @@ class ManholeDomesticChamber(db.Model):
|
|||||||
def serialize(self):
|
def serialize(self):
|
||||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
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):
|
def serialize(self):
|
||||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
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):
|
def serialize(self):
|
||||||
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
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 matplotlib.pyplot as plt
|
||||||
import io
|
import io
|
||||||
import base64
|
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__, url_prefix="/dashboard")
|
||||||
|
|
||||||
|
# dashboard_bp = Blueprint("dashboard", __name__)
|
||||||
|
|
||||||
# charts
|
# charts
|
||||||
def plot_to_base64():
|
# def plot_to_base64():
|
||||||
img = io.BytesIO()
|
# img = io.BytesIO()
|
||||||
plt.savefig(img, format="png", bbox_inches="tight")
|
# plt.savefig(img, format="png", bbox_inches="tight")
|
||||||
plt.close()
|
# plt.close()
|
||||||
img.seek(0)
|
# img.seek(0)
|
||||||
return base64.b64encode(img.getvalue()).decode()
|
# return base64.b64encode(img.getvalue()).decode()
|
||||||
|
|
||||||
# bar chart
|
# bar chart
|
||||||
def bar_chart():
|
def bar_chart():
|
||||||
@@ -28,7 +32,7 @@ def bar_chart():
|
|||||||
plt.ylabel("Count")
|
plt.ylabel("Count")
|
||||||
|
|
||||||
|
|
||||||
return plot_to_base64()
|
return plot_to_base64(plt)
|
||||||
|
|
||||||
# Pie chart
|
# Pie chart
|
||||||
def pie_chart():
|
def pie_chart():
|
||||||
@@ -39,7 +43,7 @@ def pie_chart():
|
|||||||
plt.pie(sizes, labels=labels, autopct="%1.1f%%", startangle=140)
|
plt.pie(sizes, labels=labels, autopct="%1.1f%%", startangle=140)
|
||||||
plt.title("Project Status")
|
plt.title("Project Status")
|
||||||
|
|
||||||
return plot_to_base64()
|
return plot_to_base64(plt)
|
||||||
|
|
||||||
# Histogram chart
|
# Histogram chart
|
||||||
def histogram_chart():
|
def histogram_chart():
|
||||||
@@ -51,7 +55,7 @@ def histogram_chart():
|
|||||||
plt.xlabel("Work Units")
|
plt.xlabel("Work Units")
|
||||||
plt.ylabel("Frequency")
|
plt.ylabel("Frequency")
|
||||||
|
|
||||||
return plot_to_base64()
|
return plot_to_base64(plt)
|
||||||
|
|
||||||
# Dashboaed page
|
# Dashboaed page
|
||||||
@dashboard_bp.route("/")
|
@dashboard_bp.route("/")
|
||||||
@@ -68,15 +72,16 @@ def dashboard():
|
|||||||
)
|
)
|
||||||
|
|
||||||
# subcontractor dashboard
|
# subcontractor dashboard
|
||||||
@dashboard_bp.route("/subcontractor_dashboard")
|
@dashboard_bp.route("/subcontractor_dashboard", methods=["GET", "POST"])
|
||||||
def subcontractor_dashboard():
|
def subcontractor_dashboard():
|
||||||
if not session.get("user_id"):
|
if not session.get("user_id"):
|
||||||
return redirect(url_for("auth.login"))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
tr_dash = DashboardService().bar_chart_of_tr_ex
|
||||||
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"subcontractor_dashboard.html",
|
"subcontractor_dashboard.html",
|
||||||
title="Dashboard",
|
title="Dashboard",
|
||||||
bar_chart=bar_chart(),
|
bar_chart=tr_dash
|
||||||
pie_chart=pie_chart(),
|
|
||||||
histogram=histogram_chart()
|
|
||||||
)
|
)
|
||||||
@@ -73,6 +73,11 @@ class SubcontractorBill:
|
|||||||
if not df.empty:
|
if not df.empty:
|
||||||
df.drop(columns=drop_cols, errors="ignore", inplace=True)
|
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 ---
|
# --- subcontractor report only ---
|
||||||
@file_report_bp.route("/Subcontractor_report", methods=["GET", "POST"])
|
@file_report_bp.route("/Subcontractor_report", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -153,7 +158,7 @@ def client_vs_all_subcontractor():
|
|||||||
|
|
||||||
if not RA_Bill_No:
|
if not RA_Bill_No:
|
||||||
flash("Please enter RA Bill No.", "danger")
|
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 = ClientBill()
|
||||||
clientBill.Fetch(RA_Bill_No=RA_Bill_No)
|
clientBill.Fetch(RA_Bill_No=RA_Bill_No)
|
||||||
@@ -163,10 +168,11 @@ def client_vs_all_subcontractor():
|
|||||||
# --- SAFETY CHECK: Verify data exists before merging ---
|
# --- SAFETY CHECK: Verify data exists before merging ---
|
||||||
if clientBill.df_tr.empty and clientBill.df_mh.empty:
|
if clientBill.df_tr.empty and clientBill.df_mh.empty:
|
||||||
flash(f"No Client records found for RA Bill {RA_Bill_No}", "warning")
|
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)
|
qty_cols = [...] # (Keep your existing list)
|
||||||
mh_dc_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):
|
def aggregate_df(df, group_cols, sum_cols):
|
||||||
if df.empty:
|
if df.empty:
|
||||||
@@ -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_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_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_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 ---
|
# --- FINAL MERGE LOGIC ---
|
||||||
# We check if "Location" exists in the client data. If not, we add it to prevent the KeyError.
|
# 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:
|
if not df_client.empty and "Location" not in df_client.columns:
|
||||||
df_client["Location"] = "Unknown"
|
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_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_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_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:
|
except KeyError as e:
|
||||||
flash(f"Merge Error: Missing column {str(e)}. Check if 'Location' is defined in your database models.", "danger")
|
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
|
# Convert to HTML for preview
|
||||||
tables["tr"] = df_tr_cmp.to_html(classes='table table-striped table-hover table-sm', index=False)
|
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["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["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)
|
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.models.laying_client_model import LayingClient
|
||||||
|
|
||||||
from app.utils.helpers import login_required
|
from app.utils.helpers import login_required
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
generate_report_bp = Blueprint("generate_report", __name__, url_prefix="/report")
|
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
|
# NORMALIZER
|
||||||
def normalize_key(value):
|
def normalize_key(value):
|
||||||
if value is None:
|
if value is None:
|
||||||
@@ -81,13 +89,13 @@ def build_comparison(client_rows, contractor_rows, key_field):
|
|||||||
client_total = sum(
|
client_total = sum(
|
||||||
float(v or 0)
|
float(v or 0)
|
||||||
for k, v in c.items()
|
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(
|
sub_total = sum(
|
||||||
float(v or 0)
|
float(v or 0)
|
||||||
for k, v in s.items()
|
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
|
diff = client_total - sub_total
|
||||||
@@ -129,10 +137,10 @@ def write_sheet(writer, df, sheet_name, subcontractor_name):
|
|||||||
ws = writer.sheets[sheet_name]
|
ws = writer.sheets[sheet_name]
|
||||||
|
|
||||||
title_fmt = workbook.add_format({"bold": True, "font_size": 14})
|
title_fmt = workbook.add_format({"bold": True, "font_size": 14})
|
||||||
client_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#D9EDF7"})
|
client_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#B6DAED"})
|
||||||
sub_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F7E1D9"})
|
sub_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F3A081"})
|
||||||
total_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#FFF2CC"})
|
total_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F7D261"})
|
||||||
diff_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#E2EFDA"})
|
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"})
|
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
|
title_fmt
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
for col_num, col_name in enumerate(df.columns):
|
for col_num, col_name in enumerate(df.columns):
|
||||||
if col_name.startswith("Client-"):
|
if col_name.startswith("Client-"):
|
||||||
ws.write(3, col_num, col_name, client_fmt)
|
ws.write(3, col_num, col_name, client_fmt)
|
||||||
elif col_name.startswith("Subcontractor-"):
|
elif col_name.startswith("Subcontractor-"):
|
||||||
ws.write(3, col_num, col_name, sub_fmt)
|
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)
|
ws.write(3, col_num, col_name, total_fmt)
|
||||||
elif col_name == "Diff":
|
elif col_name == "Diff":
|
||||||
ws.write(3, col_num, col_name, diff_fmt)
|
ws.write(3, col_num, col_name, diff_fmt)
|
||||||
@@ -194,12 +203,14 @@ def comparison_report():
|
|||||||
subcontractor_id=subcontractor_id
|
subcontractor_id=subcontractor_id
|
||||||
).all()]
|
).all()]
|
||||||
df_dc = build_comparison(dc_client, dc_sub, "MH_NO")
|
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_client = [r.serialize() for r in LayingClient.query.all()]
|
||||||
lay_sub = [r.serialize() for r in Laying.query.filter_by(
|
lay_sub = [r.serialize() for r in Laying.query.filter_by(
|
||||||
subcontractor_id=subcontractor_id
|
subcontractor_id=subcontractor_id
|
||||||
).all()]
|
).all()]
|
||||||
df_lay = build_comparison(lay_client, lay_sub, "MH_NO")
|
df_lay = build_comparison(lay_client, lay_sub, "MH_NO")
|
||||||
|
# df_lay = build_comparison_laying(lay_client, lay_sub, "MH_NO")
|
||||||
|
|
||||||
|
|
||||||
# -------- EXCEL --------
|
# -------- EXCEL --------
|
||||||
@@ -223,3 +234,104 @@ def comparison_report():
|
|||||||
return render_template("generate_comparison_report.html",subcontractors=subcontractors)
|
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
|
||||||
|
|||||||
@@ -10,21 +10,42 @@ subcontractor_bp = Blueprint("subcontractor", __name__, url_prefix="/subcontract
|
|||||||
@login_required
|
@login_required
|
||||||
def add_subcontractor():
|
def add_subcontractor():
|
||||||
return render_template("subcontractor/add.html")
|
return render_template("subcontractor/add.html")
|
||||||
|
|
||||||
@subcontractor_bp.route("/save", methods=["POST"])
|
@subcontractor_bp.route("/save", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def save_subcontractor():
|
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 = Subcontractor(
|
||||||
subcontractor_name=request.form.get("subcontractor_name"),
|
subcontractor_name=name,
|
||||||
contact_person=request.form.get("contact_person"),
|
contact_person=request.form.get("contact_person"),
|
||||||
mobile_no=request.form.get("mobile_no"),
|
mobile_no=request.form.get("mobile_no"),
|
||||||
email_id=request.form.get("email_id"),
|
email_id=request.form.get("email_id"),
|
||||||
gst_no=request.form.get("gst_no")
|
gst_no=request.form.get("gst_no")
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(subcontractor)
|
db.session.add(subcontractor)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash("Subcontractor added successfully!", "success")
|
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")
|
return redirect("/subcontractor/list")
|
||||||
|
|
||||||
# ---------------- LIST -----------------
|
# ---------------- LIST -----------------
|
||||||
@@ -46,12 +67,19 @@ def edit_subcontractor(id):
|
|||||||
@login_required
|
@login_required
|
||||||
def update_subcontractor(id):
|
def update_subcontractor(id):
|
||||||
subcontractor = Subcontractor.query.get_or_404(id)
|
subcontractor = Subcontractor.query.get_or_404(id)
|
||||||
|
new_name = request.form.get("subcontractor_name")
|
||||||
|
|
||||||
subcontractor.subcontractor_name = request.form.get("subcontractor_name")
|
# Check if the new name is taken by someone ELSE (not this current ID)
|
||||||
subcontractor.contact_person = request.form.get("contact_person")
|
duplicate = Subcontractor.query.filter(
|
||||||
subcontractor.mobile_no = request.form.get("mobile_no")
|
Subcontractor.subcontractor_name == new_name,
|
||||||
subcontractor.email_id = request.form.get("email_id")
|
Subcontractor.id != id
|
||||||
subcontractor.gst_no = request.form.get("gst_no")
|
).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()
|
db.session.commit()
|
||||||
|
|
||||||
@@ -69,3 +97,95 @@ def delete_subcontractor(id):
|
|||||||
|
|
||||||
flash("Subcontractor deleted successfully!", "success")
|
flash("Subcontractor deleted successfully!", "success")
|
||||||
return redirect("/subcontractor/list")
|
return redirect("/subcontractor/list")
|
||||||
|
from flask import Blueprint, render_template, request, redirect, flash
|
||||||
|
from app import db
|
||||||
|
from app.models.subcontractor_model import Subcontractor
|
||||||
|
from app.utils.helpers import login_required
|
||||||
|
|
||||||
|
subcontractor_bp = Blueprint("subcontractor", __name__, url_prefix="/subcontractor")
|
||||||
|
|
||||||
|
# ---------------- ADD -----------------
|
||||||
|
@subcontractor_bp.route("/add")
|
||||||
|
@login_required
|
||||||
|
def add_subcontractor():
|
||||||
|
return render_template("subcontractor/add.html")
|
||||||
|
|
||||||
|
@subcontractor_bp.route("/save", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def save_subcontractor():
|
||||||
|
name = request.form.get("subcontractor_name", "").strip()
|
||||||
|
if not name:
|
||||||
|
flash("Subcontractor name cannot be empty.", "danger")
|
||||||
|
return redirect("/subcontractor/add")
|
||||||
|
existing_sub = Subcontractor.query.filter_by(subcontractor_name=name).first()
|
||||||
|
|
||||||
|
if existing_sub:
|
||||||
|
flash(f"Subcontractor with name '{name}' already exists!", "danger")
|
||||||
|
return redirect("/subcontractor/add")
|
||||||
|
try:
|
||||||
|
subcontractor = Subcontractor(
|
||||||
|
subcontractor_name=name,
|
||||||
|
contact_person=request.form.get("contact_person"),
|
||||||
|
mobile_no=request.form.get("mobile_no"),
|
||||||
|
email_id=request.form.get("email_id"),
|
||||||
|
gst_no=request.form.get("gst_no")
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(subcontractor)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Subcontractor added successfully!", "success")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash("An error occurred while saving. Please try again.", "danger")
|
||||||
|
|
||||||
|
return redirect("/subcontractor/list")
|
||||||
|
|
||||||
|
# ---------------- LIST -----------------
|
||||||
|
@subcontractor_bp.route("/list")
|
||||||
|
@login_required
|
||||||
|
def subcontractor_list():
|
||||||
|
subcontractors = Subcontractor.query.all()
|
||||||
|
return render_template("subcontractor/list.html", subcontractors=subcontractors)
|
||||||
|
|
||||||
|
# ---------------- EDIT -----------------
|
||||||
|
@subcontractor_bp.route("/edit/<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")
|
||||||
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>
|
</form>
|
||||||
</div>
|
</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">
|
<div class="card shadow-sm p-3">
|
||||||
<h4 class="mb-3">Comparison Preview</h4>
|
<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
|
<button class="nav-link" id="dc-tab" data-bs-toggle="tab" data-bs-target="#dc" type="button">MH & DC
|
||||||
Comparison</button>
|
Comparison</button>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content mt-3" id="reportTabsContent">
|
<div class="tab-content mt-3" id="reportTabsContent">
|
||||||
@@ -68,6 +71,11 @@
|
|||||||
{{ tables.dc|safe }}
|
{{ tables.dc|safe }}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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