Compare commits
63 Commits
9cec54c929
...
pankaj-dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 12b2032430 | |||
| 6e4406519a | |||
| 1d83534a95 | |||
| 5ddfb1f440 | |||
| 96a3a79731 | |||
| c5b3e7bd60 | |||
| 21f8576e33 | |||
| 5ab11bc5a6 | |||
| 45bfd4b592 | |||
| 568428b5d0 | |||
| cf7d1636f9 | |||
| 163c7814ed | |||
| 5b557efd80 | |||
| 71ce8ca819 | |||
| 68a694d2c7 | |||
| 90c18383da | |||
| ac9d964ef4 | |||
| f22dc0cccd | |||
| 0007d6e87d | |||
| 8cba86323e | |||
| e4837b203a | |||
| 4fa35fe05a | |||
| a07c1ddf8a | |||
| 15b07b3d55 | |||
| dd4b494435 | |||
| ff5da926f3 | |||
| 1022923509 | |||
| c9a27b098b | |||
| 8f817c94fd | |||
| 0bb670b152 | |||
| aad9b2b967 | |||
| e0541ff80c | |||
| ed85313d42 | |||
| a853992e64 | |||
| 677df59863 | |||
| d46c7c0936 | |||
| de074c68ae | |||
| ae5fee8f46 | |||
| a73d8e5ed1 | |||
| 58eff71c7c | |||
| 64fbb91199 | |||
| 5afe8e7096 | |||
| 54f3d16b57 | |||
| fe9b056128 | |||
| bdd7312a5e | |||
| 7cb01bd9fb | |||
| 54c3b8f160 | |||
| f58df94bae | |||
| 90a5eea79c | |||
| a7de81be73 | |||
| 47df8a2668 | |||
| 5284fa9e11 | |||
| 18a402edd4 | |||
| 51088975d3 | |||
| fe89d5e2eb | |||
| dc7acc7592 | |||
| a0edb173cb | |||
| 14eac5b958 | |||
| 1d476c4851 | |||
| 89328c9066 | |||
| b11a974869 | |||
| 2afd6c904a | |||
| f2f3a689bb |
19
.dockerignore
Normal file
19
.dockerignore
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
instance/
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
26
.env
Normal file
26
.env
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# -----------------------------
|
||||||
|
# Flask App Configuration
|
||||||
|
# -----------------------------
|
||||||
|
FLASK_ENV=development
|
||||||
|
FLASK_DEBUG=True
|
||||||
|
FLASK_HOST=0.0.0.0
|
||||||
|
FLASK_PORT=5011
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# 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=root
|
||||||
|
|
||||||
|
# DATABASE_URL=mysql+pymysql://root:root@localhost/comparisondb
|
||||||
|
|
||||||
22
.gitignore
vendored
22
.gitignore
vendored
@@ -1,9 +1,19 @@
|
|||||||
# Ignore folders
|
# Python
|
||||||
instance/
|
app/__pycache__/
|
||||||
static/uploads/
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.__pycache__
|
||||||
|
|
||||||
# Ignore files
|
# Ingnor upload files
|
||||||
.env
|
app/static/uploads/
|
||||||
|
|
||||||
# Ignore by type
|
# Ignore env files
|
||||||
|
venv
|
||||||
|
|
||||||
|
# Ignore Log files ss
|
||||||
|
logs/
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
gcc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements and install Python dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create necessary directories
|
||||||
|
RUN mkdir -p app/logs app/static/uploads app/static/downloads
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 5001
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["python", "run.py"]
|
||||||
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
|
||||||
@@ -1,47 +1,61 @@
|
|||||||
from flask import Flask
|
from flask import Flask, redirect, url_for
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
|
||||||
from app.config import Config
|
from app.config import Config
|
||||||
|
from app.services.db_service import db
|
||||||
db = SQLAlchemy()
|
from app.services.logger_service import LoggerService
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object(Config)
|
app.config.from_object(Config)
|
||||||
|
|
||||||
|
# Initialize extensions
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|
||||||
# Register Blueprints
|
# Initialize Logger
|
||||||
from app.routes.dashboard import dashboard_bp
|
LoggerService.init_app(app)
|
||||||
from app.routes.file_import import file_import_bp
|
|
||||||
# from app.routes.user import user_bp
|
|
||||||
|
|
||||||
app.register_blueprint(dashboard_bp)
|
# Register blueprints
|
||||||
app.register_blueprint(file_import_bp)
|
register_blueprints(app)
|
||||||
# app.register_blueprint(user_bp)
|
# Register error handlers
|
||||||
|
register_error_handlers(app)
|
||||||
|
|
||||||
from app.routes.subcontractor_routes import subcontractor_bp
|
# ROOT → LOGIN
|
||||||
app.register_blueprint(subcontractor_bp)
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def register_blueprints(app):
|
||||||
|
from app.routes.auth import auth_bp
|
||||||
|
from app.routes.user import user_bp
|
||||||
|
from app.routes.dashboard import dashboard_bp
|
||||||
|
from app.routes.subcontractor_routes import subcontractor_bp
|
||||||
|
from app.routes.file_import import file_import_bp
|
||||||
|
from app.routes.file_report import file_report_bp
|
||||||
|
from app.routes.generate_comparison_report import generate_report_bp
|
||||||
|
from app.routes.file_format import file_format_bp
|
||||||
|
|
||||||
|
app.register_blueprint(auth_bp)
|
||||||
|
app.register_blueprint(user_bp)
|
||||||
|
app.register_blueprint(dashboard_bp)
|
||||||
|
app.register_blueprint(subcontractor_bp)
|
||||||
|
app.register_blueprint(file_import_bp)
|
||||||
|
app.register_blueprint(file_report_bp)
|
||||||
|
app.register_blueprint(generate_report_bp)
|
||||||
|
app.register_blueprint(file_format_bp )
|
||||||
|
|
||||||
|
|
||||||
|
def register_error_handlers(app):
|
||||||
|
|
||||||
# from flask import Flask
|
from flask import current_app
|
||||||
# from app.config import Config
|
|
||||||
|
|
||||||
# def create_app():
|
@app.errorhandler(404)
|
||||||
# app = Flask(__name__)
|
def page_not_found(e):
|
||||||
# app.config.from_object(Config)
|
current_app.logger.warning("404 Page Not Found")
|
||||||
|
return "Page Not Found", 404
|
||||||
|
|
||||||
# # Register Blueprints
|
@app.errorhandler(500)
|
||||||
# from app.routes.dashboard import dashboard_bp
|
def internal_error(e):
|
||||||
# from app.routes.file_import import file_import_bp
|
current_app.logger.exception("500 Internal Server Error")
|
||||||
# from app.routes.user import user_bp
|
return "Internal Server Error", 500
|
||||||
|
|
||||||
# app.register_blueprint(dashboard_bp)
|
|
||||||
# app.register_blueprint(file_import_bp)
|
|
||||||
# app.register_blueprint(user_bp)
|
|
||||||
|
|
||||||
# return app
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,24 +1,24 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
# secret key
|
||||||
SQLALCHEMY_DATABASE_URI = "mysql+pymysql://root:root@localhost/comparisondb"
|
SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key")
|
||||||
|
|
||||||
|
# Database variables
|
||||||
|
DB_DIALECT = os.getenv("DB_DIALECT")
|
||||||
|
DB_DRIVER = os.getenv("DB_DRIVER")
|
||||||
|
DB_USER = os.getenv("DB_USER")
|
||||||
|
DB_PASSWORD = os.getenv("DB_PASSWORD")
|
||||||
|
DB_HOST = os.getenv("DB_HOST")
|
||||||
|
DB_PORT = os.getenv("DB_PORT")
|
||||||
|
DB_NAME = os.getenv("DB_NAME")
|
||||||
|
# database connection url
|
||||||
|
SQLALCHEMY_DATABASE_URI = (
|
||||||
|
f"{DB_DIALECT}+{DB_DRIVER}://"
|
||||||
|
f"{DB_USER}:{DB_PASSWORD}@"
|
||||||
|
f"{DB_HOST}:{DB_PORT}/"
|
||||||
|
f"{DB_NAME}"
|
||||||
|
)
|
||||||
|
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
SECRET_KEY = "secret123"
|
|
||||||
|
|
||||||
UPLOAD_FOLDER = "app/static/uploads/"
|
|
||||||
ALLOWED_EXTENSIONS = {"xlsx", "xls", "csv"}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# class Config:
|
|
||||||
# SECRET_KEY = os.getenv("SECRET_KEY", "dev_key_12345")
|
|
||||||
|
|
||||||
# UPLOAD_FOLDER = "app/static/uploads/"
|
|
||||||
# ALLOWED_EXTENSIONS = {"xlsx", "xls", "csv"}
|
|
||||||
|
|
||||||
# DB_HOST = "localhost"
|
|
||||||
# DB_USER = "root"
|
|
||||||
# DB_PASSWORD = "root"
|
|
||||||
# DB_NAME = "comparisondb"
|
|
||||||
11
app/constants/http_status.py
Normal file
11
app/constants/http_status.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
class HTTPStatus:
|
||||||
|
OK = 200
|
||||||
|
CREATED = 201
|
||||||
|
BAD_REQUEST = 400
|
||||||
|
UNAUTHORIZED = 401
|
||||||
|
FORBIDDEN = 403
|
||||||
|
NOT_FOUND = 404
|
||||||
|
METHOD_NOT_ALLOWED = 405
|
||||||
|
CONFLICT = 409
|
||||||
|
UNPROCESSABLE_ENTITY = 422
|
||||||
|
INTERNAL_SERVER_ERROR = 500
|
||||||
17
app/constants/messages.py
Normal file
17
app/constants/messages.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
class SuccessMessage:
|
||||||
|
FETCHED = "Data fetched successfully"
|
||||||
|
CREATED = "Resource created successfully"
|
||||||
|
UPDATED = "Resource updated successfully"
|
||||||
|
DELETED = "Resource deleted successfully"
|
||||||
|
LOGIN = "Login successful"
|
||||||
|
LOGOUT = "Logout successful"
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorMessage:
|
||||||
|
INVALID_REQUEST = "Invalid request data"
|
||||||
|
UNAUTHORIZED = "Unauthorized access"
|
||||||
|
FORBIDDEN = "Access forbidden"
|
||||||
|
NOT_FOUND = "Resource not found"
|
||||||
|
VALIDATION_FAILED = "Validation failed"
|
||||||
|
INTERNAL_ERROR = "Internal server error"
|
||||||
|
DUPLICATE_ENTRY = "Duplicate record found"
|
||||||
66
app/errors/error_handlers.py
Normal file
66
app/errors/error_handlers.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from flask import request, render_template
|
||||||
|
from app.utils.response_handler import ResponseHandler
|
||||||
|
from app.utils.exceptions import APIException
|
||||||
|
from app.constants.http_status import HTTPStatus
|
||||||
|
from app.constants.messages import ErrorMessage
|
||||||
|
from app.services.db_service import db
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
def register_error_handlers(app):
|
||||||
|
|
||||||
|
# Custom API Exception
|
||||||
|
@app.errorhandler(APIException)
|
||||||
|
def handle_api_exception(e):
|
||||||
|
db.session.rollback()
|
||||||
|
|
||||||
|
if request.path.startswith("/api"):
|
||||||
|
return ResponseHandler.error(
|
||||||
|
message=e.message,
|
||||||
|
errors=e.errors,
|
||||||
|
status_code=e.status_code
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template("errors/500.html"), e.status_code
|
||||||
|
|
||||||
|
|
||||||
|
# 404
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def handle_404(e):
|
||||||
|
if request.path.startswith("/api"):
|
||||||
|
return ResponseHandler.error(
|
||||||
|
message=ErrorMessage.NOT_FOUND,
|
||||||
|
status_code=HTTPStatus.NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template("errors/404.html"), 404
|
||||||
|
|
||||||
|
|
||||||
|
# 500
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def handle_500(e):
|
||||||
|
db.session.rollback()
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
if request.path.startswith("/api"):
|
||||||
|
return ResponseHandler.error(
|
||||||
|
message=ErrorMessage.INTERNAL_ERROR,
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template("errors/500.html"), 500
|
||||||
|
|
||||||
|
|
||||||
|
# Catch All
|
||||||
|
@app.errorhandler(Exception)
|
||||||
|
def handle_general_exception(e):
|
||||||
|
db.session.rollback()
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
if request.path.startswith("/api"):
|
||||||
|
return ResponseHandler.error(
|
||||||
|
message=ErrorMessage.INTERNAL_ERROR,
|
||||||
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template("errors/500.html"), 500
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
63
app/models/laying_client_model.py
Normal file
63
app/models/laying_client_model.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from app import db
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import event
|
||||||
|
from app.utils.regex_utils import RegularExpression
|
||||||
|
|
||||||
|
class LayingClient(db.Model):
|
||||||
|
__tablename__ = "laying_client"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
|
# Basic Fields
|
||||||
|
Location = db.Column(db.String(500))
|
||||||
|
MH_NO = db.Column(db.String(100))
|
||||||
|
CC_length = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
# Bedding Qty.
|
||||||
|
Outer_dia_of_MH_m = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Bedding_Length = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Width = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Depth = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Qty = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
# PIPE LAYING Qty.
|
||||||
|
Pipe_Dia_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
ID_of_MH_m = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Laying_Length = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
pipe_150_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_200_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_250_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_300_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_350_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_400_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_450_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_500_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_600_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_700_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_900_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_1200_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Total = db.Column(db.Numeric(12, 4), default=0)
|
||||||
|
Remarks = db.Column(db.String(500))
|
||||||
|
RA_Bill_No=db.Column(db.String(500))
|
||||||
|
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.today)
|
||||||
|
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<LayingModel {self.Location}>"
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||||
|
|
||||||
|
|
||||||
|
# AUTO TOTAL USING REGEX
|
||||||
|
def calculate_laying_total(mapper, connection, target):
|
||||||
|
total = 0
|
||||||
|
for column in target.__table__.columns:
|
||||||
|
if RegularExpression.PIPE_MM_PATTERN.match(column.name):
|
||||||
|
total += getattr(target, column.name) or 0
|
||||||
|
target.Total = round(total)
|
||||||
|
|
||||||
|
event.listen(LayingClient, "before_insert", calculate_laying_total)
|
||||||
|
event.listen(LayingClient, "before_update", calculate_laying_total)
|
||||||
59
app/models/laying_model.py
Normal file
59
app/models/laying_model.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
from app import db
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import event
|
||||||
|
from app.utils.regex_utils import RegularExpression
|
||||||
|
|
||||||
|
class Laying(db.Model):
|
||||||
|
__tablename__ = "laying"
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Pipe Laying Fields
|
||||||
|
Location = db.Column(db.String(500))
|
||||||
|
MH_NO = db.Column(db.String(100))
|
||||||
|
CC_length = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Pipe_Dia_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
ID_of_MH_m = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Laying_Length = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
pipe_150_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_200_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_250_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_300_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_350_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_400_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_450_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_500_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_600_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_700_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_900_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
pipe_1200_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Total = db.Column(db.Numeric(12, 4), default=0)
|
||||||
|
Remarks = db.Column(db.String(500))
|
||||||
|
RA_Bill_No=db.Column(db.String(500))
|
||||||
|
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.today)
|
||||||
|
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<LayingModel {self.Location}>"
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||||
|
|
||||||
|
|
||||||
|
# AUTO TOTAL USING REGEX
|
||||||
|
def calculate_laying_total(mapper, connection, target):
|
||||||
|
total = 0
|
||||||
|
for column in target.__table__.columns:
|
||||||
|
if RegularExpression.PIPE_MM_PATTERN.match(column.name):
|
||||||
|
total += getattr(target, column.name) or 0
|
||||||
|
target.Total = round(total)
|
||||||
|
|
||||||
|
event.listen(Laying, "before_insert", calculate_laying_total)
|
||||||
|
event.listen(Laying, "before_update", calculate_laying_total)
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
from app import db
|
from app import db
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from sqlalchemy import event
|
||||||
|
from app.utils.regex_utils import RegularExpression
|
||||||
|
|
||||||
class ManholeDomesticChamber(db.Model):
|
class ManholeDomesticChamber(db.Model):
|
||||||
__tablename__ = "manhole_domestic_chamber"
|
__tablename__ = "manhole_domestic_chamber"
|
||||||
@@ -11,35 +13,53 @@ class ManholeDomesticChamber(db.Model):
|
|||||||
subcontractor = db.relationship("Subcontractor", backref="manhole_domestic_chamber_records")
|
subcontractor = db.relationship("Subcontractor", backref="manhole_domestic_chamber_records")
|
||||||
|
|
||||||
# Basic Fields
|
# Basic Fields
|
||||||
Location = db.Column(db.String(255))
|
Location = db.Column(db.String(500))
|
||||||
Node_No = db.Column(db.String(100))
|
MH_NO = db.Column(db.String(100))
|
||||||
Depth_of_MH = db.Column(db.Float)
|
Depth_of_MH = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
# Excavation categories
|
# Excavation categories
|
||||||
d_0_to_0_75 = db.Column(db.Float)
|
d_0_to_0_75 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
d_1_06_to_1_65 = db.Column(db.Float)
|
d_0_76_to_1_05 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
d_1_06_to_1_65 = db.Column(db.Float)
|
d_1_06_to_1_65 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
d_1_66_to_2_15 = db.Column(db.Float)
|
d_1_66_to_2_15 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
d_2_16_to_2_65 = db.Column(db.Float)
|
d_2_16_to_2_65 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
d_2_66_to_3_15 = db.Column(db.Float)
|
d_2_66_to_3_15 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
d_3_16_to_3_65= db.Column(db.Float)
|
d_3_16_to_3_65= db.Column(db.Numeric(10, 4), default=0)
|
||||||
d_3_66_to_4_15 = db.Column(db.Float)
|
d_3_66_to_4_15 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
d_4_16_to_4_65 = db.Column(db.Float)
|
d_4_16_to_4_65 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
d_4_66_to_5_15 = db.Column(db.Float)
|
d_4_66_to_5_15 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
d_5_16_to_5_65 = db.Column(db.Float)
|
d_5_16_to_5_65 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_5_66_to_6_15 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_6_16_to_6_65 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_6_66_to_7_15 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_7_16_to_7_65 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_7_66_to_8_15 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_8_16_to_8_65 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_8_66_to_9_15 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_9_16_to_9_65 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
d_5_66_to_6_15 = db.Column(db.Float)
|
Domestic_Chambers = db.Column(db.Numeric(10, 4), default=0)
|
||||||
d_6_16_to_6_65 = db.Column(db.Float)
|
DWC_Pipe_Length = db.Column(db.Numeric(10, 4), default=0)
|
||||||
d_6_66_to_7_15 = db.Column(db.Float)
|
UPVC_Pipe_Length = db.Column(db.Numeric(10, 4), default=0)
|
||||||
d_7_16_to_7_65 = db.Column(db.Float)
|
RA_Bill_No=db.Column(db.String(500))
|
||||||
d_7_66_to_8_15 = db.Column(db.Float)
|
|
||||||
d_8_16_to_8_65 = db.Column(db.Float)
|
|
||||||
d_8_66_to_9_15 = db.Column(db.Float)
|
|
||||||
d_9_16_to_9_65 = db.Column(db.Float)
|
|
||||||
|
|
||||||
Domestic_Chambers = db.Column(db.Float)
|
Total = db.Column(db.Numeric(12, 4), default=0)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.today)
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<HanholeDomesticChamberConstruction {self.Location}>"
|
return f"<HanholeDomesticChamberConstruction {self.Location}>"
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||||
|
|
||||||
|
|
||||||
|
# AUTO TOTAL USING REGEX
|
||||||
|
def calculate_mh_dc_total(mapper, connection, target):
|
||||||
|
total = 0
|
||||||
|
for column in target.__table__.columns:
|
||||||
|
if RegularExpression.D_RANGE_PATTERN.match(column.name):
|
||||||
|
total += getattr(target, column.name) or 0
|
||||||
|
target.Total = round(total)
|
||||||
|
|
||||||
|
event.listen(ManholeDomesticChamber, "before_insert", calculate_mh_dc_total)
|
||||||
|
event.listen(ManholeDomesticChamber, "before_update", calculate_mh_dc_total)
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
from app import db
|
from app import db
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from sqlalchemy import event
|
||||||
|
from app.utils.regex_utils import RegularExpression
|
||||||
|
|
||||||
class ManholeExcavation(db.Model):
|
class ManholeExcavation(db.Model):
|
||||||
__tablename__ = "manhole_excavation"
|
__tablename__ = "manhole_excavation"
|
||||||
@@ -11,53 +13,69 @@ class ManholeExcavation(db.Model):
|
|||||||
subcontractor = db.relationship("Subcontractor", backref="manhole_records")
|
subcontractor = db.relationship("Subcontractor", backref="manhole_records")
|
||||||
|
|
||||||
# Basic Fields
|
# Basic Fields
|
||||||
Location = db.Column(db.String(255))
|
Location = db.Column(db.String(500))
|
||||||
MH_NO = db.Column(db.String(100))
|
MH_NO = db.Column(db.String(100))
|
||||||
|
|
||||||
Upto_IL_Depth = db.Column(db.Float)
|
Upto_IL_Depth = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Cutting_Depth = db.Column(db.Float)
|
Cutting_Depth = db.Column(db.Numeric(10, 4), default=0)
|
||||||
ID_of_MH_m = db.Column(db.Float)
|
ID_of_MH_m = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Ex_Dia_of_Manhole = db.Column(db.Float)
|
Ex_Dia_of_Manhole = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Area_of_Manhole = db.Column(db.Float)
|
Area_of_Manhole = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
# Excavation categories
|
# Excavation categories
|
||||||
Soft_Murum_0_to_1_5 = db.Column(db.Float)
|
Soft_Murum_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Soft_Murum_1_5_to_3_0 = db.Column(db.Float)
|
Soft_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Soft_Murum_3_0_to_4_5 = db.Column(db.Float)
|
Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
Hard_Murum_0_to_1_5 = db.Column(db.Float)
|
Hard_Murum_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Murum_1_5_to_3_0 = db.Column(db.Float)
|
Hard_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
Soft_Rock_0_to_1_5 = db.Column(db.Float)
|
Soft_Rock_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Soft_Rock_1_5_to_3_0 = db.Column(db.Float)
|
Soft_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
Hard_Rock_0_to_1_5 = db.Column(db.Float)
|
Hard_Rock_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Rock_1_5_to_3_0 = db.Column(db.Float)
|
Hard_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Rock_3_0_to_4_5 = db.Column(db.Float)
|
Hard_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Rock_4_5_to_6_0 = db.Column(db.Float)
|
Hard_Rock_4_5_to_6_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Rock_6_0_to_7_5 = db.Column(db.Float)
|
Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
# Totals
|
# Totals
|
||||||
Soft_Murum_0_to_1_5_total = db.Column(db.Float)
|
Soft_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Soft_Murum_1_5_to_3_0_total = db.Column(db.Float)
|
Soft_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Soft_Murum_3_0_to_4_5_total = db.Column(db.Float)
|
Soft_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
Hard_Murum_0_to_1_5_total = db.Column(db.Float)
|
Hard_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Murum_1_5_and_above_total = db.Column(db.Float)
|
Hard_Murum_1_5_and_above_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
Soft_Rock_0_to_1_5_total = db.Column(db.Float)
|
Soft_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Soft_Rock_1_5_and_above_total = db.Column(db.Float)
|
Soft_Rock_1_5_and_above_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
Hard_Rock_0_to_1_5_total = db.Column(db.Float)
|
Hard_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Rock_1_5_and_above_total = db.Column(db.Float)
|
Hard_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Rock_3_0_to_4_5_total = db.Column(db.Float)
|
Hard_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Rock_4_5_to_6_0_total = db.Column(db.Float)
|
Hard_Rock_4_5_to_6_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Rock_6_0_to_7_5_total = db.Column(db.Float)
|
Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Total = db.Column(db.Numeric(12, 4), default=0)
|
||||||
Remarks = db.Column(db.String(500))
|
Remarks = db.Column(db.String(500))
|
||||||
Total = db.Column(db.Float)
|
RA_Bill_No=db.Column(db.String(500))
|
||||||
|
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.today)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<HanholeExcavation {self.Location}>"
|
return f"<HanholeExcavation {self.Location}>"
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||||
|
|
||||||
|
# AUTO TOTAL USING REGEX
|
||||||
|
def calculate_Manhole_total(mapper, connection, target):
|
||||||
|
total = 0
|
||||||
|
for column in target.__table__.columns:
|
||||||
|
if RegularExpression.STR_TOTAL_PATTERN.match(column.name):
|
||||||
|
total += getattr(target, column.name) or 0
|
||||||
|
target.Total = round(total)
|
||||||
|
|
||||||
|
|
||||||
|
event.listen(ManholeExcavation, "before_insert", calculate_Manhole_total)
|
||||||
|
event.listen(ManholeExcavation, "before_update", calculate_Manhole_total)
|
||||||
55
app/models/mh_dc_client_model.py
Normal file
55
app/models/mh_dc_client_model.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from app import db
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import event
|
||||||
|
from app.utils.regex_utils import RegularExpression
|
||||||
|
|
||||||
|
class ManholeDomesticChamberClient(db.Model):
|
||||||
|
__tablename__ = "mh_dc_client"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
|
# Basic Fields
|
||||||
|
RA_Bill_No=db.Column(db.String(500))
|
||||||
|
Location = db.Column(db.String(500))
|
||||||
|
MH_NO = db.Column(db.String(100))
|
||||||
|
MH_TOP_LEVEL = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
MH_IL_LEVEL = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Depth_of_MH = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
# Excavation categories
|
||||||
|
d_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_1_5_to_2_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_2_0_to_2_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_2_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_3_0_to_3_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_3_5_to_4_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_4_0_to_4_5= db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_4_5_to_5_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_5_0_to_5_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_5_5_to_6_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
d_6_0_to_6_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Domestic_Chambers = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Total = db.Column(db.Numeric(12, 4), default=0)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.today)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<HanholeDomesticChamberConstruction {self.Location}>"
|
||||||
|
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||||
|
|
||||||
|
|
||||||
|
# AUTO TOTAL USING REGEX
|
||||||
|
def calculate_mh_dc_total(mapper, connection, target):
|
||||||
|
total = 0
|
||||||
|
for column in target.__table__.columns:
|
||||||
|
if RegularExpression.D_RANGE_PATTERN.match(column.name):
|
||||||
|
total += getattr(target, column.name) or 0
|
||||||
|
target.Total = round(total)
|
||||||
|
|
||||||
|
|
||||||
|
event.listen(ManholeDomesticChamberClient, "before_insert", calculate_mh_dc_total)
|
||||||
|
event.listen(ManholeDomesticChamberClient, "before_update", calculate_mh_dc_total)
|
||||||
94
app/models/mh_ex_client_model.py
Normal file
94
app/models/mh_ex_client_model.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from app import db
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import event
|
||||||
|
from app.utils.regex_utils import RegularExpression
|
||||||
|
|
||||||
|
class ManholeExcavationClient(db.Model):
|
||||||
|
__tablename__ = "mh_ex_client"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
|
# Basic Fields
|
||||||
|
RA_Bill_No=db.Column(db.String(500))
|
||||||
|
Location = db.Column(db.String(500))
|
||||||
|
MH_NO = db.Column(db.String(100))
|
||||||
|
Ground_Level = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
MH_Invert_Level = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
MH_Top_Level = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Ex_Level = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Cutting_Depth = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
MH_Depth = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
ID_of_MH_m = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Dia_of_MH_Cutting = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Area_of_Manhole = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
# Excavation categories
|
||||||
|
Marshi_Muddy_Slushy_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Marshi_Muddy_Slushy_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Marshi_Muddy_Slushy_3_0_to_4_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Soft_Murum_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Soft_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Hard_Murum_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Soft_Rock_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Soft_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Hard_Rock_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Rock_4_5_to_6_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
# Totals
|
||||||
|
Marshi_Muddy_Slushy_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Marshi_Muddy_Slushy_1_5_to_3_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Marshi_Muddy_Slushy_3_0_to_4_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Soft_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Soft_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Soft_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Hard_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Soft_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Soft_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Soft_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Hard_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Rock_4_5_to_6_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Remarks = db.Column(db.String(500))
|
||||||
|
Total = db.Column(db.Numeric(12, 4), default=0)
|
||||||
|
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.today)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<HanholeExcavation {self.Location}>"
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||||
|
|
||||||
|
|
||||||
|
# AUTO TOTAL USING REGEX
|
||||||
|
def calculate_Manhole_total(mapper, connection, target):
|
||||||
|
total = 0
|
||||||
|
for column in target.__table__.columns:
|
||||||
|
if RegularExpression.STR_TOTAL_PATTERN.match(column.name):
|
||||||
|
total += getattr(target, column.name) or 0
|
||||||
|
target.Total = round(total)
|
||||||
|
|
||||||
|
|
||||||
|
event.listen(ManholeExcavationClient, "before_insert", calculate_Manhole_total)
|
||||||
|
event.listen(ManholeExcavationClient, "before_update", calculate_Manhole_total)
|
||||||
@@ -14,7 +14,7 @@ class Subcontractor(db.Model):
|
|||||||
email_id = db.Column(db.String(150))
|
email_id = db.Column(db.String(150))
|
||||||
contact_person = db.Column(db.String(150))
|
contact_person = db.Column(db.String(150))
|
||||||
status = db.Column(db.String(20), default="Active")
|
status = db.Column(db.String(20), default="Active")
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.today)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Subcontractor {self.subcontractor_name}>"
|
return f"<Subcontractor {self.subcontractor_name}>"
|
||||||
|
|||||||
98
app/models/tr_ex_client_model.py
Normal file
98
app/models/tr_ex_client_model.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
from app import db
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import event
|
||||||
|
from app.utils.regex_utils import RegularExpression
|
||||||
|
|
||||||
|
class TrenchExcavationClient(db.Model):
|
||||||
|
__tablename__ = "tr_ex_client"
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
|
# Basic Fields
|
||||||
|
RA_Bill_No=db.Column(db.String(500))
|
||||||
|
Location = db.Column(db.String(500))
|
||||||
|
MH_NO = db.Column(db.String(100))
|
||||||
|
CC_length = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Actual_Trench_Length = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Ground_Level = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Invert_Level = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Excavated_level = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Cutting_Depth = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Avg_Depth = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Pipe_Dia_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
# width
|
||||||
|
Width_0_to_1_5_m = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Width_1_5_to_3_0_m = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Width_3_0_to_4_5_m = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Width_4_5_to_6_0_m = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Width_6_0_to_7_5_m = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
# Excavation categories
|
||||||
|
Marshi_Muddy_Slushy_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Marshi_Muddy_Slushy_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Marshi_Muddy_Slushy_3_0_to_4_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Soft_Murum_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Soft_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Hard_Murum_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Soft_Rock_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Soft_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Soft_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Hard_Rock_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Rock_4_5_to_6_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
# Totals
|
||||||
|
Marshi_Muddy_Slushy_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Marshi_Muddy_Slushy_1_5_to_3_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Marshi_Muddy_Slushy_3_0_to_4_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Soft_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Soft_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Soft_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Hard_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Soft_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Soft_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Soft_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Hard_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Rock_4_5_to_6_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Total = db.Column(db.Numeric(12, 4), default=0)
|
||||||
|
Remarks = db.Column(db.String(500))
|
||||||
|
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.today)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<TrenchExcavation {self.Location}>"
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||||
|
|
||||||
|
|
||||||
|
# AUTO TOTAL USING REGEX
|
||||||
|
def calculate_trench_client_total(mapper, connection, target):
|
||||||
|
total = 0
|
||||||
|
for column in target.__table__.columns:
|
||||||
|
if RegularExpression.STR_TOTAL_PATTERN.match(column.name):
|
||||||
|
total += getattr(target, column.name) or 0
|
||||||
|
target.Total = round(total)
|
||||||
|
|
||||||
|
event.listen(TrenchExcavationClient, "before_insert", calculate_trench_client_total)
|
||||||
|
event.listen(TrenchExcavationClient, "before_update", calculate_trench_client_total)
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
from app import db
|
from app import db
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from sqlalchemy import event
|
||||||
|
from app.utils.regex_utils import RegularExpression
|
||||||
|
|
||||||
class TrenchExcavation(db.Model):
|
class TrenchExcavation(db.Model):
|
||||||
__tablename__ = "trench_excavation"
|
__tablename__ = "trench_excavation"
|
||||||
@@ -11,63 +13,82 @@ class TrenchExcavation(db.Model):
|
|||||||
subcontractor = db.relationship("Subcontractor", backref="trench_records")
|
subcontractor = db.relationship("Subcontractor", backref="trench_records")
|
||||||
|
|
||||||
# Basic Fields
|
# Basic Fields
|
||||||
Location = db.Column(db.String(255))
|
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.Numeric(10, 4), default=0)
|
||||||
Invert_Level = db.Column(db.Float)
|
Invert_Level = db.Column(db.Numeric(10, 4), default=0)
|
||||||
MH_Top_Level = db.Column(db.Float)
|
MH_Top_Level = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Ground_Level = db.Column(db.Float)
|
Ground_Level = db.Column(db.Numeric(10, 4), default=0)
|
||||||
ID_of_MH_m = db.Column(db.Float)
|
ID_of_MH_m = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Actual_Trench_Length = db.Column(db.Float)
|
Actual_Trench_Length = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Pipe_Dia_mm = db.Column(db.Float)
|
Pipe_Dia_mm = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
Width_0_to_2_5 = db.Column(db.Float)
|
# width
|
||||||
Width_2_5_to_3_0 = db.Column(db.Float)
|
Width_0_to_2_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Width_3_0_to_4_5 = db.Column(db.Float)
|
Width_2_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Width_4_5_to_6_0 = db.Column(db.Float)
|
Width_3_0_to_4_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
Width_4_5_to_6_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
Upto_IL_Depth = db.Column(db.Float)
|
Upto_IL_Depth = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Cutting_Depth = db.Column(db.Float)
|
Cutting_Depth = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Avg_Depth = db.Column(db.Float)
|
Avg_Depth = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
# Excavation categories
|
# Excavation categories
|
||||||
Soft_Murum_0_to_1_5 = db.Column(db.Float)
|
Soft_Murum_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Soft_Murum_1_5_to_3_0 = db.Column(db.Float)
|
Soft_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Soft_Murum_3_0_to_4_5 = db.Column(db.Float)
|
Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
Hard_Murum_0_to_1_5 = db.Column(db.Float)
|
Hard_Murum_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Murum_1_5_to_3_0 = db.Column(db.Float)
|
Hard_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
Soft_Rock_0_to_1_5 = db.Column(db.Float)
|
Soft_Rock_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Soft_Rock_1_5_to_3_0 = db.Column(db.Float)
|
Soft_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
Hard_Rock_0_to_1_5 = db.Column(db.Float)
|
Hard_Rock_0_to_1_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Rock_1_5_to_3_0 = db.Column(db.Float)
|
Hard_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Rock_3_0_to_4_5 = db.Column(db.Float)
|
Hard_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Rock_4_5_to_6_0 = db.Column(db.Float)
|
Hard_Rock_4_5_to_6_0 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Rock_6_0_to_7_5 = db.Column(db.Float)
|
Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
# Totals
|
# Totals
|
||||||
Soft_Murum_0_to_1_5_total = db.Column(db.Float)
|
Soft_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Soft_Murum_1_5_to_3_0_total = db.Column(db.Float)
|
Soft_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Soft_Murum_3_0_to_4_5_total = db.Column(db.Float)
|
Soft_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
Hard_Murum_0_to_1_5_total = db.Column(db.Float)
|
Hard_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Murum_1_5_and_above_total = db.Column(db.Float)
|
Hard_Murum_1_5_and_above_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
Soft_Rock_0_to_1_5_total = db.Column(db.Float)
|
Soft_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Soft_Rock_1_5_and_above_total = db.Column(db.Float)
|
Soft_Rock_1_5_and_above_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
Hard_Rock_0_to_1_5_total = db.Column(db.Float)
|
Hard_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Rock_1_5_and_above_total = db.Column(db.Float)
|
Hard_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Rock_3_0_to_4_5_total = db.Column(db.Float)
|
Hard_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Rock_4_5_to_6_0_total = db.Column(db.Float)
|
Hard_Rock_4_5_to_6_0_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
Hard_Rock_6_0_to_7_5_total = db.Column(db.Float)
|
Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 4), default=0)
|
||||||
|
|
||||||
|
Total = db.Column(db.Numeric(12, 4), default=0)
|
||||||
Remarks = db.Column(db.String(500))
|
Remarks = db.Column(db.String(500))
|
||||||
Total = db.Column(db.Float)
|
RA_Bill_No=db.Column(db.String(500))
|
||||||
|
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.today)
|
||||||
|
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<TrenchExcavation {self.Location}>"
|
return f"<TrenchExcavation {self.Location}>"
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
|
||||||
|
|
||||||
|
|
||||||
|
# AUTO TOTAL USING REGEX
|
||||||
|
def calculate_trench_total(mapper, connection, target):
|
||||||
|
total = 0
|
||||||
|
for column in target.__table__.columns:
|
||||||
|
if RegularExpression.STR_TOTAL_PATTERN.match(column.name):
|
||||||
|
total += getattr(target, column.name) or 0
|
||||||
|
target.Total = total
|
||||||
|
|
||||||
|
|
||||||
|
event.listen(TrenchExcavation, "before_insert", calculate_trench_total)
|
||||||
|
event.listen(TrenchExcavation, "before_update", calculate_trench_total)
|
||||||
@@ -1,21 +1,16 @@
|
|||||||
# from app.services.db_service import db
|
from app.services.db_service import db
|
||||||
# from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
|
||||||
# class User(db.Model):
|
class User(db.Model):
|
||||||
# id = db.Column(db.Integer, primary_key=True)
|
__tablename__ = "users"
|
||||||
# name = db.Column(db.String(120))
|
|
||||||
# email = db.Column(db.String(120), unique=True)
|
|
||||||
# password_hash = db.Column(db.String(255))
|
|
||||||
|
|
||||||
# def set_password(self, password):
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
# self.password_hash = generate_password_hash(password)
|
name = db.Column(db.String(200), nullable=False)
|
||||||
|
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
|
password_hash = db.Column(db.String(255), nullable=False)
|
||||||
|
|
||||||
# def check_password(self, password):
|
def set_password(self, password):
|
||||||
# return check_password_hash(self.password_hash, password)
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
class User:
|
return check_password_hash(self.password_hash, password)
|
||||||
def __init__(self, id, name, email):
|
|
||||||
self.id = id
|
|
||||||
self.name = name
|
|
||||||
self.email = email
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,31 +1,48 @@
|
|||||||
from flask import Blueprint, request, render_template, redirect, flash, session
|
from flask import Blueprint, render_template, request, redirect, url_for, flash, session
|
||||||
from app.services.user_service import UserService
|
from app.services.user_service import UserService
|
||||||
|
|
||||||
auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
|
auth_bp = Blueprint("auth", __name__)
|
||||||
|
|
||||||
# LOGIN PAGE
|
|
||||||
@auth_bp.route("/login", methods=["GET", "POST"])
|
@auth_bp.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
|
if session.get("user_id"):
|
||||||
|
return redirect(url_for("dashboard.dashboard"))
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
user = UserService.validate_login(
|
email = request.form.get("email")
|
||||||
request.form["email"],
|
password = request.form.get("password")
|
||||||
request.form["password"]
|
|
||||||
)
|
user = UserService.validate_login(email, password)
|
||||||
if user:
|
if user:
|
||||||
session["user_id"] = user.id
|
session["user_id"] = user.id
|
||||||
return redirect("/dashboard")
|
session["user_name"] = user.name
|
||||||
flash("Invalid credentials", "danger")
|
flash("Login successful", "success")
|
||||||
return render_template("login.html")
|
return redirect(url_for("dashboard.dashboard"))
|
||||||
|
|
||||||
|
flash("Invalid email or password", "danger")
|
||||||
|
|
||||||
|
return render_template("login.html", title="Login")
|
||||||
|
|
||||||
# REGISTER API ONLY
|
|
||||||
@auth_bp.route("/register", methods=["POST"])
|
|
||||||
def register():
|
|
||||||
data = request.json
|
|
||||||
UserService.register_user(data["name"], data["email"], data["password"])
|
|
||||||
return {"message": "User registered successfully"}, 201
|
|
||||||
|
|
||||||
# LOGOUT
|
|
||||||
@auth_bp.route("/logout")
|
@auth_bp.route("/logout")
|
||||||
def logout():
|
def logout():
|
||||||
session.clear()
|
session.clear()
|
||||||
return redirect("/auth/login")
|
flash("Logged out successfully", "info")
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
@auth_bp.route("/register", methods=["GET", "POST"])
|
||||||
|
def register():
|
||||||
|
if request.method == "POST":
|
||||||
|
name = request.form.get("name")
|
||||||
|
email = request.form.get("email")
|
||||||
|
password = request.form.get("password")
|
||||||
|
|
||||||
|
user = UserService.register_user(name, email, password)
|
||||||
|
if not user:
|
||||||
|
flash("Email already exists", "danger")
|
||||||
|
return redirect(url_for("auth.register"))
|
||||||
|
|
||||||
|
flash("User registered successfully", "success")
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
return render_template("register.html", title="Register")
|
||||||
|
|||||||
@@ -1,8 +1,244 @@
|
|||||||
from flask import Blueprint, render_template
|
import matplotlib
|
||||||
|
matplotlib.use("Agg")
|
||||||
|
|
||||||
|
from flask import Blueprint, render_template, session, redirect, url_for, jsonify, request
|
||||||
|
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
|
||||||
|
from sqlalchemy import func
|
||||||
|
from app import db
|
||||||
|
from app.models.trench_excavation_model import TrenchExcavation
|
||||||
|
from app.models.manhole_excavation_model import ManholeExcavation
|
||||||
|
from app.models.laying_model import Laying
|
||||||
|
|
||||||
|
from app.models.subcontractor_model import Subcontractor
|
||||||
|
|
||||||
|
dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/dashboard")
|
||||||
|
|
||||||
dashboard_bp = Blueprint("dashboard", __name__)
|
|
||||||
|
|
||||||
@dashboard_bp.route("/")
|
@dashboard_bp.route("/")
|
||||||
@dashboard_bp.route("/dashboard")
|
|
||||||
def dashboard():
|
def dashboard():
|
||||||
return render_template("dashboard.html", title="Dashboard")
|
if not session.get("user_id"):
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
return render_template("dashboard.html", title="Business Intelligence Dashboard")
|
||||||
|
|
||||||
|
|
||||||
|
@dashboard_bp.route("/api/live-stats")
|
||||||
|
def live_stats():
|
||||||
|
try:
|
||||||
|
# 1. Overall Volume
|
||||||
|
t_count = TrenchExcavation.query.count()
|
||||||
|
m_count = ManholeExcavation.query.count()
|
||||||
|
l_count = Laying.query.count()
|
||||||
|
|
||||||
|
# 2. Location Distribution (Business reach)
|
||||||
|
loc_results = db.session.query(
|
||||||
|
TrenchExcavation.Location,
|
||||||
|
func.count(TrenchExcavation.id)
|
||||||
|
).group_by(TrenchExcavation.Location).all()
|
||||||
|
|
||||||
|
# 3. Work Timeline (Business productivity trend)
|
||||||
|
# Assuming your models have a 'created_at' field
|
||||||
|
timeline_results = db.session.query(
|
||||||
|
func.date(TrenchExcavation.created_at),
|
||||||
|
func.count(TrenchExcavation.id)
|
||||||
|
).group_by(func.date(TrenchExcavation.created_at)).order_by(func.date(TrenchExcavation.created_at)).all()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"summary": {
|
||||||
|
"trench": t_count,
|
||||||
|
"manhole": m_count,
|
||||||
|
"laying": l_count,
|
||||||
|
"total": t_count + m_count + l_count
|
||||||
|
},
|
||||||
|
"locations": {row[0]: row[1] for row in loc_results if row[0]},
|
||||||
|
"timeline": {str(row[0]): row[1] for row in timeline_results}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# subcontractor dashboard
|
||||||
|
@dashboard_bp.route("/subcontractor_dashboard")
|
||||||
|
def subcontractor_dashboard():
|
||||||
|
|
||||||
|
if not session.get("user_id"):
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
subcontractors = Subcontractor.query.all()
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"subcontractor_dashboard.html",
|
||||||
|
subcontractors=subcontractors
|
||||||
|
)
|
||||||
|
|
||||||
|
# ✅ API: Get Unique RA Bills
|
||||||
|
@dashboard_bp.route("/api/get-ra-bills")
|
||||||
|
def get_ra_bills():
|
||||||
|
|
||||||
|
subcontractor_id = request.args.get("subcontractor")
|
||||||
|
category = request.args.get("category")
|
||||||
|
|
||||||
|
if not subcontractor_id or not category:
|
||||||
|
return {"ra_bills": []}
|
||||||
|
|
||||||
|
match category:
|
||||||
|
|
||||||
|
case "trench_excavation":
|
||||||
|
results = db.session.query(
|
||||||
|
TrenchExcavation.RA_Bill_No
|
||||||
|
).filter(
|
||||||
|
TrenchExcavation.subcontractor_id == subcontractor_id
|
||||||
|
).distinct().order_by(TrenchExcavation.RA_Bill_No).all()
|
||||||
|
|
||||||
|
# (Add others same pattern later)
|
||||||
|
case _:
|
||||||
|
results = []
|
||||||
|
|
||||||
|
ra_bills = [r[0] for r in results if r[0]]
|
||||||
|
|
||||||
|
return {"ra_bills": ra_bills}
|
||||||
|
|
||||||
|
|
||||||
|
# API FOR CHART DATA
|
||||||
|
# @dashboard_bp.route("/api/subcontractor-chart")
|
||||||
|
# def subcontractor_chart():
|
||||||
|
|
||||||
|
# subcontractor_id = request.args.get("subcontractor")
|
||||||
|
# category = request.args.get("category")
|
||||||
|
# ra_bill = request.args.get("ra_bill")
|
||||||
|
|
||||||
|
# labels = []
|
||||||
|
# values = []
|
||||||
|
|
||||||
|
# match category:
|
||||||
|
|
||||||
|
# # ✅ Trench
|
||||||
|
# case "trench_excavation":
|
||||||
|
# query = db.session.query(
|
||||||
|
# TrenchExcavation.excavation_category,
|
||||||
|
# func.sum(TrenchExcavation.Total)
|
||||||
|
# )
|
||||||
|
|
||||||
|
# if subcontractor_id:
|
||||||
|
# query = query.filter(TrenchExcavation.subcontractor_id == subcontractor_id)
|
||||||
|
|
||||||
|
# if ra_bill:
|
||||||
|
# query = query.filter(TrenchExcavation.RA_Bill_No == ra_bill)
|
||||||
|
|
||||||
|
# results = query.group_by(TrenchExcavation.excavation_category).all()
|
||||||
|
|
||||||
|
# # ✅ Manhole
|
||||||
|
# case "manhole_excavation":
|
||||||
|
# query = db.session.query(
|
||||||
|
# ManholeExcavation.excavation_category,
|
||||||
|
# func.sum(ManholeExcavation.Total)
|
||||||
|
# )
|
||||||
|
|
||||||
|
# if subcontractor_id:
|
||||||
|
# query = query.filter(ManholeExcavation.subcontractor_id == subcontractor_id)
|
||||||
|
|
||||||
|
# if ra_bill:
|
||||||
|
# query = query.filter(ManholeExcavation.RA_Bill_No == ra_bill)
|
||||||
|
|
||||||
|
# results = query.group_by(ManholeExcavation.excavation_category).all()
|
||||||
|
|
||||||
|
# case _:
|
||||||
|
# results = []
|
||||||
|
|
||||||
|
# for r in results:
|
||||||
|
# labels.append(r[0])
|
||||||
|
# values.append(float(r[1] or 0))
|
||||||
|
|
||||||
|
# return jsonify({
|
||||||
|
# "labels": labels,
|
||||||
|
# "values": values
|
||||||
|
# })
|
||||||
|
|
||||||
|
|
||||||
|
@dashboard_bp.route("/api/trench-analysis")
|
||||||
|
def trench_analysis():
|
||||||
|
|
||||||
|
subcontractor_id = request.args.get("subcontractor")
|
||||||
|
ra_bill = request.args.get("ra_bill")
|
||||||
|
|
||||||
|
query = TrenchExcavation.query
|
||||||
|
|
||||||
|
if subcontractor_id:
|
||||||
|
query = query.filter(TrenchExcavation.subcontractor_id == subcontractor_id)
|
||||||
|
|
||||||
|
if ra_bill:
|
||||||
|
query = query.filter(TrenchExcavation.RA_Bill_No == ra_bill)
|
||||||
|
|
||||||
|
data = query.all()
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"Soft Murum": {"depth": 0, "qty": 0},
|
||||||
|
"Hard Murum": {"depth": 0, "qty": 0},
|
||||||
|
"Soft Rock": {"depth": 0, "qty": 0},
|
||||||
|
"Hard Rock": {"depth": 0, "qty": 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for r in data:
|
||||||
|
|
||||||
|
# Soft Murum
|
||||||
|
result["Soft Murum"]["depth"] += (
|
||||||
|
(r.Soft_Murum_0_to_1_5 or 0) +
|
||||||
|
(r.Soft_Murum_1_5_to_3_0 or 0) +
|
||||||
|
(r.Soft_Murum_3_0_to_4_5 or 0)
|
||||||
|
)
|
||||||
|
result["Soft Murum"]["qty"] += (
|
||||||
|
(r.Soft_Murum_0_to_1_5_total or 0) +
|
||||||
|
(r.Soft_Murum_1_5_to_3_0_total or 0) +
|
||||||
|
(r.Soft_Murum_3_0_to_4_5_total or 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Hard Murum
|
||||||
|
result["Hard Murum"]["depth"] += (
|
||||||
|
(r.Hard_Murum_0_to_1_5 or 0) +
|
||||||
|
(r.Hard_Murum_1_5_to_3_0 or 0)
|
||||||
|
)
|
||||||
|
result["Hard Murum"]["qty"] += (
|
||||||
|
(r.Hard_Murum_0_to_1_5_total or 0) +
|
||||||
|
(r.Hard_Murum_1_5_and_above_total or 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Soft Rock
|
||||||
|
result["Soft Rock"]["depth"] += (
|
||||||
|
(r.Soft_Rock_0_to_1_5 or 0) +
|
||||||
|
(r.Soft_Rock_1_5_to_3_0 or 0)
|
||||||
|
)
|
||||||
|
result["Soft Rock"]["qty"] += (
|
||||||
|
(r.Soft_Rock_0_to_1_5_total or 0) +
|
||||||
|
(r.Soft_Rock_1_5_and_above_total or 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Hard Rock
|
||||||
|
result["Hard Rock"]["depth"] += (
|
||||||
|
(r.Hard_Rock_0_to_1_5 or 0) +
|
||||||
|
(r.Hard_Rock_1_5_to_3_0 or 0) +
|
||||||
|
(r.Hard_Rock_3_0_to_4_5 or 0) +
|
||||||
|
(r.Hard_Rock_4_5_to_6_0 or 0) +
|
||||||
|
(r.Hard_Rock_6_0_to_7_5 or 0)
|
||||||
|
)
|
||||||
|
result["Hard Rock"]["qty"] += (
|
||||||
|
(r.Hard_Rock_0_to_1_5_total or 0) +
|
||||||
|
(r.Hard_Rock_1_5_to_3_0_total or 0) +
|
||||||
|
(r.Hard_Rock_3_0_to_4_5_total or 0) +
|
||||||
|
(r.Hard_Rock_4_5_to_6_0_total or 0) +
|
||||||
|
(r.Hard_Rock_6_0_to_7_5_total or 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
labels = list(result.keys())
|
||||||
|
depth = [result[k]["depth"] for k in labels]
|
||||||
|
qty = [result[k]["qty"] for k in labels]
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"labels": labels,
|
||||||
|
"depth": depth,
|
||||||
|
"qty": qty
|
||||||
|
})
|
||||||
28
app/routes/file_format.py
Normal file
28
app/routes/file_format.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from flask import Blueprint, render_template, send_from_directory, abort, current_app
|
||||||
|
from app.utils.helpers import login_required
|
||||||
|
from app.utils.file_utils import get_download_format_folder
|
||||||
|
import os
|
||||||
|
|
||||||
|
file_format_bp = Blueprint("file_format", __name__)
|
||||||
|
|
||||||
|
@file_format_bp .route("/file_format")
|
||||||
|
@login_required
|
||||||
|
def download_format():
|
||||||
|
return render_template("file_format.html", title="Download File Formats")
|
||||||
|
|
||||||
|
|
||||||
|
@file_format_bp .route("/file_format/download/<filename>")
|
||||||
|
@login_required
|
||||||
|
def download_excel_format(filename):
|
||||||
|
|
||||||
|
download_folder = get_download_format_folder()
|
||||||
|
file_path = os.path.join(download_folder, filename)
|
||||||
|
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
return send_from_directory(
|
||||||
|
directory=download_folder,
|
||||||
|
path=filename,
|
||||||
|
as_attachment=True
|
||||||
|
)
|
||||||
@@ -1,21 +1,46 @@
|
|||||||
from flask import Blueprint, render_template, request, flash
|
from flask import Blueprint, render_template, request, flash
|
||||||
from app.services.file_service import FileService
|
from app.services.file_service import FileService
|
||||||
from app.models.subcontractor_model import Subcontractor
|
from app.models.subcontractor_model import Subcontractor
|
||||||
|
from app.utils.helpers import login_required
|
||||||
|
|
||||||
file_import_bp = Blueprint("file_import", __name__, url_prefix="/file")
|
file_import_bp = Blueprint("file_import", __name__, url_prefix="/file")
|
||||||
|
|
||||||
@file_import_bp.route("/import", methods=["GET", "POST"])
|
# this is contractractor immport routes
|
||||||
|
@file_import_bp.route("/import_Subcontractor", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
def import_file():
|
def import_file():
|
||||||
subcontractors = Subcontractor.query.all()
|
subcontractors = Subcontractor.query.all()
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
file = request.files.get("file")
|
file = request.files.get("file")
|
||||||
subcontractor_id = request.form.get("subcontractor_id")
|
subcontractor_id = request.form.get("subcontractor_id")
|
||||||
file_type = request.form.get("file_type")
|
RA_Bill_No = request.form.get("RA_Bill_No")
|
||||||
|
|
||||||
service = FileService()
|
service = FileService()
|
||||||
success, msg = service.handle_file_upload(file, subcontractor_id, file_type)
|
success, msg = service.handle_file_upload(file, subcontractor_id, RA_Bill_No)
|
||||||
|
|
||||||
flash(msg, "success" if success else "danger")
|
flash(msg, "success" if success else "danger")
|
||||||
|
|
||||||
return render_template("file_import.html", title="File Import", subcontractors=subcontractors)
|
return render_template(
|
||||||
|
"file_import_subcontractor.html",
|
||||||
|
title="Sub-cont. File Import",
|
||||||
|
subcontractors=subcontractors
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# this is client import routes
|
||||||
|
@file_import_bp.route("/import_client", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def client_import_file():
|
||||||
|
subcontractors = Subcontractor.query.all()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
file = request.files.get("file")
|
||||||
|
RA_Bill_No = request.form.get("RA_Bill_No")
|
||||||
|
|
||||||
|
service = FileService()
|
||||||
|
success, msg = service.handle_client_file_upload(file, RA_Bill_No)
|
||||||
|
|
||||||
|
flash(msg, "success" if success else "danger")
|
||||||
|
|
||||||
|
return render_template("file_import_client.html", title="Client File Import", subcontractors=subcontractors)
|
||||||
205
app/routes/file_report.py
Normal file
205
app/routes/file_report.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import io
|
||||||
|
from flask import Blueprint, render_template, request, send_file, flash
|
||||||
|
from app.utils.helpers import login_required
|
||||||
|
|
||||||
|
from app.models.subcontractor_model import Subcontractor
|
||||||
|
|
||||||
|
from app.models.manhole_excavation_model import ManholeExcavation
|
||||||
|
from app.models.trench_excavation_model import TrenchExcavation
|
||||||
|
from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber
|
||||||
|
from app.models.laying_model import Laying
|
||||||
|
|
||||||
|
from app.models.mh_ex_client_model import ManholeExcavationClient
|
||||||
|
from app.models.tr_ex_client_model import TrenchExcavationClient
|
||||||
|
from app.models.mh_dc_client_model import ManholeDomesticChamberClient
|
||||||
|
from app.models.laying_client_model import LayingClient
|
||||||
|
|
||||||
|
|
||||||
|
# --- BLUEPRINT DEFINITION ---
|
||||||
|
file_report_bp = Blueprint("file_report", __name__, url_prefix="/file")
|
||||||
|
|
||||||
|
# --- Client class ---
|
||||||
|
class ClientBill:
|
||||||
|
def __init__(self):
|
||||||
|
self.df_tr = pd.DataFrame()
|
||||||
|
self.df_mh = pd.DataFrame()
|
||||||
|
self.df_dc = pd.DataFrame()
|
||||||
|
self.df_laying = pd.DataFrame()
|
||||||
|
|
||||||
|
def Fetch(self, RA_Bill_No):
|
||||||
|
trench = TrenchExcavationClient.query.filter_by(RA_Bill_No=RA_Bill_No).all()
|
||||||
|
mh = ManholeExcavationClient.query.filter_by(RA_Bill_No=RA_Bill_No).all()
|
||||||
|
dc = ManholeDomesticChamberClient.query.filter_by(RA_Bill_No=RA_Bill_No).all()
|
||||||
|
lay = LayingClient.query.filter_by(RA_Bill_No=RA_Bill_No).all()
|
||||||
|
|
||||||
|
self.df_tr = pd.DataFrame([c.serialize() for c in trench])
|
||||||
|
self.df_mh = pd.DataFrame([c.serialize() for c in mh])
|
||||||
|
self.df_dc = pd.DataFrame([c.serialize() for c in dc])
|
||||||
|
self.df_laying = pd.DataFrame([c.serialize() for c in lay])
|
||||||
|
|
||||||
|
drop_cols = ["id", "created_at", "_sa_instance_state"]
|
||||||
|
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)
|
||||||
|
|
||||||
|
# --- Subcontractor class ---
|
||||||
|
class SubcontractorBill:
|
||||||
|
def __init__(self):
|
||||||
|
self.df_tr = pd.DataFrame()
|
||||||
|
self.df_mh = pd.DataFrame()
|
||||||
|
self.df_dc = pd.DataFrame()
|
||||||
|
self.df_laying = pd.DataFrame()
|
||||||
|
|
||||||
|
def Fetch(self, RA_Bill_No=None, subcontractor_id=None):
|
||||||
|
filters = {}
|
||||||
|
if subcontractor_id:
|
||||||
|
filters["subcontractor_id"] = subcontractor_id
|
||||||
|
if RA_Bill_No:
|
||||||
|
filters["RA_Bill_No"] = RA_Bill_No
|
||||||
|
|
||||||
|
trench = TrenchExcavation.query.filter_by(**filters).all()
|
||||||
|
mh = ManholeExcavation.query.filter_by(**filters).all()
|
||||||
|
dc = ManholeDomesticChamber.query.filter_by(**filters).all()
|
||||||
|
lay = Laying.query.filter_by(**filters).all()
|
||||||
|
|
||||||
|
self.df_tr = pd.DataFrame([c.serialize() for c in trench])
|
||||||
|
self.df_mh = pd.DataFrame([c.serialize() for c in mh])
|
||||||
|
self.df_dc = pd.DataFrame([c.serialize() for c in dc])
|
||||||
|
self.df_laying = pd.DataFrame([c.serialize() for c in lay])
|
||||||
|
|
||||||
|
drop_cols = ["id", "created_at", "_sa_instance_state"]
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
# --- subcontractor report only ---
|
||||||
|
@file_report_bp.route("/Subcontractor_report", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def report_file():
|
||||||
|
subcontractors = Subcontractor.query.all()
|
||||||
|
tables = None
|
||||||
|
selected_sc_id = None
|
||||||
|
ra_bill_no = None
|
||||||
|
download_all = False
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
subcontractor_id = request.form.get("subcontractor_id")
|
||||||
|
ra_bill_no = request.form.get("ra_bill_no")
|
||||||
|
download_all = request.form.get("download_all") == "true"
|
||||||
|
action = request.form.get("action")
|
||||||
|
|
||||||
|
if not subcontractor_id:
|
||||||
|
flash("Please select a subcontractor.", "danger")
|
||||||
|
return render_template("subcontractor_report.html", subcontractors=subcontractors)
|
||||||
|
|
||||||
|
subcontractor = Subcontractor.query.get(subcontractor_id)
|
||||||
|
bill_gen = SubcontractorBill()
|
||||||
|
|
||||||
|
if download_all:
|
||||||
|
bill_gen.Fetch(subcontractor_id=subcontractor_id)
|
||||||
|
file_name = f"{subcontractor.subcontractor_name}_ALL_BILLS.xlsx"
|
||||||
|
else:
|
||||||
|
if not ra_bill_no:
|
||||||
|
flash("Please enter an RA Bill Number.", "danger")
|
||||||
|
return render_template("subcontractor_report.html", subcontractors=subcontractors)
|
||||||
|
bill_gen.Fetch(RA_Bill_No=ra_bill_no, subcontractor_id=subcontractor_id)
|
||||||
|
file_name = f"{subcontractor.subcontractor_name}_RA_{ra_bill_no}_Report.xlsx"
|
||||||
|
|
||||||
|
if bill_gen.df_tr.empty and bill_gen.df_mh.empty and bill_gen.df_dc.empty:
|
||||||
|
flash("No data found for this selection.", "warning")
|
||||||
|
return render_template("subcontractor_report.html", subcontractors=subcontractors)
|
||||||
|
|
||||||
|
# If download is clicked, return file immediately
|
||||||
|
if action == "download":
|
||||||
|
output = io.BytesIO()
|
||||||
|
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
|
||||||
|
bill_gen.df_tr.to_excel(writer, index=False, sheet_name="Tr.Ex.")
|
||||||
|
bill_gen.df_mh.to_excel(writer, index=False, sheet_name="MH.Ex.")
|
||||||
|
bill_gen.df_dc.to_excel(writer, index=False, sheet_name="MH & DC")
|
||||||
|
bill_gen.df_laying.to_excel(writer, index=False, sheet_name="Laying")
|
||||||
|
output.seek(0)
|
||||||
|
return send_file(output, download_name=file_name, as_attachment=True)
|
||||||
|
|
||||||
|
# We add bootstrap classes directly to the pandas output
|
||||||
|
table_classes = "table table-bordered table-striped table-hover table-sm mb-0"
|
||||||
|
tables = {
|
||||||
|
"tr": bill_gen.df_tr.to_html(classes=table_classes, index=False),
|
||||||
|
"mh": bill_gen.df_mh.to_html(classes=table_classes, index=False),
|
||||||
|
"dc": bill_gen.df_dc.to_html(classes=table_classes, index=False),
|
||||||
|
"laying": bill_gen.df_laying.to_html(classes=table_classes, index=False)
|
||||||
|
}
|
||||||
|
selected_sc_id = subcontractor_id
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"subcontractor_report.html",
|
||||||
|
subcontractors=subcontractors,
|
||||||
|
tables=tables,
|
||||||
|
selected_sc_id=selected_sc_id,
|
||||||
|
ra_bill_no=ra_bill_no,
|
||||||
|
download_all=download_all
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- CLIENT REPORT (PREVIEW + DOWNLOAD) ---
|
||||||
|
@file_report_bp.route("/client_report", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def client_report():
|
||||||
|
|
||||||
|
tables = {"tr": None, "mh": None, "dc": None, "laying": None}
|
||||||
|
ra_val = ""
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
|
||||||
|
# ⚠ MUST match HTML name
|
||||||
|
RA_Bill_No = request.form.get("RA_Bill_No")
|
||||||
|
action = request.form.get("action")
|
||||||
|
ra_val = RA_Bill_No
|
||||||
|
|
||||||
|
if not RA_Bill_No:
|
||||||
|
flash("Please enter RA Bill No.", "danger")
|
||||||
|
return render_template("client_report.html", tables=tables, ra_val=ra_val)
|
||||||
|
|
||||||
|
# -------- FETCH CLIENT DATA --------
|
||||||
|
bill_gen = ClientBill()
|
||||||
|
bill_gen.Fetch(RA_Bill_No)
|
||||||
|
|
||||||
|
# If no data
|
||||||
|
if (
|
||||||
|
bill_gen.df_tr.empty and
|
||||||
|
bill_gen.df_mh.empty and
|
||||||
|
bill_gen.df_dc.empty and
|
||||||
|
bill_gen.df_laying.empty
|
||||||
|
):
|
||||||
|
flash(f"No Client records found for RA Bill {RA_Bill_No}", "warning")
|
||||||
|
return render_template("client_report.html", tables=tables, ra_val=ra_val)
|
||||||
|
|
||||||
|
# -------- DOWNLOAD --------
|
||||||
|
if action == "download":
|
||||||
|
|
||||||
|
output = io.BytesIO()
|
||||||
|
|
||||||
|
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
|
||||||
|
bill_gen.df_tr.to_excel(writer, index=False, sheet_name="Trench")
|
||||||
|
bill_gen.df_mh.to_excel(writer, index=False, sheet_name="MH")
|
||||||
|
bill_gen.df_dc.to_excel(writer, index=False, sheet_name="MH & DC")
|
||||||
|
bill_gen.df_laying.to_excel(writer, index=False, sheet_name="Laying")
|
||||||
|
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
return send_file(
|
||||||
|
output,
|
||||||
|
download_name=f"Client_RA_{RA_Bill_No}_Report.xlsx",
|
||||||
|
as_attachment=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# -------- PREVIEW --------
|
||||||
|
table_class = "table table-bordered table-striped table-hover table-sm"
|
||||||
|
|
||||||
|
tables["tr"] = bill_gen.df_tr.to_html(classes=table_class, index=False)
|
||||||
|
tables["mh"] = bill_gen.df_mh.to_html(classes=table_class, index=False)
|
||||||
|
tables["dc"] = bill_gen.df_dc.to_html(classes=table_class, index=False)
|
||||||
|
tables["laying"] = bill_gen.df_laying.to_html(classes=table_class, index=False)
|
||||||
|
|
||||||
|
return render_template("client_report.html", tables=tables, ra_val=ra_val)
|
||||||
253
app/routes/generate_comparison_report.py
Normal file
253
app/routes/generate_comparison_report.py
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
from flask import Blueprint, render_template, request, send_file, flash
|
||||||
|
from collections import defaultdict
|
||||||
|
import pandas as pd
|
||||||
|
import io
|
||||||
|
|
||||||
|
from app.models.subcontractor_model import Subcontractor
|
||||||
|
from app.models.trench_excavation_model import TrenchExcavation
|
||||||
|
from app.models.manhole_excavation_model import ManholeExcavation
|
||||||
|
from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber
|
||||||
|
from app.models.laying_model import Laying
|
||||||
|
|
||||||
|
from app.models.tr_ex_client_model import TrenchExcavationClient
|
||||||
|
from app.models.mh_ex_client_model import ManholeExcavationClient
|
||||||
|
from app.models.mh_dc_client_model import ManholeDomesticChamberClient
|
||||||
|
from app.models.laying_client_model import LayingClient
|
||||||
|
|
||||||
|
from app.utils.helpers import login_required
|
||||||
|
from app.utils.regex_utils import RegularExpression
|
||||||
|
|
||||||
|
|
||||||
|
generate_report_bp = Blueprint("generate_report", __name__, url_prefix="/report")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# NORMALIZER
|
||||||
|
def normalize_key(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return str(value).strip().upper()
|
||||||
|
|
||||||
|
|
||||||
|
# HEADER FORMATTER
|
||||||
|
def format_header(header):
|
||||||
|
if "-" in header:
|
||||||
|
prefix, rest = header.split("-", 1)
|
||||||
|
prefix = prefix.title()
|
||||||
|
else:
|
||||||
|
prefix, rest = None, header
|
||||||
|
|
||||||
|
parts = rest.split("_")
|
||||||
|
result = []
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
while i < len(parts):
|
||||||
|
if i + 1 < len(parts) and parts[i].isdigit() and parts[i + 1].isdigit():
|
||||||
|
result.append(f"{parts[i]}.{parts[i + 1]}")
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
result.append(parts[i].title())
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
final_text = " ".join(result)
|
||||||
|
return f"{prefix}-{final_text}" if prefix else final_text
|
||||||
|
|
||||||
|
|
||||||
|
# LOOKUP CREATOR
|
||||||
|
def make_lookup(rows, key_field):
|
||||||
|
lookup = {}
|
||||||
|
for r in rows:
|
||||||
|
location = normalize_key(r.get("Location"))
|
||||||
|
key_val = normalize_key(r.get(key_field))
|
||||||
|
|
||||||
|
if location and key_val:
|
||||||
|
lookup.setdefault((location, key_val), []).append(r)
|
||||||
|
|
||||||
|
return lookup
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# COMPARISON BUILDER
|
||||||
|
def build_comparison(client_rows, contractor_rows, key_field):
|
||||||
|
contractor_lookup = make_lookup(contractor_rows, key_field)
|
||||||
|
output = []
|
||||||
|
|
||||||
|
used_index = defaultdict(int) # 🔥 THIS FIXES YOUR ISSUE
|
||||||
|
|
||||||
|
for c in client_rows:
|
||||||
|
|
||||||
|
client_location = normalize_key(c.get("Location"))
|
||||||
|
client_key = normalize_key(c.get(key_field))
|
||||||
|
|
||||||
|
if not client_location or not client_key:
|
||||||
|
continue
|
||||||
|
|
||||||
|
subs = contractor_lookup.get((client_location, client_key))
|
||||||
|
if not subs:
|
||||||
|
continue
|
||||||
|
|
||||||
|
idx = used_index[(client_location, client_key)]
|
||||||
|
|
||||||
|
# ❗ If subcontractor rows are exhausted, skip
|
||||||
|
if idx >= len(subs):
|
||||||
|
continue
|
||||||
|
|
||||||
|
s = subs[idx] # ✅ take NEXT subcontractor row
|
||||||
|
used_index[(client_location, client_key)] += 1
|
||||||
|
|
||||||
|
# ---- totals ----
|
||||||
|
client_total = sum(
|
||||||
|
float(v or 0)
|
||||||
|
for k, v in c.items()
|
||||||
|
if k.endswith("_total")
|
||||||
|
or RegularExpression.D_RANGE_PATTERN.match(k)
|
||||||
|
or RegularExpression.PIPE_MM_PATTERN.match(k)
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_total = sum(
|
||||||
|
float(v or 0)
|
||||||
|
for k, v in s.items()
|
||||||
|
if k.endswith("_total")
|
||||||
|
or RegularExpression.D_RANGE_PATTERN.match(k)
|
||||||
|
or RegularExpression.PIPE_MM_PATTERN.match(k)
|
||||||
|
)
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"Location": client_location,
|
||||||
|
key_field.replace("_", " "): client_key
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v in c.items():
|
||||||
|
if k not in ["id", "created_at"]:
|
||||||
|
row[f"Client-{k}"] = v
|
||||||
|
|
||||||
|
row["Client-Total"] = round(client_total, 2)
|
||||||
|
row[" "] = ""
|
||||||
|
|
||||||
|
for k, v in s.items():
|
||||||
|
if k not in ["id", "created_at", "subcontractor_id"]:
|
||||||
|
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)
|
||||||
|
# formatting headers
|
||||||
|
df.columns = [format_header(col) for col in df.columns]
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# EXCEL SHEET WRITER
|
||||||
|
def write_sheet(writer, df, sheet_name, subcontractor_name):
|
||||||
|
workbook = writer.book
|
||||||
|
|
||||||
|
# write dataframe (data already correct)
|
||||||
|
df.to_excel(writer, sheet_name=sheet_name, index=False, startrow=3)
|
||||||
|
ws = writer.sheets[sheet_name]
|
||||||
|
|
||||||
|
# formats
|
||||||
|
title_fmt = workbook.add_format({"bold": True, "font_size": 14})
|
||||||
|
client_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#D9EDF7"})
|
||||||
|
sub_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#F7E1D9"})
|
||||||
|
total_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#FFF2CC"})
|
||||||
|
diff_fmt = workbook.add_format({"bold": True, "border": 1, "bg_color": "#E2EFDA"})
|
||||||
|
default_header_fmt = workbook.add_format({
|
||||||
|
"bold": True,
|
||||||
|
"border": 1,
|
||||||
|
"bg_color": "#E7E6E6",
|
||||||
|
"align": "center",
|
||||||
|
"valign": "vcenter"
|
||||||
|
})
|
||||||
|
|
||||||
|
# titles
|
||||||
|
ws.merge_range(
|
||||||
|
0, 0, 0, len(df.columns) - 1,
|
||||||
|
"CLIENT vs SUBCONTRACTOR",
|
||||||
|
title_fmt
|
||||||
|
)
|
||||||
|
ws.merge_range(
|
||||||
|
1, 0, 1, len(df.columns) - 1,
|
||||||
|
f"Subcontractor Name - {subcontractor_name}",
|
||||||
|
title_fmt
|
||||||
|
)
|
||||||
|
|
||||||
|
# header formatting
|
||||||
|
for col_num, col_name in enumerate(df.columns):
|
||||||
|
if col_name.startswith("Client-"):
|
||||||
|
ws.write(3, col_num, col_name, client_fmt)
|
||||||
|
elif col_name.startswith("Subcontractor-"):
|
||||||
|
ws.write(3, col_num, col_name, sub_fmt)
|
||||||
|
elif col_name.endswith("Total"):
|
||||||
|
ws.write(3, col_num, col_name, total_fmt)
|
||||||
|
elif col_name == "Diff":
|
||||||
|
ws.write(3, col_num, col_name, diff_fmt)
|
||||||
|
else:
|
||||||
|
ws.write(3, col_num, col_name, default_header_fmt)
|
||||||
|
|
||||||
|
ws.set_column(col_num, col_num, 20)
|
||||||
|
|
||||||
|
|
||||||
|
# REPORT ROUTE
|
||||||
|
@generate_report_bp.route("/comparison_report", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
def comparison_report():
|
||||||
|
subcontractors = Subcontractor.query.all()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
subcontractor_id = request.form.get("subcontractor_id")
|
||||||
|
if not subcontractor_id:
|
||||||
|
flash("Please select subcontractor", "danger")
|
||||||
|
return render_template("generate_comparison_report.html",subcontractors=subcontractors)
|
||||||
|
|
||||||
|
subcontractor = Subcontractor.query.get_or_404(subcontractor_id)
|
||||||
|
|
||||||
|
# -------- DATA --------
|
||||||
|
tr_client = [r.serialize() for r in TrenchExcavationClient.query.all()]
|
||||||
|
tr_sub = [r.serialize() for r in TrenchExcavation.query.filter_by(
|
||||||
|
subcontractor_id=subcontractor_id
|
||||||
|
).all()]
|
||||||
|
df_tr = build_comparison(tr_client, tr_sub, "MH_NO")
|
||||||
|
|
||||||
|
mh_client = [r.serialize() for r in ManholeExcavationClient.query.all()]
|
||||||
|
mh_sub = [r.serialize() for r in ManholeExcavation.query.filter_by(
|
||||||
|
subcontractor_id=subcontractor_id
|
||||||
|
).all()]
|
||||||
|
df_mh = build_comparison(mh_client, mh_sub, "MH_NO")
|
||||||
|
|
||||||
|
dc_client = [r.serialize() for r in ManholeDomesticChamberClient.query.all()]
|
||||||
|
dc_sub = [r.serialize() for r in ManholeDomesticChamber.query.filter_by(
|
||||||
|
subcontractor_id=subcontractor_id
|
||||||
|
).all()]
|
||||||
|
df_dc = build_comparison(dc_client, dc_sub, "MH_NO")
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
# -------- EXCEL --------
|
||||||
|
output = io.BytesIO()
|
||||||
|
filename = f"{subcontractor.subcontractor_name}_Comparison_Report.xlsx"
|
||||||
|
|
||||||
|
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
|
||||||
|
write_sheet(writer, df_tr, "Tr.Ex", subcontractor.subcontractor_name)
|
||||||
|
write_sheet(writer, df_mh, "Mh.Ex", subcontractor.subcontractor_name)
|
||||||
|
write_sheet(writer, df_dc, "MH & DC", subcontractor.subcontractor_name)
|
||||||
|
write_sheet(writer, df_lay, "Laying", subcontractor.subcontractor_name)
|
||||||
|
|
||||||
|
output.seek(0)
|
||||||
|
return send_file(
|
||||||
|
output,
|
||||||
|
as_attachment=True,
|
||||||
|
download_name=filename,
|
||||||
|
mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template("generate_comparison_report.html",subcontractors=subcontractors)
|
||||||
|
|
||||||
@@ -1,64 +1,159 @@
|
|||||||
from flask import Blueprint, render_template, request, redirect, flash
|
from flask import Blueprint, render_template, request, redirect, flash, current_app, url_for
|
||||||
from app import db
|
from app.services.db_service import db
|
||||||
from app.models.subcontractor_model import Subcontractor
|
from app.models.subcontractor_model import Subcontractor
|
||||||
|
from app.utils.helpers import login_required
|
||||||
|
|
||||||
subcontractor_bp = Blueprint("subcontractor", __name__, url_prefix="/subcontractor")
|
subcontractor_bp = Blueprint("subcontractor", __name__, url_prefix="/subcontractor")
|
||||||
|
|
||||||
|
|
||||||
# ---------------- ADD -----------------
|
# ---------------- ADD -----------------
|
||||||
@subcontractor_bp.route("/add")
|
@subcontractor_bp.route("/add")
|
||||||
|
@login_required
|
||||||
def add_subcontractor():
|
def add_subcontractor():
|
||||||
|
current_app.logger.info("Opened Add Subcontractor Page")
|
||||||
return render_template("subcontractor/add.html")
|
return render_template("subcontractor/add.html")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- SAVE -----------------
|
||||||
@subcontractor_bp.route("/save", methods=["POST"])
|
@subcontractor_bp.route("/save", methods=["POST"])
|
||||||
|
@login_required
|
||||||
def save_subcontractor():
|
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()
|
||||||
return redirect("/subcontractor/list")
|
|
||||||
|
|
||||||
# ---------------- LIST -----------------
|
if not name:
|
||||||
|
current_app.logger.warning("Empty subcontractor name submitted")
|
||||||
|
flash("Subcontractor name cannot be empty.", "danger")
|
||||||
|
return redirect(url_for("subcontractor.add_subcontractor"))
|
||||||
|
|
||||||
|
existing_sub = Subcontractor.query.filter_by(subcontractor_name=name).first()
|
||||||
|
|
||||||
|
if existing_sub:
|
||||||
|
current_app.logger.warning(f"Duplicate subcontractor attempt: {name}")
|
||||||
|
flash(f"Subcontractor with name '{name}' already exists!", "danger")
|
||||||
|
return redirect(url_for("subcontractor.add_subcontractor"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
subcontractor = Subcontractor(
|
||||||
|
subcontractor_name=name,
|
||||||
|
contact_person=request.form.get("contact_person"),
|
||||||
|
address=request.form.get("address"),
|
||||||
|
mobile_no=request.form.get("mobile_no"),
|
||||||
|
email_id=request.form.get("email_id"),
|
||||||
|
gst_no=request.form.get("gst_no"),
|
||||||
|
pan_no=request.form.get("pan_no")
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(subcontractor)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
current_app.logger.info(f"Subcontractor Created Successfully: {name}")
|
||||||
|
flash("Subcontractor added successfully!", "success")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.exception("Error while saving subcontractor")
|
||||||
|
flash("An error occurred while saving.", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for("subcontractor.subcontractor_list"))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- LIST (UPDATED WITH PAGINATION) -----------------
|
||||||
@subcontractor_bp.route("/list")
|
@subcontractor_bp.route("/list")
|
||||||
|
@login_required
|
||||||
def subcontractor_list():
|
def subcontractor_list():
|
||||||
subcontractors = Subcontractor.query.all()
|
|
||||||
return render_template("subcontractor/list.html", subcontractors=subcontractors)
|
page = request.args.get("page", 1, type=int)
|
||||||
|
per_page = 10 # Change how many records per page
|
||||||
|
|
||||||
|
pagination = Subcontractor.query.order_by(
|
||||||
|
Subcontractor.created_at
|
||||||
|
).paginate(
|
||||||
|
page=page,
|
||||||
|
per_page=per_page,
|
||||||
|
error_out=False
|
||||||
|
)
|
||||||
|
|
||||||
|
subcontractors = pagination.items
|
||||||
|
|
||||||
|
current_app.logger.info(f"Viewed Subcontractor List - Page {page}")
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"subcontractor/list.html",
|
||||||
|
subcontractors=subcontractors,
|
||||||
|
pagination=pagination
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------- EDIT -----------------
|
# ---------------- EDIT -----------------
|
||||||
@subcontractor_bp.route("/edit/<int:id>")
|
@subcontractor_bp.route("/edit/<int:id>")
|
||||||
|
@login_required
|
||||||
def edit_subcontractor(id):
|
def edit_subcontractor(id):
|
||||||
subcontractor = Subcontractor.query.get_or_404(id)
|
subcontractor = Subcontractor.query.get_or_404(id)
|
||||||
|
current_app.logger.info(f"Editing Subcontractor ID: {id}")
|
||||||
return render_template("subcontractor/edit.html", subcontractor=subcontractor)
|
return render_template("subcontractor/edit.html", subcontractor=subcontractor)
|
||||||
|
|
||||||
|
|
||||||
# ---------------- UPDATE -----------------
|
# ---------------- UPDATE -----------------
|
||||||
@subcontractor_bp.route("/update/<int:id>", methods=["POST"])
|
@subcontractor_bp.route("/update/<int:id>", methods=["POST"])
|
||||||
|
@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", "").strip()
|
||||||
|
|
||||||
subcontractor.subcontractor_name = request.form.get("subcontractor_name")
|
duplicate = Subcontractor.query.filter(
|
||||||
subcontractor.contact_person = request.form.get("contact_person")
|
Subcontractor.subcontractor_name == new_name,
|
||||||
subcontractor.mobile_no = request.form.get("mobile_no")
|
Subcontractor.id != id
|
||||||
subcontractor.email_id = request.form.get("email_id")
|
).first()
|
||||||
subcontractor.gst_no = request.form.get("gst_no")
|
|
||||||
|
|
||||||
db.session.commit()
|
if duplicate:
|
||||||
|
current_app.logger.warning(f"Duplicate update attempt: {new_name}")
|
||||||
|
flash("Another subcontractor already uses this name.", "danger")
|
||||||
|
return redirect(url_for("subcontractor.edit_subcontractor", id=id))
|
||||||
|
|
||||||
|
try:
|
||||||
|
old_name = subcontractor.subcontractor_name
|
||||||
|
|
||||||
|
subcontractor.subcontractor_name = new_name
|
||||||
|
subcontractor.contact_person = request.form.get("contact_person")
|
||||||
|
subcontractor.address = request.form.get("address")
|
||||||
|
subcontractor.mobile_no = request.form.get("mobile_no")
|
||||||
|
subcontractor.email_id = request.form.get("email_id")
|
||||||
|
subcontractor.gst_no = request.form.get("gst_no")
|
||||||
|
subcontractor.pan_no = request.form.get("pan_no")
|
||||||
|
subcontractor.status = request.form.get("status")
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
current_app.logger.info(f"Subcontractor Updated: {old_name} → {new_name}")
|
||||||
|
flash("Subcontractor updated successfully!", "success")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.exception("Error updating subcontractor")
|
||||||
|
flash("Update failed!", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for("subcontractor.subcontractor_list"))
|
||||||
|
|
||||||
flash("Subcontractor updated successfully!", "success")
|
|
||||||
return redirect("/subcontractor/list")
|
|
||||||
|
|
||||||
# ---------------- DELETE -----------------
|
# ---------------- DELETE -----------------
|
||||||
@subcontractor_bp.route("/delete/<int:id>")
|
@subcontractor_bp.route("/delete/<int:id>")
|
||||||
|
@login_required
|
||||||
def delete_subcontractor(id):
|
def delete_subcontractor(id):
|
||||||
subcontractor = Subcontractor.query.get_or_404(id)
|
subcontractor = Subcontractor.query.get_or_404(id)
|
||||||
|
|
||||||
db.session.delete(subcontractor)
|
try:
|
||||||
db.session.commit()
|
name = subcontractor.subcontractor_name
|
||||||
|
db.session.delete(subcontractor)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
flash("Subcontractor deleted successfully!", "success")
|
current_app.logger.info(f"Subcontractor Deleted: {name}")
|
||||||
return redirect("/subcontractor/list")
|
flash("Subcontractor deleted successfully!", "success")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.exception("Error deleting subcontractor")
|
||||||
|
flash("Delete failed!", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for("subcontractor.subcontractor_list"))
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
from flask import Blueprint, render_template
|
from flask import Blueprint, render_template
|
||||||
from app.services.user_service import UserService
|
from app.services.user_service import UserService
|
||||||
|
from app.utils.helpers import login_required
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
user_bp = Blueprint("user", __name__, url_prefix="/user")
|
user_bp = Blueprint("user", __name__, url_prefix="/user")
|
||||||
|
|
||||||
@user_bp.route("/list")
|
@user_bp.route("/list")
|
||||||
|
@login_required
|
||||||
def list_users():
|
def list_users():
|
||||||
users = UserService().get_all_users()
|
current_app.logger.info("User list viewed")
|
||||||
return render_template("users.html", users=users, title="Users")
|
users = UserService.get_all_users()
|
||||||
|
return render_template("users.html", users=users, title="Users")
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
0
app/services/comparison_service.py
Normal file
0
app/services/comparison_service.py
Normal file
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
|
||||||
|
|
||||||
@@ -1,34 +1,60 @@
|
|||||||
|
from app import db
|
||||||
import os
|
import os
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from app.config import Config
|
from app.utils.file_utils import ensure_upload_folder
|
||||||
from app import db
|
from app.utils.file_utils import get_uploads_folder
|
||||||
|
from app.utils.file_utils import ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
|
# Subcontractor models import
|
||||||
from app.models.trench_excavation_model import TrenchExcavation
|
from app.models.trench_excavation_model import TrenchExcavation
|
||||||
from app.models.manhole_excavation_model import ManholeExcavation
|
from app.models.manhole_excavation_model import ManholeExcavation
|
||||||
from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber
|
from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber
|
||||||
|
from app.models.laying_model import Laying
|
||||||
|
|
||||||
|
# Client models import
|
||||||
|
from app.models.tr_ex_client_model import TrenchExcavationClient
|
||||||
|
from app.models.mh_ex_client_model import ManholeExcavationClient
|
||||||
|
from app.models.mh_dc_client_model import ManholeDomesticChamberClient
|
||||||
|
from app.models.laying_client_model import LayingClient
|
||||||
|
|
||||||
from app.utils.file_utils import ensure_upload_folder
|
|
||||||
|
|
||||||
|
|
||||||
class FileService:
|
class FileService:
|
||||||
|
|
||||||
|
# ---------------- COMMON HELPERS ----------------
|
||||||
def allowed_file(self, filename):
|
def allowed_file(self, filename):
|
||||||
return "." in filename and filename.rsplit(".", 1)[1].lower() in Config.ALLOWED_EXTENSIONS
|
return ("." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS)
|
||||||
|
|
||||||
def handle_file_upload(self, file, subcontractor_id, file_type):
|
def normalize(self, val):
|
||||||
|
if val is None or pd.isna(val):
|
||||||
|
return None
|
||||||
|
|
||||||
|
val = str(val).strip()
|
||||||
|
if val.lower() in ["", "nan", "none", "-", "—"]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return val.upper()
|
||||||
|
|
||||||
|
# ---------------- SUBCONTRACTOR FILE UPLOAD ----------------
|
||||||
|
def handle_file_upload(self, file, subcontractor_id, RA_Bill_No):
|
||||||
|
|
||||||
if not subcontractor_id:
|
if not subcontractor_id:
|
||||||
return False, "Please select subcontractor."
|
return False, "Please select subcontractor."
|
||||||
if not file_type:
|
|
||||||
return False, "Please select file type."
|
if not RA_Bill_No:
|
||||||
|
return False, "Please Enter RA Bill No."
|
||||||
|
|
||||||
if not file or file.filename == "":
|
if not file or file.filename == "":
|
||||||
return False, "No file selected."
|
return False, "No file selected."
|
||||||
|
|
||||||
if not self.allowed_file(file.filename):
|
if not self.allowed_file(file.filename):
|
||||||
return False, "Invalid file type! Allowed: CSV, XLSX, XLS"
|
return False, "Invalid file type! Allowed: CSV, XLSX, XLS"
|
||||||
|
|
||||||
ensure_upload_folder()
|
ensure_upload_folder()
|
||||||
|
path = get_uploads_folder()
|
||||||
|
|
||||||
folder = os.path.join(Config.UPLOAD_FOLDER, f"sub_{subcontractor_id}")
|
folder = os.path.join(path, f"sub_{subcontractor_id}")
|
||||||
os.makedirs(folder, exist_ok=True)
|
os.makedirs(folder, exist_ok=True)
|
||||||
|
|
||||||
filename = secure_filename(file.filename)
|
filename = secure_filename(file.filename)
|
||||||
@@ -36,190 +62,315 @@ class FileService:
|
|||||||
file.save(filepath)
|
file.save(filepath)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
df = pd.read_csv(filepath) if filename.endswith(".csv") else pd.read_excel(filepath)
|
df_tr_ex = pd.read_excel(filepath, sheet_name="Tr.Ex.", header=12, dtype={"MH No": str})
|
||||||
|
df_mh_ex = pd.read_excel(filepath, sheet_name="MH Ex.", header=12, dtype={"MH No": str})
|
||||||
|
df_mh_dc = pd.read_excel(filepath, sheet_name="MH & DC", header=11, dtype={"MH No": str})
|
||||||
|
df_laying = pd.read_excel(filepath, sheet_name="Laying", header=11, dtype={"MH No": str})
|
||||||
|
|
||||||
print("\n=== Uploaded File Preview ===")
|
self.process_trench_excavation(df_tr_ex, subcontractor_id, RA_Bill_No)
|
||||||
print(df.head())
|
self.process_manhole_excavation(df_mh_ex, subcontractor_id, RA_Bill_No)
|
||||||
print("=============================\n")
|
self.process_manhole_domestic_chamber(df_mh_dc, subcontractor_id, RA_Bill_No)
|
||||||
|
self.process_laying(df_laying, subcontractor_id, RA_Bill_No)
|
||||||
|
|
||||||
# Trench Excavation save
|
return True, "SUBCONTRACTOR File uploaded successfully."
|
||||||
if file_type == "trench_excavation":
|
|
||||||
return self.process_trench_excavation(df, subcontractor_id)
|
|
||||||
|
|
||||||
# Manhole Excavation save
|
|
||||||
if file_type == "manhole_excavation":
|
|
||||||
return self.process_manhole_excavation(df, subcontractor_id)
|
|
||||||
|
|
||||||
# Manhole and Domestic Chamber Construction save
|
|
||||||
if file_type == "manhole_domestic_chamber":
|
|
||||||
return self.process_manhole_excavation(df, subcontractor_id)
|
|
||||||
|
|
||||||
|
|
||||||
return True, "File uploaded successfully."
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return False, f"Processing failed: {e}"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Trench Excavation save method (TrenchExcavation model)
|
|
||||||
def process_trench_excavation(self, df, subcontractor_id):
|
|
||||||
|
|
||||||
# Clean column names (strip whitespace)
|
|
||||||
df.columns = [str(c).strip() for c in df.columns]
|
|
||||||
|
|
||||||
# If the sheet has merged cells -> forward fill Location
|
|
||||||
if "Location" in df.columns:
|
|
||||||
df["Location"] = df["Location"].ffill()
|
|
||||||
|
|
||||||
# REMOVE empty rows
|
|
||||||
df = df.dropna(how="all")
|
|
||||||
|
|
||||||
# Identify missing location rows before insert
|
|
||||||
missing_loc = df[df["Location"].isna() | (df["Location"].astype(str).str.strip() == "")]
|
|
||||||
if not missing_loc.empty:
|
|
||||||
return False, f"Error: Some rows have empty Location. Rows: {missing_loc.index.tolist()}"
|
|
||||||
|
|
||||||
saved_count = 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
for index, row in df.iterrows():
|
|
||||||
|
|
||||||
record_data = {}
|
|
||||||
|
|
||||||
# Insert only fields that exist in model
|
|
||||||
for col in df.columns:
|
|
||||||
if hasattr(TrenchExcavation, col):
|
|
||||||
value = row[col]
|
|
||||||
|
|
||||||
# Normalize empty values
|
|
||||||
if pd.isna(value) or str(value).strip() in ["", "-", "—", "nan", "NaN"]:
|
|
||||||
value = None
|
|
||||||
|
|
||||||
record_data[col] = value
|
|
||||||
|
|
||||||
record = TrenchExcavation(
|
|
||||||
subcontractor_id=subcontractor_id,
|
|
||||||
**record_data
|
|
||||||
)
|
|
||||||
|
|
||||||
db.session.add(record)
|
|
||||||
saved_count += 1
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
return True, f"Trench Excavation data saved successfully. Total rows: {saved_count}"
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return False, f"Trench Excavation Save Failed: {e}"
|
return False, f"Import failed: {e}"
|
||||||
|
|
||||||
|
|
||||||
# Manhole Excavation save method (ManholeExcavation model)
|
# ---------------- Trench Excavation (Subcontractor) ----------------
|
||||||
def process_manhole_excavation(self, df, subcontractor_id):
|
def process_trench_excavation(self, df, subcontractor_id, RA_Bill_No):
|
||||||
|
|
||||||
# Clean column names (strip whitespace)
|
df.columns = (
|
||||||
df.columns = [str(c).strip() for c in df.columns]
|
df.columns.astype(str)
|
||||||
|
.str.strip()
|
||||||
|
.str.replace(r"[^\w]", "_", regex=True)
|
||||||
|
.str.replace("__+", "_", regex=True)
|
||||||
|
.str.strip("_")
|
||||||
|
)
|
||||||
|
|
||||||
|
df = df.dropna(how="all")
|
||||||
|
|
||||||
# If the sheet has merged cells -> forward fill Location
|
|
||||||
if "Location" in df.columns:
|
if "Location" in df.columns:
|
||||||
df["Location"] = df["Location"].ffill()
|
df["Location"] = df["Location"].ffill()
|
||||||
|
|
||||||
# REMOVE empty rows
|
errors = []
|
||||||
|
|
||||||
|
for idx, row in df.iterrows():
|
||||||
|
location = self.normalize(row.get("Location"))
|
||||||
|
mh_no = self.normalize(row.get("MH_NO"))
|
||||||
|
|
||||||
|
if not location or not mh_no:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# exists = TrenchExcavation.query.filter_by(
|
||||||
|
# subcontractor_id=subcontractor_id,
|
||||||
|
# RA_Bill_No=RA_Bill_No,
|
||||||
|
# Location=location,
|
||||||
|
# MH_NO=mh_no,
|
||||||
|
# ).first()
|
||||||
|
|
||||||
|
# if exists:
|
||||||
|
# errors.append(
|
||||||
|
# f"Model-Tr.Ex. (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}"
|
||||||
|
# )
|
||||||
|
# continue
|
||||||
|
|
||||||
|
record_data = {}
|
||||||
|
for col in df.columns:
|
||||||
|
if hasattr(TrenchExcavation, col):
|
||||||
|
val = row[col]
|
||||||
|
if pd.isna(val) or str(val).strip() in ["", "-", "—", "nan"]:
|
||||||
|
val = None
|
||||||
|
record_data[col] = val
|
||||||
|
|
||||||
|
record = TrenchExcavation(
|
||||||
|
subcontractor_id=subcontractor_id,
|
||||||
|
RA_Bill_No=RA_Bill_No,
|
||||||
|
**record_data,
|
||||||
|
)
|
||||||
|
db.session.add(record)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
raise Exception(" | ".join(errors))
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# ---------------- Manhole Excavation (Subcontractor) ----------------
|
||||||
|
def process_manhole_excavation(self, df, subcontractor_id, RA_Bill_No):
|
||||||
|
|
||||||
|
df.columns = (
|
||||||
|
df.columns.astype(str)
|
||||||
|
.str.strip()
|
||||||
|
.str.replace(r"[^\w]", "_", regex=True)
|
||||||
|
.str.replace("__+", "_", regex=True)
|
||||||
|
.str.strip("_")
|
||||||
|
)
|
||||||
|
|
||||||
df = df.dropna(how="all")
|
df = df.dropna(how="all")
|
||||||
|
|
||||||
# Identify missing location rows before insert
|
|
||||||
missing_loc = df[df["Location"].isna() | (df["Location"].astype(str).str.strip() == "")]
|
|
||||||
if not missing_loc.empty:
|
|
||||||
return False, f"Error: Some rows have empty Location. Rows: {missing_loc.index.tolist()}"
|
|
||||||
|
|
||||||
saved_count = 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
for index, row in df.iterrows():
|
|
||||||
|
|
||||||
record_data = {}
|
|
||||||
|
|
||||||
# Insert only fields that exist in model
|
|
||||||
for col in df.columns:
|
|
||||||
if hasattr(ManholeExcavation, col):
|
|
||||||
value = row[col]
|
|
||||||
|
|
||||||
# Normalize empty values
|
|
||||||
if pd.isna(value) or str(value).strip() in ["", "-", "—", "nan", "NaN"]:
|
|
||||||
value = None
|
|
||||||
|
|
||||||
record_data[col] = value
|
|
||||||
|
|
||||||
record = ManholeExcavation(
|
|
||||||
subcontractor_id=subcontractor_id,
|
|
||||||
**record_data
|
|
||||||
)
|
|
||||||
|
|
||||||
db.session.add(record)
|
|
||||||
saved_count += 1
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
return True, f"Manhole Excavation data saved successfully. Total rows: {saved_count}"
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
db.session.rollback()
|
|
||||||
return False, f"Manhole Excavation Save Failed: {e}"
|
|
||||||
|
|
||||||
|
|
||||||
# Manhole and Domestic Chamber Construction save method (ManholeDomesticChamber model)
|
|
||||||
def process_manhole_excavation(self, df, subcontractor_id):
|
|
||||||
|
|
||||||
# Clean column names (strip whitespace)
|
|
||||||
df.columns = [str(c).strip() for c in df.columns]
|
|
||||||
|
|
||||||
# If the sheet has merged cells -> forward fill Location
|
|
||||||
if "Location" in df.columns:
|
if "Location" in df.columns:
|
||||||
df["Location"] = df["Location"].ffill()
|
df["Location"] = df["Location"].ffill()
|
||||||
|
|
||||||
# REMOVE empty rows
|
errors = []
|
||||||
|
|
||||||
|
for idx, row in df.iterrows():
|
||||||
|
location = self.normalize(row.get("Location"))
|
||||||
|
mh_no = self.normalize(row.get("MH_NO"))
|
||||||
|
|
||||||
|
if not location or not mh_no:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# exists = ManholeExcavation.query.filter_by(
|
||||||
|
# subcontractor_id=subcontractor_id,
|
||||||
|
# RA_Bill_No=RA_Bill_No,
|
||||||
|
# Location=location,
|
||||||
|
# MH_NO=mh_no,
|
||||||
|
# ).first()
|
||||||
|
|
||||||
|
# if exists:
|
||||||
|
# errors.append(
|
||||||
|
# f"Model-MH Ex. (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}"
|
||||||
|
# )
|
||||||
|
# continue
|
||||||
|
|
||||||
|
record_data = {}
|
||||||
|
for col in df.columns:
|
||||||
|
if hasattr(ManholeExcavation, col):
|
||||||
|
val = row[col]
|
||||||
|
if pd.isna(val) or str(val).strip() in ["", "-", "—", "nan"]:
|
||||||
|
val = None
|
||||||
|
record_data[col] = val
|
||||||
|
|
||||||
|
record = ManholeExcavation(
|
||||||
|
subcontractor_id=subcontractor_id,
|
||||||
|
RA_Bill_No=RA_Bill_No,
|
||||||
|
**record_data,
|
||||||
|
)
|
||||||
|
db.session.add(record)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
raise Exception(" | ".join(errors))
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# ---------------- Manhole & Domestic Chamber (Subcontractor) ----------------
|
||||||
|
def process_manhole_domestic_chamber(self, df, subcontractor_id, RA_Bill_No):
|
||||||
|
|
||||||
|
df.columns = (
|
||||||
|
df.columns.astype(str)
|
||||||
|
.str.strip()
|
||||||
|
.str.replace(r"[^\w]", "_", regex=True)
|
||||||
|
.str.replace("__+", "_", regex=True)
|
||||||
|
.str.strip("_")
|
||||||
|
)
|
||||||
|
|
||||||
df = df.dropna(how="all")
|
df = df.dropna(how="all")
|
||||||
|
|
||||||
# Identify missing location rows before insert
|
if "Location" in df.columns:
|
||||||
missing_loc = df[df["Location"].isna() | (df["Location"].astype(str).str.strip() == "")]
|
df["Location"] = df["Location"].ffill()
|
||||||
if not missing_loc.empty:
|
|
||||||
return False, f"Error: Some rows have empty Location. Rows: {missing_loc.index.tolist()}"
|
|
||||||
|
|
||||||
saved_count = 0
|
errors = []
|
||||||
|
|
||||||
|
for idx, row in df.iterrows():
|
||||||
|
location = self.normalize(row.get("Location"))
|
||||||
|
mh_no = self.normalize(row.get("MH_NO"))
|
||||||
|
|
||||||
|
if not location or not mh_no:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# exists = ManholeDomesticChamber.query.filter_by(
|
||||||
|
# subcontractor_id=subcontractor_id,
|
||||||
|
# RA_Bill_No=RA_Bill_No,
|
||||||
|
# Location=location,
|
||||||
|
# MH_NO=mh_no,
|
||||||
|
# ).first()
|
||||||
|
|
||||||
|
# if exists:
|
||||||
|
# errors.append(
|
||||||
|
# f"Model-MH & DC (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}"
|
||||||
|
# )
|
||||||
|
# continue
|
||||||
|
|
||||||
|
record_data = {}
|
||||||
|
for col in df.columns:
|
||||||
|
if hasattr(ManholeDomesticChamber, col):
|
||||||
|
val = row[col]
|
||||||
|
if pd.isna(val) or str(val).strip() in ["", "-", "—", "nan"]:
|
||||||
|
val = None
|
||||||
|
record_data[col] = val
|
||||||
|
|
||||||
|
record = ManholeDomesticChamber(
|
||||||
|
subcontractor_id=subcontractor_id,
|
||||||
|
RA_Bill_No=RA_Bill_No,
|
||||||
|
**record_data,
|
||||||
|
)
|
||||||
|
db.session.add(record)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
raise Exception(" | ".join(errors))
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# ---------------- Laying (Subcontractor) ----------------
|
||||||
|
def process_laying(self, df, subcontractor_id, RA_Bill_No):
|
||||||
|
|
||||||
|
df.columns = (
|
||||||
|
df.columns.astype(str)
|
||||||
|
.str.strip()
|
||||||
|
.str.replace(r"[^\w]", "_", regex=True)
|
||||||
|
.str.replace("__+", "_", regex=True)
|
||||||
|
.str.strip("_")
|
||||||
|
)
|
||||||
|
|
||||||
|
df = df.dropna(how="all")
|
||||||
|
|
||||||
|
if "Location" in df.columns:
|
||||||
|
df["Location"] = df["Location"].ffill()
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for idx, row in df.iterrows():
|
||||||
|
location = self.normalize(row.get("Location"))
|
||||||
|
mh_no = self.normalize(row.get("MH_NO"))
|
||||||
|
|
||||||
|
if not location or not mh_no:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# exists = ManholeDomesticChamber.query.filter_by(
|
||||||
|
# subcontractor_id=subcontractor_id,
|
||||||
|
# RA_Bill_No=RA_Bill_No,
|
||||||
|
# Location=location,
|
||||||
|
# MH_NO=mh_no,
|
||||||
|
# ).first()
|
||||||
|
|
||||||
|
# if exists:
|
||||||
|
# errors.append(
|
||||||
|
# f"Model-MH & DC (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}"
|
||||||
|
# )
|
||||||
|
# continue
|
||||||
|
|
||||||
|
record_data = {}
|
||||||
|
for col in df.columns:
|
||||||
|
if hasattr(Laying, col):
|
||||||
|
val = row[col]
|
||||||
|
if pd.isna(val) or str(val).strip() in ["", "-", "—", "nan"]:
|
||||||
|
val = None
|
||||||
|
record_data[col] = val
|
||||||
|
|
||||||
|
record = Laying(
|
||||||
|
subcontractor_id=subcontractor_id,
|
||||||
|
RA_Bill_No=RA_Bill_No,
|
||||||
|
**record_data,
|
||||||
|
)
|
||||||
|
db.session.add(record)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
raise Exception(" | ".join(errors))
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- CLIENT FILE UPLOAD ----------------
|
||||||
|
def handle_client_file_upload(self, file, RA_Bill_No):
|
||||||
|
|
||||||
|
if not RA_Bill_No:
|
||||||
|
return False, "Please Enter RA Bill No."
|
||||||
|
|
||||||
|
if not file or file.filename == "":
|
||||||
|
return False, "No file selected."
|
||||||
|
|
||||||
|
if not self.allowed_file(file.filename):
|
||||||
|
return False, "Invalid file type! Allowed: CSV, XLSX, XLS"
|
||||||
|
|
||||||
|
ensure_upload_folder()
|
||||||
|
path = get_uploads_folder()
|
||||||
|
|
||||||
|
folder = os.path.join(path, f"Client_Bill_{RA_Bill_No}")
|
||||||
|
os.makedirs(folder, exist_ok=True)
|
||||||
|
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
filepath = os.path.join(folder, filename)
|
||||||
|
file.save(filepath)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for index, row in df.iterrows():
|
df_tr_ex = pd.read_excel(filepath, sheet_name="Tr.Ex.", header=4)
|
||||||
|
df_mh_ex = pd.read_excel(filepath, sheet_name="MH Ex.", header=4)
|
||||||
|
df_mh_dc = pd.read_excel(filepath, sheet_name="MH & DC", header=3)
|
||||||
|
df_lay = pd.read_excel(filepath, sheet_name="Laying & Bedding", header=3)
|
||||||
|
|
||||||
record_data = {}
|
self.save_client_data(df_tr_ex, TrenchExcavationClient, RA_Bill_No)
|
||||||
|
self.save_client_data(df_mh_ex, ManholeExcavationClient, RA_Bill_No)
|
||||||
# Insert only fields that exist in model
|
self.save_client_data(df_mh_dc, ManholeDomesticChamberClient, RA_Bill_No)
|
||||||
for col in df.columns:
|
self.save_client_data(df_lay, LayingClient, RA_Bill_No)
|
||||||
if hasattr(ManholeDomesticChamber, col):
|
|
||||||
value = row[col]
|
|
||||||
|
|
||||||
# Normalize empty values
|
|
||||||
if pd.isna(value) or str(value).strip() in ["", "-", "—", "nan", "NaN"]:
|
|
||||||
value = None
|
|
||||||
|
|
||||||
record_data[col] = value
|
|
||||||
|
|
||||||
record = ManholeDomesticChamber(
|
|
||||||
subcontractor_id=subcontractor_id,
|
|
||||||
**record_data
|
|
||||||
)
|
|
||||||
|
|
||||||
db.session.add(record)
|
|
||||||
saved_count += 1
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return True, f"Manhole Domestic Chamber Construction data saved successfully. Total rows: {saved_count}"
|
return True, "Client file uploaded successfully."
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return False, f"Manhole Domestic Chamber Construction Save Failed: {e}"
|
return False, f"Client import failed: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- CLIENT SAVE METHOD ----------------
|
||||||
|
def save_client_data(self, df, model, RA_Bill_No):
|
||||||
|
|
||||||
|
df.columns = [str(c).strip() for c in df.columns]
|
||||||
|
|
||||||
|
if "Location" in df.columns:
|
||||||
|
df["Location"] = df["Location"].ffill()
|
||||||
|
|
||||||
|
df = df.dropna(how="all")
|
||||||
|
|
||||||
|
for idx, row in df.iterrows():
|
||||||
|
record_data = {}
|
||||||
|
|
||||||
|
for col in df.columns:
|
||||||
|
if hasattr(model, col):
|
||||||
|
val = row[col]
|
||||||
|
if pd.isna(val) or str(val).strip() in ["", "-", "—", "nan"]:
|
||||||
|
val = None
|
||||||
|
record_data[col] = val
|
||||||
|
|
||||||
|
record = model(RA_Bill_No=RA_Bill_No, **record_data)
|
||||||
|
db.session.add(record)
|
||||||
|
|
||||||
|
|
||||||
79
app/services/logger_service.py
Normal file
79
app/services/logger_service.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
from flask import request, session, has_request_context
|
||||||
|
|
||||||
|
|
||||||
|
class RequestContextFilter(logging.Filter):
|
||||||
|
|
||||||
|
def filter(self, record):
|
||||||
|
|
||||||
|
if has_request_context():
|
||||||
|
record.user = session.get("user_email", "Anonymous")
|
||||||
|
record.ip = request.remote_addr
|
||||||
|
record.method = request.method
|
||||||
|
record.url = request.url
|
||||||
|
else:
|
||||||
|
record.user = "System"
|
||||||
|
record.ip = "N/A"
|
||||||
|
record.method = "N/A"
|
||||||
|
record.url = "N/A"
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class LoggerService:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def init_app(app):
|
||||||
|
|
||||||
|
if not os.path.exists("logs"):
|
||||||
|
os.makedirs("logs")
|
||||||
|
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
"%(asctime)s | %(levelname)s | "
|
||||||
|
"User:%(user)s | IP:%(ip)s | "
|
||||||
|
"Method:%(method)s | URL:%(url)s | "
|
||||||
|
"%(message)s"
|
||||||
|
)
|
||||||
|
|
||||||
|
# INFO LOG
|
||||||
|
info_handler = RotatingFileHandler(
|
||||||
|
"logs/app.log",
|
||||||
|
maxBytes=5 * 1024 * 1024,
|
||||||
|
backupCount=5
|
||||||
|
)
|
||||||
|
info_handler.setLevel(logging.INFO)
|
||||||
|
info_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
# ERROR LOG
|
||||||
|
error_handler = RotatingFileHandler(
|
||||||
|
"logs/error.log",
|
||||||
|
maxBytes=5 * 1024 * 1024,
|
||||||
|
backupCount=5
|
||||||
|
)
|
||||||
|
error_handler.setLevel(logging.ERROR)
|
||||||
|
error_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
# CONSOLE
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setLevel(logging.DEBUG)
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
# 🔹 ADD FILTER (important)
|
||||||
|
context_filter = RequestContextFilter()
|
||||||
|
|
||||||
|
info_handler.addFilter(context_filter)
|
||||||
|
error_handler.addFilter(context_filter)
|
||||||
|
console_handler.addFilter(context_filter)
|
||||||
|
|
||||||
|
app.logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
app.logger.addHandler(info_handler)
|
||||||
|
app.logger.addHandler(error_handler)
|
||||||
|
app.logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
# Log every request automatically
|
||||||
|
@app.before_request
|
||||||
|
def log_request():
|
||||||
|
app.logger.info("Request Started")
|
||||||
@@ -1,47 +1,27 @@
|
|||||||
# from app.models.user_model import User
|
|
||||||
# from app.services.db_service import db
|
|
||||||
# import logging
|
|
||||||
|
|
||||||
# class UserService:
|
|
||||||
|
|
||||||
# @staticmethod
|
|
||||||
# def register_user(name, email, password):
|
|
||||||
# user = User(name=name, email=email)
|
|
||||||
# user.set_password(password)
|
|
||||||
# db.session.add(user)
|
|
||||||
# db.session.commit()
|
|
||||||
# logging.info(f"New user registered: {email}")
|
|
||||||
# return user
|
|
||||||
|
|
||||||
# @staticmethod
|
|
||||||
# def validate_login(email, password):
|
|
||||||
# user = User.query.filter_by(email=email).first()
|
|
||||||
# if user and user.check_password(password):
|
|
||||||
# logging.info(f"Login success: {email}")
|
|
||||||
# return user
|
|
||||||
# logging.warning(f"Login failed for: {email}")
|
|
||||||
# return None
|
|
||||||
|
|
||||||
# @staticmethod
|
|
||||||
# def get_all_users():
|
|
||||||
# return User.query.all()
|
|
||||||
|
|
||||||
|
|
||||||
from app.services.db_service import DBService
|
|
||||||
from app.models.user_model import User
|
from app.models.user_model import User
|
||||||
|
from app.services.db_service import db
|
||||||
|
|
||||||
class UserService:
|
class UserService:
|
||||||
|
|
||||||
def get_all_users(self):
|
@staticmethod
|
||||||
db = DBService().connect()
|
def register_user(name, email, password):
|
||||||
cursor = db.cursor(dictionary=True)
|
if User.query.filter_by(email=email).first():
|
||||||
|
return None
|
||||||
|
|
||||||
cursor.execute("SELECT id, name, email FROM users")
|
user = User(name=name, email=email)
|
||||||
rows = cursor.fetchall()
|
user.set_password(password)
|
||||||
|
|
||||||
users = [User(**row) for row in rows]
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
return user
|
||||||
|
|
||||||
cursor.close()
|
@staticmethod
|
||||||
db.close()
|
def validate_login(email, password):
|
||||||
|
user = User.query.filter_by(email=email).first()
|
||||||
|
if user and user.check_password(password):
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
return users
|
@staticmethod
|
||||||
|
def get_all_users():
|
||||||
|
return User.query.all()
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
body {
|
|
||||||
background: #f5f7fa;
|
|
||||||
font-family: Arial;
|
|
||||||
padding: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
width: 420px;
|
|
||||||
padding: 20px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
margin-top: 10px;
|
|
||||||
display: block;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: #0055ff;
|
|
||||||
color: #fff;
|
|
||||||
border: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
BIN
app/static/downloads/format/client_format.xlsx
Normal file
BIN
app/static/downloads/format/client_format.xlsx
Normal file
Binary file not shown.
BIN
app/static/downloads/format/subcontractor_format.xlsx
Normal file
BIN
app/static/downloads/format/subcontractor_format.xlsx
Normal file
Binary file not shown.
BIN
app/static/images/lcepl.png
Normal file
BIN
app/static/images/lcepl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -3,62 +3,211 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{{ title if title else "ComparisonSoftware" }}</title>
|
<title>{{ title if title else "Comparison Software" }}</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
|
||||||
|
<!-- Bootstrap Icons -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body style="background:#f5f7fa;">
|
<body class="bg-light">
|
||||||
|
|
||||||
|
<!-- NAVBAR -->
|
||||||
|
<!-- <nav class="navbar navbar-expand-lg navbar-dark bg-dark shadow-sm"> -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark shadow-sm fixed-top">
|
||||||
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="/">LCEPL</a>
|
|
||||||
|
|
||||||
|
<!-- Brand -->
|
||||||
|
<a class="navbar-brand fw-bold" href="/">
|
||||||
|
<i class="bi bi-buildings me-1"></i> LCEPL
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Mobile toggle -->
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMenu">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMenu">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Menu -->
|
||||||
<div class="collapse navbar-collapse" id="navMenu">
|
<div class="collapse navbar-collapse" id="navMenu">
|
||||||
<ul class="navbar-nav ms-auto">
|
<ul class="navbar-nav ms-auto align-items-lg-center">
|
||||||
|
|
||||||
<li class="nav-item"><a class="nav-link" href="/dashboard">Dashboard</a></li>
|
<!-- Dashboard -->
|
||||||
<li class="nav-item"><a class="nav-link" href="/file/import">File Import</a></li>
|
<li class="nav-item">
|
||||||
<!-- <li class="nav-item"><a class="nav-link" href="/user/list">Users</a></li> -->
|
<a class="nav-link" href="/dashboard">
|
||||||
<!-- <li class="nav-item"><a class="nav-link" href="/subcontractor/add">Subcontractor</a></li> -->
|
<i class="bi bi-speedometer2 me-1"></i> Dashboard
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Subcontractor Model -->
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">Subcontractor</a>
|
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
|
||||||
|
<i class="bi bi-people-fill me-1"></i> Subcontractor Model
|
||||||
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-dark">
|
<ul class="dropdown-menu dropdown-menu-dark">
|
||||||
<li><a class="dropdown-item" href="/subcontractor/add">Add Subcontractor</a></li>
|
<li>
|
||||||
<li><a class="dropdown-item" href="/subcontractor/list">Subcontractor List</a></li>
|
<a class="dropdown-item" href="/subcontractor/add">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i> Add Subcontractor
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="/subcontractor/list">
|
||||||
|
<i class="bi bi-list-ul me-2"></i> Subcontractor List
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/user/list">Users</a></li>
|
|
||||||
|
<!-- Subcontractor File System -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
|
||||||
|
<i class="bi bi-folder-fill me-1"></i>Subcontractor File System
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-dark">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="/file/import_Subcontractor">
|
||||||
|
<i class="bi bi-upload me-2"></i> Import File
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="/file/Subcontractor_report">
|
||||||
|
<i class="bi bi-download me-2"></i> Show Reports
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="/dashboard/subcontractor_dashboard">
|
||||||
|
<i class="bi bi-speedometer2 me-2"></i> Subcontractor Dashboard
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Client System -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
|
||||||
|
<i class="bi bi-building me-1"></i> Client File System
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-dark">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="/file/import_client">
|
||||||
|
<i class="bi bi-upload me-2"></i> Import Client File
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="/file/client_report">
|
||||||
|
<i class="bi bi-arrow-left-right me-2"></i> Show Report
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Reports -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
|
||||||
|
<i class="bi bi-building me-1"></i> Reports
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-dark">
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="/report/comparison_report">
|
||||||
|
<i class="bi bi-arrow-left-right me-2"></i> client vs sub-cont. Comparison Report
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- <li>
|
||||||
|
<a class="dropdown-item" href="/file/client_vs_subcont">
|
||||||
|
<i class="bi bi-arrow-left-right me-2"></i> Comparison Report
|
||||||
|
</a>
|
||||||
|
</li> -->
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Formats -->
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/file_format">
|
||||||
|
<i class="bi bi-file-earmark-text me-1"></i> Formats
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- USER DROPDOWN -->
|
||||||
|
{% if session.get("user_id") %}
|
||||||
|
<li class="nav-item dropdown ms-lg-3">
|
||||||
|
|
||||||
|
<a class="nav-link dropdown-toggle d-flex align-items-center gap-2" href="#"
|
||||||
|
data-bs-toggle="dropdown">
|
||||||
|
<i class="bi bi-person-circle fs-5"></i>
|
||||||
|
<span class="d-none d-lg-inline">
|
||||||
|
{{ session.get("user_name") }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark shadow">
|
||||||
|
|
||||||
|
<!-- User card -->
|
||||||
|
<li class="px-3 py-3 text-center border-bottom">
|
||||||
|
<i class="bi bi-person-circle fs-1"></i>
|
||||||
|
<div class="fw-semibold mt-1">
|
||||||
|
{{ session.get("user_name") }}
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">Logged in user</small>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="/dashboard">
|
||||||
|
<i class="bi bi-speedometer2 me-2"></i> Dashboard
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item text-warning" href="/logout">
|
||||||
|
<i class="bi bi-box-arrow-right me-2"></i> Logout
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="container mt-3">
|
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
<!-- PAGE CONTENT -->
|
||||||
{% for category, message in messages %}
|
<div class="container-fluid vh-100 pt-5 overflow-hidden">
|
||||||
<div class="alert alert-{{ category }} alert-dismissible fade show">
|
<!-- FLASH MESSAGES -->
|
||||||
{{ message }}
|
<div class="container mt-3">
|
||||||
<button class="btn-close" data-bs-dismiss="alert"></button>
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }} alert-dismissible fade show">
|
||||||
|
{{ message }}
|
||||||
|
<button class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
|
||||||
{% block content %}{% endblock %}
|
<div class="overflow-auto h-100">
|
||||||
|
<div class="container mt-4">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
75
app/templates/client_report.html
Normal file
75
app/templates/client_report.html
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid mt-4">
|
||||||
|
<h2 class="mb-4">Client File Reports</h2>
|
||||||
|
|
||||||
|
<div class="card p-4 shadow-sm mb-5">
|
||||||
|
<form method="POST">
|
||||||
|
<label class="form-label fw-bold">RA Bill No</label>
|
||||||
|
<input type="text" name="RA_Bill_No" class="form-control mb-3" value="{{ ra_val }}" required>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<button type="submit" name="action" value="preview" class="btn btn-secondary w-100">Preview
|
||||||
|
Data</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<button type="submit" name="action" value="download" class="btn btn-primary w-100">Download Excel
|
||||||
|
Report</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% 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>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs" id="reportTabs" role="tablist">
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link active" id="tr-tab" data-bs-toggle="tab" data-bs-target="#tr"
|
||||||
|
type="button">Tr.Ex Comparison</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link" id="mh-tab" data-bs-toggle="tab" data-bs-target="#mh" type="button">Mh.Ex
|
||||||
|
Comparison</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<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">
|
||||||
|
<div class="tab-pane fade show active" id="tr" role="tabpanel">
|
||||||
|
<div class="table-responsive" style="max-height: 500px;">
|
||||||
|
{{ tables.tr|safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="mh" role="tabpanel">
|
||||||
|
<div class="table-responsive" style="max-height: 500px;">
|
||||||
|
{{ tables.mh|safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="dc" role="tabpanel">
|
||||||
|
<div class="table-responsive" style="max-height: 500px;">
|
||||||
|
{{ 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 %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,10 +1,118 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2 class="mb-4">Dashboard</h2>
|
<div class="container-fluid px-2 px-md-4">
|
||||||
|
<h4 class="mb-3 text-center text-md-start">Comparison Software Solapur (UGD) - Live Dashboard</h4>
|
||||||
|
|
||||||
<div class="card p-4 shadow-sm">
|
<div class="row g-3 mb-4">
|
||||||
<h5>Welcome to Comparison Project</h5>
|
<div class="col-12 col-md-4">
|
||||||
<p>This is dashboard panel.</p>
|
<div class="card text-white bg-primary shadow h-100">
|
||||||
|
<div class="card-body text-center text-md-start">
|
||||||
|
<h6>Trenching Units</h6>
|
||||||
|
<h3 class="fw-bold" id="card-trench">0</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<div class="card text-white bg-success shadow h-100">
|
||||||
|
<div class="card-body text-center text-md-start">
|
||||||
|
<h6>Manhole Units</h6>
|
||||||
|
<h3 class="fw-bold" id="card-manhole">0</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<div class="card text-dark bg-warning shadow h-100">
|
||||||
|
<div class="card-body text-center text-md-start">
|
||||||
|
<h6>Laying Units</h6>
|
||||||
|
<h3 class="fw-bold" id="card-laying">0</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header bg-dark text-white">Live Category Bar Chart</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="liveBarChart" style="max-height:300px;"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header bg-dark text-white">Location Distribution Pie Chart</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="livePieChart" style="max-height:300px;"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 2. Initialize the Bar Chart
|
||||||
|
const barCtx = document.getElementById('liveBarChart').getContext('2d');
|
||||||
|
let liveBarChart = new Chart(barCtx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['Trenching', 'Manholes', 'Laying'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Units Completed',
|
||||||
|
data: [0, 0, 0],
|
||||||
|
backgroundColor: ['#0d6efd', '#198754', '#ffc107']
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: { responsive: true, maintainAspectRatio: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Initialize the Pie Chart
|
||||||
|
const pieCtx = document.getElementById('livePieChart').getContext('2d');
|
||||||
|
let livePieChart = new Chart(pieCtx, {
|
||||||
|
type: 'pie',
|
||||||
|
data: {
|
||||||
|
labels: [], // Will be filled from SQL
|
||||||
|
datasets: [{
|
||||||
|
data: [],
|
||||||
|
backgroundColor: ['#0d6efd', '#198754', '#ffc107', '#6f42c1', '#fd7e14']
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: { responsive: true, maintainAspectRatio: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Function to Fetch Live Data from your Python API
|
||||||
|
function fetchLiveData() {
|
||||||
|
fetch('/dashboard/api/live-stats') // This matches the route we created in the "Kitchen"
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Update the Summary Cards
|
||||||
|
document.getElementById('card-trench').innerText = data.summary.trench;
|
||||||
|
document.getElementById('card-manhole').innerText = data.summary.manhole;
|
||||||
|
document.getElementById('card-laying').innerText = data.summary.laying;
|
||||||
|
|
||||||
|
// Update Bar Chart
|
||||||
|
liveBarChart.data.datasets[0].data = [
|
||||||
|
data.summary.trench,
|
||||||
|
data.summary.manhole,
|
||||||
|
data.summary.laying
|
||||||
|
];
|
||||||
|
liveBarChart.update();
|
||||||
|
|
||||||
|
// Update Pie Chart (Location stats)
|
||||||
|
livePieChart.data.labels = Object.keys(data.locations);
|
||||||
|
livePieChart.data.datasets[0].data = Object.values(data.locations);
|
||||||
|
livePieChart.update();
|
||||||
|
})
|
||||||
|
.catch(err => console.error("Error fetching live data:", err));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Check for updates every 10 seconds (Real-time effect)
|
||||||
|
setInterval(fetchLiveData, 10000);
|
||||||
|
fetchLiveData(); // Load immediately on page open
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
66
app/templates/file_format.html
Normal file
66
app/templates/file_format.html
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2 class="mb-4">Download File Formats</h2>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
|
||||||
|
<!-- Subcontractor Format -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-center shadow-sm h-100">
|
||||||
|
<div class="card-body d-flex flex-column justify-content-center">
|
||||||
|
<div class="mb-3 fs-1 text-primary">
|
||||||
|
⬇️
|
||||||
|
</div>
|
||||||
|
<h5 class="card-title">Subcontractor Upload Format</h5>
|
||||||
|
<p class="text-muted mb-2">Excel (.xlsx)</p>
|
||||||
|
<p class="small text-secondary">File size: 245 KB</p>
|
||||||
|
|
||||||
|
<a href="{{ url_for('file_format.download_excel_format', filename='subcontractor_format.xlsx') }}"
|
||||||
|
class="btn btn-outline-primary mt-auto">
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Client Format -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-center shadow-sm h-100">
|
||||||
|
<div class="card-body d-flex flex-column justify-content-center">
|
||||||
|
<div class="mb-3 fs-1 text-success">
|
||||||
|
⬇️
|
||||||
|
</div>
|
||||||
|
<h5 class="card-title">Client Upload Format</h5>
|
||||||
|
<p class="text-muted mb-2">Excel (.xlsx)</p>
|
||||||
|
<p class="small text-secondary">File size: 310 KB</p>
|
||||||
|
|
||||||
|
<a href="{{ url_for('file_format.download_excel_format', filename='client_format.xlsx') }}"
|
||||||
|
class="btn btn-outline-success mt-auto">
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MH & DC Format -->
|
||||||
|
<!-- <div class="col-md-4">
|
||||||
|
<div class="card text-center shadow-sm h-100">
|
||||||
|
<div class="card-body d-flex flex-column justify-content-center">
|
||||||
|
<div class="mb-3 fs-1 text-warning">
|
||||||
|
⬇️
|
||||||
|
</div>
|
||||||
|
<h5 class="card-title">Manhole & DC Format</h5>
|
||||||
|
<p class="text-muted mb-2">Excel (.xlsx)</p>
|
||||||
|
<p class="small text-secondary">File size: 190 KB</p>
|
||||||
|
|
||||||
|
<a href="{{ url_for('file_format.download_excel_format', filename='mh_dc_format.xlsx') }}"
|
||||||
|
class="btn btn-outline-warning mt-auto">
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,33 +1,35 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2 class="mb-4">File Import</h2>
|
<h2 class="mb-4">Client File Import</h2>
|
||||||
|
|
||||||
<div class="card p-4 shadow-sm">
|
<div class="card p-4 shadow-sm">
|
||||||
|
|
||||||
<form method="POST" enctype="multipart/form-data">
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
|
||||||
<!-- 1. SELECT SUBCONTRACTOR -->
|
<!-- 1. SELECT SUBCONTRACTOR -->
|
||||||
<label class="form-label">Select Subcontractor</label>
|
<!-- <label class="form-label">Select Subcontractor vs Client</label>
|
||||||
<select name="subcontractor_id" id="subcontractor_id" class="form-select mb-3" required>
|
<select name="subcontractor_id" id="subcontractor_id" class="form-select mb-3" required>
|
||||||
<option value="">-- Select Subcontractor --</option>
|
<option value="">-- Select Subcontractor --</option>
|
||||||
|
|
||||||
{% for sc in subcontractors %}
|
{% for sc in subcontractors %}
|
||||||
<option value="{{ sc.id }}">{{ sc.subcontractor_name }}</option>
|
<option value="{{ sc.id }}">{{ sc.subcontractor_name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select> -->
|
||||||
|
|
||||||
<!-- 2. FILE TYPE (MODEL NAME) -->
|
<!-- 2. FILE TYPE (MODEL NAME) -->
|
||||||
<label class="form-label">Select File Type</label>
|
<!-- <label class="form-label">Select File Type</label>
|
||||||
<select name="file_type" id="file_type" class="form-select mb-3" required>
|
<select name="file_type" id="file_type" class="form-select mb-3" required>
|
||||||
<option value="">-- Select File Type --</option>
|
<option value="">-- Select File Type --</option>
|
||||||
<option value="">Subcontractor Sheet</option>
|
<option value="">Sheet</option>
|
||||||
<option value="trench_excavation">Trench Excavation (Tr.Ex)</option>
|
<option value="tr_ex_client">Tr. Ex</option>
|
||||||
<option value="manhole_excavation">Manhole Excavation (Mh.Ex)</option>
|
<option value="mh_ex_client">Mh. Ex </option>
|
||||||
<option value="manhole_domestic_chamber">Manhole & Domestic Chambers Construction (MH & DC)</option>
|
<option value="mh_dc_client">MH & DC </option>
|
||||||
<option value="">Laying Sheet</option>
|
<option value="">Laying Sheet</option>
|
||||||
</select>
|
</select> -->
|
||||||
|
|
||||||
|
<label class="form-label">RA Bill No</label>
|
||||||
|
<input type="text" name="RA_Bill_No" class="form-control mb-3" required>
|
||||||
<!-- 3. FILE UPLOAD -->
|
<!-- 3. FILE UPLOAD -->
|
||||||
<label class="form-label">Choose File</label>
|
<label class="form-label">Choose File</label>
|
||||||
<input type="file" name="file" class="form-control mb-3" required>
|
<input type="file" name="file" class="form-control mb-3" required>
|
||||||
32
app/templates/file_import_subcontractor.html
Normal file
32
app/templates/file_import_subcontractor.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2 class="mb-4">Sub-Contractor File Import</h2>
|
||||||
|
|
||||||
|
<div class="card p-4 shadow-sm">
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
|
||||||
|
<!-- 1. SELECT SUBCONTRACTOR -->
|
||||||
|
<label class="form-label">Select Subcontractor</label>
|
||||||
|
<select name="subcontractor_id" id="subcontractor_id" class="form-select mb-3" required>
|
||||||
|
<option value="">-- Select Subcontractor --</option>
|
||||||
|
|
||||||
|
{% for sc in subcontractors %}
|
||||||
|
<option value="{{ sc.id }}">{{ sc.subcontractor_name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 2. RA bill no -->
|
||||||
|
<label class="form-label">RA Bill No</label>
|
||||||
|
<input type="text" name="RA_Bill_No" class="form-control mb-3" required>
|
||||||
|
|
||||||
|
<!-- 3. FILE UPLOAD -->
|
||||||
|
<label class="form-label">Choose File</label>
|
||||||
|
<input type="file" name="file" class="form-control mb-3" required>
|
||||||
|
|
||||||
|
<button class="btn btn-primary w-100">Upload</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
39
app/templates/generate_comparison_report.html
Normal file
39
app/templates/generate_comparison_report.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
|
||||||
|
<h2 class="mb-4">Subcontractor vs Client Comparison</h2>
|
||||||
|
|
||||||
|
<!-- FLASH MESSAGES -->
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="card p-4 shadow-sm">
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
|
||||||
|
<!-- SELECT SUBCONTRACTOR -->
|
||||||
|
<label class="form-label fw-semibold">Select Subcontractor</label>
|
||||||
|
<select name="subcontractor_id" id="subcontractor_id" class="form-select mb-3" required>
|
||||||
|
<option value="">-- Select Subcontractor --</option>
|
||||||
|
|
||||||
|
{% for sc in subcontractors %}
|
||||||
|
<option value="{{ sc.id }}">{{ sc.subcontractor_name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button class="btn btn-primary w-100">Generate Report</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,17 +1,82 @@
|
|||||||
{% extends "base.html" %}
|
<!DOCTYPE html>
|
||||||
{% block content %}
|
<html lang="en">
|
||||||
<h2 class="mb-4">Login</h2>
|
|
||||||
|
|
||||||
<div class="card p-4 shadow-sm">
|
<head>
|
||||||
<form method="POST">
|
<meta charset="UTF-8">
|
||||||
|
<title>LCEPL | Login</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<label>Email:</label>
|
<!-- Bootstrap CSS -->
|
||||||
<input type="email" name="email" class="form-control mb-3" required>
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
|
||||||
<label>Password:</label>
|
<body class="bg-light">
|
||||||
<input type="password" name="password" class="form-control mb-3" required>
|
|
||||||
|
|
||||||
<button class="btn btn-success">Login</button>
|
<div class="container-fluid vh-100">
|
||||||
</form>
|
<div class="row h-100 justify-content-center align-items-center">
|
||||||
</div>
|
|
||||||
{% endblock %}
|
<!-- Increased column width -->
|
||||||
|
<div class="col-12 col-sm-10 col-md-8 col-lg-5 col-xl-4">
|
||||||
|
|
||||||
|
<div class="card shadow-lg border-0">
|
||||||
|
<!-- Increased padding -->
|
||||||
|
<div class="card-body p-5">
|
||||||
|
|
||||||
|
<!-- Branding -->
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<img src="{{ url_for('static', filename='images/lcepl.png') }}" alt="LCEPL Logo"
|
||||||
|
class="img-fluid mb-3" style="max-height:80px;">
|
||||||
|
|
||||||
|
<h4 class="fw-bold mb-1">
|
||||||
|
Laxmi Civil Engineering Services Pvt Ltd
|
||||||
|
</h4>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Data Comparison Software Solapur(UGD)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Flash messages -->
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<!-- Login Form -->
|
||||||
|
<form method="POST">
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-semibold">User Name</label>
|
||||||
|
<input type="email" name="email" class="form-control " placeholder="Enter email"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-semibold">Password</label>
|
||||||
|
<input type="password" name="password" class="form-control" placeholder="Enter password"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-success btn-lg w-100">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bootstrap JS -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2 class="mb-4">User Registration</h2>
|
<h2>Register User</h2>
|
||||||
|
|
||||||
<div class="card p-4 shadow-sm">
|
<div class="card p-4 shadow-sm">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
|
<label>Name</label>
|
||||||
|
<input class="form-control mb-3" name="name" required>
|
||||||
|
|
||||||
<label>Name:</label>
|
<label>Email</label>
|
||||||
<input type="text" name="name" class="form-control mb-3" required>
|
<input type="email" class="form-control mb-3" name="email" required>
|
||||||
|
|
||||||
<label>Email:</label>
|
<label>Password</label>
|
||||||
<input type="email" name="email" class="form-control mb-3" required>
|
<input type="password" class="form-control mb-3" name="password" required>
|
||||||
|
|
||||||
<label>Password:</label>
|
|
||||||
<input type="password" name="password" class="form-control mb-3" required>
|
|
||||||
|
|
||||||
<button class="btn btn-primary">Register</button>
|
<button class="btn btn-primary">Register</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -2,39 +2,49 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="card shadow-sm p-4">
|
<div class="card shadow-sm p-4">
|
||||||
|
|
||||||
<h4 class="mb-3">Add New Subcontractor</h4>
|
<h4 class="mb-3">Add New Subcontractor</h4>
|
||||||
|
|
||||||
<form action="/subcontractor/save" method="POST">
|
<form action="{{ url_for('subcontractor.save_subcontractor') }}" method="POST">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Subcontractor Name</label>
|
<label class="form-label">Subcontractor Name:</label>
|
||||||
<input type="text" class="form-control" name="subcontractor_name" required>
|
<input type="text" class="form-control" name="subcontractor_name" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Contact Person</label>
|
<label class="form-label">Contact Person Name:</label>
|
||||||
<input type="text" class="form-control" name="contact_person">
|
<input type="text" class="form-control" name="contact_person">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Mobile</label>
|
<label class="form-label">Address:</label>
|
||||||
|
<textarea type="text" class="form-control" name="address"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Mobile No:</label>
|
||||||
<input type="text" class="form-control" name="mobile_no">
|
<input type="text" class="form-control" name="mobile_no">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Email</label>
|
<label class="form-label">Email:</label>
|
||||||
<input type="email" class="form-control" name="email_id">
|
<input type="email" class="form-control" name="email_id">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">GST No</label>
|
<label class="form-label">GST No:</label>
|
||||||
<input type="text" class="form-control" name="gst_no">
|
<input type="text" class="form-control" name="gst_no">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-success">Save</button>
|
<div class="mb-3">
|
||||||
</form>
|
<label class="form-label">PAN No:</label>
|
||||||
|
<input type="text" class="form-control" name="pan_no">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-success">Save</button>
|
||||||
|
<a href="{{ url_for('subcontractor.subcontractor_list') }}" class="btn btn-secondary">Back</a>
|
||||||
|
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -4,36 +4,58 @@
|
|||||||
<div class="card shadow-sm p-4">
|
<div class="card shadow-sm p-4">
|
||||||
<h4 class="mb-3">Edit Subcontractor</h4>
|
<h4 class="mb-3">Edit Subcontractor</h4>
|
||||||
|
|
||||||
<form action="/subcontractor/update/{{ subcontractor.id }}" method="POST">
|
<form action="{{ url_for('subcontractor.update_subcontractor', id=subcontractor.id) }}" method="POST">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Subcontractor Name</label>
|
<label class="form-label">Subcontractor Name:</label>
|
||||||
<input type="text" class="form-control" name="subcontractor_name"
|
<input type="text" class="form-control" name="subcontractor_name"
|
||||||
value="{{ subcontractor.subcontractor_name }}" required>
|
value="{{ subcontractor.subcontractor_name }}" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Contact Person</label>
|
<label class="form-label">Contact Person Name:</label>
|
||||||
<input type="text" class="form-control" name="contact_person" value="{{ subcontractor.contact_person }}">
|
<input type="text" class="form-control" name="contact_person" value="{{ subcontractor.contact_person }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Mobile</label>
|
<label class="form-label">Address:</label>
|
||||||
|
<input type="text" class="form-control" name="address" value="{{ subcontractor.address }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Mobile No:</label>
|
||||||
<input type="text" class="form-control" name="mobile_no" value="{{ subcontractor.mobile_no }}">
|
<input type="text" class="form-control" name="mobile_no" value="{{ subcontractor.mobile_no }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Email</label>
|
<label class="form-label">Email:</label>
|
||||||
<input type="email" class="form-control" name="email_id" value="{{ subcontractor.email_id }}">
|
<input type="email" class="form-control" name="email_id" value="{{ subcontractor.email_id }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">GST No</label>
|
<label class="form-label">GST No:</label>
|
||||||
<input type="text" class="form-control" name="gst_no" value="{{ subcontractor.gst_no }}">
|
<input type="text" class="form-control" name="gst_no" value="{{ subcontractor.gst_no }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">PAN No:</label>
|
||||||
|
<input type="text" class="form-control" name="pan_no" value="{{ subcontractor.pan_no }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Status:</label>
|
||||||
|
<select name="status" class="form-control">
|
||||||
|
<option value="Active" {% if subcontractor.status=="Active" %}selected{% endif %}>
|
||||||
|
Active
|
||||||
|
</option>
|
||||||
|
<option value="Inactive" {% if subcontractor.status=="Inactive" %}selected{% endif %}>
|
||||||
|
Inactive
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-success">Update</button>
|
<button class="btn btn-success">Update</button>
|
||||||
<a href="/subcontractor/list" class="btn btn-secondary">Back</a>
|
<a href="{{ url_for('subcontractor.subcontractor_list') }}" class="btn btn-secondary">Back</a>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,38 +1,162 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="card shadow-sm p-4">
|
<div class="container mt-4">
|
||||||
<h4 class="mb-3">Subcontractor List</h4>
|
|
||||||
|
|
||||||
<table class="table table-bordered table-striped">
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-3">
|
||||||
<thead class="table-dark">
|
<h4 class="mb-3 mb-md-0">Subcontractor List</h4>
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
<a href="{{ url_for('subcontractor.add_subcontractor') }}" class="btn btn-primary btn-sm">
|
||||||
<th>Name</th>
|
+ Add Subcontractor
|
||||||
<th>Mobile</th>
|
</a>
|
||||||
<th>Email</th>
|
</div>
|
||||||
<th>GST No</th>
|
|
||||||
<th>Action</th>
|
<div class="card shadow-sm">
|
||||||
</tr>
|
<div class="card-body p-2 p-md-3">
|
||||||
</thead>
|
|
||||||
|
<!-- Desktop Table View -->
|
||||||
|
<div class="table-responsive d-none d-md-block">
|
||||||
|
<table class="table table-bordered table-hover align-middle">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Sr. No</th>
|
||||||
|
<th>Subcontractor Name</th>
|
||||||
|
<th>GST No</th>
|
||||||
|
<th>Mobile</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th width="150">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for s in subcontractors %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ s.id }}</td>
|
||||||
|
<td>{{ s.subcontractor_name }}</td>
|
||||||
|
<td>{{ s.gst_no }}</td>
|
||||||
|
<td>{{ s.mobile_no }}</td>
|
||||||
|
<td>{{ s.email_id }}</td>
|
||||||
|
<td>
|
||||||
|
{% if s.status == "Active" %}
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url_for('subcontractor.edit_subcontractor', id=s.id) }}"
|
||||||
|
class="btn btn-sm btn-warning mb-1">
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ url_for('subcontractor.delete_subcontractor', id=s.id) }}"
|
||||||
|
class="btn btn-sm btn-danger" onclick="return confirm('Are you sure to delete?')">
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Card View -->
|
||||||
|
<div class="d-md-none">
|
||||||
|
{% for s in subcontractors %}
|
||||||
|
<div class="card mb-3 shadow-sm">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<h5 class="fw-bold mb-2">{{ s.subcontractor_name }}</h5>
|
||||||
|
|
||||||
|
<p class="mb-1"><strong>GST No:</strong> {{ s.gst_no }}</p>
|
||||||
|
<p class="mb-1"><strong>Mobile:</strong> {{ s.mobile_no }}</p>
|
||||||
|
<p class="mb-1"><strong>Email:</strong> {{ s.email_id }}</p>
|
||||||
|
|
||||||
|
<p class="mb-2">
|
||||||
|
<strong>Status:</strong>
|
||||||
|
{% if s.status == "Active" %}
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{{ url_for('subcontractor.edit_subcontractor', id=s.id) }}"
|
||||||
|
class="btn btn-sm btn-warning w-50">
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ url_for('subcontractor.delete_subcontractor', id=s.id) }}"
|
||||||
|
class="btn btn-sm btn-danger w-50" onclick="return confirm('Are you sure to delete?')">
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<nav>
|
||||||
|
<ul class="pagination justify-content-center flex-wrap mt-4">
|
||||||
|
|
||||||
|
{% if pagination.has_prev %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{{ url_for('subcontractor.subcontractor_list', page=1) }}">
|
||||||
|
First
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link"
|
||||||
|
href="{{ url_for('subcontractor.subcontractor_list', page=pagination.prev_num) }}">
|
||||||
|
Prev
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for page_num in pagination.iter_pages() %}
|
||||||
|
{% if page_num %}
|
||||||
|
{% if page_num != pagination.page %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{{ url_for('subcontractor.subcontractor_list', page=page_num) }}">
|
||||||
|
{{ page_num }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">{{ page_num }}</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link">...</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if pagination.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link"
|
||||||
|
href="{{ url_for('subcontractor.subcontractor_list', page=pagination.next_num) }}">
|
||||||
|
Next
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link"
|
||||||
|
href="{{ url_for('subcontractor.subcontractor_list', page=pagination.pages) }}">
|
||||||
|
Last
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{% for s in subcontractors %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ s.id }}</td>
|
|
||||||
<td>{{ s.subcontractor_name }}</td>
|
|
||||||
<td>{{ s.mobile_no }}</td>
|
|
||||||
<td>{{ s.email_id }}</td>
|
|
||||||
<td>{{ s.gst_no }}</td>
|
|
||||||
<td>
|
|
||||||
<a href="/subcontractor/edit/{{ s.id }}" class="btn btn-sm btn-warning">Edit</a>
|
|
||||||
<a href="/subcontractor/delete/{{ s.id }}" class="btn btn-sm btn-danger"
|
|
||||||
onclick="return confirm('Are you sure?')">Delete</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
118
app/templates/subcontractor_dashboard.html
Normal file
118
app/templates/subcontractor_dashboard.html
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="container mt-3">
|
||||||
|
|
||||||
|
<h4>RA Bill Dashboard</h4>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
|
||||||
|
<!-- Contractor -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<select id="subcontractor" class="form-control">
|
||||||
|
<option value="">Select Contractor</option>
|
||||||
|
{% for s in subcontractors %}
|
||||||
|
<option value="{{s.id}}">{{s.subcontractor_name}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Category -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<select id="category" class="form-control">
|
||||||
|
<option value="">Select Category</option>
|
||||||
|
<option value="trench_excavation">Trench Excavation</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RA Bill -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<select id="ra_bill" class="form-control">
|
||||||
|
<option value="">RA Bill</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="comparisonChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
let chart;
|
||||||
|
|
||||||
|
// ✅ Load RA Bills
|
||||||
|
function loadRABills() {
|
||||||
|
|
||||||
|
let subcontractor = document.getElementById("subcontractor").value
|
||||||
|
let category = document.getElementById("category").value
|
||||||
|
|
||||||
|
if (!subcontractor || !category) return
|
||||||
|
|
||||||
|
fetch(`/dashboard/api/get-ra-bills?subcontractor=${subcontractor}&category=${category}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
|
||||||
|
let ra = document.getElementById("ra_bill")
|
||||||
|
ra.innerHTML = '<option value="">RA Bill</option>'
|
||||||
|
|
||||||
|
data.ra_bills.forEach(bill => {
|
||||||
|
ra.innerHTML += `<option value="${bill}">${bill}</option>`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Load Chart
|
||||||
|
function loadChart() {
|
||||||
|
|
||||||
|
let subcontractor = document.getElementById("subcontractor").value
|
||||||
|
let ra_bill = document.getElementById("ra_bill").value
|
||||||
|
|
||||||
|
fetch(`/dashboard/api/trench-analysis?subcontractor=${subcontractor}&ra_bill=${ra_bill}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
|
||||||
|
if (chart) chart.destroy()
|
||||||
|
|
||||||
|
chart = new Chart(document.getElementById("comparisonChart"), {
|
||||||
|
type: "bar",
|
||||||
|
data: {
|
||||||
|
labels: data.labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "Depth",
|
||||||
|
data: data.depth,
|
||||||
|
backgroundColor: "green"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Excavation Qty (cum)",
|
||||||
|
data: data.qty,
|
||||||
|
backgroundColor: "blue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
document.getElementById("subcontractor").addEventListener("change", () => {
|
||||||
|
loadRABills()
|
||||||
|
})
|
||||||
|
|
||||||
|
document.getElementById("category").addEventListener("change", () => {
|
||||||
|
loadRABills()
|
||||||
|
})
|
||||||
|
|
||||||
|
document.getElementById("ra_bill").addEventListener("change", loadChart)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
142
app/templates/subcontractor_report.html
Normal file
142
app/templates/subcontractor_report.html
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-4">
|
||||||
|
|
||||||
|
<h2 class="text-center mb-4">Generate Subcontractor Report</h2>
|
||||||
|
|
||||||
|
<!-- FORM -->
|
||||||
|
<div class="card shadow-sm p-3 p-md-4 mx-auto" style="max-width:600px;">
|
||||||
|
<form method="POST">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Select Subcontractor</label>
|
||||||
|
<select name="subcontractor_id" class="form-select" required>
|
||||||
|
<option value="">-- Select Subcontractor --</option>
|
||||||
|
{% for sc in subcontractors %}
|
||||||
|
<option value="{{ sc.id }}" {% if selected_sc_id==sc.id|string %}selected{% endif %}>
|
||||||
|
{{ sc.subcontractor_name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check form-switch mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="downloadAllSwitch" name="download_all" value="true"
|
||||||
|
{% if download_all %}checked{% endif %}>
|
||||||
|
<label class="form-check-label fw-bold text-primary">
|
||||||
|
Download All RA Bills
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4" id="ra_bill_container">
|
||||||
|
<label class="form-label fw-semibold">RA Bill Number</label>
|
||||||
|
<input type="text" name="ra_bill_no" id="ra_bill_input" class="form-control"
|
||||||
|
value="{{ ra_bill_no or '' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<button class="btn btn-outline-primary w-100" name="action" value="preview">
|
||||||
|
Preview Data
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<button class="btn btn-primary w-100" name="action" value="download">
|
||||||
|
Download Excel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if tables %}
|
||||||
|
<!-- REPORT PREVIEW -->
|
||||||
|
<div class="mt-5">
|
||||||
|
|
||||||
|
<h3 class="text-center mb-3">Report Preview</h3>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
|
||||||
|
<!-- MOBILE SCROLLABLE TABS -->
|
||||||
|
<div class="card-header p-0">
|
||||||
|
<ul class="nav nav-tabs flex-nowrap overflow-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tr">
|
||||||
|
Trench Excavation
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#mh">
|
||||||
|
Manhole Excavation
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#dc">
|
||||||
|
Manhole & Domestic Chambers
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#laying">
|
||||||
|
Pipe Laying
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB CONTENT -->
|
||||||
|
<div class="card-body tab-content">
|
||||||
|
|
||||||
|
<div class="tab-pane fade show active" id="tr">
|
||||||
|
<div class="table-responsive overflow-auto">
|
||||||
|
{{ tables.tr | safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="mh">
|
||||||
|
<div class="table-responsive overflow-auto">
|
||||||
|
{{ tables.mh | safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="dc">
|
||||||
|
<div class="table-responsive overflow-auto">
|
||||||
|
{{ tables.dc | safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="laying">
|
||||||
|
<div class="table-responsive overflow-auto">
|
||||||
|
{{ tables.laying | safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- BOOTSTRAP-ONLY JS -->
|
||||||
|
<script>
|
||||||
|
function toggleRAInput() {
|
||||||
|
const checkbox = document.getElementById("downloadAllSwitch");
|
||||||
|
const input = document.getElementById("ra_bill_input");
|
||||||
|
const container = document.getElementById("ra_bill_container");
|
||||||
|
|
||||||
|
if (checkbox.checked) {
|
||||||
|
input.value = "";
|
||||||
|
input.disabled = true;
|
||||||
|
container.classList.add("opacity-50");
|
||||||
|
} else {
|
||||||
|
input.disabled = false;
|
||||||
|
container.classList.remove("opacity-50");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", toggleRAInput);
|
||||||
|
document.getElementById("downloadAllSwitch").addEventListener("change", toggleRAInput);
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Binary file not shown.
8
app/utils/exceptions.py
Normal file
8
app/utils/exceptions.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from app.constants.http_status import HTTPStatus
|
||||||
|
|
||||||
|
class APIException(Exception):
|
||||||
|
def __init__(self, message, status_code=HTTPStatus.BAD_REQUEST, errors=None):
|
||||||
|
self.message = message
|
||||||
|
self.status_code = status_code
|
||||||
|
self.errors = errors
|
||||||
|
super().__init__(self.message)
|
||||||
@@ -1,6 +1,26 @@
|
|||||||
import os
|
import os
|
||||||
from app.config import Config
|
from flask import current_app
|
||||||
|
|
||||||
|
|
||||||
|
# file extension
|
||||||
|
ALLOWED_EXTENSIONS = {"xlsx", "xls", "csv"}
|
||||||
|
|
||||||
|
|
||||||
|
def get_download_format_folder():
|
||||||
|
return os.path.join(
|
||||||
|
current_app.root_path,
|
||||||
|
"static",
|
||||||
|
"downloads",
|
||||||
|
"format"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_uploads_folder():
|
||||||
|
return os.path.join(
|
||||||
|
current_app.root_path,
|
||||||
|
"static",
|
||||||
|
"uploads"
|
||||||
|
)
|
||||||
|
|
||||||
def ensure_upload_folder():
|
def ensure_upload_folder():
|
||||||
if not os.path.exists(Config.UPLOAD_FOLDER):
|
if not os.path.exists(get_uploads_folder()):
|
||||||
os.makedirs(Config.UPLOAD_FOLDER)
|
os.makedirs(get_uploads_folder())
|
||||||
@@ -1,2 +1,14 @@
|
|||||||
def is_logged_in(session):
|
# def is_logged_in(session):
|
||||||
return session.get("user_id") is not None
|
# return session.get("user_id") is not None
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
from flask import session, redirect, url_for, flash
|
||||||
|
|
||||||
|
def login_required(view):
|
||||||
|
@wraps(view)
|
||||||
|
def wrapped_view(*args, **kwargs):
|
||||||
|
if "user_id" not in session:
|
||||||
|
flash("Please login first", "warning")
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
return view(*args, **kwargs)
|
||||||
|
return wrapped_view
|
||||||
|
|||||||
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")
|
||||||
12
app/utils/regex_utils.py
Normal file
12
app/utils/regex_utils.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
class RegularExpression:
|
||||||
|
|
||||||
|
# sum fields of TrEx, MhEx (_total)
|
||||||
|
STR_TOTAL_PATTERN = re.compile(r".*_total$")
|
||||||
|
|
||||||
|
# sum fields 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+)?$")
|
||||||
23
app/utils/response_handler.py
Normal file
23
app/utils/response_handler.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from flask import jsonify
|
||||||
|
from app.constants.http_status import HTTPStatus
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseHandler:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def success(message, data=None, status_code=HTTPStatus.OK):
|
||||||
|
return jsonify({
|
||||||
|
"status": "success",
|
||||||
|
"message": message,
|
||||||
|
"data": data if data else {},
|
||||||
|
"errors": []
|
||||||
|
}), status_code
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def error(message, errors=None, status_code=HTTPStatus.BAD_REQUEST):
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": message,
|
||||||
|
"data": {},
|
||||||
|
"errors": errors if errors else []
|
||||||
|
}), status_code
|
||||||
46
docker-compose.yml
Normal file
46
docker-compose.yml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mysql:8.0
|
||||||
|
container_name: comparison_db
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: root
|
||||||
|
MYSQL_DATABASE: comparisondb
|
||||||
|
ports:
|
||||||
|
- "3307:3306"
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
container_name: comparison_app
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
FLASK_ENV: development
|
||||||
|
FLASK_DEBUG: "True"
|
||||||
|
FLASK_HOST: "0.0.0.0"
|
||||||
|
FLASK_PORT: "5001"
|
||||||
|
|
||||||
|
DB_DIALECT: mysql
|
||||||
|
DB_DRIVER: pymysql
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 3306
|
||||||
|
DB_NAME: comparisondb
|
||||||
|
DB_USER: root
|
||||||
|
DB_PASSWORD: root
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "5001:5001"
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- ./app/logs:/app/app/logs
|
||||||
|
- ./app/static/uploads:/app/app/static/uploads
|
||||||
|
- ./app/static/downloads:/app/app/static/downloads
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
Binary file not shown.
200
logs/app.log
200
logs/app.log
@@ -1,200 +0,0 @@
|
|||||||
2025-12-09 13:11:05,606 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5000
|
|
||||||
2025-12-09 13:11:05,607 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:11:05,608 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:11:06,239 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:11:06,240 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:11:48,880 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5000
|
|
||||||
2025-12-09 13:11:48,881 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:11:48,882 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:11:49,519 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:11:49,521 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:12:05,727 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\services\\user_service.py', reloading
|
|
||||||
2025-12-09 13:12:05,826 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:12:06,499 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:12:06,501 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:12:09,545 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\config.py', reloading
|
|
||||||
2025-12-09 13:12:09,654 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:12:10,286 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:12:10,288 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:12:12,311 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\routes\\auth.py', reloading
|
|
||||||
2025-12-09 13:12:12,407 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:12:13,071 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:12:13,072 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:12:16,128 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\config.py', reloading
|
|
||||||
2025-12-09 13:12:16,257 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:12:16,898 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:12:16,900 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:12:20,944 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\routes\\user.py', reloading
|
|
||||||
2025-12-09 13:12:21,042 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:12:21,719 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:12:21,721 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:12:23,762 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\routes\\file_import.py', reloading
|
|
||||||
2025-12-09 13:12:23,870 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:12:24,505 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:12:24,507 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:12:27,561 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\services\\__init__.py', reloading
|
|
||||||
2025-12-09 13:12:27,670 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:12:28,294 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:12:28,296 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:12:31,336 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\services\\db_service.py', reloading
|
|
||||||
2025-12-09 13:12:31,448 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:12:32,097 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:12:32,099 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:13:05,662 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\config.py', reloading
|
|
||||||
2025-12-09 13:13:05,773 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:13:06,466 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:13:06,469 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:13:10,944 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5000
|
|
||||||
2025-12-09 13:13:10,944 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:13:10,945 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:13:11,623 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:13:11,625 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:14:11,295 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading
|
|
||||||
2025-12-09 13:14:11,393 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:14:12,004 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:14:12,006 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:14:32,108 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5001
|
|
||||||
2025-12-09 13:14:32,109 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:14:32,110 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:14:32,699 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:14:32,701 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:15:58,632 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading
|
|
||||||
2025-12-09 13:15:58,733 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:15:59,415 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:15:59,416 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:16:03,475 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading
|
|
||||||
2025-12-09 13:16:03,583 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:16:04,204 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:16:04,206 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:16:33,504 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading
|
|
||||||
2025-12-09 13:16:33,605 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:16:34,213 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:16:34,215 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:16:41,815 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5000
|
|
||||||
2025-12-09 13:16:41,816 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:18:12,302 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5000
|
|
||||||
2025-12-09 13:18:12,302 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:22:07,114 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5001
|
|
||||||
2025-12-09 13:22:07,114 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:22:07,116 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:22:07,935 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:22:07,937 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:23:21,204 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading
|
|
||||||
2025-12-09 13:23:21,305 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:24:06,973 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5001
|
|
||||||
2025-12-09 13:24:06,973 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:24:06,974 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:24:07,689 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:24:07,691 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:24:36,315 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\app.py', reloading
|
|
||||||
2025-12-09 13:24:36,418 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:24:37,074 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:24:37,076 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:26:54,442 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading
|
|
||||||
2025-12-09 13:26:54,543 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:26:59,170 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5001
|
|
||||||
2025-12-09 13:26:59,170 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:26:59,171 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:26:59,827 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:26:59,829 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:28:47,631 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading
|
|
||||||
2025-12-09 13:28:47,747 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:28:48,478 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:28:48,480 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:28:51,150 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5001
|
|
||||||
2025-12-09 13:28:51,151 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:28:51,153 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:28:51,788 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:28:51,790 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:28:54,904 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading
|
|
||||||
2025-12-09 13:28:55,010 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:28:55,608 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:28:55,610 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:28:56,644 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading
|
|
||||||
2025-12-09 13:28:56,752 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:29:04,454 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5001
|
|
||||||
2025-12-09 13:29:04,454 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:29:04,455 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:29:05,096 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:29:05,098 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:30:01,657 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5001
|
|
||||||
2025-12-09 13:30:01,657 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:30:01,658 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:30:02,278 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:30:02,280 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:30:27,872 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5001
|
|
||||||
2025-12-09 13:30:27,872 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:30:27,873 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:30:28,474 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:30:28,476 | INFO | * Debugger PIN: 105-645-384
|
|
||||||
2025-12-09 13:33:22,709 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5001
|
|
||||||
2025-12-09 13:33:22,709 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:33:22,710 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:33:23,778 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:33:23,781 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:33:29,939 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\services\\db_service.py', reloading
|
|
||||||
2025-12-09 13:33:30,080 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:33:44,462 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5001
|
|
||||||
2025-12-09 13:33:44,462 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:33:44,464 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:33:45,216 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:33:45,218 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:35:23,298 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5001
|
|
||||||
2025-12-09 13:35:23,299 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:35:23,301 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:35:24,098 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:35:24,100 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:38:25,991 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading
|
|
||||||
2025-12-09 13:38:26,126 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:38:27,120 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:38:27,122 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:38:37,386 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\config.py', reloading
|
|
||||||
2025-12-09 13:38:37,513 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:38:38,297 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:38:38,300 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:38:45,485 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\run.py', reloading
|
|
||||||
2025-12-09 13:38:45,605 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:38:46,348 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:38:46,350 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:38:55,109 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5001
|
|
||||||
2025-12-09 13:38:55,109 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:38:55,110 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:38:55,959 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:38:55,961 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:39:27,813 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading
|
|
||||||
2025-12-09 13:39:27,937 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:39:28,684 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:39:28,687 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:40:00,602 | INFO | * Detected change in 'C:\\Work\\lcepl_Projects\\Comparison Project\\app\\__init__.py', reloading
|
|
||||||
2025-12-09 13:40:00,728 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:40:01,428 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:40:01,430 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 13:40:21,531 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5001
|
|
||||||
2025-12-09 13:40:21,531 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 13:40:21,533 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 13:40:22,307 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 13:40:22,309 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
2025-12-09 14:03:58,363 | INFO | [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
|
||||||
* Running on http://127.0.0.1:5001
|
|
||||||
2025-12-09 14:03:58,363 | INFO | [33mPress CTRL+C to quit[0m
|
|
||||||
2025-12-09 14:03:58,364 | INFO | * Restarting with stat
|
|
||||||
2025-12-09 14:03:59,038 | WARNING | * Debugger is active!
|
|
||||||
2025-12-09 14:03:59,041 | INFO | * Debugger PIN: 697-115-033
|
|
||||||
|
|||||||
@@ -4,4 +4,8 @@ openpyxl
|
|||||||
xlrd
|
xlrd
|
||||||
Werkzeug
|
Werkzeug
|
||||||
python-dotenv
|
python-dotenv
|
||||||
cryptography
|
cryptography
|
||||||
|
xlsxwriter
|
||||||
|
matplotlib
|
||||||
|
flask_sqlalchemy
|
||||||
|
flask_migrate
|
||||||
18
run.py
18
run.py
@@ -1,9 +1,17 @@
|
|||||||
from app import create_app, db
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
from app import create_app
|
||||||
|
from app.services.db_service import db
|
||||||
|
import os
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
with app.app_context():
|
|
||||||
db.create_all()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(debug=True, port=5001)
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
app.run(
|
||||||
|
host=os.getenv("FLASK_HOST"),
|
||||||
|
port=int(os.getenv("FLASK_PORT")),
|
||||||
|
debug=os.getenv("FLASK_DEBUG") == "True"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user