Compare commits
82 Commits
2afd6c904a
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
| 861a61c6ec | |||
| 375e899857 | |||
| 18874cc84e | |||
| de7b85034d | |||
| 2f99c290df | |||
| e7805b71a8 | |||
| aa759c9d96 | |||
| bc4b9c778e | |||
| e98e1422a0 | |||
| 2d7d146ec3 | |||
| 604e948986 | |||
| 46d11ec01f | |||
| bf1df3a817 | |||
| 5577576112 | |||
| 12b2032430 | |||
| 6e4406519a | |||
| ce0e4f90cf | |||
| 1d83534a95 | |||
| 446778a50c | |||
| 5ddfb1f440 | |||
| 96a3a79731 | |||
| 310ab40bf9 | |||
| c5b3e7bd60 | |||
| bb56f00a6c | |||
| 8353bb6426 | |||
| 21f8576e33 | |||
| 5ab11bc5a6 | |||
| 45bfd4b592 | |||
| 568428b5d0 | |||
| cf7d1636f9 | |||
| 163c7814ed | |||
| 5b557efd80 | |||
| 71ce8ca819 | |||
| 68a694d2c7 | |||
| 1dceb640bd | |||
| 359847958d | |||
| 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 |
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
|
||||
37
.env
Normal file
37
.env
Normal file
@@ -0,0 +1,37 @@
|
||||
# -----------------------------
|
||||
# Flask App Configuration
|
||||
# -----------------------------
|
||||
FLASK_ENV=development
|
||||
FLASK_DEBUG=True
|
||||
FLASK_HOST=0.0.0.0
|
||||
FLASK_PORT=5015
|
||||
|
||||
# -----------------------------
|
||||
# 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
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# LDAP Configuration new
|
||||
# -----------------------------
|
||||
LDAP_SERVER=ldap://host.docker.internal
|
||||
LDAP_PORT=389
|
||||
LDAP_USE_SSL=False
|
||||
|
||||
LDAP_DOMAIN=lcepl.org
|
||||
LDAP_BASE_DN=DC=lcepl,DC=org
|
||||
LDAP_SEARCH_BASE=OU=Users,DC=lcepl,DC=org
|
||||
22
.gitignore
vendored
22
.gitignore
vendored
@@ -1,9 +1,19 @@
|
||||
# Ignore folders
|
||||
instance/
|
||||
static/uploads/
|
||||
# Python
|
||||
app/__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.__pycache__
|
||||
|
||||
# Ignore files
|
||||
.env
|
||||
# Ingnor upload files
|
||||
app/static/uploads/
|
||||
|
||||
# Ignore by type
|
||||
# Ignore env files
|
||||
venv
|
||||
|
||||
# Ignore Log files ss
|
||||
logs/
|
||||
*.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"]
|
||||
585
README.md
585
README.md
@@ -1,3 +1,584 @@
|
||||
# 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
|
||||
|
||||
**Frontend Framework**: HTML, CSS, Js, Bootstrap
|
||||
**Backend Framework**: Python 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:** April 2026
|
||||
@@ -1,51 +1,69 @@
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask import Flask, redirect, url_for
|
||||
from app.config import Config
|
||||
|
||||
db = SQLAlchemy()
|
||||
from app.services.db_service import db
|
||||
from app.services.logger_service import LoggerService
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
|
||||
# Initialize extensions
|
||||
db.init_app(app)
|
||||
|
||||
# Register Blueprints
|
||||
from app.routes.subcontractor_routes import subcontractor_bp
|
||||
from app.routes.dashboard import dashboard_bp
|
||||
from app.routes.file_import import file_import_bp
|
||||
# from app.routes.user import user_bp
|
||||
from app.routes.file_report import file_report_bp
|
||||
# Initialize Logger
|
||||
LoggerService.init_app(app)
|
||||
|
||||
# Register blueprints
|
||||
register_blueprints(app)
|
||||
# Register error handlers
|
||||
register_error_handlers(app)
|
||||
|
||||
app.register_blueprint(subcontractor_bp)
|
||||
app.register_blueprint(dashboard_bp)
|
||||
app.register_blueprint(file_import_bp)
|
||||
# app.register_blueprint(user_bp)
|
||||
app.register_blueprint(file_report_bp)
|
||||
|
||||
|
||||
# ROOT → LOGIN
|
||||
@app.route("/")
|
||||
def index():
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def register_blueprints(app):
|
||||
from app.routes.auth import auth_bp
|
||||
from app.routes.user_routes 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
|
||||
|
||||
# new
|
||||
from app.routes.activity_routes import activity_bp
|
||||
from app.routes.engineering_master_routes import engi_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)
|
||||
|
||||
# from flask import Flask
|
||||
# from app.config import Config
|
||||
# new
|
||||
app.register_blueprint(activity_bp)
|
||||
app.register_blueprint(engi_bp)
|
||||
|
||||
|
||||
# def create_app():
|
||||
# app = Flask(__name__)
|
||||
# app.config.from_object(Config)
|
||||
def register_error_handlers(app):
|
||||
|
||||
# # Register Blueprints
|
||||
# from app.routes.dashboard import dashboard_bp
|
||||
# from app.routes.file_import import file_import_bp
|
||||
# from app.routes.user import user_bp
|
||||
from flask import current_app
|
||||
|
||||
# app.register_blueprint(dashboard_bp)
|
||||
# app.register_blueprint(file_import_bp)
|
||||
# app.register_blueprint(user_bp)
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
current_app.logger.warning("404 Page Not Found")
|
||||
return "Page Not Found", 404
|
||||
|
||||
# return app
|
||||
@app.errorhandler(500)
|
||||
def internal_error(e):
|
||||
current_app.logger.exception("500 Internal Server Error")
|
||||
return "Internal Server Error", 500
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,24 +1,34 @@
|
||||
import os
|
||||
|
||||
class Config:
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = "mysql+pymysql://root:root@localhost/comparisondb"
|
||||
# secret key
|
||||
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
|
||||
SECRET_KEY = "secret123"
|
||||
|
||||
UPLOAD_FOLDER = "app/static/uploads/"
|
||||
ALLOWED_EXTENSIONS = {"xlsx", "xls", "csv"}
|
||||
|
||||
|
||||
# LDAP Configuration New
|
||||
LDAP_SERVER = os.getenv("LDAP_SERVER")
|
||||
LDAP_PORT = int(os.getenv("LDAP_PORT", 389))
|
||||
LDAP_USE_SSL = os.getenv("LDAP_USE_SSL", "False").lower() == "true"
|
||||
|
||||
# class Config:
|
||||
# SECRET_KEY = os.getenv("SECRET_KEY", "dev_key_12345")
|
||||
LDAP_BASE_DN = os.getenv("LDAP_BASE_DN")
|
||||
LDAP_DOMAIN = os.getenv("LDAP_DOMAIN")
|
||||
|
||||
# UPLOAD_FOLDER = "app/static/uploads/"
|
||||
# ALLOWED_EXTENSIONS = {"xlsx", "xls", "csv"}
|
||||
|
||||
# DB_HOST = "localhost"
|
||||
# DB_USER = "root"
|
||||
# DB_PASSWORD = "root"
|
||||
# DB_NAME = "comparisondb"
|
||||
LDAP_SEARCH_BASE = os.getenv("LDAP_SEARCH_BASE")
|
||||
29
app/constants/http_status.py
Normal file
29
app/constants/http_status.py
Normal file
@@ -0,0 +1,29 @@
|
||||
class HTTPStatus:
|
||||
# ==========================
|
||||
# 2xx Success
|
||||
# ==========================
|
||||
OK = 200 # Request successful
|
||||
CREATED = 201 # Resource created successfully
|
||||
ACCEPTED = 202 # Request accepted for processing
|
||||
NO_CONTENT = 204 # Success, no response body
|
||||
|
||||
# ==========================
|
||||
# 4xx Client Errors
|
||||
# ==========================
|
||||
BAD_REQUEST = 400 # Invalid request
|
||||
UNAUTHORIZED = 401 # Authentication required
|
||||
FORBIDDEN = 403 # Access denied
|
||||
NOT_FOUND = 404 # Resource not found
|
||||
METHOD_NOT_ALLOWED = 405 # HTTP method not allowed
|
||||
CONFLICT = 409 # Resource conflict (e.g., duplicate)
|
||||
UNPROCESSABLE_ENTITY = 422 # Validation error
|
||||
TOO_MANY_REQUESTS = 429 # Rate limit exceeded
|
||||
|
||||
# ==========================
|
||||
# 5xx Server Errors
|
||||
# ==========================
|
||||
INTERNAL_SERVER_ERROR = 500 # Internal server error
|
||||
NOT_IMPLEMENTED = 501 # Feature not implemented
|
||||
BAD_GATEWAY = 502 # Invalid response from upstream server
|
||||
SERVICE_UNAVAILABLE = 503 # Server temporarily unavailable
|
||||
GATEWAY_TIMEOUT = 504 # Upstream server timeout
|
||||
65
app/constants/messages.py
Normal file
65
app/constants/messages.py
Normal file
@@ -0,0 +1,65 @@
|
||||
class SuccessMessage:
|
||||
FETCHED = "Data fetched successfully."
|
||||
CREATED = "Resource created successfully."
|
||||
UPDATED = "Resource updated successfully."
|
||||
DELETED = "Resource deleted successfully."
|
||||
|
||||
SAVED = "Data saved successfully."
|
||||
IMPORTED = "Data imported successfully."
|
||||
EXPORTED = "Data exported successfully."
|
||||
UPLOADED = "File uploaded successfully."
|
||||
DOWNLOADED = "File downloaded successfully."
|
||||
|
||||
LOGIN = "Login successful."
|
||||
LOGOUT = "Logout successful."
|
||||
PASSWORD_CHANGED = "Password changed successfully."
|
||||
PASSWORD_RESET = "Password reset successfully."
|
||||
|
||||
EMAIL_SENT = "Email sent successfully."
|
||||
STATUS_UPDATED = "Status updated successfully."
|
||||
|
||||
|
||||
class ErrorMessage:
|
||||
INVALID_REQUEST = "Invalid request."
|
||||
INVALID_DATA = "Invalid data provided."
|
||||
VALIDATION_FAILED = "Validation failed."
|
||||
|
||||
UNAUTHORIZED = "Unauthorized access."
|
||||
FORBIDDEN = "Access denied."
|
||||
NOT_FOUND = "Resource not found."
|
||||
METHOD_NOT_ALLOWED = "Method not allowed."
|
||||
|
||||
DUPLICATE_ENTRY = "Duplicate record found."
|
||||
RECORD_EXISTS = "Record already exists."
|
||||
RECORD_NOT_FOUND = "Record does not exist."
|
||||
|
||||
FILE_NOT_FOUND = "File not found."
|
||||
FILE_UPLOAD_FAILED = "File upload failed."
|
||||
FILE_IMPORT_FAILED = "File import failed."
|
||||
|
||||
DATABASE_ERROR = "Database operation failed."
|
||||
INTERNAL_SERVER_ERROR = "Internal server error."
|
||||
SERVICE_UNAVAILABLE = "Service temporarily unavailable."
|
||||
|
||||
LOGIN_FAILED = "Invalid username or password."
|
||||
SESSION_EXPIRED = "Session expired. Please login again."
|
||||
|
||||
PASSWORD_MISMATCH = "Passwords do not match."
|
||||
INVALID_TOKEN = "Invalid or expired token."
|
||||
|
||||
|
||||
class WarningMessage:
|
||||
NO_DATA_FOUND = "No data found."
|
||||
ALREADY_EXISTS = "Record already exists."
|
||||
UNSAVED_CHANGES = "You have unsaved changes."
|
||||
DELETE_CONFIRMATION = "Are you sure you want to delete the selected record(s)?"
|
||||
INVALID_FILTER = "No records match the selected filters."
|
||||
|
||||
|
||||
class InfoMessage:
|
||||
PROCESSING = "Request is being processed."
|
||||
LOADING = "Loading data..."
|
||||
SAVING = "Saving data..."
|
||||
DELETING = "Deleting record..."
|
||||
IMPORTING = "Importing data..."
|
||||
EXPORTING = "Preparing export..."
|
||||
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.
74
app/models/laying_client_model.py
Normal file
74
app/models/laying_client_model.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from app import db
|
||||
from datetime import datetime
|
||||
from sqlalchemy import event
|
||||
from app.utils.regex_utils import RegularExpression
|
||||
from decimal import Decimal
|
||||
|
||||
class LayingClient(db.Model):
|
||||
__tablename__ = "laying_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, 2), default=Decimal("0.00"))
|
||||
# Bedding Qty.
|
||||
Outer_dia_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Bedding_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Width = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Qty = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
# PIPE LAYING Qty.
|
||||
Pipe_Dia_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Laying_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
pipe_150_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_250_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_300_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_350_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_400_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_450_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_500_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_600_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_700_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_900_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_1200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
np4_pipe_200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
np4_pipe_250_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
np4_pipe_300_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
np4_pipe_350_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
np4_pipe_400_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
np4_pipe_450_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
np4_pipe_500_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
np4_pipe_600_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00"))
|
||||
Remarks = db.Column(db.String(500), default="Import File")
|
||||
created_at = db.Column(db.DateTime,default=datetime.now,nullable=False)
|
||||
|
||||
|
||||
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 = Decimal("0.00")
|
||||
for column in target.__table__.columns:
|
||||
if RegularExpression.PIPE_MM_PATTERN.match(column.name):
|
||||
value = getattr(target, column.name)
|
||||
if value is not None:
|
||||
total += Decimal(value)
|
||||
target.Total = total.quantize(Decimal("0.01"))
|
||||
|
||||
event.listen(LayingClient, "before_insert", calculate_laying_total)
|
||||
event.listen(LayingClient, "before_update", calculate_laying_total)
|
||||
61
app/models/laying_model.py
Normal file
61
app/models/laying_model.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from app import db
|
||||
from datetime import datetime
|
||||
from sqlalchemy import event
|
||||
from app.utils.regex_utils import RegularExpression
|
||||
from decimal import Decimal
|
||||
|
||||
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
|
||||
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, 2), default=Decimal("0.00"))
|
||||
Pipe_Dia_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Laying_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
pipe_150_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_250_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_300_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_350_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_400_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_450_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_500_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_600_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_700_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_900_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
pipe_1200_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00"))
|
||||
Remarks = db.Column(db.String(500), default="Import File")
|
||||
created_at = db.Column(db.DateTime,default=datetime.now,nullable=False)
|
||||
|
||||
|
||||
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 = Decimal("0.00")
|
||||
for column in target.__table__.columns:
|
||||
if RegularExpression.PIPE_MM_PATTERN.match(column.name):
|
||||
value = getattr(target, column.name)
|
||||
if value is not None:
|
||||
total += Decimal(value)
|
||||
target.Total = total.quantize(Decimal("0.01"))
|
||||
|
||||
event.listen(Laying, "before_insert", calculate_laying_total)
|
||||
event.listen(Laying, "before_update", calculate_laying_total)
|
||||
@@ -1,5 +1,8 @@
|
||||
from app import db
|
||||
from datetime import datetime
|
||||
from sqlalchemy import event
|
||||
from app.utils.regex_utils import RegularExpression
|
||||
from decimal import Decimal
|
||||
|
||||
class ManholeDomesticChamber(db.Model):
|
||||
__tablename__ = "manhole_domestic_chamber"
|
||||
@@ -11,35 +14,57 @@ class ManholeDomesticChamber(db.Model):
|
||||
subcontractor = db.relationship("Subcontractor", backref="manhole_domestic_chamber_records")
|
||||
|
||||
# Basic Fields
|
||||
Location = db.Column(db.String(255))
|
||||
Node_No = db.Column(db.String(100))
|
||||
Depth_of_MH = db.Column(db.Float)
|
||||
|
||||
RA_Bill_No=db.Column(db.String(500))
|
||||
Location = db.Column(db.String(500))
|
||||
MH_NO = db.Column(db.String(100))
|
||||
Depth_of_MH = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
# Excavation categories
|
||||
d_0_to_0_75 = db.Column(db.Float)
|
||||
d_1_06_to_1_65 = db.Column(db.Float)
|
||||
d_1_06_to_1_65 = db.Column(db.Float)
|
||||
d_1_66_to_2_15 = db.Column(db.Float)
|
||||
d_2_16_to_2_65 = db.Column(db.Float)
|
||||
d_2_66_to_3_15 = db.Column(db.Float)
|
||||
d_3_16_to_3_65= db.Column(db.Float)
|
||||
d_3_66_to_4_15 = db.Column(db.Float)
|
||||
d_4_16_to_4_65 = db.Column(db.Float)
|
||||
d_4_66_to_5_15 = db.Column(db.Float)
|
||||
d_5_16_to_5_65 = db.Column(db.Float)
|
||||
d_0_to_0_75 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_0_76_to_1_05 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_1_06_to_1_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_1_66_to_2_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_2_16_to_2_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_2_66_to_3_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_3_16_to_3_65= db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_3_66_to_4_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_4_16_to_4_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_4_66_to_5_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_5_16_to_5_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_5_66_to_6_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_6_16_to_6_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_6_66_to_7_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_7_16_to_7_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_7_66_to_8_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_8_16_to_8_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_8_66_to_9_15 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_9_16_to_9_65 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
d_5_66_to_6_15 = db.Column(db.Float)
|
||||
d_6_16_to_6_65 = db.Column(db.Float)
|
||||
d_6_66_to_7_15 = db.Column(db.Float)
|
||||
d_7_16_to_7_65 = db.Column(db.Float)
|
||||
d_7_66_to_8_15 = db.Column(db.Float)
|
||||
d_8_16_to_8_65 = db.Column(db.Float)
|
||||
d_8_66_to_9_15 = db.Column(db.Float)
|
||||
d_9_16_to_9_65 = db.Column(db.Float)
|
||||
Domestic_Chambers = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
DWC_Pipe_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
UPVC_Pipe_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
|
||||
Domestic_Chambers = db.Column(db.Float)
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00"))
|
||||
Remarks = db.Column(db.String(500), default="Import File")
|
||||
created_at = db.Column(db.DateTime,default=datetime.now,nullable=False)
|
||||
|
||||
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 = Decimal("0.00")
|
||||
for column in target.__table__.columns:
|
||||
if RegularExpression.D_RANGE_PATTERN.match(column.name):
|
||||
value = getattr(target, column.name)
|
||||
if value is not None:
|
||||
total += Decimal(value)
|
||||
target.Total = total.quantize(Decimal("0.01"))
|
||||
|
||||
event.listen(ManholeDomesticChamber, "before_insert", calculate_mh_dc_total)
|
||||
event.listen(ManholeDomesticChamber, "before_update", calculate_mh_dc_total)
|
||||
@@ -1,5 +1,8 @@
|
||||
from app import db
|
||||
from datetime import datetime
|
||||
from sqlalchemy import event
|
||||
from app.utils.regex_utils import RegularExpression
|
||||
from decimal import Decimal
|
||||
|
||||
class ManholeExcavation(db.Model):
|
||||
__tablename__ = "manhole_excavation"
|
||||
@@ -11,53 +14,70 @@ class ManholeExcavation(db.Model):
|
||||
subcontractor = db.relationship("Subcontractor", backref="manhole_records")
|
||||
|
||||
# Basic Fields
|
||||
Location = db.Column(db.String(255))
|
||||
RA_Bill_No=db.Column(db.String(500))
|
||||
Location = db.Column(db.String(500))
|
||||
MH_NO = db.Column(db.String(100))
|
||||
|
||||
Upto_IL_Depth = db.Column(db.Float)
|
||||
Cutting_Depth = db.Column(db.Float)
|
||||
ID_of_MH_m = db.Column(db.Float)
|
||||
Ex_Dia_of_Manhole = db.Column(db.Float)
|
||||
Area_of_Manhole = db.Column(db.Float)
|
||||
Upto_IL_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Cutting_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Ex_Dia_of_Manhole = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Area_of_Manhole = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
# Excavation categories
|
||||
Soft_Murum_0_to_1_5 = db.Column(db.Float)
|
||||
Soft_Murum_1_5_to_3_0 = db.Column(db.Float)
|
||||
Soft_Murum_3_0_to_4_5 = db.Column(db.Float)
|
||||
Soft_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Hard_Murum_0_to_1_5 = db.Column(db.Float)
|
||||
Hard_Murum_1_5_to_3_0 = db.Column(db.Float)
|
||||
Hard_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Soft_Rock_0_to_1_5 = db.Column(db.Float)
|
||||
Soft_Rock_1_5_to_3_0 = db.Column(db.Float)
|
||||
Soft_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Hard_Rock_0_to_1_5 = db.Column(db.Float)
|
||||
Hard_Rock_1_5_to_3_0 = db.Column(db.Float)
|
||||
Hard_Rock_3_0_to_4_5 = db.Column(db.Float)
|
||||
Hard_Rock_4_5_to_6_0 = db.Column(db.Float)
|
||||
Hard_Rock_6_0_to_7_5 = db.Column(db.Float)
|
||||
Hard_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_4_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
# Totals
|
||||
Soft_Murum_0_to_1_5_total = db.Column(db.Float)
|
||||
Soft_Murum_1_5_to_3_0_total = db.Column(db.Float)
|
||||
Soft_Murum_3_0_to_4_5_total = db.Column(db.Float)
|
||||
Soft_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Hard_Murum_0_to_1_5_total = db.Column(db.Float)
|
||||
Hard_Murum_1_5_and_above_total = db.Column(db.Float)
|
||||
Hard_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Murum_1_5_and_above_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Soft_Rock_0_to_1_5_total = db.Column(db.Float)
|
||||
Soft_Rock_1_5_and_above_total = db.Column(db.Float)
|
||||
Soft_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Rock_1_5_and_above_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Hard_Rock_0_to_1_5_total = db.Column(db.Float)
|
||||
Hard_Rock_1_5_and_above_total = db.Column(db.Float)
|
||||
Hard_Rock_3_0_to_4_5_total = db.Column(db.Float)
|
||||
Hard_Rock_4_5_to_6_0_total = db.Column(db.Float)
|
||||
Hard_Rock_6_0_to_7_5_total = db.Column(db.Float)
|
||||
Hard_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_4_5_to_6_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Remarks = db.Column(db.String(500))
|
||||
Total = db.Column(db.Float)
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00"))
|
||||
Remarks = db.Column(db.String(500), default="Import File")
|
||||
created_at = db.Column(db.DateTime,default=datetime.now,nullable=False)
|
||||
|
||||
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 = Decimal("0.00")
|
||||
for column in target.__table__.columns:
|
||||
if RegularExpression.STR_TOTAL_PATTERN.match(column.name):
|
||||
value = getattr(target, column.name)
|
||||
if value is not None:
|
||||
total += Decimal(value)
|
||||
target.Total = total.quantize(Decimal("0.01"))
|
||||
|
||||
|
||||
event.listen(ManholeExcavation, "before_insert", calculate_Manhole_total)
|
||||
event.listen(ManholeExcavation, "before_update", calculate_Manhole_total)
|
||||
58
app/models/mh_dc_client_model.py
Normal file
58
app/models/mh_dc_client_model.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from app import db
|
||||
from datetime import datetime
|
||||
from sqlalchemy import event
|
||||
from app.utils.regex_utils import RegularExpression
|
||||
from decimal import Decimal
|
||||
|
||||
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, 2), default=Decimal("0.00"))
|
||||
MH_IL_LEVEL = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Depth_of_MH = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
# Excavation categories
|
||||
d_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_1_5_to_2_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_2_0_to_2_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_2_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_3_0_to_3_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_3_5_to_4_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_4_0_to_4_5= db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_4_5_to_5_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_5_0_to_5_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_5_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
d_6_0_to_6_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Domestic_Chambers = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00"))
|
||||
Remarks = db.Column(db.String(500), default="Import File")
|
||||
created_at = db.Column(db.DateTime,default=datetime.now,nullable=False)
|
||||
|
||||
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 = Decimal("0.00")
|
||||
for column in target.__table__.columns:
|
||||
if RegularExpression.D_RANGE_PATTERN.match(column.name):
|
||||
value = getattr(target, column.name)
|
||||
if value is not None:
|
||||
total += Decimal(value)
|
||||
target.Total = total.quantize(Decimal("0.01"))
|
||||
|
||||
event.listen(ManholeDomesticChamberClient, "before_insert", calculate_mh_dc_total)
|
||||
event.listen(ManholeDomesticChamberClient, "before_update", calculate_mh_dc_total)
|
||||
96
app/models/mh_ex_client_model.py
Normal file
96
app/models/mh_ex_client_model.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from app import db
|
||||
from datetime import datetime
|
||||
from sqlalchemy import event
|
||||
from app.utils.regex_utils import RegularExpression
|
||||
from decimal import Decimal
|
||||
|
||||
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, 2), default=Decimal("0.00"))
|
||||
MH_Invert_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
MH_Top_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Ex_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Cutting_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
MH_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Dia_of_MH_Cutting = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Area_of_Manhole = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
# Excavation categories
|
||||
Marshi_Muddy_Slushy_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Marshi_Muddy_Slushy_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Marshi_Muddy_Slushy_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Soft_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Hard_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Soft_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Hard_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_4_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
# Totals
|
||||
Marshi_Muddy_Slushy_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Marshi_Muddy_Slushy_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Marshi_Muddy_Slushy_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Soft_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Hard_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Soft_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Hard_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_4_5_to_6_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
|
||||
Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00"))
|
||||
Remarks = db.Column(db.String(500), default="Import File")
|
||||
created_at = db.Column(db.DateTime,default=datetime.now,nullable=False)
|
||||
|
||||
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 = Decimal("0.00")
|
||||
for column in target.__table__.columns:
|
||||
if RegularExpression.STR_TOTAL_PATTERN.match(column.name):
|
||||
value = getattr(target, column.name)
|
||||
if value is not None:
|
||||
total += Decimal(value)
|
||||
target.Total = total.quantize(Decimal("0.01"))
|
||||
|
||||
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))
|
||||
contact_person = db.Column(db.String(150))
|
||||
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):
|
||||
return f"<Subcontractor {self.subcontractor_name}>"
|
||||
|
||||
23
app/models/subcontractor_rate_model.py
Normal file
23
app/models/subcontractor_rate_model.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from app import db
|
||||
from datetime import datetime
|
||||
|
||||
class SubcontractorRate(db.Model):
|
||||
__tablename__ = "subcontractor_rates"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
subcontractor_id = db.Column(db.Integer, db.ForeignKey("subcontractors.id"), nullable=False)
|
||||
subcontractor = db.relationship("Subcontractor", backref="rate_master")
|
||||
|
||||
category = db.Column(db.String(50), nullable=False)
|
||||
item_code = db.Column(db.String(50), nullable=False)
|
||||
item_name = db.Column(db.String(200), nullable=False)
|
||||
unit = db.Column(db.String(20))
|
||||
rate = db.Column(db.Numeric(12,2), nullable=False)
|
||||
effective_from = db.Column(db.Date, nullable=False)
|
||||
effective_to = db.Column(db.Date)
|
||||
status = db.Column(db.String(20), default="Active")
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<subcontractor Rate {self.item_name}>"
|
||||
|
||||
101
app/models/tr_ex_client_model.py
Normal file
101
app/models/tr_ex_client_model.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from app import db
|
||||
from datetime import datetime
|
||||
from sqlalchemy import event
|
||||
from app.utils.regex_utils import RegularExpression
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
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, 2), default=Decimal("0.00"))
|
||||
Actual_Trench_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Ground_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Invert_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Excavated_level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Cutting_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Avg_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Pipe_Dia_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
# width
|
||||
Width_0_to_1_5_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Width_1_5_to_3_0_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Width_3_0_to_4_5_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Width_4_5_to_6_0_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Width_6_0_to_7_5_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
# Excavation categories
|
||||
Marshi_Muddy_Slushy_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Marshi_Muddy_Slushy_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Marshi_Muddy_Slushy_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Soft_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Hard_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Soft_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Hard_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_4_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
# Totals
|
||||
Marshi_Muddy_Slushy_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Marshi_Muddy_Slushy_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Marshi_Muddy_Slushy_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Soft_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Hard_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Soft_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Hard_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_4_5_to_6_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00"))
|
||||
Remarks = db.Column(db.String(500), default="Import File")
|
||||
created_at = db.Column(db.DateTime,default=datetime.now,nullable=False)
|
||||
|
||||
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 = Decimal("0.00")
|
||||
for column in target.__table__.columns:
|
||||
if RegularExpression.STR_TOTAL_PATTERN.match(column.name):
|
||||
value = getattr(target, column.name)
|
||||
if value is not None:
|
||||
total += Decimal(value)
|
||||
target.Total = total.quantize(Decimal("0.01"))
|
||||
|
||||
event.listen(TrenchExcavationClient, "before_insert", calculate_trench_client_total)
|
||||
event.listen(TrenchExcavationClient, "before_update", calculate_trench_client_total)
|
||||
@@ -1,5 +1,8 @@
|
||||
from app import db
|
||||
from datetime import datetime
|
||||
from sqlalchemy import event
|
||||
from app.utils.regex_utils import RegularExpression
|
||||
from decimal import Decimal
|
||||
|
||||
class TrenchExcavation(db.Model):
|
||||
__tablename__ = "trench_excavation"
|
||||
@@ -11,63 +14,82 @@ class TrenchExcavation(db.Model):
|
||||
subcontractor = db.relationship("Subcontractor", backref="trench_records")
|
||||
|
||||
# Basic Fields
|
||||
Location = db.Column(db.String(255))
|
||||
RA_Bill_No=db.Column(db.String(500))
|
||||
Location = db.Column(db.String(500))
|
||||
MH_NO = db.Column(db.String(100))
|
||||
CC_length = db.Column(db.Float)
|
||||
Invert_Level = db.Column(db.Float)
|
||||
MH_Top_Level = db.Column(db.Float)
|
||||
Ground_Level = db.Column(db.Float)
|
||||
ID_of_MH_m = db.Column(db.Float)
|
||||
Actual_Trench_Length = db.Column(db.Float)
|
||||
Pipe_Dia_mm = db.Column(db.Float)
|
||||
CC_length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Invert_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
MH_Top_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Ground_Level = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
ID_of_MH_m = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Actual_Trench_Length = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Pipe_Dia_mm = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Width_0_to_2_5 = db.Column(db.Float)
|
||||
Width_2_5_to_3_0 = db.Column(db.Float)
|
||||
Width_3_0_to_4_5 = db.Column(db.Float)
|
||||
Width_4_5_to_6_0 = db.Column(db.Float)
|
||||
# width
|
||||
Width_0_to_2_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Width_2_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Width_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Width_4_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Upto_IL_Depth = db.Column(db.Float)
|
||||
Cutting_Depth = db.Column(db.Float)
|
||||
Avg_Depth = db.Column(db.Float)
|
||||
Upto_IL_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Cutting_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Avg_Depth = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
# Excavation categories
|
||||
Soft_Murum_0_to_1_5 = db.Column(db.Float)
|
||||
Soft_Murum_1_5_to_3_0 = db.Column(db.Float)
|
||||
Soft_Murum_3_0_to_4_5 = db.Column(db.Float)
|
||||
Soft_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Hard_Murum_0_to_1_5 = db.Column(db.Float)
|
||||
Hard_Murum_1_5_to_3_0 = db.Column(db.Float)
|
||||
Hard_Murum_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Murum_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Soft_Rock_0_to_1_5 = db.Column(db.Float)
|
||||
Soft_Rock_1_5_to_3_0 = db.Column(db.Float)
|
||||
Soft_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Hard_Rock_0_to_1_5 = db.Column(db.Float)
|
||||
Hard_Rock_1_5_to_3_0 = db.Column(db.Float)
|
||||
Hard_Rock_3_0_to_4_5 = db.Column(db.Float)
|
||||
Hard_Rock_4_5_to_6_0 = db.Column(db.Float)
|
||||
Hard_Rock_6_0_to_7_5 = db.Column(db.Float)
|
||||
Hard_Rock_0_to_1_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_1_5_to_3_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_3_0_to_4_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_4_5_to_6_0 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_6_0_to_7_5 = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
# Totals
|
||||
Soft_Murum_0_to_1_5_total = db.Column(db.Float)
|
||||
Soft_Murum_1_5_to_3_0_total = db.Column(db.Float)
|
||||
Soft_Murum_3_0_to_4_5_total = db.Column(db.Float)
|
||||
Soft_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Murum_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Hard_Murum_0_to_1_5_total = db.Column(db.Float)
|
||||
Hard_Murum_1_5_and_above_total = db.Column(db.Float)
|
||||
Hard_Murum_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Murum_1_5_and_above_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Soft_Rock_0_to_1_5_total = db.Column(db.Float)
|
||||
Soft_Rock_1_5_and_above_total = db.Column(db.Float)
|
||||
Soft_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Soft_Rock_1_5_and_above_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Hard_Rock_0_to_1_5_total = db.Column(db.Float)
|
||||
Hard_Rock_1_5_and_above_total = db.Column(db.Float)
|
||||
Hard_Rock_3_0_to_4_5_total = db.Column(db.Float)
|
||||
Hard_Rock_4_5_to_6_0_total = db.Column(db.Float)
|
||||
Hard_Rock_6_0_to_7_5_total = db.Column(db.Float)
|
||||
Hard_Rock_0_to_1_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_1_5_to_3_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_3_0_to_4_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_4_5_to_6_0_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
Hard_Rock_6_0_to_7_5_total = db.Column(db.Numeric(10, 2), default=Decimal("0.00"))
|
||||
|
||||
Remarks = db.Column(db.String(500))
|
||||
Total = db.Column(db.Float)
|
||||
Total = db.Column(db.Numeric(12, 2), default=Decimal("0.00"))
|
||||
Remarks = db.Column(db.String(500), default="Import File")
|
||||
created_at = db.Column(db.DateTime,default=datetime.now,nullable=False)
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
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_total(mapper, connection, target):
|
||||
total = Decimal("0.00")
|
||||
for column in target.__table__.columns:
|
||||
if RegularExpression.STR_TOTAL_PATTERN.match(column.name):
|
||||
value = getattr(target, column.name)
|
||||
if value is not None:
|
||||
total += Decimal(value)
|
||||
target.Total = total.quantize(Decimal("0.01"))
|
||||
|
||||
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 werkzeug.security import generate_password_hash, check_password_hash
|
||||
from app.services.db_service import db
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
# class User(db.Model):
|
||||
# id = db.Column(db.Integer, primary_key=True)
|
||||
# name = db.Column(db.String(120))
|
||||
# email = db.Column(db.String(120), unique=True)
|
||||
# password_hash = db.Column(db.String(255))
|
||||
class User(db.Model):
|
||||
__tablename__ = "users"
|
||||
|
||||
# def set_password(self, password):
|
||||
# self.password_hash = generate_password_hash(password)
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
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):
|
||||
# return check_password_hash(self.password_hash, password)
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
|
||||
class User:
|
||||
def __init__(self, id, name, email):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.email = email
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
21
app/models/width_model.py
Normal file
21
app/models/width_model.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from app import db
|
||||
|
||||
class Width(db.Model):
|
||||
__tablename__ = "width"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
Dia_mm = db.Column(db.Numeric(10, 2), default=0)
|
||||
|
||||
pipe_150_mm = db.Column(db.Numeric(10, 2), default=0)
|
||||
pipe_200_mm = db.Column(db.Numeric(10, 2), default=0)
|
||||
pipe_250_mm = db.Column(db.Numeric(10, 2), default=0)
|
||||
pipe_300_mm = db.Column(db.Numeric(10, 2), default=0)
|
||||
pipe_350_mm = db.Column(db.Numeric(10, 2), default=0)
|
||||
pipe_400_mm = db.Column(db.Numeric(10, 2), default=0)
|
||||
pipe_450_mm = db.Column(db.Numeric(10, 2), default=0)
|
||||
pipe_500_mm = db.Column(db.Numeric(10, 2), default=0)
|
||||
pipe_600_mm = db.Column(db.Numeric(10, 2), default=0)
|
||||
pipe_700_mm = db.Column(db.Numeric(10, 2), default=0)
|
||||
pipe_900_mm = db.Column(db.Numeric(10, 2), default=0)
|
||||
pipe_1200_mm = db.Column(db.Numeric(10, 2), default=0)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
59
app/routes/activity_routes.py
Normal file
59
app/routes/activity_routes.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from flask import Blueprint, render_template, request, send_file, abort
|
||||
import os
|
||||
|
||||
from app.utils.helpers import login_required
|
||||
from app.utils.file_utils import get_logs_folder , ALLOWED_LOG_FILE
|
||||
from app.services.activity_service import ActivityService
|
||||
|
||||
activity_bp = Blueprint("activity", __name__, url_prefix="/activity")
|
||||
|
||||
# call activity_log page
|
||||
@activity_bp.route("/")
|
||||
@login_required
|
||||
def activity():
|
||||
file_name = request.args.get("file", "app.log")
|
||||
search = request.args.get("search", "")
|
||||
level = request.args.get("level", "").upper()
|
||||
user = request.args.get("user", "")
|
||||
from_date = request.args.get("from_date", "")
|
||||
to_date = request.args.get("to_date", "")
|
||||
|
||||
logs = ActivityService.read_logs(
|
||||
file_name=file_name,
|
||||
search=search,
|
||||
level=level,
|
||||
user=user,
|
||||
from_date=from_date,
|
||||
to_date=to_date
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"activity/activity_log.html",
|
||||
logs=logs,
|
||||
file_name=file_name,
|
||||
search=search,
|
||||
level=level,
|
||||
user=user,
|
||||
from_date=from_date,
|
||||
to_date=to_date
|
||||
)
|
||||
|
||||
# Download activity_log files
|
||||
@activity_bp.route("/logs")
|
||||
@login_required
|
||||
def download_log():
|
||||
|
||||
filename = request.args.get("file", "app.log")
|
||||
if filename not in ALLOWED_LOG_FILE:
|
||||
abort(400, "Invalid log file.")
|
||||
|
||||
file_path = os.path.join(get_logs_folder(), filename)
|
||||
if not os.path.isfile(file_path):
|
||||
abort(404, "Log file not found.")
|
||||
|
||||
return send_file(
|
||||
file_path,
|
||||
as_attachment=True,
|
||||
download_name=filename,
|
||||
mimetype="text/plain"
|
||||
)
|
||||
@@ -1,31 +1,98 @@
|
||||
from flask import Blueprint, request, render_template, redirect, flash, session
|
||||
from app.services.user_service import UserService
|
||||
from flask import (Blueprint, render_template, request, redirect, url_for, flash, session, current_app)
|
||||
|
||||
auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
|
||||
from app.services.user_service import UserService
|
||||
from app.constants.messages import SuccessMessage, ErrorMessage
|
||||
from app.constants.http_status import HTTPStatus
|
||||
|
||||
# LOGIN PAGE
|
||||
auth_bp = Blueprint("auth", __name__)
|
||||
|
||||
|
||||
# ==========================
|
||||
# LOGIN
|
||||
# ==========================
|
||||
@auth_bp.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
|
||||
if session.get("user_id"):
|
||||
current_app.logger.info("User already logged in.")
|
||||
return redirect(url_for("dashboard.dashboard"))
|
||||
|
||||
if request.method == "POST":
|
||||
user = UserService.validate_login(
|
||||
request.form["email"],
|
||||
request.form["password"]
|
||||
)
|
||||
if user:
|
||||
session["user_id"] = user.id
|
||||
return redirect("/dashboard")
|
||||
flash("Invalid credentials", "danger")
|
||||
return render_template("login.html")
|
||||
|
||||
# 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
|
||||
try:
|
||||
email = request.form.get("email", "").strip()
|
||||
password = request.form.get("password", "")
|
||||
|
||||
if not email or not password:
|
||||
flash(ErrorMessage.INVALID_REQUEST, "danger")
|
||||
current_app.logger.warning("Login failed. Email or password missing.")
|
||||
return render_template("login.html", title="Login")
|
||||
|
||||
user = UserService.validate_login(email, password)
|
||||
|
||||
if user:
|
||||
session.clear()
|
||||
session["user_id"] = user.id
|
||||
session["user_name"] = user.name
|
||||
session["email"] = user.email
|
||||
session.permanent = True
|
||||
|
||||
current_app.logger.info(f"Login successful. User={user.name}")
|
||||
flash(SuccessMessage.LOGIN, "success")
|
||||
return redirect(url_for("dashboard.dashboard"))
|
||||
|
||||
current_app.logger.warning(f"Invalid login attempt. Email={email}")
|
||||
flash(ErrorMessage.LOGIN_FAILED,"danger")
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.exception("Login Error" )
|
||||
flash(ErrorMessage.INTERNAL_SERVER_ERROR,"danger")
|
||||
|
||||
return render_template("login.html", title="Login")
|
||||
|
||||
|
||||
# ==========================
|
||||
# LOGOUT
|
||||
# ==========================
|
||||
@auth_bp.route("/logout")
|
||||
def logout():
|
||||
username = session.get("user_name", "Unknown")
|
||||
session.clear()
|
||||
return redirect("/auth/login")
|
||||
current_app.logger.info(f"Logout successful. User={username}")
|
||||
flash(SuccessMessage.LOGOUT,"info")
|
||||
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
|
||||
# ==========================
|
||||
# REGISTER
|
||||
# ==========================
|
||||
@auth_bp.route("/register", methods=["GET", "POST"])
|
||||
def register():
|
||||
|
||||
if request.method == "POST":
|
||||
try:
|
||||
name = request.form.get("name", "").strip()
|
||||
email = request.form.get("email", "").strip()
|
||||
password = request.form.get("password", "")
|
||||
|
||||
if not name or not email or not password:
|
||||
flash(ErrorMessage.INVALID_REQUEST,"danger")
|
||||
return redirect(url_for("auth.register"))
|
||||
|
||||
user = UserService.register_user(name, email, password)
|
||||
|
||||
if not user:
|
||||
current_app.logger.warning(f"Duplicate Registration: {email}")
|
||||
flash(ErrorMessage.DUPLICATE_ENTRY,"danger")
|
||||
return redirect(url_for("auth.register"))
|
||||
|
||||
current_app.logger.info(f"New user registered: {email}")
|
||||
flash(SuccessMessage.CREATED,"success")
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
except Exception:
|
||||
current_app.logger.exception("User Registration Failed" )
|
||||
flash(ErrorMessage.INTERNAL_SERVER_ERROR,"danger")
|
||||
|
||||
return render_template("register.html",title="Register")
|
||||
@@ -1,8 +1,614 @@
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
dashboard_bp = Blueprint("dashboard", __name__)
|
||||
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.utils.helpers import login_required
|
||||
from app.services.dashboard_service import DashboardService
|
||||
from sqlalchemy import func
|
||||
from app import db
|
||||
from sqlalchemy import tuple_
|
||||
|
||||
# Subcontractor models import
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
dashboard_bp = Blueprint("dashboard", __name__, url_prefix="/dashboard")
|
||||
|
||||
|
||||
@dashboard_bp.route("/")
|
||||
@dashboard_bp.route("/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")
|
||||
@login_required
|
||||
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")
|
||||
@login_required
|
||||
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")
|
||||
@login_required
|
||||
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 "manhole_excavation":
|
||||
results = db.session.query(
|
||||
ManholeExcavation.RA_Bill_No
|
||||
).filter(
|
||||
ManholeExcavation.subcontractor_id == subcontractor_id
|
||||
).distinct().order_by(ManholeExcavation.RA_Bill_No).all()
|
||||
|
||||
case "Manhole_Domestic_Chamber":
|
||||
results = db.session.query(
|
||||
ManholeDomesticChamber.RA_Bill_No
|
||||
).filter(
|
||||
ManholeDomesticChamber.subcontractor_id == subcontractor_id
|
||||
).distinct().order_by(ManholeDomesticChamber.RA_Bill_No).all()
|
||||
|
||||
case "Laying":
|
||||
results = db.session.query(
|
||||
Laying.RA_Bill_No
|
||||
).filter(
|
||||
Laying.subcontractor_id == subcontractor_id
|
||||
).distinct().order_by(Laying.RA_Bill_No).all()
|
||||
|
||||
ra_bills = [r[0] for r in results if r[0]]
|
||||
|
||||
return {"ra_bills": ra_bills}
|
||||
|
||||
|
||||
def total(records, field):
|
||||
return float(sum(getattr(r, field) or 0 for r in records))
|
||||
|
||||
# category= trench_excavation
|
||||
@dashboard_bp.route("/api/tr-analysis")
|
||||
def trench_analysis():
|
||||
|
||||
subcontractor_id = request.args.get("subcontractor", "").strip()
|
||||
ra_bill = request.args.get("ra_bill", "").strip()
|
||||
|
||||
# Convert "1,2,3" -> ["1", "2", "3"]
|
||||
ra_bill_list = []
|
||||
if ra_bill:
|
||||
ra_bill_list = [x.strip() for x in ra_bill.split(",") if x.strip()]
|
||||
|
||||
# Subcontractor Query
|
||||
sub_query = TrenchExcavation.query
|
||||
if subcontractor_id:
|
||||
sub_query = sub_query.filter(
|
||||
TrenchExcavation.subcontractor_id == int(subcontractor_id)
|
||||
)
|
||||
|
||||
if ra_bill_list:
|
||||
sub_query = sub_query.filter(
|
||||
TrenchExcavation.RA_Bill_No.in_(ra_bill_list)
|
||||
)
|
||||
|
||||
sub_records = sub_query.all()
|
||||
|
||||
sub_keys = [
|
||||
(
|
||||
(r.MH_NO or "").strip().upper(),
|
||||
(r.Location or "").strip().upper()
|
||||
)
|
||||
for r in sub_records
|
||||
]
|
||||
|
||||
client_query = TrenchExcavationClient.query
|
||||
|
||||
if sub_keys:
|
||||
|
||||
client_query = client_query.filter(
|
||||
tuple_(
|
||||
func.upper(func.trim(TrenchExcavationClient.MH_NO)),
|
||||
func.upper(func.trim(TrenchExcavationClient.Location))
|
||||
).in_(sub_keys)
|
||||
|
||||
)
|
||||
|
||||
client_records = client_query.all()
|
||||
|
||||
chart_data = [
|
||||
{
|
||||
"label": "Marshi 0 to 1.5",
|
||||
"client": total(client_records, "Marshi_Muddy_Slushy_0_to_1_5_total"),
|
||||
"sub": 0
|
||||
},
|
||||
{
|
||||
"label": "Marshi 1.5 to 3.0",
|
||||
"client": total(client_records, "Marshi_Muddy_Slushy_1_5_to_3_0_total"),
|
||||
"sub": 0
|
||||
},
|
||||
{
|
||||
"label": "Marshi 3.0 to 4.5",
|
||||
"client": total(client_records, "Marshi_Muddy_Slushy_3_0_to_4_5_total"),
|
||||
"sub": 0
|
||||
},
|
||||
|
||||
{
|
||||
"label": "Soft Murum 0 to 1.5",
|
||||
"client": total(client_records, "Soft_Murum_0_to_1_5_total"),
|
||||
"sub": total(sub_records, "Soft_Murum_0_to_1_5_total")
|
||||
},
|
||||
{
|
||||
"label": "Soft Murum 1.5 to 3.0",
|
||||
"client": total(client_records, "Soft_Murum_1_5_to_3_0_total"),
|
||||
"sub": total(sub_records, "Soft_Murum_1_5_to_3_0_total")
|
||||
},
|
||||
{
|
||||
"label": "Soft Murum 3.0 to 4.5",
|
||||
"client": total(client_records, "Soft_Murum_3_0_to_4_5_total"),
|
||||
"sub": total(sub_records, "Soft_Murum_3_0_to_4_5_total")
|
||||
},
|
||||
|
||||
{
|
||||
"label": "Hard Murum 0 to 1.5",
|
||||
"client": total(client_records, "Hard_Murum_0_to_1_5_total"),
|
||||
"sub": total(sub_records, "Hard_Murum_0_to_1_5_total")
|
||||
},
|
||||
{
|
||||
"label": "Hard Murum 1.5 to 3.0",
|
||||
"client": total(client_records, "Hard_Murum_1_5_to_3_0_total"),
|
||||
"sub": total(sub_records, "Hard_Murum_1_5_and_above_total")
|
||||
},
|
||||
|
||||
{
|
||||
"label": "Soft Rock 0 to 1.5",
|
||||
"client": total(client_records, "Soft_Rock_0_to_1_5_total"),
|
||||
"sub": total(sub_records, "Soft_Rock_0_to_1_5_total")
|
||||
},
|
||||
{
|
||||
"label": "Soft Rock 1.5 to 3.0",
|
||||
"client": total(client_records, "Soft_Rock_1_5_to_3_0_total"),
|
||||
"sub": total(sub_records, "Soft_Rock_1_5_and_above_total")
|
||||
},
|
||||
|
||||
{
|
||||
"label": "Hard Rock 0 to 1.5",
|
||||
"client": total(client_records, "Hard_Rock_0_to_1_5_total"),
|
||||
"sub": total(sub_records, "Hard_Rock_0_to_1_5_total")
|
||||
},
|
||||
{
|
||||
"label": "Hard Rock 1.5 to 3.0",
|
||||
"client": total(client_records, "Hard_Rock_1_5_to_3_0_total"),
|
||||
"sub": total(sub_records, "Hard_Rock_1_5_to_3_0_total")
|
||||
},
|
||||
{
|
||||
"label": "Hard Rock 3.0 to 4.5",
|
||||
"client": total(client_records, "Hard_Rock_3_0_to_4_5_total"),
|
||||
"sub": total(sub_records, "Hard_Rock_3_0_to_4_5_total")
|
||||
},
|
||||
{
|
||||
"label": "Hard Rock 4.5 to 6.0",
|
||||
"client": total(client_records, "Hard_Rock_4_5_to_6_0_total"),
|
||||
"sub": total(sub_records, "Hard_Rock_4_5_to_6_0_total")
|
||||
},
|
||||
{
|
||||
"label": "Hard Rock 6.0 to 7.5",
|
||||
"client": total(client_records, "Hard_Rock_6_0_to_7_5_total"),
|
||||
"sub": total(sub_records, "Hard_Rock_6_0_to_7_5_total")
|
||||
}
|
||||
]
|
||||
|
||||
return jsonify({
|
||||
"title": "Trench Excavation Comparison",
|
||||
"y_title": "Excavation Qty (Cum)",
|
||||
"labels": [x["label"] for x in chart_data],
|
||||
"client_qty": [x["client"] for x in chart_data],
|
||||
"sub_qty": [x["sub"] for x in chart_data]
|
||||
})
|
||||
|
||||
# category = manhole_excavation
|
||||
@dashboard_bp.route("/api/mh-analysis")
|
||||
def manhole_analysis():
|
||||
|
||||
subcontractor_id = request.args.get("subcontractor", "").strip()
|
||||
ra_bill = request.args.get("ra_bill", "").strip()
|
||||
|
||||
# Convert "1,2,3" -> ["1", "2", "3"]
|
||||
ra_bill_list = []
|
||||
if ra_bill:
|
||||
ra_bill_list = [x.strip() for x in ra_bill.split(",") if x.strip()]
|
||||
|
||||
# Subcontractor Query
|
||||
sub_query = ManholeExcavation.query
|
||||
if subcontractor_id:
|
||||
sub_query = sub_query.filter(
|
||||
ManholeExcavation.subcontractor_id == int(subcontractor_id)
|
||||
)
|
||||
|
||||
if ra_bill_list:
|
||||
sub_query = sub_query.filter(
|
||||
ManholeExcavation.RA_Bill_No.in_(ra_bill_list)
|
||||
)
|
||||
|
||||
sub_records = sub_query.all()
|
||||
|
||||
sub_keys = [
|
||||
(
|
||||
(r.MH_NO or "").strip().upper(),
|
||||
(r.Location or "").strip().upper()
|
||||
)
|
||||
for r in sub_records
|
||||
]
|
||||
|
||||
client_query = ManholeExcavationClient.query
|
||||
|
||||
if sub_keys:
|
||||
|
||||
client_query = client_query.filter(
|
||||
tuple_(
|
||||
func.upper(func.trim(ManholeExcavationClient.MH_NO)),
|
||||
func.upper(func.trim(ManholeExcavationClient.Location))
|
||||
).in_(sub_keys)
|
||||
|
||||
)
|
||||
|
||||
client_records = client_query.all()
|
||||
|
||||
chart_data = [
|
||||
{
|
||||
"label": "Marshi 0 to 1.5",
|
||||
"client": total(client_records, "Marshi_Muddy_Slushy_0_to_1_5_total"),
|
||||
"sub": 0
|
||||
},
|
||||
{
|
||||
"label": "Marshi 1.5 to 3.0",
|
||||
"client": total(client_records, "Marshi_Muddy_Slushy_1_5_to_3_0_total"),
|
||||
"sub": 0
|
||||
},
|
||||
{
|
||||
"label": "Marshi 3.0 to 4.5",
|
||||
"client": total(client_records, "Marshi_Muddy_Slushy_3_0_to_4_5_total"),
|
||||
"sub": 0
|
||||
},
|
||||
|
||||
{
|
||||
"label": "Soft Murum 0 to 1.5",
|
||||
"client": total(client_records, "Soft_Murum_0_to_1_5_total"),
|
||||
"sub": total(sub_records, "Soft_Murum_0_to_1_5_total")
|
||||
},
|
||||
{
|
||||
"label": "Soft Murum 1.5 to 3.0",
|
||||
"client": total(client_records, "Soft_Murum_1_5_to_3_0_total"),
|
||||
"sub": total(sub_records, "Soft_Murum_1_5_to_3_0_total")
|
||||
},
|
||||
{
|
||||
"label": "Soft Murum 3.0 to 4.5",
|
||||
"client": total(client_records, "Soft_Murum_3_0_to_4_5_total"),
|
||||
"sub": total(sub_records, "Soft_Murum_3_0_to_4_5_total")
|
||||
},
|
||||
|
||||
{
|
||||
"label": "Hard Murum 0 to 1.5",
|
||||
"client": total(client_records, "Hard_Murum_0_to_1_5_total"),
|
||||
"sub": total(sub_records, "Hard_Murum_0_to_1_5_total")
|
||||
},
|
||||
{
|
||||
"label": "Hard Murum 1.5 to 3.0",
|
||||
"client": total(client_records, "Hard_Murum_1_5_to_3_0_total"),
|
||||
"sub": total(sub_records, "Hard_Murum_1_5_and_above_total")
|
||||
},
|
||||
|
||||
{
|
||||
"label": "Soft Rock 0 to 1.5",
|
||||
"client": total(client_records, "Soft_Rock_0_to_1_5_total"),
|
||||
"sub": total(sub_records, "Soft_Rock_0_to_1_5_total")
|
||||
},
|
||||
{
|
||||
"label": "Soft Rock 1.5 to 3.0",
|
||||
"client": total(client_records, "Soft_Rock_1_5_to_3_0_total"),
|
||||
"sub": total(sub_records, "Soft_Rock_1_5_and_above_total")
|
||||
},
|
||||
|
||||
{
|
||||
"label": "Hard Rock 0 to 1.5",
|
||||
"client": total(client_records, "Hard_Rock_0_to_1_5_total"),
|
||||
"sub": total(sub_records, "Hard_Rock_0_to_1_5_total")
|
||||
},
|
||||
{
|
||||
"label": "Hard Rock 1.5 to 3.0",
|
||||
"client": total(client_records, "Hard_Rock_1_5_to_3_0_total"),
|
||||
"sub": total(sub_records, "Hard_Rock_1_5_to_3_0_total")
|
||||
},
|
||||
{
|
||||
"label": "Hard Rock 3.0 to 4.5",
|
||||
"client": total(client_records, "Hard_Rock_3_0_to_4_5_total"),
|
||||
"sub": total(sub_records, "Hard_Rock_3_0_to_4_5_total")
|
||||
},
|
||||
{
|
||||
"label": "Hard Rock 4.5 to 6.0",
|
||||
"client": total(client_records, "Hard_Rock_4_5_to_6_0_total"),
|
||||
"sub": total(sub_records, "Hard_Rock_4_5_to_6_0_total")
|
||||
},
|
||||
{
|
||||
"label": "Hard Rock 6.0 to 7.5",
|
||||
"client": total(client_records, "Hard_Rock_6_0_to_7_5_total"),
|
||||
"sub": total(sub_records, "Hard_Rock_6_0_to_7_5_total")
|
||||
}
|
||||
]
|
||||
|
||||
return jsonify({
|
||||
"title": "Manhole Excavation Comparison",
|
||||
"y_title": "Manhole Qty (Nos)",
|
||||
"labels": [x["label"] for x in chart_data],
|
||||
"client_qty": [x["client"] for x in chart_data],
|
||||
"sub_qty": [x["sub"] for x in chart_data]
|
||||
})
|
||||
|
||||
|
||||
|
||||
# category = Manhole_Domestic_Chamber
|
||||
@dashboard_bp.route("/api/mdc-analysis")
|
||||
def Manhole_Domestic_Chamber_analysis():
|
||||
|
||||
subcontractor_id = request.args.get("subcontractor", "").strip()
|
||||
ra_bill = request.args.get("ra_bill", "").strip()
|
||||
|
||||
# Convert "1,2,3" -> ["1", "2", "3"]
|
||||
ra_bill_list = []
|
||||
if ra_bill:
|
||||
ra_bill_list = [x.strip() for x in ra_bill.split(",") if x.strip()]
|
||||
|
||||
# Subcontractor Query
|
||||
sub_query = ManholeDomesticChamber.query
|
||||
if subcontractor_id:
|
||||
sub_query = sub_query.filter(
|
||||
ManholeDomesticChamber.subcontractor_id == int(subcontractor_id)
|
||||
)
|
||||
|
||||
if ra_bill_list:
|
||||
sub_query = sub_query.filter(
|
||||
ManholeDomesticChamber.RA_Bill_No.in_(ra_bill_list)
|
||||
)
|
||||
|
||||
sub_records = sub_query.all()
|
||||
|
||||
sub_keys = [
|
||||
(
|
||||
(r.MH_NO or "").strip().upper(),
|
||||
(r.Location or "").strip().upper()
|
||||
)
|
||||
for r in sub_records
|
||||
]
|
||||
|
||||
client_query = ManholeDomesticChamberClient.query
|
||||
|
||||
if sub_keys:
|
||||
|
||||
client_query = client_query.filter(
|
||||
tuple_(
|
||||
func.upper(func.trim(ManholeDomesticChamberClient.MH_NO)),
|
||||
func.upper(func.trim(ManholeDomesticChamberClient.Location))
|
||||
).in_(sub_keys)
|
||||
|
||||
)
|
||||
|
||||
client_records = client_query.all()
|
||||
|
||||
chart_data = [
|
||||
{
|
||||
"label": "Depth of MH",
|
||||
"client": total(client_records, "Depth_of_MH"),
|
||||
"sub": total(sub_records, "Depth_of_MH")
|
||||
},
|
||||
{
|
||||
"label": "Domestic_Chambers Total",
|
||||
"client": total(client_records, "Total"),
|
||||
"sub": total(sub_records, "Total")
|
||||
}
|
||||
]
|
||||
|
||||
return jsonify({
|
||||
"title": "Domestic Chamber Comparison",
|
||||
"y_title": "Quantity (Nos)",
|
||||
"labels": [x["label"] for x in chart_data],
|
||||
"client_qty": [x["client"] for x in chart_data],
|
||||
"sub_qty": [x["sub"] for x in chart_data]
|
||||
})
|
||||
|
||||
|
||||
|
||||
# category = Laying
|
||||
@dashboard_bp.route("/api/laying-analysis")
|
||||
def laying_analysis():
|
||||
|
||||
subcontractor_id = request.args.get("subcontractor", "").strip()
|
||||
ra_bill = request.args.get("ra_bill", "").strip()
|
||||
|
||||
# Convert "1,2,3" -> ["1", "2", "3"]
|
||||
ra_bill_list = []
|
||||
if ra_bill:
|
||||
ra_bill_list = [x.strip() for x in ra_bill.split(",") if x.strip()]
|
||||
|
||||
# Subcontractor Query
|
||||
sub_query = Laying.query
|
||||
if subcontractor_id:
|
||||
sub_query = sub_query.filter(
|
||||
Laying.subcontractor_id == int(subcontractor_id)
|
||||
)
|
||||
|
||||
if ra_bill_list:
|
||||
sub_query = sub_query.filter(
|
||||
Laying.RA_Bill_No.in_(ra_bill_list)
|
||||
)
|
||||
|
||||
sub_records = sub_query.all()
|
||||
|
||||
sub_keys = [
|
||||
(
|
||||
(r.MH_NO or "").strip().upper(),
|
||||
(r.Location or "").strip().upper()
|
||||
)
|
||||
for r in sub_records
|
||||
]
|
||||
|
||||
client_query = LayingClient.query
|
||||
|
||||
if sub_keys:
|
||||
|
||||
client_query = client_query.filter(
|
||||
tuple_(
|
||||
func.upper(func.trim(LayingClient.MH_NO)),
|
||||
func.upper(func.trim(LayingClient.Location))
|
||||
).in_(sub_keys)
|
||||
|
||||
)
|
||||
|
||||
client_records = client_query.all()
|
||||
|
||||
chart_data = [
|
||||
{
|
||||
"label": "150 mm",
|
||||
"client": total(client_records, "pipe_150_mm"),
|
||||
"sub": total(sub_records, "pipe_150_mm")
|
||||
},
|
||||
{
|
||||
"label": "200 mm",
|
||||
"client": total(client_records, "pipe_200_mm"),
|
||||
"sub": total(sub_records, "pipe_200_mm")
|
||||
},
|
||||
{
|
||||
"label": "250 mm",
|
||||
"client": total(client_records, "pipe_250_mm"),
|
||||
"sub": total(sub_records, "pipe_250_mm")
|
||||
},
|
||||
|
||||
{
|
||||
"label": "300 mm",
|
||||
"client": total(client_records, "pipe_300_mm"),
|
||||
"sub": total(sub_records, "pipe_300_mm")
|
||||
},
|
||||
{
|
||||
"label": "350 mm",
|
||||
"client": total(client_records, "pipe_350_mm"),
|
||||
"sub": total(sub_records, "pipe_350_mm")
|
||||
},
|
||||
|
||||
{
|
||||
"label": "400 mm",
|
||||
"client": total(client_records, "pipe_400_mm"),
|
||||
"sub": total(sub_records, "pipe_400_mm")
|
||||
},
|
||||
{
|
||||
"label": "450 mm",
|
||||
"client": total(client_records, "pipe_450_mm"),
|
||||
"sub": total(sub_records, "pipe_450_mm")
|
||||
},
|
||||
|
||||
{
|
||||
"label": "500 mm",
|
||||
"client": total(client_records, "pipe_500_mm"),
|
||||
"sub": total(sub_records, "pipe_500_mm")
|
||||
},
|
||||
{
|
||||
"label": "600 mm",
|
||||
"client": total(client_records, "pipe_600_mm"),
|
||||
"sub": total(sub_records, "pipe_600_mm")
|
||||
},
|
||||
{
|
||||
"label": "700 mm",
|
||||
"client": total(client_records, "pipe_700_mm"),
|
||||
"sub": total(sub_records, "pipe_700_mm")
|
||||
},
|
||||
{
|
||||
"label": "900 mm",
|
||||
"client": total(client_records, "pipe_900_mm"),
|
||||
"sub": total(sub_records, "pipe_900_mm")
|
||||
},
|
||||
{
|
||||
"label": "1200 mm",
|
||||
"client": total(client_records, "pipe_1200_mm"),
|
||||
"sub": total(sub_records, "pipe_1200_mm")
|
||||
}
|
||||
]
|
||||
|
||||
return jsonify({
|
||||
"title": "Pipe Laying Comparison",
|
||||
"y_title": "Pipe Length (Mtr)",
|
||||
"labels": [x["label"] for x in chart_data],
|
||||
"client_qty": [x["client"] for x in chart_data],
|
||||
"sub_qty": [x["sub"] for x in chart_data]
|
||||
})
|
||||
|
||||
|
||||
|
||||
107
app/routes/engineering_master_routes.py
Normal file
107
app/routes/engineering_master_routes.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from flask import (
|
||||
Blueprint,
|
||||
render_template,
|
||||
request,
|
||||
redirect,
|
||||
url_for,
|
||||
flash, jsonify
|
||||
)
|
||||
|
||||
from app.models.subcontractor_model import Subcontractor
|
||||
from app.services.subcontractor_rate_service import SubcontractorRateService
|
||||
from app.constants.messages import SuccessMessage, ErrorMessage
|
||||
|
||||
engi_bp = Blueprint(
|
||||
"engineering",
|
||||
__name__,
|
||||
url_prefix="/engi"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@engi_bp.route("/")
|
||||
def engineering_master():
|
||||
|
||||
return render_template(
|
||||
"engineering/index.html",
|
||||
title="Engineering Masters"
|
||||
)
|
||||
|
||||
# Client rate model
|
||||
@engi_bp.route("/client-rate")
|
||||
def client_rates():
|
||||
|
||||
return render_template(
|
||||
"engineering/client_rate.html",
|
||||
title="Client Rate Master"
|
||||
)
|
||||
|
||||
@engi_bp.route("/subcontractor-rate", methods=["GET", "POST"])
|
||||
def add_subcontractor_rates():
|
||||
|
||||
subcontractors = Subcontractor.query.filter_by(status="Active").all()
|
||||
|
||||
if request.method == "POST":
|
||||
result = SubcontractorRateService.save_or_update(request.form)
|
||||
if result["success"]:
|
||||
flash(result["message"], "success")
|
||||
return redirect(url_for("engineering.add_subcontractor_rates"))
|
||||
else:
|
||||
flash(result["message"], "danger")
|
||||
|
||||
rates = SubcontractorRateService.get_all_rates()
|
||||
|
||||
return render_template(
|
||||
"engineering/contractor_rate.html",
|
||||
title="Subcontractor Rate Master",
|
||||
subcontractors=subcontractors,
|
||||
rates=rates
|
||||
)
|
||||
|
||||
@engi_bp.route("/subcontractor-rate/edit/<int:rate_id>", methods=["GET", "POST"])
|
||||
def edit_rate(rate_id):
|
||||
|
||||
subcontractors = Subcontractor.query.filter_by(status="Active").all()
|
||||
|
||||
if request.method == "POST":
|
||||
|
||||
result = SubcontractorRateService.save_or_update(request.form)
|
||||
|
||||
if result["success"]:
|
||||
flash(result["message"], "success")
|
||||
return redirect(url_for("engineering.add_subcontractor_rates"))
|
||||
|
||||
flash(result["message"], "danger")
|
||||
|
||||
rate = SubcontractorRateService.get_rate(rate_id)
|
||||
|
||||
rates = SubcontractorRateService.get_all_rates()
|
||||
|
||||
return render_template(
|
||||
"engineering/contractor_rate.html",
|
||||
subcontractors=subcontractors,
|
||||
rate=rate,
|
||||
rates=rates
|
||||
)
|
||||
|
||||
@engi_bp.route("/subcontractor-rate/delete/<int:rate_id>")
|
||||
def delete_rate(rate_id):
|
||||
SubcontractorRateService.delete_rate(rate_id)
|
||||
flash("Rate deleted successfully.", "success")
|
||||
return redirect(url_for("engineering.add_subcontractor_rates"))
|
||||
|
||||
|
||||
@engi_bp.route("/check-rate")
|
||||
def check_rate():
|
||||
|
||||
exists = SubcontractorRateService.check_duplicate(
|
||||
subcontractor_id=request.args.get("subcontractor_id"),
|
||||
category=request.args.get("category"),
|
||||
item_name=request.args.get("item_name"),
|
||||
rate_id=request.args.get("rate_id")
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"exists": exists
|
||||
})
|
||||
|
||||
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 app.services.file_service import FileService
|
||||
from app.models.subcontractor_model import Subcontractor
|
||||
from app.utils.helpers import login_required
|
||||
|
||||
file_import_bp = Blueprint("file_import", __name__, url_prefix="/file")
|
||||
|
||||
@file_import_bp.route("/import", methods=["GET", "POST"])
|
||||
# this is contractractor immport routes
|
||||
@file_import_bp.route("/import_Subcontractor", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def import_file():
|
||||
subcontractors = Subcontractor.query.all()
|
||||
|
||||
if request.method == "POST":
|
||||
file = request.files.get("file")
|
||||
subcontractor_id = request.form.get("subcontractor_id")
|
||||
file_type = request.form.get("file_type")
|
||||
RA_Bill_No = request.form.get("RA_Bill_No")
|
||||
|
||||
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")
|
||||
|
||||
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)
|
||||
@@ -1,22 +1,596 @@
|
||||
# app/routes/file_report.py
|
||||
import pandas as pd
|
||||
import io
|
||||
from flask import Blueprint, render_template, request, send_file, flash, jsonify,redirect, url_for
|
||||
from app.utils.helpers import login_required
|
||||
from app.utils.regex_utils import RegularExpression
|
||||
from app import db
|
||||
import re
|
||||
|
||||
from flask import Blueprint, render_template, request, flash
|
||||
from app.models.subcontractor_model import Subcontractor
|
||||
from app.models.manhole_excavation_model import ManholeExcavation
|
||||
from app.models.trench_excavation_model import TrenchExcavation
|
||||
from app.models.manhole_domestic_chamber_model import ManholeDomesticChamber
|
||||
from app.models.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
|
||||
|
||||
from app.services.abstract_service import AbstractReportService
|
||||
|
||||
|
||||
# --- BLUEPRINT DEFINITION ---
|
||||
file_report_bp = Blueprint("file_report", __name__, url_prefix="/file")
|
||||
|
||||
|
||||
@file_report_bp.route("/report", methods=["GET", "POST"])
|
||||
def report_file():
|
||||
subcontractors = Subcontractor.query.all()
|
||||
# ---------------- ACTION COLUMN ----------------
|
||||
def add_action_columns(df, model_key):
|
||||
if df.empty:
|
||||
return df
|
||||
|
||||
# Edit + Delete side by side in one "Action" column, both as icon buttons.
|
||||
df.insert(0, "Action", df["Id"].apply(
|
||||
lambda x: (
|
||||
f'<div class="d-flex gap-1">'
|
||||
f'<a href="/file/edit/{model_key}/{x}" class="btn btn-sm btn-warning edit-btn" title="Edit">'
|
||||
f'<i class="bi bi-pencil-square"></i></a>'
|
||||
f'<button class="btn btn-sm btn-danger delete-btn" data-id="{x}" data-model="{model_key}" title="Delete">'
|
||||
f'<i class="bi bi-trash"></i></button>'
|
||||
f'</div>'
|
||||
)
|
||||
))
|
||||
|
||||
df.insert(1, "Select", df["Id"].apply(
|
||||
lambda x: f'<input type="checkbox" class="row-check" data-id="{x}">'
|
||||
))
|
||||
|
||||
df["Id"] = range(1, len(df) + 1)
|
||||
df = df.rename(columns={"Id": "Sr No"})
|
||||
|
||||
return df
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ---------------- FETCH ----------------
|
||||
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()
|
||||
# self.df_abstract = pd.DataFrame() # NEW
|
||||
|
||||
def Fetch(self, RA_Bill_No=None, subcontractor_id=None, location=None):
|
||||
|
||||
filters = {}
|
||||
if subcontractor_id:
|
||||
filters["subcontractor_id"] = subcontractor_id
|
||||
if RA_Bill_No:
|
||||
filters["RA_Bill_No"] = RA_Bill_No
|
||||
|
||||
# Fetch data in database
|
||||
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()
|
||||
|
||||
# LOCATION FILTER
|
||||
if location:
|
||||
search = location.strip().lower()
|
||||
print("location::",search)
|
||||
trench = [
|
||||
t for t in trench
|
||||
if search in (t.Location or "").strip().lower()
|
||||
]
|
||||
|
||||
mh = [
|
||||
t for t in mh
|
||||
if search in (t.Location or "").strip().lower()
|
||||
]
|
||||
|
||||
dc = [
|
||||
t for t in dc
|
||||
if search in (t.Location or "").strip().lower()
|
||||
]
|
||||
|
||||
lay = [
|
||||
t for t in lay
|
||||
if search in (t.Location or "").strip().lower()
|
||||
]
|
||||
|
||||
# Set dataframe
|
||||
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 = ["11", "_sa_instance_state", "subcontractor_id" , "created_at"]
|
||||
|
||||
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)
|
||||
format_column_names(df)
|
||||
|
||||
name = ""
|
||||
if subcontractor_id:
|
||||
sc = Subcontractor.query.get(subcontractor_id)
|
||||
if sc:
|
||||
name = sc.subcontractor_name
|
||||
|
||||
|
||||
# ---------------- DELETE ----------------
|
||||
@file_report_bp.route("/delete_records", methods=["POST"])
|
||||
@login_required
|
||||
def delete_records():
|
||||
|
||||
data = request.json or {}
|
||||
model = data.get("model")
|
||||
ids = data.get("ids", [])
|
||||
|
||||
model_map = {
|
||||
"tr": TrenchExcavation,
|
||||
"mh": ManholeExcavation,
|
||||
"dc": ManholeDomesticChamber,
|
||||
"laying": Laying
|
||||
}
|
||||
|
||||
ModelClass = model_map.get(model)
|
||||
|
||||
# validate model BEFORE using it
|
||||
if not ModelClass:
|
||||
return jsonify({"status": "error", "message": f"Invalid model '{model}'"}), 400
|
||||
|
||||
if not ids:
|
||||
return jsonify({"status": "error", "message": "No IDs provided"}), 400
|
||||
|
||||
try:
|
||||
for record_id in ids:
|
||||
obj = ModelClass.query.get(record_id)
|
||||
if obj:
|
||||
db.session.delete(obj)
|
||||
|
||||
db.session.commit()
|
||||
return jsonify({"status": "success"})
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({"status": "error", "message": str(e)}), 500
|
||||
|
||||
|
||||
@file_report_bp.route("/edit/<string:model>/<int:record_id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_record(model, record_id):
|
||||
|
||||
model_map = {
|
||||
"tr": TrenchExcavation,
|
||||
"mh": ManholeExcavation,
|
||||
"dc": ManholeDomesticChamber,
|
||||
"laying": Laying
|
||||
}
|
||||
|
||||
ModelClass = model_map.get(model)
|
||||
|
||||
if not ModelClass:
|
||||
flash("Invalid Model.", "danger")
|
||||
return redirect(url_for("file_report.report_file"))
|
||||
|
||||
record = ModelClass.query.get_or_404(record_id)
|
||||
|
||||
if request.method == "POST":
|
||||
|
||||
# Update all fields except id
|
||||
for column in record.__table__.columns:
|
||||
|
||||
if column.name == "id":
|
||||
continue
|
||||
|
||||
if column.name in request.form:
|
||||
setattr(record, column.name, request.form.get(column.name))
|
||||
|
||||
try:
|
||||
db.session.commit()
|
||||
flash("Record updated successfully.", "success")
|
||||
# ✅ fixed: correct blueprint name
|
||||
return redirect(url_for("file_report.report_file"))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(str(e), "danger")
|
||||
|
||||
return render_template(
|
||||
"edit_record.html",
|
||||
record=record,
|
||||
model=model
|
||||
)
|
||||
|
||||
|
||||
@file_report_bp.route("/Subcontractor_report", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def report_file():
|
||||
# get all subcontractor data
|
||||
subcontractors = Subcontractor.query.all()
|
||||
|
||||
tables = None
|
||||
abstract_html = ""
|
||||
selected_sc_id = None
|
||||
ra_bill_no = ""
|
||||
location = ""
|
||||
category = ""
|
||||
|
||||
# Search or load data
|
||||
if request.method == "POST":
|
||||
# get from data
|
||||
subcontractor_id = request.form.get("subcontractor_id")
|
||||
ra_bill_no = request.form.get("ra_bill_no", "").strip()
|
||||
location = request.form.get("location", "").strip()
|
||||
category = request.form.get("category", "")
|
||||
action = request.form.get("action", "preview")
|
||||
|
||||
if not subcontractor_id:
|
||||
flash("Please select a subcontractor.", "danger")
|
||||
flash("Select Subcontractor", "danger")
|
||||
return render_template(
|
||||
"subcontractor_report.html",
|
||||
subcontractors=subcontractors
|
||||
)
|
||||
|
||||
selected_sc_id = subcontractor_id
|
||||
bill = SubcontractorBill()
|
||||
|
||||
if action == "excel_all":
|
||||
bill.Fetch(subcontractor_id=subcontractor_id)
|
||||
else:
|
||||
flash(f"Report generated for Subcontractor ID: {subcontractor_id}", "success")
|
||||
bill.Fetch(ra_bill_no,subcontractor_id,location)
|
||||
|
||||
return render_template("report.html", title="Report Download", subcontractors=subcontractors)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
if (
|
||||
bill.df_tr.empty and
|
||||
bill.df_mh.empty and
|
||||
bill.df_dc.empty and
|
||||
bill.df_laying.empty
|
||||
):
|
||||
flash(
|
||||
f"No records found for RA Bill No '{ra_bill_no}'. "
|
||||
"Please check the RA Bill No and try again."
|
||||
if ra_bill_no else
|
||||
"No records found for the selected filters.",
|
||||
"warning"
|
||||
)
|
||||
return render_template(
|
||||
"subcontractor_report.html",
|
||||
subcontractors=subcontractors,
|
||||
selected_sc_id=selected_sc_id,
|
||||
selected_ra_bill=ra_bill_no,
|
||||
selected_location=location,
|
||||
selected_category=category
|
||||
)
|
||||
|
||||
# -----------------------------------------
|
||||
# Generate Abstract Report for Web
|
||||
# -----------------------------------------
|
||||
abstract_service = AbstractReportService(
|
||||
subcontractor_id=subcontractor_id,
|
||||
ra_bill_no=ra_bill_no
|
||||
)
|
||||
abstract_html = abstract_service.generate_html()
|
||||
|
||||
return render_template("report.html", title="Report Download", subcontractors=subcontractors)
|
||||
# ---------------- CATEGORY FILTER ----------------
|
||||
if category == "tr":
|
||||
bill.df_mh = bill.df_dc = bill.df_laying = pd.DataFrame()
|
||||
elif category == "mh":
|
||||
bill.df_tr = bill.df_dc = bill.df_laying = pd.DataFrame()
|
||||
elif category == "dc":
|
||||
bill.df_tr = bill.df_mh = bill.df_laying = pd.DataFrame()
|
||||
elif category == "laying":
|
||||
bill.df_tr = bill.df_mh = bill.df_dc = pd.DataFrame()
|
||||
|
||||
|
||||
if (
|
||||
category in ("tr", "mh", "dc", "laying")
|
||||
and bill.df_tr.empty and bill.df_mh.empty
|
||||
and bill.df_dc.empty and bill.df_laying.empty
|
||||
):
|
||||
category_labels = {
|
||||
"tr": "Trench Excavation",
|
||||
"mh": "Manhole Excavation",
|
||||
"dc": "Domestic Chamber",
|
||||
"laying": "Pipe Laying",
|
||||
}
|
||||
flash(
|
||||
f"No {category_labels[category]} records found for the "
|
||||
"selected filters.",
|
||||
"warning"
|
||||
)
|
||||
return render_template(
|
||||
"subcontractor_report.html",
|
||||
subcontractors=subcontractors,
|
||||
selected_sc_id=selected_sc_id,
|
||||
selected_ra_bill=ra_bill_no,
|
||||
selected_location=location,
|
||||
selected_category=category
|
||||
)
|
||||
|
||||
|
||||
# ===================================================
|
||||
# DOWNLOAD EXCEL
|
||||
# ===================================================
|
||||
if action in ["excel", "excel_all"]:
|
||||
output = io.BytesIO()
|
||||
|
||||
with pd.ExcelWriter(output,engine="xlsxwriter") as writer:
|
||||
workbook = writer.book
|
||||
abstract = AbstractReportService(subcontractor_id=subcontractor_id,ra_bill_no=ra_bill_no)
|
||||
abstract.generate(workbook)
|
||||
|
||||
|
||||
sheet_map = [
|
||||
(bill.df_tr, "Tr.Ex"),
|
||||
(bill.df_mh, "Mh.Ex"),
|
||||
(bill.df_dc, "MH & DC"),
|
||||
(bill.df_laying, "Pipe Laying"),
|
||||
]
|
||||
for df, sheet_name in sheet_map:
|
||||
if not df.empty:
|
||||
df.to_excel(writer, sheet_name=sheet_name, index=False)
|
||||
writer.close()
|
||||
output.seek(0)
|
||||
|
||||
|
||||
sc_obj = next(
|
||||
(s for s in subcontractors if str(s.id) == str(subcontractor_id)),
|
||||
None
|
||||
)
|
||||
sc_name = sc_obj.subcontractor_name if sc_obj else "Subcontractor"
|
||||
sc_name = re.sub(r'[^A-Za-z0-9_-]+', '_', sc_name).strip('_')
|
||||
|
||||
name_parts = [sc_name]
|
||||
|
||||
if ra_bill_no:
|
||||
name_parts.append(f"RA{re.sub(r'[^A-Za-z0-9_-]+', '_', ra_bill_no)}")
|
||||
|
||||
if location:
|
||||
name_parts.append(re.sub(r'[^A-Za-z0-9_-]+', '_', location).strip('_'))
|
||||
|
||||
if category and category != "all":
|
||||
name_parts.append(category.upper())
|
||||
|
||||
if action == "excel_all":
|
||||
name_parts.append("All")
|
||||
|
||||
filename = "_".join(name_parts) + "_Report.xlsx"
|
||||
|
||||
return send_file(
|
||||
output,
|
||||
download_name=filename,
|
||||
as_attachment=True
|
||||
)
|
||||
|
||||
# ===================================================
|
||||
# PDF
|
||||
# ===================================================
|
||||
if action == "pdf":
|
||||
flash(
|
||||
"PDF Export Coming Soon.",
|
||||
"info"
|
||||
)
|
||||
|
||||
# ===================================================
|
||||
# ADD ACTIONS
|
||||
# ===================================================
|
||||
bill.df_tr = add_action_columns(bill.df_tr, "tr")
|
||||
bill.df_mh = add_action_columns(bill.df_mh, "mh")
|
||||
bill.df_dc = add_action_columns(bill.df_dc, "dc")
|
||||
bill.df_laying = add_action_columns(bill.df_laying, "laying")
|
||||
|
||||
# this are html classes
|
||||
# table_class = ( "table " "table-bordered" "table-hover " "table-striped " "table-sm " "align-middle " "datatable " "mb-0")
|
||||
table_class = (
|
||||
"table "
|
||||
"table-bordered "
|
||||
"table-hover "
|
||||
"table-striped "
|
||||
"table-sm "
|
||||
"align-middle "
|
||||
"datatable "
|
||||
"text-nowrap "
|
||||
"mb-0"
|
||||
)
|
||||
|
||||
# This are showing on web tables
|
||||
tables = {
|
||||
"tr": bill.df_tr.to_html(classes=table_class, index=False, escape=False),
|
||||
"mh": bill.df_mh.to_html(classes=table_class, index=False, escape=False),
|
||||
"dc": bill.df_dc.to_html(classes=table_class, index=False, escape=False ),
|
||||
"laying": bill.df_laying.to_html(classes=table_class, index=False, escape=False)
|
||||
}
|
||||
|
||||
return render_template(
|
||||
"subcontractor_report.html",
|
||||
subcontractors=subcontractors,
|
||||
selected_sc_id=selected_sc_id,
|
||||
selected_ra_bill=ra_bill_no,
|
||||
selected_location=location,
|
||||
selected_category=category,
|
||||
tables=tables,
|
||||
abstract_html=abstract_html
|
||||
)
|
||||
|
||||
|
||||
# --- 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)
|
||||
|
||||
|
||||
|
||||
# --- 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)
|
||||
|
||||
|
||||
def format_column_names(df):
|
||||
if df.empty:
|
||||
return df
|
||||
|
||||
new_columns = []
|
||||
|
||||
for col in df.columns:
|
||||
|
||||
# ----------------------------------------
|
||||
# Pipe columns
|
||||
# pipe_150_mm -> Pipe 150 MM
|
||||
# ----------------------------------------
|
||||
if RegularExpression.PIPE_MM_PATTERN.match(col):
|
||||
m = re.match(r"pipe_(\d+)_mm", col)
|
||||
new_columns.append(f"Pipe {m.group(1)} MM")
|
||||
continue
|
||||
|
||||
# ----------------------------------------
|
||||
# Domestic Chamber
|
||||
# d_0_to_0_75 -> 0.00 To 0.75
|
||||
# d_1_5_to_3_0 -> 1.50 To 3.00
|
||||
# ----------------------------------------
|
||||
if RegularExpression.D_RANGE_PATTERN.match(col):
|
||||
|
||||
value = col[2:] # remove d_
|
||||
|
||||
value = re.sub(
|
||||
r'(\d+)_(\d+)',
|
||||
lambda m: f"{m.group(1)}.{m.group(2)}",
|
||||
value
|
||||
)
|
||||
|
||||
value = value.replace("_to_", " To ")
|
||||
|
||||
new_columns.append(value)
|
||||
continue
|
||||
|
||||
# ----------------------------------------
|
||||
# Total columns
|
||||
# Soft_Murum_0_to_1_5_total
|
||||
# ->
|
||||
# Soft Murum 0 To 1.5 Total
|
||||
# ----------------------------------------
|
||||
if RegularExpression.STR_TOTAL_PATTERN.match(col):
|
||||
|
||||
value = col[:-6] # remove _total
|
||||
|
||||
value = re.sub(
|
||||
r'(\d+)_(\d+)',
|
||||
lambda m: f"{m.group(1)}.{m.group(2)}",
|
||||
value
|
||||
)
|
||||
|
||||
value = value.replace("_to_", " To ")
|
||||
value = value.replace("_", " ")
|
||||
|
||||
new_columns.append(value.title() + " Total")
|
||||
continue
|
||||
|
||||
# ----------------------------------------
|
||||
# General columns
|
||||
# ----------------------------------------
|
||||
value = col.replace("_", " ").title()
|
||||
|
||||
replacements = {
|
||||
"Mh No": "MH No",
|
||||
"Ra Bill No": "RA Bill No",
|
||||
"Cc Length": "CC Length",
|
||||
"Id Of Mh M": "ID of MH (m)",
|
||||
"Pipe Dia Mm": "Pipe Dia (MM)",
|
||||
"Mh Top Level": "MH Top Level",
|
||||
"Upto Il Depth": "Upto IL Depth",
|
||||
"Actual Trench Length": "Actual Trench Length",
|
||||
"Ground Level": "Ground Level",
|
||||
"Invert Level": "Invert Level",
|
||||
"Ex Dia Of Manhole": "External Dia of Manhole",
|
||||
"Area Of Manhole": "Area of Manhole",
|
||||
"Depth Of Mh": "Depth of MH",
|
||||
}
|
||||
|
||||
value = replacements.get(value, value)
|
||||
|
||||
new_columns.append(value)
|
||||
|
||||
df.columns = new_columns
|
||||
|
||||
return df
|
||||
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.utils.helpers import login_required
|
||||
from app.utils.regex_utils import RegularExpression
|
||||
|
||||
# Contractor models import
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
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 app import db
|
||||
from flask import Blueprint, render_template, request, redirect, flash, current_app, url_for
|
||||
from app.services.db_service import db
|
||||
from app.models.subcontractor_model import Subcontractor
|
||||
from app.utils.helpers import login_required
|
||||
|
||||
subcontractor_bp = Blueprint("subcontractor", __name__, url_prefix="/subcontractor")
|
||||
|
||||
|
||||
# ---------------- ADD -----------------
|
||||
@subcontractor_bp.route("/add")
|
||||
@login_required
|
||||
def add_subcontractor():
|
||||
current_app.logger.info("Opened Add Subcontractor Page")
|
||||
return render_template("subcontractor/add.html")
|
||||
|
||||
|
||||
# ---------------- SAVE -----------------
|
||||
@subcontractor_bp.route("/save", methods=["POST"])
|
||||
@login_required
|
||||
def save_subcontractor():
|
||||
subcontractor = Subcontractor(
|
||||
subcontractor_name=request.form.get("subcontractor_name"),
|
||||
contact_person=request.form.get("contact_person"),
|
||||
mobile_no=request.form.get("mobile_no"),
|
||||
email_id=request.form.get("email_id"),
|
||||
gst_no=request.form.get("gst_no")
|
||||
)
|
||||
db.session.add(subcontractor)
|
||||
db.session.commit()
|
||||
|
||||
flash("Subcontractor added successfully!", "success")
|
||||
return redirect("/subcontractor/list")
|
||||
name = request.form.get("subcontractor_name", "").strip()
|
||||
|
||||
# ---------------- 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")
|
||||
@login_required
|
||||
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 -----------------
|
||||
@subcontractor_bp.route("/edit/<int:id>")
|
||||
@login_required
|
||||
def edit_subcontractor(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)
|
||||
|
||||
|
||||
# ---------------- UPDATE -----------------
|
||||
@subcontractor_bp.route("/update/<int:id>", methods=["POST"])
|
||||
@login_required
|
||||
def update_subcontractor(id):
|
||||
|
||||
subcontractor = Subcontractor.query.get_or_404(id)
|
||||
new_name = request.form.get("subcontractor_name", "").strip()
|
||||
|
||||
subcontractor.subcontractor_name = request.form.get("subcontractor_name")
|
||||
subcontractor.contact_person = request.form.get("contact_person")
|
||||
subcontractor.mobile_no = request.form.get("mobile_no")
|
||||
subcontractor.email_id = request.form.get("email_id")
|
||||
subcontractor.gst_no = request.form.get("gst_no")
|
||||
duplicate = Subcontractor.query.filter(
|
||||
Subcontractor.subcontractor_name == new_name,
|
||||
Subcontractor.id != id
|
||||
).first()
|
||||
|
||||
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 -----------------
|
||||
@subcontractor_bp.route("/delete/<int:id>")
|
||||
@login_required
|
||||
def delete_subcontractor(id):
|
||||
subcontractor = Subcontractor.query.get_or_404(id)
|
||||
|
||||
db.session.delete(subcontractor)
|
||||
db.session.commit()
|
||||
try:
|
||||
name = subcontractor.subcontractor_name
|
||||
db.session.delete(subcontractor)
|
||||
db.session.commit()
|
||||
|
||||
flash("Subcontractor deleted successfully!", "success")
|
||||
return redirect("/subcontractor/list")
|
||||
current_app.logger.info(f"Subcontractor Deleted: {name}")
|
||||
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 +0,0 @@
|
||||
from flask import Blueprint, render_template
|
||||
from app.services.user_service import UserService
|
||||
|
||||
user_bp = Blueprint("user", __name__, url_prefix="/user")
|
||||
|
||||
@user_bp.route("/list")
|
||||
def list_users():
|
||||
users = UserService().get_all_users()
|
||||
return render_template("users.html", users=users, title="Users")
|
||||
50
app/routes/user_routes.py
Normal file
50
app/routes/user_routes.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# from flask import Blueprint, render_template
|
||||
# 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.route("/list")
|
||||
# @login_required
|
||||
# def list_users():
|
||||
# current_app.logger.info("User list viewed")
|
||||
# users = UserService.get_all_users()
|
||||
# return render_template("/user/users.html", users=users, title="Users | List")
|
||||
|
||||
|
||||
|
||||
|
||||
from flask import (Blueprint,render_template,current_app,flash)
|
||||
from app.services.user_service import UserService
|
||||
from app.utils.helpers import login_required
|
||||
from app.constants.messages import SuccessMessage, ErrorMessage
|
||||
from app.constants.http_status import HTTPStatus
|
||||
|
||||
|
||||
user_bp = Blueprint("user", __name__, url_prefix="/user")
|
||||
|
||||
|
||||
# ==================================================
|
||||
# User List
|
||||
# ==================================================
|
||||
@user_bp.route("/list", methods=["GET"])
|
||||
@login_required
|
||||
def list_users():
|
||||
|
||||
try:
|
||||
|
||||
current_app.logger.info("Fetching user list.")
|
||||
users = UserService.get_all_users()
|
||||
current_app.logger.info(f"User list loaded successfully. Total Users: {len(users)}")
|
||||
|
||||
return render_template("user/users.html", users=users, title="Users | List")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
current_app.logger.exception("Failed to load user list.")
|
||||
|
||||
flash(ErrorMessage.INTERNAL_SERVER_ERROR,"danger")
|
||||
|
||||
return render_template("user/users.html",users=[],title="Users | List"), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
359
app/services/abstract_service.py
Normal file
359
app/services/abstract_service.py
Normal file
@@ -0,0 +1,359 @@
|
||||
from sqlalchemy import func
|
||||
from app import db
|
||||
|
||||
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
|
||||
|
||||
|
||||
class AbstractReportService:
|
||||
|
||||
def __init__(self, subcontractor_id=None, ra_bill_no=None):
|
||||
|
||||
self.subcontractor_id = subcontractor_id
|
||||
self.ra_bill_no = ra_bill_no
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# FILTER
|
||||
# ---------------------------------------------------------
|
||||
def filters(self):
|
||||
filters = {}
|
||||
|
||||
if self.subcontractor_id:
|
||||
filters["subcontractor_id"] = self.subcontractor_id
|
||||
|
||||
if self.ra_bill_no:
|
||||
filters["RA_Bill_No"] = self.ra_bill_no
|
||||
|
||||
return filters
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# CONTRACTOR
|
||||
# ---------------------------------------------------------
|
||||
def contractor_name(self):
|
||||
|
||||
if not self.subcontractor_id:
|
||||
return ""
|
||||
|
||||
contractor = Subcontractor.query.get(self.subcontractor_id)
|
||||
|
||||
if contractor:
|
||||
return contractor.subcontractor_name
|
||||
|
||||
return ""
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# WRITE ABSTRACT SHEET
|
||||
# ---------------------------------------------------------
|
||||
def generate(self, workbook):
|
||||
|
||||
worksheet = workbook.add_worksheet("Abstract")
|
||||
|
||||
title = workbook.add_format({
|
||||
"bold": True,
|
||||
"font_size": 16,
|
||||
"align": "center",
|
||||
"valign": "vcenter",
|
||||
"border": 1
|
||||
})
|
||||
|
||||
heading = workbook.add_format({
|
||||
"bold": True,
|
||||
"bg_color": "#D9EAD3",
|
||||
"border": 1,
|
||||
"align": "center"
|
||||
})
|
||||
|
||||
cell = workbook.add_format({"border": 1})
|
||||
number = workbook.add_format({"border": 1,"num_format": "#,##0.00"})
|
||||
|
||||
# -----------------------------------------------------
|
||||
# HEADER
|
||||
# -----------------------------------------------------
|
||||
worksheet.merge_range(
|
||||
"A1:D1",
|
||||
"ABSTRACT OF QUANTITY",
|
||||
title
|
||||
)
|
||||
|
||||
worksheet.write("A3", "Contractor", heading)
|
||||
worksheet.write("B3", self.contractor_name(), cell)
|
||||
|
||||
worksheet.write("C3", "RA Bill", heading)
|
||||
worksheet.write("D3", self.ra_bill_no or "", cell)
|
||||
|
||||
worksheet.write_row(
|
||||
"A5",
|
||||
[
|
||||
"Sr",
|
||||
"Description",
|
||||
"UOM",
|
||||
"Qty"
|
||||
],
|
||||
heading
|
||||
)
|
||||
|
||||
row = 5
|
||||
sr = 1
|
||||
|
||||
# -----------------------------------------------------
|
||||
# TRENCH
|
||||
# -----------------------------------------------------
|
||||
worksheet.write(row, 0, "")
|
||||
worksheet.write(row, 1, "TRENCH EXCAVATION", heading)
|
||||
|
||||
row += 1
|
||||
|
||||
for item in self.trench_summary():
|
||||
worksheet.write(row, 0, sr, cell)
|
||||
worksheet.write(row, 1, item["Description"], cell)
|
||||
worksheet.write(row, 2, item["UOM"], cell)
|
||||
worksheet.write(row, 3, item["Qty"], number)
|
||||
|
||||
sr += 1
|
||||
row += 1
|
||||
|
||||
# -----------------------------------------------------
|
||||
# MANHOLE
|
||||
# -----------------------------------------------------
|
||||
worksheet.write(row, 1, "MANHOLE EXCAVATION", heading)
|
||||
row += 1
|
||||
|
||||
for item in self.manhole_summary():
|
||||
worksheet.write(row, 0, sr, cell)
|
||||
worksheet.write(row, 1, item["Description"], cell)
|
||||
worksheet.write(row, 2, item["UOM"], cell)
|
||||
worksheet.write(row, 3, item["Qty"], number)
|
||||
|
||||
sr += 1
|
||||
row += 1
|
||||
|
||||
# -----------------------------------------------------
|
||||
# DOMESTIC CHAMBER
|
||||
# -----------------------------------------------------
|
||||
worksheet.write(row, 1, "DOMESTIC CHAMBER", heading)
|
||||
|
||||
row += 1
|
||||
|
||||
for item in self.domestic_summary():
|
||||
worksheet.write(row, 0, sr, cell)
|
||||
worksheet.write(row, 1, item["Description"], cell)
|
||||
worksheet.write(row, 2, item["UOM"], cell)
|
||||
worksheet.write(row, 3, item["Qty"], number)
|
||||
|
||||
sr += 1
|
||||
row += 1
|
||||
|
||||
# -----------------------------------------------------
|
||||
# PIPE LAYING
|
||||
# -----------------------------------------------------
|
||||
worksheet.write(row, 1, "PIPE LAYING", heading)
|
||||
row += 1
|
||||
|
||||
for item in self.laying_summary():
|
||||
worksheet.write(row, 0, sr, cell)
|
||||
worksheet.write(row, 1, item["Description"], cell)
|
||||
worksheet.write(row, 2, item["UOM"], cell)
|
||||
worksheet.write(row, 3, item["Qty"], number)
|
||||
|
||||
sr += 1
|
||||
row += 1
|
||||
|
||||
worksheet.set_column("A:A", 8)
|
||||
worksheet.set_column("B:B", 55)
|
||||
worksheet.set_column("C:C", 10)
|
||||
worksheet.set_column("D:D", 18)
|
||||
|
||||
# ============================================================
|
||||
# TRENCH
|
||||
# ============================================================
|
||||
def trench_summary(self):
|
||||
|
||||
f = self.filters()
|
||||
|
||||
data = [
|
||||
("Soft Murum 0-1.5 mm","Cum",TrenchExcavation.Soft_Murum_0_to_1_5_total),
|
||||
("Soft Murum 1.5-3.0 mm","Cum",TrenchExcavation.Soft_Murum_1_5_to_3_0_total),
|
||||
("Soft Murum 3.0-4.5 mm","Cum",TrenchExcavation.Soft_Murum_3_0_to_4_5_total),
|
||||
|
||||
("Hard Murum 0-1.5 mm","Cum",TrenchExcavation.Hard_Murum_0_to_1_5_total),
|
||||
("Hard Murum Above 1.5 mm","Cum",TrenchExcavation.Hard_Murum_1_5_and_above_total),
|
||||
|
||||
("Soft Rock 0-1.5 mm","Cum",TrenchExcavation.Soft_Rock_0_to_1_5_total),
|
||||
("Soft Rock Above 1.5 mm","Cum",TrenchExcavation.Soft_Rock_1_5_and_above_total),
|
||||
|
||||
("Hard Rock 0-1.5 mm","Cum",TrenchExcavation.Hard_Rock_0_to_1_5_total),
|
||||
("Hard Rock 1.5-3.0 mm","Cum",TrenchExcavation.Hard_Rock_1_5_to_3_0_total),
|
||||
("Hard Rock 3.0-4.5 mm","Cum",TrenchExcavation.Hard_Rock_3_0_to_4_5_total),
|
||||
("Hard Rock 4.5-6.0 mm","Cum",TrenchExcavation.Hard_Rock_4_5_to_6_0_total),
|
||||
("Hard Rock 6.0-7.5 mm","Cum",TrenchExcavation.Hard_Rock_6_0_to_7_5_total),
|
||||
]
|
||||
|
||||
return self.make_summary(data, TrenchExcavation, f)
|
||||
|
||||
# ============================================================
|
||||
# MANHOLE
|
||||
# ============================================================
|
||||
def manhole_summary(self):
|
||||
|
||||
f = self.filters()
|
||||
|
||||
data = [
|
||||
|
||||
("Soft Murum 0-1.5 mm","Cum",ManholeExcavation.Soft_Murum_0_to_1_5_total),
|
||||
("Soft Murum 1.5-3.0 mm","Cum",ManholeExcavation.Soft_Murum_1_5_to_3_0_total),
|
||||
("Soft Murum 3.0-4.5 mm","Cum",ManholeExcavation.Soft_Murum_3_0_to_4_5_total),
|
||||
|
||||
("Hard Murum 0-1.5 mm","Cum",ManholeExcavation.Hard_Murum_0_to_1_5_total),
|
||||
("Hard Murum Above 1.5 mm","Cum",ManholeExcavation.Hard_Murum_1_5_and_above_total),
|
||||
|
||||
("Soft Rock 0-1.5 mm","Cum",ManholeExcavation.Soft_Rock_0_to_1_5_total),
|
||||
("Soft Rock Above 1.5 mm","Cum",ManholeExcavation.Soft_Rock_1_5_and_above_total),
|
||||
|
||||
("Hard Rock 0-1.5 mm","Cum",ManholeExcavation.Hard_Rock_0_to_1_5_total),
|
||||
("Hard Rock 1.5-3.0 mm","Cum",ManholeExcavation.Hard_Rock_1_5_to_3_0_total),
|
||||
("Hard Rock 3.0-4.5 mm","Cum",ManholeExcavation.Hard_Rock_3_0_to_4_5_total),
|
||||
("Hard Rock 4.5-6.0 mm","Cum",ManholeExcavation.Hard_Rock_4_5_to_6_0_total),
|
||||
("Hard Rock 6.0-7.5 mm","Cum",ManholeExcavation.Hard_Rock_6_0_to_7_5_total),
|
||||
|
||||
]
|
||||
|
||||
return self.make_summary(data, ManholeExcavation, f)
|
||||
|
||||
# ============================================================
|
||||
# DOMESTIC CHAMBER
|
||||
# ============================================================
|
||||
def domestic_summary(self):
|
||||
f = self.filters()
|
||||
data = [
|
||||
("0-0.75 mm","Nos",ManholeDomesticChamber.d_0_to_0_75),
|
||||
("0.76-1.05 mm","Nos",ManholeDomesticChamber.d_0_76_to_1_05),
|
||||
("1.06-1.65 mm","Nos",ManholeDomesticChamber.d_1_06_to_1_65),
|
||||
("1.66-2.15 mm","Nos",ManholeDomesticChamber.d_1_66_to_2_15),
|
||||
("2.16-2.65 mm","Nos",ManholeDomesticChamber.d_2_16_to_2_65),
|
||||
("2.66-3.15 mm","Nos",ManholeDomesticChamber.d_2_66_to_3_15),
|
||||
("3.16-3.65 mm","Nos",ManholeDomesticChamber.d_3_16_to_3_65),
|
||||
("3.66-4.15 mm","Nos",ManholeDomesticChamber.d_3_66_to_4_15),
|
||||
("4.16-4.65 mm","Nos",ManholeDomesticChamber.d_4_16_to_4_65),
|
||||
("4.66-5.15 mm","Nos",ManholeDomesticChamber.d_4_66_to_5_15),
|
||||
("5.16-5.65 mm","Nos",ManholeDomesticChamber.d_5_16_to_5_65),
|
||||
("5.66-6.15 mm","Nos",ManholeDomesticChamber.d_5_66_to_6_15),
|
||||
("6.16-6.65 mm","Nos",ManholeDomesticChamber.d_6_16_to_6_65),
|
||||
("6.66-7.15 mm","Nos",ManholeDomesticChamber.d_6_66_to_7_15),
|
||||
("7.16-7.65 mm","Nos",ManholeDomesticChamber.d_7_16_to_7_65),
|
||||
("7.66-8.15 mm","Nos",ManholeDomesticChamber.d_7_66_to_8_15),
|
||||
("8.16-8.65 mm","Nos",ManholeDomesticChamber.d_8_16_to_8_65),
|
||||
("8.66-9.15 mm","Nos",ManholeDomesticChamber.d_8_66_to_9_15),
|
||||
("9.16-9.65 mm","Nos",ManholeDomesticChamber.d_9_16_to_9_65),
|
||||
]
|
||||
|
||||
return self.make_summary(data, ManholeDomesticChamber, f)
|
||||
|
||||
# ============================================================
|
||||
# PIPE LAYING
|
||||
# ============================================================
|
||||
def laying_summary(self):
|
||||
f = self.filters()
|
||||
data = [
|
||||
("150 mm Dia","RM",Laying.pipe_150_mm),
|
||||
("200 mm Dia","RM",Laying.pipe_200_mm),
|
||||
("250 mm Dia","RM",Laying.pipe_250_mm),
|
||||
("300 mm Dia","RM",Laying.pipe_300_mm),
|
||||
("350 mm Dia","RM",Laying.pipe_350_mm),
|
||||
("400 mm Dia","RM",Laying.pipe_400_mm),
|
||||
("450 mm Dia","RM",Laying.pipe_450_mm),
|
||||
("500 mm Dia","RM",Laying.pipe_500_mm),
|
||||
("600 mm Dia","RM",Laying.pipe_600_mm),
|
||||
("700 mm Dia","RM",Laying.pipe_700_mm),
|
||||
("900 mm Dia","RM",Laying.pipe_900_mm),
|
||||
("1200 mm Dia","RM",Laying.pipe_1200_mm),
|
||||
]
|
||||
return self.make_summary(data, Laying, f)
|
||||
|
||||
# ============================================================
|
||||
# COMMON SUMMARY
|
||||
# ============================================================
|
||||
def make_summary(self, items, model, filters):
|
||||
summary = []
|
||||
for desc, uom, column in items:
|
||||
qty = (db.session.query(func.sum(column)).filter_by(**filters).scalar())
|
||||
summary.append({
|
||||
"Description": desc,
|
||||
"UOM": uom,
|
||||
"Qty": float(qty or 0)
|
||||
})
|
||||
return summary
|
||||
|
||||
|
||||
def generate_html(self):
|
||||
|
||||
html = """
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover table-striped">
|
||||
<thead class="table-success">
|
||||
|
||||
<tr>
|
||||
<th colspan="4" class="text-center fs-4">
|
||||
ABSTRACT OF QUANTITY
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Contractor</th>
|
||||
<td>{}</td>
|
||||
|
||||
<th>RA Bill NO</th>
|
||||
<td>{}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th width="8%">Sr</th>
|
||||
<th>Description</th>
|
||||
<th width="10%">UOM</th>
|
||||
<th width="15%">Qty</th>
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
<tbody>
|
||||
""".format(
|
||||
self.contractor_name(),
|
||||
self.ra_bill_no or ""
|
||||
)
|
||||
|
||||
sr = 1
|
||||
sections = [
|
||||
("TRENCH EXCAVATION", self.trench_summary()),
|
||||
("MANHOLE EXCAVATION", self.manhole_summary()),
|
||||
("DOMESTIC CHAMBER", self.domestic_summary()),
|
||||
("PIPE LAYING", self.laying_summary())
|
||||
]
|
||||
|
||||
for title, rows in sections:
|
||||
html += f"""
|
||||
<tr class="table-secondary fw-bold">
|
||||
<td colspan="4">{title}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
for item in rows:
|
||||
html += f"""
|
||||
<tr>
|
||||
<td>{sr}</td>
|
||||
<td>{item['Description']}</td>
|
||||
<td>{item['UOM']}</td>
|
||||
<td class="text-end">
|
||||
{item['Qty']:.2f}
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
sr += 1
|
||||
|
||||
html += """
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
|
||||
return html
|
||||
56
app/services/activity_service.py
Normal file
56
app/services/activity_service.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# Activity log Service
|
||||
class ActivityService:
|
||||
|
||||
@staticmethod
|
||||
def read_logs(file_name="", search="", level="", user="", from_date="", to_date=""):
|
||||
|
||||
file = Path("logs") / file_name
|
||||
|
||||
if not file.exists():
|
||||
return []
|
||||
|
||||
logs = []
|
||||
|
||||
with open(file, "r", encoding="utf-8") as f:
|
||||
|
||||
for line in reversed(f.readlines()):
|
||||
# Example:
|
||||
# 2026-08-01 10:15:55 | INFO | admin | Dashboard | Dashboard Opened
|
||||
parts = [x.strip() for x in line.split("|")]
|
||||
|
||||
if len(parts) < 5:
|
||||
continue
|
||||
|
||||
record = {
|
||||
"date": parts[0],
|
||||
"level": parts[1],
|
||||
"user": parts[2],
|
||||
"module": parts[3],
|
||||
"message": "|".join(parts[4:])
|
||||
}
|
||||
|
||||
# Search Filter
|
||||
if search and search.lower() not in line.lower():
|
||||
continue
|
||||
|
||||
# Level Filter
|
||||
if level and record["level"] != level:
|
||||
continue
|
||||
|
||||
# User Filter
|
||||
if user and user.lower() not in record["user"].lower():
|
||||
continue
|
||||
|
||||
# Date Filter
|
||||
if from_date and record["date"][:10] < from_date:
|
||||
continue
|
||||
|
||||
if to_date and record["date"][:10] > to_date:
|
||||
continue
|
||||
|
||||
logs.append(record)
|
||||
|
||||
return logs
|
||||
204
app/services/comparison_service.py
Normal file
204
app/services/comparison_service.py
Normal file
@@ -0,0 +1,204 @@
|
||||
# from collections import defaultdict
|
||||
# import pandas as pd
|
||||
# from app.utils.regex_utils import RegularExpression
|
||||
|
||||
|
||||
# class ComparisonService:
|
||||
|
||||
# TRENCH_MAPPING = [
|
||||
# {
|
||||
# "label": "Marshi 0 to 1.5",
|
||||
# "client": "Client_Marshi_Muddy_Slushy_0_to_1_5_total",
|
||||
# "sub": None
|
||||
# },
|
||||
# {
|
||||
# "label": "Marshi 1.5 to 3.0",
|
||||
# "client": "Client_Marshi_Muddy_Slushy_1_5_to_3_0_total",
|
||||
# "sub": None
|
||||
# },
|
||||
# {
|
||||
# "label": "Marshi 3.0 to 4.5",
|
||||
# "client": "Client_Marshi_Muddy_Slushy_3_0_to_4_5_total",
|
||||
# "sub": None
|
||||
# },
|
||||
# {
|
||||
# "label": "Soft Murum 0 to 1.5",
|
||||
# "client": "Client_Soft_Murum_0_to_1_5_total",
|
||||
# "sub": "Sub_Soft_Murum_0_to_1_5_total"
|
||||
# },
|
||||
# {
|
||||
# "label": "Soft Murum 1.5 to 3.0",
|
||||
# "client": "Client_Soft_Murum_1_5_to_3_0_total",
|
||||
# "sub": "Sub_Soft_Murum_1_5_to_3_0_total"
|
||||
# },
|
||||
# {
|
||||
# "label": "Soft Murum 3.0 to 4.5",
|
||||
# "client": "Client_Soft_Murum_3_0_to_4_5_total",
|
||||
# "sub": "Sub_Soft_Murum_3_0_to_4_5_total"
|
||||
# },
|
||||
# {
|
||||
# "label": "Hard Murum 0 to 1.5",
|
||||
# "client": "Client_Hard_Murum_0_to_1_5_total",
|
||||
# "sub": "Sub_Hard_Murum_0_to_1_5_total"
|
||||
# },
|
||||
# {
|
||||
# "label": "Hard Murum 1.5+",
|
||||
# "client": "Client_Hard_Murum_1_5_to_3_0_total",
|
||||
# "sub": "Sub_Hard_Murum_1_5_and_above_total"
|
||||
# },
|
||||
# {
|
||||
# "label": "Soft Rock 0 to 1.5",
|
||||
# "client": "Client_Soft_Rock_0_to_1_5_total",
|
||||
# "sub": "Sub_Soft_Rock_0_to_1_5_total"
|
||||
# },
|
||||
# {
|
||||
# "label": "Soft Rock 1.5+",
|
||||
# "client": "Client_Soft_Rock_1_5_to_3_0_total",
|
||||
# "sub": "Sub_Soft_Rock_1_5_and_above_total"
|
||||
# },
|
||||
# {
|
||||
# "label": "Hard Rock 0 to 1.5",
|
||||
# "client": "Client_Hard_Rock_0_to_1_5_total",
|
||||
# "sub": "Sub_Hard_Rock_0_to_1_5_total"
|
||||
# },
|
||||
# {
|
||||
# "label": "Hard Rock 1.5 to 3.0",
|
||||
# "client": "Client_Hard_Rock_1_5_to_3_0_total",
|
||||
# "sub": "Sub_Hard_Rock_1_5_to_3_0_total"
|
||||
# },
|
||||
# {
|
||||
# "label": "Hard Rock 3.0 to 4.5",
|
||||
# "client": "Client_Hard_Rock_3_0_to_4_5_total",
|
||||
# "sub": "Sub_Hard_Rock_3_0_to_4_5_total"
|
||||
# },
|
||||
# {
|
||||
# "label": "Hard Rock 4.5 to 6.0",
|
||||
# "client": "Client_Hard_Rock_4_5_to_6_0_total",
|
||||
# "sub": "Sub_Hard_Rock_4_5_to_6_0_total"
|
||||
# },
|
||||
# {
|
||||
# "label": "Hard Rock 6.0 to 7.5",
|
||||
# "client": "Client_Hard_Rock_6_0_to_7_5_total",
|
||||
# "sub": "Sub_Hard_Rock_6_0_to_7_5_total"
|
||||
# }
|
||||
# ]
|
||||
|
||||
|
||||
|
||||
# @staticmethod
|
||||
# def normalize_key(value):
|
||||
# if value is None:
|
||||
# return ""
|
||||
# return str(value).strip().upper()
|
||||
|
||||
# @classmethod
|
||||
# def make_lookup(cls, rows, key_field):
|
||||
# """
|
||||
# Create lookup dictionary using:
|
||||
# (Location, MH_NO)
|
||||
# """
|
||||
|
||||
# lookup = defaultdict(list)
|
||||
|
||||
# for row in rows:
|
||||
|
||||
# location = cls.normalize_key(row.get("Location"))
|
||||
# key = cls.normalize_key(row.get(key_field))
|
||||
|
||||
# if location and key:
|
||||
# lookup[(location, key)].append(row)
|
||||
|
||||
# return lookup
|
||||
|
||||
# @classmethod
|
||||
# def build_comparison(cls, client_rows, subcontractor_rows, key_field="MH_NO"):
|
||||
|
||||
# subcontractor_lookup = cls.make_lookup(
|
||||
# subcontractor_rows,
|
||||
# key_field
|
||||
# )
|
||||
|
||||
# used = defaultdict(int)
|
||||
|
||||
# output = []
|
||||
|
||||
# for client in client_rows:
|
||||
|
||||
# location = cls.normalize_key(client.get("Location"))
|
||||
# key = cls.normalize_key(client.get(key_field))
|
||||
|
||||
# if not location or not key:
|
||||
# continue
|
||||
|
||||
# rows = subcontractor_lookup.get((location, key))
|
||||
|
||||
# if not rows:
|
||||
# continue
|
||||
|
||||
# index = used[(location, key)]
|
||||
|
||||
# if index >= len(rows):
|
||||
# continue
|
||||
|
||||
# subcontractor = rows[index]
|
||||
|
||||
# used[(location, key)] += 1
|
||||
|
||||
# client_total = sum(
|
||||
# float(v or 0)
|
||||
# for k, v in client.items()
|
||||
# if k.endswith("_total")
|
||||
# or RegularExpression.D_RANGE_PATTERN.match(k)
|
||||
# or RegularExpression.PIPE_MM_PATTERN.match(k)
|
||||
# )
|
||||
|
||||
# subcontractor_total = sum(
|
||||
# float(v or 0)
|
||||
# for k, v in subcontractor.items()
|
||||
# if k.endswith("_total")
|
||||
# or RegularExpression.D_RANGE_PATTERN.match(k)
|
||||
# or RegularExpression.PIPE_MM_PATTERN.match(k)
|
||||
# )
|
||||
|
||||
# row = {
|
||||
|
||||
# "Location": location,
|
||||
|
||||
# key_field: key,
|
||||
|
||||
# "Client_Total": round(client_total, 2),
|
||||
|
||||
# "Subcontractor_Total": round(subcontractor_total, 2),
|
||||
|
||||
# "Difference": round(
|
||||
# client_total - subcontractor_total,
|
||||
# 2
|
||||
# )
|
||||
# }
|
||||
|
||||
# # Client Columns
|
||||
# for column, value in client.items():
|
||||
|
||||
# if column in [
|
||||
# "id",
|
||||
# "created_at"
|
||||
# ]:
|
||||
# continue
|
||||
|
||||
# row[f"Client_{column}"] = value
|
||||
|
||||
# # Subcontractor Columns
|
||||
# for column, value in subcontractor.items():
|
||||
|
||||
# if column in [
|
||||
# "id",
|
||||
# "created_at",
|
||||
# "subcontractor_id"
|
||||
# ]:
|
||||
# continue
|
||||
|
||||
# row[f"Sub_{column}"] = value
|
||||
|
||||
# output.append(row)
|
||||
|
||||
# return pd.DataFrame(output)
|
||||
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 pandas as pd
|
||||
from werkzeug.utils import secure_filename
|
||||
from app.config import Config
|
||||
from app import db
|
||||
from app.utils.file_utils import ensure_upload_folder, get_uploads_folder, ALLOWED_EXTENSIONS
|
||||
|
||||
# Subcontractor models import
|
||||
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
|
||||
|
||||
# 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:
|
||||
|
||||
# ---------------- COMMON HELPERS ----------------
|
||||
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)
|
||||
|
||||
# data normalizations
|
||||
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
|
||||
|
||||
def handle_file_upload(self, file, subcontractor_id, file_type):
|
||||
return val.upper()
|
||||
|
||||
# --------------- Sub-contractor service --------------
|
||||
# ---------------- SUBCONTRACTOR FILE UPLOAD ----------------
|
||||
def handle_file_upload(self, file, subcontractor_id, RA_Bill_No):
|
||||
|
||||
if not subcontractor_id:
|
||||
return False, "Please select subcontractor."
|
||||
if not 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 == "":
|
||||
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(Config.UPLOAD_FOLDER, f"sub_{subcontractor_id}")
|
||||
folder = os.path.join(path, f"sub_{subcontractor_id}")
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
|
||||
filename = secure_filename(file.filename)
|
||||
@@ -36,171 +62,315 @@ class FileService:
|
||||
file.save(filepath)
|
||||
|
||||
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 ===")
|
||||
print(df.head())
|
||||
print("=============================\n")
|
||||
self.process_trench_excavation(df_tr_ex, subcontractor_id, RA_Bill_No)
|
||||
self.process_manhole_excavation(df_mh_ex, subcontractor_id, RA_Bill_No)
|
||||
self.process_manhole_domestic_chamber(df_mh_dc, subcontractor_id, RA_Bill_No)
|
||||
self.process_laying(df_laying, subcontractor_id, RA_Bill_No)
|
||||
|
||||
# Trench Excavation save
|
||||
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_domestic_chamber(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):
|
||||
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()
|
||||
|
||||
df = df.dropna(how="all") # REMOVE empty rows
|
||||
# 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}"
|
||||
return True, "SUBCONTRACTOR File uploaded successfully."
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, f"Trench Excavation Save Failed: {e}"
|
||||
|
||||
return False, f"Import failed: {e}"
|
||||
|
||||
# Manhole Excavation save method (ManholeExcavation 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:
|
||||
df["Location"] = df["Location"].ffill()
|
||||
# ---------------- Trench Excavation (Subcontractor) ----------------
|
||||
def process_trench_excavation(self, df, subcontractor_id, RA_Bill_No):
|
||||
|
||||
df.columns = (
|
||||
df.columns.astype(str)
|
||||
.str.strip()
|
||||
.str.replace(r"[^\w]", "_", regex=True)
|
||||
.str.replace("__+", "_", regex=True)
|
||||
.str.strip("_")
|
||||
)
|
||||
|
||||
# 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(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_domestic_chamber(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()}"
|
||||
errors = []
|
||||
|
||||
saved_count = 0
|
||||
for idx, row in df.iterrows():
|
||||
location = self.normalize(row.get("Location"))
|
||||
mh_no = self.normalize(row.get("MH_NO"))
|
||||
|
||||
if not location or not mh_no:
|
||||
continue
|
||||
|
||||
# exists = TrenchExcavation.query.filter_by(
|
||||
# subcontractor_id=subcontractor_id,
|
||||
# RA_Bill_No=RA_Bill_No,
|
||||
# Location=location,
|
||||
# MH_NO=mh_no,
|
||||
# ).first()
|
||||
|
||||
# if exists:
|
||||
# errors.append(
|
||||
# f"Model-Tr.Ex. (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}"
|
||||
# )
|
||||
# continue
|
||||
|
||||
record_data = {}
|
||||
for col in df.columns:
|
||||
if hasattr(TrenchExcavation, col):
|
||||
val = row[col]
|
||||
if pd.isna(val) or str(val).strip() in ["", "-", "—", "nan"]:
|
||||
val = None
|
||||
record_data[col] = val
|
||||
|
||||
record = TrenchExcavation(
|
||||
subcontractor_id=subcontractor_id,
|
||||
RA_Bill_No=RA_Bill_No,
|
||||
**record_data,
|
||||
)
|
||||
db.session.add(record)
|
||||
|
||||
if errors:
|
||||
raise Exception(" | ".join(errors))
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# ---------------- Manhole Excavation (Subcontractor) ----------------
|
||||
def process_manhole_excavation(self, df, subcontractor_id, RA_Bill_No):
|
||||
|
||||
df.columns = (
|
||||
df.columns.astype(str)
|
||||
.str.strip()
|
||||
.str.replace(r"[^\w]", "_", regex=True)
|
||||
.str.replace("__+", "_", regex=True)
|
||||
.str.strip("_")
|
||||
)
|
||||
|
||||
df = df.dropna(how="all")
|
||||
|
||||
if "Location" in df.columns:
|
||||
df["Location"] = df["Location"].ffill()
|
||||
|
||||
errors = []
|
||||
|
||||
for idx, row in df.iterrows():
|
||||
location = self.normalize(row.get("Location"))
|
||||
mh_no = self.normalize(row.get("MH_NO"))
|
||||
|
||||
if not location or not mh_no:
|
||||
continue
|
||||
|
||||
# exists = ManholeExcavation.query.filter_by(
|
||||
# subcontractor_id=subcontractor_id,
|
||||
# RA_Bill_No=RA_Bill_No,
|
||||
# Location=location,
|
||||
# MH_NO=mh_no,
|
||||
# ).first()
|
||||
|
||||
# if exists:
|
||||
# errors.append(
|
||||
# f"Model-MH Ex. (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}"
|
||||
# )
|
||||
# continue
|
||||
|
||||
record_data = {}
|
||||
for col in df.columns:
|
||||
if hasattr(ManholeExcavation, col):
|
||||
val = row[col]
|
||||
if pd.isna(val) or str(val).strip() in ["", "-", "—", "nan"]:
|
||||
val = None
|
||||
record_data[col] = val
|
||||
|
||||
record = ManholeExcavation(
|
||||
subcontractor_id=subcontractor_id,
|
||||
RA_Bill_No=RA_Bill_No,
|
||||
**record_data,
|
||||
)
|
||||
db.session.add(record)
|
||||
|
||||
if errors:
|
||||
raise Exception(" | ".join(errors))
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# ---------------- Manhole & Domestic Chamber (Subcontractor) ----------------
|
||||
def process_manhole_domestic_chamber(self, df, subcontractor_id, RA_Bill_No):
|
||||
|
||||
df.columns = (
|
||||
df.columns.astype(str)
|
||||
.str.strip()
|
||||
.str.replace(r"[^\w]", "_", regex=True)
|
||||
.str.replace("__+", "_", regex=True)
|
||||
.str.strip("_")
|
||||
)
|
||||
|
||||
df = df.dropna(how="all")
|
||||
|
||||
if "Location" in df.columns:
|
||||
df["Location"] = df["Location"].ffill()
|
||||
|
||||
errors = []
|
||||
|
||||
for idx, row in df.iterrows():
|
||||
location = self.normalize(row.get("Location"))
|
||||
mh_no = self.normalize(row.get("MH_NO"))
|
||||
|
||||
if not location or not mh_no:
|
||||
continue
|
||||
|
||||
# exists = ManholeDomesticChamber.query.filter_by(
|
||||
# subcontractor_id=subcontractor_id,
|
||||
# RA_Bill_No=RA_Bill_No,
|
||||
# Location=location,
|
||||
# MH_NO=mh_no,
|
||||
# ).first()
|
||||
|
||||
# if exists:
|
||||
# errors.append(
|
||||
# f"Model-MH & DC (Row {idx+1}): Duplicate → Location={location}, MH_NO={mh_no}"
|
||||
# )
|
||||
# continue
|
||||
|
||||
record_data = {}
|
||||
for col in df.columns:
|
||||
if hasattr(ManholeDomesticChamber, col):
|
||||
val = row[col]
|
||||
if pd.isna(val) or str(val).strip() in ["", "-", "—", "nan"]:
|
||||
val = None
|
||||
record_data[col] = val
|
||||
|
||||
record = ManholeDomesticChamber(
|
||||
subcontractor_id=subcontractor_id,
|
||||
RA_Bill_No=RA_Bill_No,
|
||||
**record_data,
|
||||
)
|
||||
db.session.add(record)
|
||||
|
||||
if errors:
|
||||
raise Exception(" | ".join(errors))
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# ---------------- 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 service --------------
|
||||
# ---------------- 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:
|
||||
for index, row in df.iterrows():
|
||||
record_data = {}
|
||||
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)
|
||||
|
||||
# Insert only fields that exist in model
|
||||
for col in df.columns:
|
||||
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
|
||||
self.save_client_data(df_tr_ex, TrenchExcavationClient, RA_Bill_No)
|
||||
self.save_client_data(df_mh_ex, ManholeExcavationClient, RA_Bill_No)
|
||||
self.save_client_data(df_mh_dc, ManholeDomesticChamberClient, RA_Bill_No)
|
||||
self.save_client_data(df_lay, LayingClient, RA_Bill_No)
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
|
||||
130
app/services/ldap_service.py
Normal file
130
app/services/ldap_service.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from ldap3 import (
|
||||
Server,
|
||||
Connection,
|
||||
ALL,
|
||||
NTLM,
|
||||
SIMPLE,
|
||||
SUBTREE
|
||||
)
|
||||
|
||||
from flask import current_app
|
||||
from app.config import Config
|
||||
|
||||
|
||||
class LDAPService:
|
||||
"""
|
||||
LDAP / Active Directory Authentication Service
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def authenticate(username, password):
|
||||
"""
|
||||
Authenticate LDAP User
|
||||
|
||||
Returns:
|
||||
{
|
||||
"success": True,
|
||||
"user": {
|
||||
"username": "...",
|
||||
"name": "...",
|
||||
"email": "..."
|
||||
}
|
||||
}
|
||||
|
||||
OR
|
||||
|
||||
{
|
||||
"success": False,
|
||||
"message": "Invalid username or password"
|
||||
}
|
||||
"""
|
||||
|
||||
if not username or not password:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Username and Password are required."
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
# -----------------------------------
|
||||
# LDAP SERVER
|
||||
# -----------------------------------
|
||||
server = Server(
|
||||
Config.LDAP_SERVER,
|
||||
port=Config.LDAP_PORT,
|
||||
use_ssl=Config.LDAP_USE_SSL,
|
||||
get_info=ALL
|
||||
)
|
||||
|
||||
# -----------------------------------
|
||||
# Login Format
|
||||
#
|
||||
# username@domain.com
|
||||
# -----------------------------------
|
||||
user_dn = f"{username}@{Config.LDAP_DOMAIN}"
|
||||
|
||||
conn = Connection(
|
||||
server,
|
||||
user=user_dn,
|
||||
password=password,
|
||||
authentication=SIMPLE,
|
||||
auto_bind=True
|
||||
)
|
||||
|
||||
# -----------------------------------
|
||||
# Search User
|
||||
# -----------------------------------
|
||||
search_filter = f"(sAMAccountName={username})"
|
||||
|
||||
conn.search(
|
||||
search_base=Config.LDAP_SEARCH_BASE,
|
||||
search_filter=search_filter,
|
||||
search_scope=SUBTREE,
|
||||
attributes=[
|
||||
"displayName",
|
||||
"mail",
|
||||
"givenName",
|
||||
"sn",
|
||||
"cn"
|
||||
]
|
||||
)
|
||||
|
||||
display_name = username
|
||||
email = ""
|
||||
|
||||
if conn.entries:
|
||||
|
||||
entry = conn.entries[0]
|
||||
|
||||
if "displayName" in entry:
|
||||
display_name = str(entry.displayName)
|
||||
|
||||
if "mail" in entry:
|
||||
email = str(entry.mail)
|
||||
|
||||
conn.unbind()
|
||||
|
||||
current_app.logger.info(
|
||||
f"LDAP Login Success : {username}"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"user": {
|
||||
"username": username,
|
||||
"name": display_name,
|
||||
"email": email
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
current_app.logger.warning(
|
||||
f"LDAP Login Failed : {username} : {str(ex)}"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid Username or Password."
|
||||
}
|
||||
149
app/services/logger_service.py
Normal file
149
app/services/logger_service.py
Normal file
@@ -0,0 +1,149 @@
|
||||
import os
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from flask import request, session, has_request_context
|
||||
|
||||
|
||||
class RequestContextFilter(logging.Filter):
|
||||
"""Adds request information to every log record."""
|
||||
|
||||
def filter(self, record):
|
||||
|
||||
if has_request_context():
|
||||
record.user = session.get("user_name", "Anonymous")
|
||||
record.ip = request.remote_addr or "Unknown"
|
||||
record.method = request.method
|
||||
record.url = request.url
|
||||
else:
|
||||
record.user = "System"
|
||||
record.ip = "-"
|
||||
record.method = "-"
|
||||
record.url = "-"
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class LoggerService:
|
||||
|
||||
LOG_DIR = "logs"
|
||||
|
||||
APP_LOG = "app.log"
|
||||
ERROR_LOG = "error.log"
|
||||
DEBUG_LOG = "debug.log"
|
||||
|
||||
MAX_BYTES = 10 * 1024 * 1024 # 10 MB
|
||||
BACKUP_COUNT = 10
|
||||
|
||||
@classmethod
|
||||
def init_app(cls, app):
|
||||
|
||||
os.makedirs(cls.LOG_DIR, exist_ok=True)
|
||||
|
||||
formatter = logging.Formatter(
|
||||
fmt=(
|
||||
"%(asctime)s | "
|
||||
"%(levelname)-8s | "
|
||||
"User=%(user)s | "
|
||||
"IP=%(ip)s | "
|
||||
"%(method)s | "
|
||||
"%(url)s | "
|
||||
"%(message)s"
|
||||
),
|
||||
datefmt="%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
|
||||
context_filter = RequestContextFilter()
|
||||
|
||||
# Remove default handlers
|
||||
app.logger.handlers.clear()
|
||||
|
||||
# ==========================
|
||||
# Application Log
|
||||
# ==========================
|
||||
app_handler = RotatingFileHandler(
|
||||
os.path.join(cls.LOG_DIR, cls.APP_LOG),
|
||||
maxBytes=cls.MAX_BYTES,
|
||||
backupCount=cls.BACKUP_COUNT,
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
app_handler.setLevel(logging.INFO)
|
||||
app_handler.setFormatter(formatter)
|
||||
app_handler.addFilter(context_filter)
|
||||
|
||||
# ==========================
|
||||
# Error Log
|
||||
# ==========================
|
||||
error_handler = RotatingFileHandler(
|
||||
os.path.join(cls.LOG_DIR, cls.ERROR_LOG),
|
||||
maxBytes=cls.MAX_BYTES,
|
||||
backupCount=cls.BACKUP_COUNT,
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
error_handler.setLevel(logging.ERROR)
|
||||
error_handler.setFormatter(formatter)
|
||||
error_handler.addFilter(context_filter)
|
||||
|
||||
# ==========================
|
||||
# Debug Log
|
||||
# ==========================
|
||||
debug_handler = RotatingFileHandler(
|
||||
os.path.join(cls.LOG_DIR, cls.DEBUG_LOG),
|
||||
maxBytes=cls.MAX_BYTES,
|
||||
backupCount=cls.BACKUP_COUNT,
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
debug_handler.setLevel(logging.DEBUG)
|
||||
debug_handler.setFormatter(formatter)
|
||||
debug_handler.addFilter(context_filter)
|
||||
|
||||
# ==========================
|
||||
# Console Log
|
||||
# ==========================
|
||||
console_handler = logging.StreamHandler()
|
||||
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_handler.setFormatter(formatter)
|
||||
console_handler.addFilter(context_filter)
|
||||
|
||||
# ==========================
|
||||
# Logger Configuration
|
||||
# ==========================
|
||||
app.logger.setLevel(logging.DEBUG)
|
||||
|
||||
app.logger.addHandler(app_handler)
|
||||
app.logger.addHandler(error_handler)
|
||||
app.logger.addHandler(debug_handler)
|
||||
app.logger.addHandler(console_handler)
|
||||
|
||||
# ==========================
|
||||
# Automatic Request Logging
|
||||
# ==========================
|
||||
@app.before_request
|
||||
def before_request():
|
||||
app.logger.info("Request Started")
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
app.logger.info(
|
||||
f"Request Completed | Status={response.status_code}"
|
||||
)
|
||||
return response
|
||||
|
||||
# ==========================
|
||||
# Exception Logging
|
||||
# ==========================
|
||||
@app.errorhandler(Exception)
|
||||
def log_exception(error):
|
||||
|
||||
app.logger.exception(
|
||||
f"Unhandled Exception: {str(error)}"
|
||||
)
|
||||
|
||||
raise error
|
||||
|
||||
app.logger.info("=" * 70)
|
||||
app.logger.info("Application Started Successfully")
|
||||
app.logger.info("=" * 70)
|
||||
102
app/services/subcontractor_rate_service.py
Normal file
102
app/services/subcontractor_rate_service.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from app.services.db_service import db
|
||||
from app.models.subcontractor_rate_model import SubcontractorRate
|
||||
from sqlalchemy import func
|
||||
|
||||
|
||||
class SubcontractorRateService:
|
||||
|
||||
@staticmethod
|
||||
def save_or_update(form):
|
||||
|
||||
rate_id = form.get("id")
|
||||
|
||||
subcontractor_id = form.get("subcontractor_id")
|
||||
category = form.get("category")
|
||||
item_name = form.get("item_name").strip()
|
||||
|
||||
# -----------------------------
|
||||
# Duplicate Validation
|
||||
# -----------------------------
|
||||
duplicate = (
|
||||
SubcontractorRate.query
|
||||
.filter(
|
||||
SubcontractorRate.subcontractor_id == subcontractor_id,
|
||||
SubcontractorRate.category == category,
|
||||
func.lower(SubcontractorRate.item_name) == item_name.lower()
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
# Ignore current record while editing
|
||||
if duplicate and (not rate_id or duplicate.id != int(rate_id)):
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Category and Rate already exists for this Subcontractor."
|
||||
}
|
||||
|
||||
# -----------------------------
|
||||
# Insert / Update
|
||||
# -----------------------------
|
||||
if rate_id:
|
||||
rate = SubcontractorRate.query.get_or_404(rate_id)
|
||||
else:
|
||||
rate = SubcontractorRate()
|
||||
|
||||
rate.subcontractor_id = subcontractor_id
|
||||
rate.category = category
|
||||
rate.item_code = form.get("item_code")
|
||||
rate.item_name = item_name
|
||||
rate.unit = form.get("unit")
|
||||
rate.rate = form.get("rate")
|
||||
rate.effective_from = form.get("effective_from")
|
||||
rate.effective_to = form.get("effective_to") or None
|
||||
rate.status = form.get("status")
|
||||
|
||||
if not rate_id:
|
||||
db.session.add(rate)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Saved Successfully."
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_all_rates():
|
||||
|
||||
return (
|
||||
SubcontractorRate.query
|
||||
.order_by(SubcontractorRate.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_rate(rate_id):
|
||||
|
||||
return SubcontractorRate.query.get_or_404(rate_id)
|
||||
|
||||
def delete_rate(rate_id):
|
||||
rate = SubcontractorRate.query.get_or_404(rate_id)
|
||||
db.session.delete(rate)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def check_duplicate(subcontractor_id, category, item_name, rate_id=None):
|
||||
|
||||
query = SubcontractorRate.query.filter(
|
||||
SubcontractorRate.subcontractor_id == subcontractor_id,
|
||||
SubcontractorRate.category == category,
|
||||
func.lower(SubcontractorRate.item_name) == item_name.strip().lower()
|
||||
)
|
||||
|
||||
if rate_id:
|
||||
query = query.filter(SubcontractorRate.id != int(rate_id))
|
||||
|
||||
return query.first() is not None
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,47 +1,29 @@
|
||||
# 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.services.db_service import db
|
||||
from flask import current_app
|
||||
|
||||
class UserService:
|
||||
|
||||
def get_all_users(self):
|
||||
db = DBService().connect()
|
||||
cursor = db.cursor(dictionary=True)
|
||||
@staticmethod
|
||||
def register_user(name, email, password):
|
||||
if User.query.filter_by(email=email).first():
|
||||
return None
|
||||
|
||||
cursor.execute("SELECT id, name, email FROM users")
|
||||
rows = cursor.fetchall()
|
||||
user = User(name=name, email=email)
|
||||
user.set_password(password)
|
||||
|
||||
users = [User(**row) for row in rows]
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
current_app.logger.info("User list viewed")
|
||||
return user
|
||||
|
||||
cursor.close()
|
||||
db.close()
|
||||
@staticmethod
|
||||
def validate_login(email, password):
|
||||
user = User.query.filter_by(email=email).first()
|
||||
if user and user.check_password(password):
|
||||
return user
|
||||
return None
|
||||
|
||||
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 |
208
app/templates/activity/activity_log.html
Normal file
208
app/templates/activity/activity_log.html
Normal file
@@ -0,0 +1,208 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center">
|
||||
<div class="mb-3 mb-lg-0">
|
||||
<h3 class="fw-bold text-primary mb-1"> <i class="bi bi-clock-history me-2"></i> Activity Logs </h3>
|
||||
<small class="text-muted"> View application logs, search records and download log files. </small>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a id="downloadLogBtn" href="{{ url_for('activity.download_log') }}?file={{ file_name }}" class="btn btn-success">
|
||||
<i class="bi bi-download me-1"></i>
|
||||
Download Log
|
||||
</a>
|
||||
<a href="{{ url_for('activity.activity') }}" class="btn btn-outline-primary">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>
|
||||
Refresh
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FILTERS -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-funnel-fill me-2"></i> Filters
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<form method="GET">
|
||||
<div class="row g-3">
|
||||
<!-- Log File select -->
|
||||
<div class="col-xl-2 col-lg-4 col-md-6">
|
||||
<label class="form-label fw-bold">Log File</label>
|
||||
<select class="form-select" name="file">
|
||||
<option value="app.log" {% if file_name=="app.log" %}selected{% endif %}> Application </option>
|
||||
|
||||
<option value="debug.log" {% if file_name=="debug.log" %}selected{% endif %}> Debug </option>
|
||||
|
||||
<option value="error.log" {% if file_name=="error.log" %}selected{% endif %}> Error </option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- User search -->
|
||||
<div class="col-xl-2 col-lg-4 col-md-6">
|
||||
<label class="form-label fw-bold">User</label>
|
||||
<input type="text" class="form-control" name="user" value="{{ user }}" placeholder="Username">
|
||||
</div>
|
||||
|
||||
<!-- Level -->
|
||||
<div class="col-xl-2 col-lg-4 col-md-6">
|
||||
<label class="form-label fw-bold">Level</label>
|
||||
<select class="form-select" name="level">
|
||||
<option value="">All</option>
|
||||
|
||||
<option value="INFO"
|
||||
{% if level=="INFO" %}selected{% endif %}>
|
||||
INFO
|
||||
</option>
|
||||
|
||||
<option value="WARNING"
|
||||
{% if level=="WARNING" %}selected{% endif %}>
|
||||
WARNING
|
||||
</option>
|
||||
|
||||
<option value="ERROR"
|
||||
{% if level=="ERROR" %}selected{% endif %}>
|
||||
ERROR
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- From Date -->
|
||||
<div class="col-xl-2 col-lg-4 col-md-6">
|
||||
<label class="form-label fw-bold">From Date</label>
|
||||
<input type="date" class="form-control" name="from_date" value="{{ from_date }}">
|
||||
</div>
|
||||
|
||||
<!-- To Date -->
|
||||
<div class="col-xl-2 col-lg-4 col-md-6">
|
||||
<label class="form-label fw-bold">To Date</label>
|
||||
<input type="date" class="form-control" name="to_date" value="{{ to_date }}">
|
||||
</div>
|
||||
|
||||
<!-- Search button -->
|
||||
<div class="col-xl-2 col-lg-12">
|
||||
<label class="form-label fw-bold">Search</label>
|
||||
<input type="text" class="form-control" name="search" value="{{ search }}" placeholder="Search">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Search and Reset button -->
|
||||
<div class="d-flex gap-2">
|
||||
|
||||
<button class="btn btn-primary">
|
||||
<i class="bi bi-search"></i> Search
|
||||
</button>
|
||||
|
||||
<button type="reset" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-clockwise"></i> Reset
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- LOG TABLE -->
|
||||
<div class="card shadow-sm">
|
||||
<!-- Log Entries -->
|
||||
<div class="card-header bg-dark text-white">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-list-ul me-2"></i>
|
||||
Log Entries
|
||||
</h5>
|
||||
|
||||
<span class="badge bg-light text-dark">
|
||||
{{ logs|length }} Records
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Show table Entries -->
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height:600px; overflow-y:auto; font-size:12px;">
|
||||
<table class="table table-striped table-hover table-bordered align-middle mb-0 small">
|
||||
|
||||
<thead class="table-dark sticky-top">
|
||||
<tr>
|
||||
<th>Sr No</th>
|
||||
<th>Date & Time</th>
|
||||
<th>User</th>
|
||||
<th>Level</th>
|
||||
<th>Module</th>
|
||||
<th>Activity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
{% for log in logs %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ log.date }}</td>
|
||||
<td>{{ log.user }}</td>
|
||||
<td>
|
||||
{% if log.level=="INFO" %}
|
||||
<span class="badge bg-success">INFO</span>
|
||||
{% elif log.level=="WARNING" %}
|
||||
<span class="badge bg-warning text-dark">WARNING</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">ERROR</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>{{ log.module }}</td>
|
||||
<td>{{ log.message }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
const fileSelect = document.querySelector('select[name="file"]');
|
||||
const downloadBtn = document.getElementById("downloadLogBtn");
|
||||
|
||||
function updateDownloadLink() {
|
||||
|
||||
downloadBtn.href =
|
||||
"{{ url_for('activity.download_log') }}?file=" +
|
||||
encodeURIComponent(fileSelect.value);
|
||||
|
||||
}
|
||||
|
||||
updateDownloadLink();
|
||||
|
||||
fileSelect.addEventListener("change", updateDownloadLink);
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -3,63 +3,410 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title if title else "ComparisonSoftware" }}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ title if title else "Comparison Software" }}</title>
|
||||
<link rel="icon" type="image/png" href="{{ url_for('static', filename='images/lcepl.png') }}">
|
||||
|
||||
<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 CSS -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" >
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.1/css/buttons.dataTables.min.css">
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-multiselect@1.1.2/dist/css/bootstrap-multiselect.min.css">
|
||||
</head>
|
||||
|
||||
<body style="background:#f5f7fa;">
|
||||
<body class="bg-light">
|
||||
|
||||
<!-- NAVBAR -->
|
||||
<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">
|
||||
<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">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<!-- Menu -->
|
||||
<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>
|
||||
<li class="nav-item"><a class="nav-link" href="/file/import">File Import</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/file/report">Reports Download</a></li>
|
||||
<!-- Dashboard -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/dashboard">
|
||||
<i class="bi bi-speedometer2 me-1"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Subcontractor Model -->
|
||||
<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 List
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li><a class="dropdown-item" href="/subcontractor/add">Add Subcontractor</a></li>
|
||||
<li><a class="dropdown-item" href="/subcontractor/list">Subcontractor List</a></li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="/subcontractor/add">
|
||||
<i class="bi bi-plus-circle me-2"></i> New Add
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="/subcontractor/list">
|
||||
<i class="bi bi-list-ul me-2"></i> List
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item"><a class="nav-link" href="/user/list">Users</a></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 RA Bills
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<li>
|
||||
<a class="dropdown-item" href="/dashboard/subcontractor_dashboard">
|
||||
<i class="bi bi-speedometer2 me-2"></i> Subcontractor Dashboard
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<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-arrow-left-right me-2"></i> Show Reports
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- Client System -->
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">
|
||||
<i class="bi bi-building me-1"></i> Client RA Bills
|
||||
</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> Comparison Report
|
||||
</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 Subcontractor
|
||||
</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>
|
||||
|
||||
<!-- Masters -->
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="/engi">
|
||||
<i class="bi bi-gear me-2"></i> Masters
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-dark">
|
||||
<!-- Client Standard Rates -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/engi/subcontractor-rate">
|
||||
<i class="bi bi-file-earmark-text me-1"></i> Rate Master
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Client Standard Rates -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/engi/client-rate">
|
||||
<i class="bi bi-file-earmark-text me-1"></i> Client Standard Rates
|
||||
</a>
|
||||
</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>
|
||||
|
||||
</ul>
|
||||
</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>
|
||||
|
||||
<!-- Dashboard -->
|
||||
<li>
|
||||
<a class="dropdown-item text-light py-2" href="{{ url_for('dashboard.dashboard') }}">
|
||||
<i class="bi bi-speedometer2 me-2"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item text-light py-2" href="{{ url_for('activity.activity') }}">
|
||||
<i class="bi bi-clock-history me-2"></i> Activity Log
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Manage Account -->
|
||||
<li>
|
||||
<a class="dropdown-item py-2" href="#">
|
||||
<i class="bi bi-gear me-2"></i> Manage Account
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li><hr class="dropdown-divider border-secondary m-0"></li>
|
||||
|
||||
<!-- Logout Account -->
|
||||
<li>
|
||||
<a class="dropdown-item text-warning" href="/logout">
|
||||
<i class="bi bi-box-arrow-right me-2"></i> Logout
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-3">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show">
|
||||
{{ message }}
|
||||
<button class="btn-close" data-bs-dismiss="alert"></button>
|
||||
|
||||
<!-- PAGE CONTENT -->
|
||||
<div class="container-fluid vh-100 pt-5 overflow-hidden">
|
||||
<!-- FLASH MESSAGES -->
|
||||
<div class="position-fixed top-0 end-0 p-2 p-md-3 mt-5" style="z-index:1080; width:min(95vw,500px);">
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show shadow notification-alert mb-2"
|
||||
role="alert">
|
||||
|
||||
<div class="d-flex align-items-start flex-wrap">
|
||||
|
||||
{% if category == 'success' %}
|
||||
<i class="bi bi-check-circle-fill me-2 fs-5 flex-shrink-0"></i>
|
||||
{% elif category == 'danger' %}
|
||||
<i class="bi bi-x-circle-fill me-2 fs-5 flex-shrink-0"></i>
|
||||
{% elif category == 'warning' %}
|
||||
<i class="bi bi-exclamation-triangle-fill me-2 fs-5 flex-shrink-0"></i>
|
||||
{% elif category == 'info' %}
|
||||
<i class="bi bi-info-circle-fill me-2 fs-5 flex-shrink-0"></i>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex-grow-1">
|
||||
|
||||
<div class="fw-semibold">
|
||||
{% if category == 'success' %}
|
||||
Success
|
||||
{% elif category == 'danger' %}
|
||||
Error
|
||||
{% elif category == 'warning' %}
|
||||
Warning
|
||||
{% elif category == 'info' %}
|
||||
Information
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ message }}
|
||||
</div>
|
||||
|
||||
<!-- Timeline -->
|
||||
<div class="progress mt-2" style="height:5px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated timer-bar" role="progressbar" style="width:100%">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert">
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="overflow-auto h-100">
|
||||
<div class="container mt-4">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<div class="container mt-4">
|
||||
{% block content %}{% endblock %}
|
||||
<!-- Global Loading Overlay -->
|
||||
<div id="globalLoader"
|
||||
class="position-fixed top-0 start-0 w-100 h-100 bg-dark bg-opacity-50 d-none"
|
||||
style="z-index:9999;">
|
||||
|
||||
<div class="d-flex justify-content-center align-items-center h-100">
|
||||
<div class="card shadow-lg border-0 rounded-4" style="width:430px;">
|
||||
<div class="card-body text-center p-5">
|
||||
|
||||
<div class="spinner-border text-primary" style="width:70px;height:70px;"role="status">
|
||||
</div>
|
||||
<h4 class="fw-bold mt-4 mb-2">Please Wait...</h4>
|
||||
<p id="loaderMessage" class="text-muted mb-4">Processing your request...</p>
|
||||
|
||||
<div class="progress" style="height:8px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
style="width:100%">
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||
|
||||
<script src="https://cdn.datatables.net/buttons/2.4.1/js/dataTables.buttons.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.html5.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.print.min.js"></script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-multiselect@1.1.2/dist/js/bootstrap-multiselect.min.js"></script>
|
||||
|
||||
<script>
|
||||
// show alert msg
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.querySelectorAll(".notification-alert").forEach(function(alert){
|
||||
const progressBar = alert.querySelector(".timer-bar");
|
||||
let width = 100;
|
||||
const interval = setInterval(function() {
|
||||
width -= 2;
|
||||
progressBar.style.width = width + "%";
|
||||
if(width <= 0){
|
||||
clearInterval(interval);
|
||||
const bsAlert = bootstrap.Alert.getOrCreateInstance(alert);
|
||||
bsAlert.close();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
// show msg Processing your request
|
||||
function showLoader(message = "Processing your request...") {
|
||||
document.getElementById("loaderMessage").innerHTML = message;
|
||||
document.getElementById("globalLoader").classList.remove("d-none");
|
||||
}
|
||||
|
||||
// Automatically hide
|
||||
function hideLoader(){
|
||||
document.getElementById("globalLoader").classList.add("d-none");
|
||||
}
|
||||
|
||||
// Automatically apply to all forms having class="loading-form"
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.querySelectorAll(".loading-form").forEach(function(form){
|
||||
form.addEventListener("submit", function(){
|
||||
if(!this.checkValidity())
|
||||
return;
|
||||
|
||||
showLoader();
|
||||
const btn = this.querySelector("button[type='submit']");
|
||||
const originalBtnHtml = btn ? btn.innerHTML : null;
|
||||
|
||||
if(btn){
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
Processing...
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
setTimeout(function () {
|
||||
hideLoader();
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
window.addEventListener("pageshow", function () {
|
||||
hideLoader();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
92
app/templates/client_report.html
Normal file
92
app/templates/client_report.html
Normal file
@@ -0,0 +1,92 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid mt-4">
|
||||
<h2 class="mb-4">Client RA Bills Reports</h2>
|
||||
|
||||
<div class="card p-4 shadow-sm mb-5">
|
||||
<form method="POST" class="loading-form">
|
||||
<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-4">
|
||||
<h4 class="mb-3">Table 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 </button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<<<<<<< HEAD
|
||||
<button class="nav-link" id="mh-tab" data-bs-toggle="tab" data-bs-target="#mh" type="button">Mh.Ex
|
||||
</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
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<<<<<<< HEAD
|
||||
<button class="nav-link" id="laying-tab" data-bs-toggle="tab" data-bs-target="#laying"
|
||||
type="button">Laying
|
||||
& Bedding Comparison</button>
|
||||
=======
|
||||
<button class="nav-link" id="laying-tab" data-bs-toggle="tab" data-bs-target="#laying" type="button">Laying
|
||||
& Bedding </button>
|
||||
>>>>>>> 1dceb640bd930c37888799f10f02fe90b219be67
|
||||
=======
|
||||
<button class="nav-link" id="mh-tab" data-bs-toggle="tab" data-bs-target="#mh" type="button">
|
||||
Mh.Ex</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</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 </button>
|
||||
>>>>>>> pankaj-dev
|
||||
</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" %}
|
||||
|
||||
{% 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">
|
||||
<h5>Welcome to Comparison Project</h5>
|
||||
<p>This is dashboard panel.</p>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12 col-md-4">
|
||||
<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>
|
||||
|
||||
<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 %}
|
||||
84
app/templates/edit_record.html
Normal file
84
app/templates/edit_record.html
Normal file
@@ -0,0 +1,84 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h2 class="fw-bold text-primary mb-1">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
Edit Record
|
||||
</h2>
|
||||
<small class="text-muted">
|
||||
Model: <strong>{{ model|upper }}</strong> | Record ID: {{ record.id }}
|
||||
</small>
|
||||
</div>
|
||||
<a href="{{ url_for('file_report.report_file') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to Report
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Form -->
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-card-checklist"></i>
|
||||
Update Record Details
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="row g-3">
|
||||
|
||||
{% for column in record.__table__.columns %}
|
||||
{% if column.name != "id" %}
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label fw-semibold">
|
||||
{{ column.name.replace('_', ' ')|title }}
|
||||
</label>
|
||||
|
||||
{% set value = record|attr(column.name) %}
|
||||
|
||||
{% if column.type.python_type.__name__ == 'bool' %}
|
||||
<select name="{{ column.name }}" class="form-select">
|
||||
<option value="true" {% if value %}selected{% endif %}>Yes</option>
|
||||
<option value="false" {% if not value %}selected{% endif %}>No</option>
|
||||
</select>
|
||||
|
||||
{% elif column.type.python_type.__name__ in ['int', 'float', 'Decimal'] %}
|
||||
<input type="number" step="any" name="{{ column.name }}"
|
||||
class="form-control" value="{{ value if value is not none else '' }}">
|
||||
|
||||
{% elif column.type.python_type.__name__ == 'date' %}
|
||||
<input type="date" name="{{ column.name }}"
|
||||
class="form-control" value="{{ value if value is not none else '' }}">
|
||||
|
||||
{% else %}
|
||||
<input type="text" name="{{ column.name }}"
|
||||
class="form-control" value="{{ value if value is not none else '' }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex justify-content-end gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> Save Changes
|
||||
</button>
|
||||
<a href="{{ url_for('file_report.report_file') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-x-circle"></i> Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
20
app/templates/engineering/client_rate.html
Normal file
20
app/templates/engineering/client_rate.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
|
||||
<div class="card shadow">
|
||||
|
||||
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-currency-rupee"></i>
|
||||
Client Rate Master
|
||||
</h4>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
384
app/templates/engineering/contractor_rate.html
Normal file
384
app/templates/engineering/contractor_rate.html
Normal file
@@ -0,0 +1,384 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
|
||||
<div class="card shadow">
|
||||
|
||||
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-currency-rupee"></i>
|
||||
Subcontractor Rate Master
|
||||
</h4>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<form method="POST" id="rateForm">
|
||||
|
||||
<!-- Hidden ID -->
|
||||
<input type="hidden" id="rate_id" name="id" value="{{ rate.id if rate else '' }}">
|
||||
|
||||
<div class="row">
|
||||
|
||||
<!-- Subcontractor -->
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-bold"> Subcontractor </label>
|
||||
<select id="subcontractor_id" name="subcontractor_id" class="form-select" required>
|
||||
<option value="">-- Select Subcontractor --</option>
|
||||
{% for sub in subcontractors %}
|
||||
<option value="{{ sub.id }}"
|
||||
{% if rate and rate.subcontractor_id==sub.id %}
|
||||
selected
|
||||
{% endif %}>
|
||||
{{ sub.subcontractor_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Category -->
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-bold"> Category </label>
|
||||
<select id="category" name="category" class="form-select" required>
|
||||
<option value="">-- Select Category --</option>
|
||||
|
||||
<option value="trench_excavation"
|
||||
{% if rate and rate.category=="trench_excavation" %}selected{% endif %}>
|
||||
Trench Excavation
|
||||
</option>
|
||||
<option value="manhole_excavation"
|
||||
{% if rate and rate.category=="manhole_excavation" %}selected{% endif %}>
|
||||
Manhole Excavation
|
||||
</option>
|
||||
|
||||
<option value="Manhole_Domestic_Chamber"
|
||||
{% if rate and rate.category=="Manhole_Domestic_Chamber" %}selected{% endif %}>
|
||||
MH & Domestic Chamber
|
||||
</option>
|
||||
|
||||
<option value="Laying"
|
||||
{% if rate and rate.category=="Laying" %}selected{% endif %}>
|
||||
Pipe Laying
|
||||
</option>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-bold"> Status </label>
|
||||
|
||||
<select name="status" class="form-select">
|
||||
<option value="Active"
|
||||
{% if not rate or rate.status=="Active" %}selected{% endif %}>
|
||||
Active
|
||||
</option>
|
||||
|
||||
<option value="Inactive"
|
||||
{% if rate and rate.status=="Inactive" %}selected{% endif %}>
|
||||
Inactive
|
||||
</option>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<!-- Item Code -->
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label fw-bold"> Item Code </label>
|
||||
<input type="text" name="item_code" class="form-control" value="{{ rate.item_code if rate else '' }}" required>
|
||||
</div>
|
||||
|
||||
<!-- Item Name -->
|
||||
<div class="col-md-5 mb-3">
|
||||
<label class="form-label fw-bold"> Item Name </label>
|
||||
<input type="text"
|
||||
id="item_name"
|
||||
name="item_name"
|
||||
class="form-control"
|
||||
autocomplete="off"
|
||||
value="{{ rate.item_name if rate else '' }}"
|
||||
required>
|
||||
|
||||
<small id="duplicateMessage"
|
||||
class="text-danger"
|
||||
style="display:none;">
|
||||
</small>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Unit -->
|
||||
<div class="col-md-2 mb-3">
|
||||
<label class="form-label fw-bold">Unit</label>
|
||||
<select name="unit" class="form-select">
|
||||
<option value="Cum"
|
||||
{% if not rate or rate.unit=="Cum" %}selected{% endif %}>
|
||||
Cum
|
||||
</option>
|
||||
<option value="Nos"
|
||||
{% if rate and rate.unit=="Nos" %}selected{% endif %}>
|
||||
Nos
|
||||
</option>
|
||||
<option value="Rmt"
|
||||
{% if rate and rate.unit=="Rmt" %}selected{% endif %}>
|
||||
Rmt
|
||||
</option>
|
||||
<option value="Sqm"
|
||||
{% if rate and rate.unit=="Sqm" %}selected{% endif %}>
|
||||
Sqm
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Rate -->
|
||||
<div class="col-md-2 mb-3">
|
||||
<label class="form-label fw-bold">Rate</label>
|
||||
<input type="number" step="0.01" name="rate" class="form-control" value="{{ rate.rate if rate else '' }}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label fw-bold"> Effective From </label>
|
||||
<input type="date" name="effective_from" class="form-control" value="{{ rate.effective_from if rate else '' }}" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label fw-bold"> Effective To </label>
|
||||
<input type="date" name="effective_to" class="form-control" value="{{ rate.effective_to if rate else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="text-end">
|
||||
<a href="{{ url_for('engineering.add_subcontractor_rates') }}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-clockwise"></i> Reset
|
||||
</a>
|
||||
|
||||
<button type="submit" id="saveBtn" class="btn btn-success">
|
||||
{% if rate %}
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
Update Rate
|
||||
{% else %}
|
||||
<i class="bi bi-check-circle"></i>
|
||||
Save Rate
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card shadow mt-4">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-table"></i>Rate List
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<table class="table table-bordered table-hover table-striped" id="rateTable">
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Subcontractor</th>
|
||||
<th>Category</th>
|
||||
<th>Item Code</th>
|
||||
<th>Item Name</th>
|
||||
<th>Unit</th>
|
||||
<th>Rate</th>
|
||||
<th>Status</th>
|
||||
<th width="130">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for row in rates %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ row.subcontractor.subcontractor_name }}</td>
|
||||
<td>{{ row.category }}</td>
|
||||
<td>{{ row.item_code }}</td>
|
||||
<td>{{ row.item_name }}</td>
|
||||
<td>{{ row.unit }}</td>
|
||||
<td>{{ row.rate }}</td>
|
||||
<td>
|
||||
{% if row.status=="Active" %}
|
||||
<span class="badge bg-success">
|
||||
Active
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">
|
||||
Inactive
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="{{ url_for('engineering.edit_rate', rate_id=row.id) }}" class="btn btn-warning btn-sm">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('engineering.delete_rate', rate_id=row.id) }}"
|
||||
class="btn btn-danger btn-sm" onclick="return confirm('Delete this Item:{{row.item_name}} & Rate:{{row.rate}} ?')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
// ----------------------------
|
||||
// DataTable
|
||||
// ----------------------------
|
||||
$("#rateTable").DataTable({
|
||||
responsive: true,
|
||||
pageLength: 10,
|
||||
destroy: true
|
||||
});
|
||||
|
||||
// ----------------------------
|
||||
// Validate on field change
|
||||
// ----------------------------
|
||||
$("#subcontractor_id, #category").on("change", function () {
|
||||
clearValidation();
|
||||
checkDuplicateRate();
|
||||
|
||||
});
|
||||
|
||||
// ----------------------------
|
||||
// Validate while typing
|
||||
// ----------------------------
|
||||
let timer;
|
||||
|
||||
$("#item_name").on("keyup input", function () {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(function () {
|
||||
checkDuplicateRate();
|
||||
}, 300);
|
||||
|
||||
});
|
||||
|
||||
// Edit Mode
|
||||
checkDuplicateRate();
|
||||
|
||||
// ----------------------------
|
||||
// Prevent Submit
|
||||
// ----------------------------
|
||||
$("#rateForm").submit(function (e) {
|
||||
|
||||
if ($("#saveBtn").prop("disabled")) {
|
||||
e.preventDefault();
|
||||
$("#item_name").focus();
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
//=========================================
|
||||
// Reset Validation
|
||||
//=========================================
|
||||
function clearValidation() {
|
||||
$("#duplicateMessage")
|
||||
.hide()
|
||||
.text("")
|
||||
.removeClass("text-success text-danger");
|
||||
|
||||
$("#item_name")
|
||||
.removeClass("is-valid is-invalid");
|
||||
$("#saveBtn").prop("disabled", false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//=========================================
|
||||
// Duplicate Validation
|
||||
//=========================================
|
||||
function checkDuplicateRate() {
|
||||
let subcontractor = $("#subcontractor_id").val();
|
||||
let category = $("#category").val();
|
||||
let item_name = $("#item_name").val().trim();
|
||||
let rate_id = $("#rate_id").val();
|
||||
|
||||
// Mandatory fields
|
||||
if (
|
||||
subcontractor == "" ||
|
||||
category == "" ||
|
||||
item_name.length < 2
|
||||
) {
|
||||
clearValidation();
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
|
||||
url: "{{ url_for('engineering.check_rate') }}",
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
data: {
|
||||
subcontractor_id: subcontractor,
|
||||
category: category,
|
||||
item_name: item_name,
|
||||
rate_id: rate_id
|
||||
},
|
||||
|
||||
success: function (response) {
|
||||
if (response.exists) {
|
||||
$("#duplicateMessage")
|
||||
.text("This Item Name already exists for the selected Subcontractor and Category.")
|
||||
.removeClass("text-success")
|
||||
.addClass("text-danger")
|
||||
.show();
|
||||
|
||||
$("#item_name")
|
||||
.removeClass("is-valid")
|
||||
.addClass("is-invalid");
|
||||
|
||||
$("#saveBtn").prop("disabled", true);
|
||||
|
||||
}
|
||||
else {
|
||||
$("#duplicateMessage")
|
||||
.text("Item Name is available.")
|
||||
.removeClass("text-danger")
|
||||
.addClass("text-success")
|
||||
.show();
|
||||
|
||||
$("#item_name")
|
||||
.removeClass("is-invalid")
|
||||
.addClass("is-valid");
|
||||
|
||||
$("#saveBtn").prop("disabled", false);
|
||||
}
|
||||
},
|
||||
|
||||
error: function () {
|
||||
clearValidation();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
83
app/templates/engineering/index.html
Normal file
83
app/templates/engineering/index.html
Normal file
@@ -0,0 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h3 class="fw-bold">
|
||||
<i class="bi bi-gear-fill text-primary"></i>
|
||||
Engineering Masters
|
||||
</h3>
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Subcontractor Rate Master -->
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card shadow h-100">
|
||||
|
||||
<div class="card-body text-center">
|
||||
|
||||
<i class="bi bi-cash-stack text-success"
|
||||
style="font-size:55px;"></i>
|
||||
|
||||
<h5 class="mt-3">
|
||||
Subcontractor Rate Master
|
||||
</h5>
|
||||
|
||||
<p class="text-muted">
|
||||
Manage subcontractor-wise rates.
|
||||
</p>
|
||||
|
||||
<a href="{{ url_for('engineering.add_subcontractor_rates') }}"
|
||||
class="btn btn-success">
|
||||
|
||||
<i class="bi bi-arrow-right-circle"></i>
|
||||
Open Module
|
||||
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client Rate Master -->
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card shadow h-100">
|
||||
|
||||
<div class="card-body text-center">
|
||||
|
||||
<i class="bi bi-building text-primary"
|
||||
style="font-size:55px;"></i>
|
||||
|
||||
<h5 class="mt-3">
|
||||
Client Standard Rate Master
|
||||
</h5>
|
||||
|
||||
<p class="text-muted">
|
||||
Manage client standard rates.
|
||||
</p>
|
||||
|
||||
<a href="{{ url_for('engineering.client_rates') }}"
|
||||
class="btn btn-primary">
|
||||
|
||||
<i class="bi bi-arrow-right-circle"></i>
|
||||
Open Module
|
||||
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
66
app/templates/file_format.html
Normal file
66
app/templates/file_format.html
Normal file
@@ -0,0 +1,66 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4">Download File Formats</h2>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Subcontractor Format -->
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center shadow-sm h-100">
|
||||
<div class="card-body d-flex flex-column justify-content-center">
|
||||
<div class="mb-3 fs-1 text-primary">
|
||||
⬇️
|
||||
</div>
|
||||
<h5 class="card-title">Subcontractor Upload Format</h5>
|
||||
<p class="text-muted mb-2">Excel (.xlsx)</p>
|
||||
<p class="small text-secondary">File size: 245 KB</p>
|
||||
|
||||
<a href="{{ url_for('file_format.download_excel_format', filename='subcontractor_format.xlsx') }}"
|
||||
class="btn btn-outline-primary mt-auto">
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Client Format -->
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center shadow-sm h-100">
|
||||
<div class="card-body d-flex flex-column justify-content-center">
|
||||
<div class="mb-3 fs-1 text-success">
|
||||
⬇️
|
||||
</div>
|
||||
<h5 class="card-title">Client Upload Format</h5>
|
||||
<p class="text-muted mb-2">Excel (.xlsx)</p>
|
||||
<p class="small text-secondary">File size: 310 KB</p>
|
||||
|
||||
<a href="{{ url_for('file_format.download_excel_format', filename='client_format.xlsx') }}"
|
||||
class="btn btn-outline-success mt-auto">
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MH & DC Format -->
|
||||
<!-- <div class="col-md-4">
|
||||
<div class="card text-center shadow-sm h-100">
|
||||
<div class="card-body d-flex flex-column justify-content-center">
|
||||
<div class="mb-3 fs-1 text-warning">
|
||||
⬇️
|
||||
</div>
|
||||
<h5 class="card-title">Manhole & DC Format</h5>
|
||||
<p class="text-muted mb-2">Excel (.xlsx)</p>
|
||||
<p class="small text-secondary">File size: 190 KB</p>
|
||||
|
||||
<a href="{{ url_for('file_format.download_excel_format', filename='mh_dc_format.xlsx') }}"
|
||||
class="btn btn-outline-warning mt-auto">
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,39 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4">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. FILE TYPE (MODEL NAME) -->
|
||||
<label class="form-label">Select File Type</label>
|
||||
<select name="file_type" id="file_type" class="form-select mb-3" required>
|
||||
<option value="">-- Select File Type --</option>
|
||||
<option value="">Subcontractor Sheet</option>
|
||||
<option value="trench_excavation">Trench Excavation (Tr.Ex)</option>
|
||||
<option value="manhole_excavation">Manhole Excavation (Mh.Ex)</option>
|
||||
<option value="manhole_domestic_chamber">Manhole & Domestic Chambers Construction (MH & DC)</option>
|
||||
<option value="">Laying Sheet</option>
|
||||
</select>
|
||||
|
||||
<!-- 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 %}
|
||||
145
app/templates/file_import_client.html
Normal file
145
app/templates/file_import_client.html
Normal file
@@ -0,0 +1,145 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="row mb-4">
|
||||
|
||||
<div class="col-12">
|
||||
|
||||
<div class="card border-0 shadow-sm bg-success text-white">
|
||||
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
|
||||
<div>
|
||||
|
||||
<h3 class="fw-bold mb-1">
|
||||
<i class="bi bi-building-fill-up me-2"></i>
|
||||
Client RA Bill Import
|
||||
</h3>
|
||||
|
||||
<p class="mb-0 opacity-75">
|
||||
Upload the Client RA Bill Excel file for comparison.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="display-5">
|
||||
<i class="bi bi-file-earmark-excel-fill"></i>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Upload Card -->
|
||||
<div class="row justify-content-center">
|
||||
|
||||
<div class="col-lg-7">
|
||||
|
||||
<div class="card border-0 shadow">
|
||||
|
||||
<div class="card-header bg-light">
|
||||
|
||||
<h5 class="mb-0 fw-bold text-success">
|
||||
<i class="bi bi-upload me-2"></i>
|
||||
Upload Details
|
||||
</h5>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="card-body p-4">
|
||||
|
||||
<form method="POST"
|
||||
enctype="multipart/form-data"
|
||||
class="loading-form"
|
||||
onsubmit="showLoader('Reading Excel file...<br>Validating records...<br>Saving data to database...')">
|
||||
|
||||
<!-- RA Bill -->
|
||||
<div class="mb-4">
|
||||
|
||||
<label class="form-label fw-semibold">
|
||||
|
||||
<i class="bi bi-receipt-cutoff text-success me-2"></i>
|
||||
|
||||
RA Bill No
|
||||
|
||||
<span class="text-danger">*</span>
|
||||
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="RA_Bill_No"
|
||||
class="form-control"
|
||||
placeholder="Enter RA Bill Number"
|
||||
required>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Upload -->
|
||||
<div class="mb-4">
|
||||
|
||||
<label class="form-label fw-semibold">
|
||||
|
||||
<i class="bi bi-file-earmark-arrow-up text-primary me-2"></i>
|
||||
|
||||
Select Excel File
|
||||
|
||||
<span class="text-danger">*</span>
|
||||
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
name="file"
|
||||
class="form-control"
|
||||
accept=".xlsx,.xls,.csv"
|
||||
required>
|
||||
|
||||
<div class="form-text">
|
||||
|
||||
Supported Formats:
|
||||
<strong>.xlsx</strong>,
|
||||
<strong>.xls</strong>,
|
||||
<strong>.csv</strong>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-flex justify-content-between">
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-success px-5">
|
||||
|
||||
<i class="bi bi-cloud-upload-fill me-2"></i>
|
||||
Upload File
|
||||
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
125
app/templates/file_import_subcontractor.html
Normal file
125
app/templates/file_import_subcontractor.html
Normal file
@@ -0,0 +1,125 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-12">
|
||||
<div class="card border-0 shadow-sm bg-primary text-white">
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
|
||||
<div>
|
||||
<h3 class="mb-1 fw-bold">
|
||||
<i class="bi bi-cloud-arrow-up-fill me-2"></i>
|
||||
Sub-Contractor File Import
|
||||
</h3>
|
||||
|
||||
<p class="mb-0 opacity-75">
|
||||
Upload RA Bill Excel file for the selected subcontractor.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="display-5">
|
||||
<i class="bi bi-file-earmark-excel-fill"></i>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Card -->
|
||||
<div class="row justify-content-center">
|
||||
|
||||
<div class="col-lg-8">
|
||||
|
||||
<div class="card border-0 shadow">
|
||||
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0 fw-bold text-primary">
|
||||
<i class="bi bi-upload me-2"></i>
|
||||
Upload Details
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-4">
|
||||
<form method="POST"
|
||||
enctype="multipart/form-data"
|
||||
class="loading-form"
|
||||
onsubmit="showLoader('Reading Excel file...<br>Saving data to database...')">
|
||||
|
||||
<!-- Select Subcontractor -->
|
||||
<div class="mb-4">
|
||||
|
||||
<label class="form-label fw-semibold">
|
||||
<i class="bi bi-building me-1 text-primary"></i>
|
||||
Select Subcontractor
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
|
||||
<select name="subcontractor_id" id="subcontractor_id"
|
||||
class="form-select" required>
|
||||
|
||||
<option value=""> -- Select Subcontractor -- </option>
|
||||
{% for sc in subcontractors %}
|
||||
<option value="{{ sc.id }}">{{ sc.subcontractor_name }} </option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- RA Bill No -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">
|
||||
<i class="bi bi-receipt me-1 text-success"></i>
|
||||
RA Bill No
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
|
||||
<input type="text"
|
||||
name="RA_Bill_No"
|
||||
class="form-control"
|
||||
placeholder="Enter RA Bill Number"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<!-- File Upload -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">
|
||||
<i class="bi bi-file-earmark-arrow-up me-1 text-warning"></i>
|
||||
Upload Excel File
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
|
||||
<input type="file"
|
||||
name="file"
|
||||
class="form-control"
|
||||
accept=".xlsx,.xls,.csv"
|
||||
required>
|
||||
|
||||
<div class="form-text">
|
||||
Supported formats:
|
||||
<strong>.xlsx</strong>,
|
||||
<strong>.xls</strong>,
|
||||
<strong>.csv</strong>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<button type="submit" class="btn btn-primary px-5">
|
||||
<i class="bi bi-cloud-upload-fill me-2"></i>
|
||||
Upload File
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
26
app/templates/generate_comparison_report.html
Normal file
26
app/templates/generate_comparison_report.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
|
||||
<h2 class="mb-4">Subcontractor vs Client Comparison</h2>
|
||||
|
||||
<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,207 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h2 class="mb-4">Login</h2>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<div class="card p-4 shadow-sm">
|
||||
<form method="POST">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>LCEPL | Login</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<label>Email:</label>
|
||||
<input type="email" name="email" class="form-control mb-3" required>
|
||||
<!-- 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-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
|
||||
<label>Password:</label>
|
||||
<input type="password" name="password" class="form-control mb-3" required>
|
||||
|
||||
<button class="btn btn-success">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
|
||||
<!-- Flash Messages -->
|
||||
<div class="position-fixed top-0 end-0 p-3"
|
||||
style="z-index:1080; width:min(95vw,420px);">
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
|
||||
<div class="alert alert-{{ category }} alert-dismissible fade show shadow notification-alert">
|
||||
|
||||
<strong>
|
||||
{% if category=="success" %}
|
||||
<i class="bi bi-check-circle-fill me-2"></i>Success
|
||||
{% elif category=="danger" %}
|
||||
<i class="bi bi-x-circle-fill me-2"></i>Error
|
||||
{% elif category=="warning" %}
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>Warning
|
||||
{% else %}
|
||||
<i class="bi bi-info-circle-fill me-2"></i>Information
|
||||
{% endif %}
|
||||
</strong>
|
||||
|
||||
<div>{{ message }}</div>
|
||||
|
||||
<div class="progress mt-2" style="height:4px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated timer-bar"
|
||||
style="width:100%"></div>
|
||||
</div>
|
||||
|
||||
<button class="btn-close"
|
||||
data-bs-dismiss="alert">
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div class="row justify-content-center align-items-center min-vh-100">
|
||||
<div class="col-12 col-sm-10 col-md-8 col-lg-6 col-xl-5">
|
||||
<div class="card shadow-lg border-0">
|
||||
<div class="card-body p-5">
|
||||
<div class="text-center mb-4">
|
||||
<img src="{{ url_for('static', filename='images/lcepl.png') }}"
|
||||
class="img-fluid mb-3"
|
||||
style="max-height:100px;">
|
||||
|
||||
<h3 class="fw-bold text-success">Laxmi Civil Engineering Services Pvt Ltd</h3>
|
||||
<p class="text-muted">Data Comparison Software (UGD)</p>
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Username</label>
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-envelope-fill"></i>
|
||||
</span>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="email"
|
||||
class="form-control"
|
||||
placeholder="Enter Domain Username"
|
||||
autocomplete="username"
|
||||
required>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-semibold">Password</label>
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-lock-fill"></i>
|
||||
</span>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
class="form-control"
|
||||
placeholder="Enter Password"
|
||||
required>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
id="togglePassword"
|
||||
class="input-group-text bg-white border-start-0"
|
||||
style="cursor:pointer;">
|
||||
<i class="bi bi-eye text-secondary"></i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-success w-100 btn-lg">
|
||||
<i class="bi bi-box-arrow-in-right me-2"></i>
|
||||
Login
|
||||
</button>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<a href="#">Forgot Password?</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<small class="text-muted">
|
||||
© 2026 Laxmi Civil Engineering Services Pvt Ltd
|
||||
</small>
|
||||
</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>
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
// Password Show / Hide
|
||||
const togglePassword = document.getElementById("togglePassword");
|
||||
const password = document.getElementById("password");
|
||||
|
||||
if (togglePassword && password) {
|
||||
|
||||
togglePassword.addEventListener("click", function () {
|
||||
|
||||
const type =
|
||||
password.getAttribute("type") === "password"
|
||||
? "text"
|
||||
: "password";
|
||||
|
||||
password.setAttribute("type", type);
|
||||
|
||||
this.innerHTML =
|
||||
type === "password"
|
||||
? '<i class="bi bi-eye"></i>'
|
||||
: '<i class="bi bi-eye-slash"></i>';
|
||||
});
|
||||
}
|
||||
|
||||
// Auto Close Notification after 5 sec
|
||||
document.querySelectorAll(".notification-alert").forEach(function (alert) {
|
||||
|
||||
const progressBar = alert.querySelector(".timer-bar");
|
||||
|
||||
let width = 100;
|
||||
|
||||
const interval = setInterval(function () {
|
||||
|
||||
width -= 2;
|
||||
progressBar.style.width = width + "%";
|
||||
|
||||
if (width <= 0) {
|
||||
|
||||
clearInterval(interval);
|
||||
|
||||
bootstrap.Alert
|
||||
.getOrCreateInstance(alert)
|
||||
.close();
|
||||
}
|
||||
|
||||
}, 100);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,18 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h2 class="mb-4">User Registration</h2>
|
||||
<h2>Register User</h2>
|
||||
|
||||
<div class="card p-4 shadow-sm">
|
||||
<form method="POST">
|
||||
<label>Name</label>
|
||||
<input class="form-control mb-3" name="name" required>
|
||||
|
||||
<label>Name:</label>
|
||||
<input type="text" name="name" class="form-control mb-3" required>
|
||||
<label>Email</label>
|
||||
<input type="email" class="form-control mb-3" name="email" required>
|
||||
|
||||
<label>Email:</label>
|
||||
<input type="email" name="email" class="form-control mb-3" required>
|
||||
|
||||
<label>Password:</label>
|
||||
<input type="password" name="password" class="form-control mb-3" required>
|
||||
<label>Password</label>
|
||||
<input type="password" class="form-control mb-3" name="password" required>
|
||||
|
||||
<button class="btn btn-primary">Register</button>
|
||||
</form>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4">File Report</h2>
|
||||
|
||||
<div class="card p-4 shadow-sm">
|
||||
|
||||
<form method="POST">
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<button class="btn btn-primary w-100">Generate Report</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,39 +1,169 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="card shadow-sm p-4">
|
||||
<div class="container-fluid">
|
||||
|
||||
<h4 class="mb-3">Add New Subcontractor</h4>
|
||||
|
||||
<form action="/subcontractor/save" method="POST">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Subcontractor Name</label>
|
||||
<input type="text" class="form-control" name="subcontractor_name" required>
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h3 class="fw-bold mb-1">
|
||||
<i class="bi bi-person-plus-fill text-success me-2"></i>
|
||||
Add New Subcontractor
|
||||
</h3>
|
||||
<p class="text-muted mb-0">
|
||||
Enter the subcontractor details below.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Contact Person</label>
|
||||
<input type="text" class="form-control" name="contact_person">
|
||||
<a href="{{ url_for('subcontractor.subcontractor_list') }}"
|
||||
class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-2"></i>
|
||||
Back to List
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow border-0">
|
||||
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-building-fill-add me-2"></i>
|
||||
Subcontractor Information
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Mobile</label>
|
||||
<input type="text" class="form-control" name="mobile_no">
|
||||
<div class="card-body">
|
||||
|
||||
<form action="{{ url_for('subcontractor.save_subcontractor') }}"
|
||||
method="POST"
|
||||
class="loading-form">
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
<!-- Subcontractor Name -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">
|
||||
<i class="bi bi-building me-1"></i>
|
||||
Subcontractor Name
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="subcontractor_name"
|
||||
placeholder="Enter Subcontractor Name"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<!-- Contact Person -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">
|
||||
<i class="bi bi-person-fill me-1"></i>
|
||||
Contact Person
|
||||
</label>
|
||||
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="contact_person"
|
||||
placeholder="Enter Contact Person">
|
||||
</div>
|
||||
|
||||
<!-- Mobile -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">
|
||||
<i class="bi bi-telephone-fill me-1"></i>
|
||||
Mobile Number
|
||||
</label>
|
||||
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="mobile_no"
|
||||
maxlength="10"
|
||||
placeholder="Enter Mobile Number">
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">
|
||||
<i class="bi bi-envelope-fill me-1"></i>
|
||||
Email Address
|
||||
</label>
|
||||
|
||||
<input type="email"
|
||||
class="form-control"
|
||||
name="email_id"
|
||||
placeholder="Enter Email Address">
|
||||
</div>
|
||||
|
||||
<!-- GST -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">
|
||||
<i class="bi bi-receipt-cutoff me-1"></i>
|
||||
GST Number
|
||||
</label>
|
||||
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="gst_no"
|
||||
placeholder="Enter GST Number">
|
||||
</div>
|
||||
|
||||
<!-- PAN -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">
|
||||
<i class="bi bi-credit-card-2-front-fill me-1"></i>
|
||||
PAN Number
|
||||
</label>
|
||||
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="pan_no"
|
||||
placeholder="Enter PAN Number">
|
||||
</div>
|
||||
|
||||
<!-- Address -->
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold">
|
||||
<i class="bi bi-geo-alt-fill me-1"></i>
|
||||
Address
|
||||
</label>
|
||||
|
||||
<textarea class="form-control"
|
||||
rows="4"
|
||||
name="address"
|
||||
placeholder="Enter Complete Address"></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
|
||||
<button type="reset"
|
||||
class="btn btn-outline-warning">
|
||||
<i class="bi bi-arrow-clockwise me-2"></i>
|
||||
Reset
|
||||
</button>
|
||||
|
||||
<a href="{{ url_for('subcontractor.subcontractor_list') }}"
|
||||
class="btn btn-outline-secondary">
|
||||
<i class="bi bi-x-circle me-2"></i>
|
||||
Cancel
|
||||
</a>
|
||||
|
||||
<button type="submit"
|
||||
class="btn btn-success">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>
|
||||
Save Subcontractor
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" class="form-control" name="email_id">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">GST No</label>
|
||||
<input type="text" class="form-control" name="gst_no">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,36 +4,58 @@
|
||||
<div class="card shadow-sm p-4">
|
||||
<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">
|
||||
<label class="form-label">Subcontractor Name</label>
|
||||
<label class="form-label">Subcontractor Name:</label>
|
||||
<input type="text" class="form-control" name="subcontractor_name"
|
||||
value="{{ subcontractor.subcontractor_name }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Contact Person</label>
|
||||
<label class="form-label">Contact Person Name:</label>
|
||||
<input type="text" class="form-control" name="contact_person" value="{{ subcontractor.contact_person }}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Mobile</label>
|
||||
<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 }}">
|
||||
</div>
|
||||
|
||||
<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 }}">
|
||||
</div>
|
||||
|
||||
<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 }}">
|
||||
</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>
|
||||
<a href="/subcontractor/list" class="btn btn-secondary">Back</a>
|
||||
<a href="{{ url_for('subcontractor.subcontractor_list') }}" class="btn btn-secondary">Back</a>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,38 +1,162 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="card shadow-sm p-4">
|
||||
<h4 class="mb-3">Subcontractor List</h4>
|
||||
<div class="container mt-4">
|
||||
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Mobile</th>
|
||||
<th>Email</th>
|
||||
<th>GST No</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-3">
|
||||
<h4 class="mb-3 mb-md-0">Subcontractor List</h4>
|
||||
|
||||
<a href="{{ url_for('subcontractor.add_subcontractor') }}" class="btn btn-primary btn-sm">
|
||||
+ Add Subcontractor
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-2 p-md-3">
|
||||
|
||||
<!-- 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>
|
||||
|
||||
{% endblock %}
|
||||
379
app/templates/subcontractor_dashboard.html
Normal file
379
app/templates/subcontractor_dashboard.html
Normal file
@@ -0,0 +1,379 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/choices.min.css">
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js"></script>
|
||||
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="card shadow border-0 mb-4">
|
||||
<div class="card-body">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
|
||||
<div>
|
||||
<h3 class="fw-bold text-primary">
|
||||
<i class="bi bi-bar-chart-fill"></i>
|
||||
RA Bill Analytics Dashboard
|
||||
</h3>
|
||||
|
||||
<small class="text-muted">
|
||||
Compare Quantity, Depth & RA Bill Analytics
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-success" id="exportExcel">
|
||||
<i class="bi bi-file-earmark-excel"></i>
|
||||
Excel
|
||||
</button>
|
||||
|
||||
<button class="btn btn-danger" id="exportPDF">
|
||||
<i class="bi bi-file-earmark-pdf"></i>
|
||||
PDF
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FILTERS -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-funnel-fill"></i>
|
||||
Filters
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- Contractor -->
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label fw-bold"> Subcontractor </label>
|
||||
<select class="form-select" id="subcontractor">
|
||||
<option value="">--- select contractor ---</option>
|
||||
{% for s in subcontractors %}
|
||||
<option value="{{s.id}}">
|
||||
{{s.subcontractor_name}}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Category -->
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label fw-bold">Category</label>
|
||||
<select class="form-select" id="category">
|
||||
<option value="">--- select category ---</option>
|
||||
<option value="trench_excavation">Trench Excavation</option>
|
||||
<option value="manhole_excavation">Manhole Excavation</option>
|
||||
<option value="Manhole_Domestic_Chamber">Manhole Domestic Chamber</option>
|
||||
<option value="Laying">Pipe Laying</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- RA Bill -->
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label fw-bold"> RA Bills</label>
|
||||
<select id="ra_bill" class="form-select" multiple></select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<!-- Search button -->
|
||||
<button class="btn btn-primary" id="searchBtn">
|
||||
<i class="bi bi-search"></i>Search
|
||||
</button>
|
||||
<!-- Reset button -->
|
||||
<button class="btn btn-secondary" id="resetBtn">
|
||||
<i class="bi bi-arrow-clockwise"></i>Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TABS -->
|
||||
<div class="card shadow">
|
||||
|
||||
<div class="card-header">
|
||||
<ul class="nav nav-pills">
|
||||
<!--Bar Chart Tab -->
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#barTab">
|
||||
<i class="bi bi-bar-chart"></i> Bar Chart
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<!-- Data Table Tab -->
|
||||
<li class="nav-item">
|
||||
<button class="nav-link"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#tableTab">
|
||||
<i class="bi bi-table"></i> Data Table
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<!-- BAR TAB -->
|
||||
<div class="tab-pane fade show active" id="barTab">
|
||||
<div style="height:600px">
|
||||
<canvas id="barChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TABLE TAB -->
|
||||
<div class="tab-pane fade" id="tableTab">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover" id="resultTable">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Sr No</th>
|
||||
<th>Strata Type & Depth</th>
|
||||
<th class="text-end">Client Qty</th>
|
||||
<th class="text-end">Sub Contractor Qty</th>
|
||||
<th class="text-end">Difference</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
let barChart;
|
||||
let raBillChoice;
|
||||
|
||||
/* Load RA Bills */
|
||||
function loadRABills(){
|
||||
|
||||
let subcontractor = document.getElementById("subcontractor").value
|
||||
let category = document.getElementById("category").value
|
||||
|
||||
if (!raBillChoice)
|
||||
return;
|
||||
|
||||
if (!subcontractor || !category) {
|
||||
|
||||
raBillChoice.clearStore();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/dashboard/api/get-ra-bills?subcontractor=${subcontractor}&category=${category}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
|
||||
raBillChoice.clearStore();
|
||||
|
||||
let choices = [];
|
||||
|
||||
data.ra_bills.forEach(function(bill){
|
||||
|
||||
choices.push({
|
||||
|
||||
value: bill,
|
||||
|
||||
label: bill
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
raBillChoice.setChoices(
|
||||
choices,
|
||||
"value",
|
||||
"label",
|
||||
true
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/* Search */
|
||||
function loadDashboard() {
|
||||
const subcontractor = document.getElementById("subcontractor").value;
|
||||
const category = document.getElementById("category").value;
|
||||
|
||||
let raBills = [];
|
||||
|
||||
if (raBillChoice) {
|
||||
raBills = raBillChoice.getValue(true);
|
||||
}
|
||||
|
||||
let apiUrl = "";
|
||||
|
||||
switch (category) {
|
||||
|
||||
case "trench_excavation":
|
||||
apiUrl = "/dashboard/api/tr-analysis";
|
||||
break;
|
||||
|
||||
case "manhole_excavation":
|
||||
apiUrl = "/dashboard/api/mh-analysis";
|
||||
break;
|
||||
|
||||
case "Manhole_Domestic_Chamber":
|
||||
apiUrl = "/dashboard/api/mdc-analysis";
|
||||
break;
|
||||
|
||||
case "Laying":
|
||||
apiUrl = "/dashboard/api/laying-analysis";
|
||||
break;
|
||||
|
||||
default:
|
||||
alert("Please select category");
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`${apiUrl}?subcontractor=${subcontractor}&category=${category}&ra_bill=${raBills.join(",")}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
drawBar(data);
|
||||
drawTable(data);
|
||||
})
|
||||
.catch(err => console.error(err));
|
||||
|
||||
}
|
||||
|
||||
/* BAR */
|
||||
function drawBar(data) {
|
||||
if (barChart) {
|
||||
barChart.destroy();
|
||||
}
|
||||
const ctx = document.getElementById("barChart");
|
||||
barChart = new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Client Qty",
|
||||
data: data.client_qty,
|
||||
backgroundColor: "#0d6efd",
|
||||
borderColor: "#0d6efd",
|
||||
borderWidth: 1
|
||||
},
|
||||
{
|
||||
label: "Sub-Contractor Qty",
|
||||
data: data.sub_qty,
|
||||
backgroundColor: "#fd7e14",
|
||||
borderColor: "#fd7e14",
|
||||
borderWidth: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: "index",
|
||||
intersect: false
|
||||
},
|
||||
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: data.title
|
||||
},
|
||||
legend: {
|
||||
position: "bottom"
|
||||
}
|
||||
},
|
||||
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
autoSkip: false,
|
||||
maxRotation: 45,
|
||||
minRotation: 45
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: data.y_title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/* TABLE */
|
||||
function drawTable(data){
|
||||
|
||||
let html='';
|
||||
for(let i=0;i<data.labels.length;i++){
|
||||
const clientQty = Number(data.client_qty[i] || 0);
|
||||
const subQty = Number(data.sub_qty[i] || 0);
|
||||
const diff = clientQty - subQty;
|
||||
|
||||
html+=`
|
||||
<tr>
|
||||
<td class="text-center">${i + 1}</td>
|
||||
<td>${data.labels[i]}</td>
|
||||
<td class="text-end">${data.client_qty[i]}</td>
|
||||
<td class="text-end">${data.sub_qty[i]}</td>
|
||||
<td class="text-end fw-bold ${diff >= 0 ? 'text-success' : 'text-danger'}">${diff.toFixed(2)}</td>
|
||||
</tr>
|
||||
|
||||
`;
|
||||
|
||||
}
|
||||
|
||||
document.querySelector("#resultTable tbody").innerHTML = html;
|
||||
|
||||
}
|
||||
|
||||
/* EVENTS */
|
||||
document.getElementById("subcontractor").addEventListener("change", loadRABills);
|
||||
|
||||
document.getElementById("category").addEventListener("change", loadRABills);
|
||||
|
||||
document.getElementById("searchBtn").addEventListener("click", loadDashboard);
|
||||
|
||||
document.getElementById("resetBtn").addEventListener("click", function () {location.reload();});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
raBillChoice = new Choices("#ra_bill", {
|
||||
removeItemButton: true,
|
||||
searchEnabled: true,
|
||||
searchPlaceholderValue: "Search RA Bills",
|
||||
placeholder: true,
|
||||
placeholderValue: "Select RA Bills",
|
||||
shouldSort: false,
|
||||
itemSelectText: ""
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
375
app/templates/subcontractor_report.html
Normal file
375
app/templates/subcontractor_report.html
Normal file
@@ -0,0 +1,375 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h2 class="fw-bold text-primary mb-1">
|
||||
<i class="bi bi-file-earmark-bar-graph"></i>
|
||||
Subcontractor Report
|
||||
</h2>
|
||||
<small class="text-muted">
|
||||
View, Filter, Edit and Delete Subcontractor Records
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Card -->
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-funnel-fill"></i>
|
||||
Report Filters
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form method="POST" class="loading-form">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label fw-semibold">
|
||||
Subcontractor
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
<select name="subcontractor_id" class="form-select" required>
|
||||
<option value="">--- Select Contractor ---</option>
|
||||
{% for sc in subcontractors %}
|
||||
<option value="{{ sc.id }}"
|
||||
{% if selected_sc_id|string == sc.id|string %}selected{% endif %}>
|
||||
{{ sc.subcontractor_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label fw-semibold"> RA Bill No</label>
|
||||
<input
|
||||
type="text"
|
||||
name="ra_bill_no"
|
||||
class="form-control"
|
||||
placeholder="Enter RA Bill"
|
||||
value="{{ selected_ra_bill or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label fw-semibold"> Location </label>
|
||||
<input
|
||||
type="text"
|
||||
name="location"
|
||||
class="form-control"
|
||||
placeholder="Project Location"
|
||||
value="{{ selected_location or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label fw-semibold">Work Category</label>
|
||||
<select name="category" class="form-select">
|
||||
|
||||
<option value="all">All Categories</option>
|
||||
|
||||
<option value="tr"
|
||||
{% if request.form.get('category')=='tr' %}selected{% endif %}>
|
||||
Trench Excavation
|
||||
</option>
|
||||
|
||||
<option value="mh"
|
||||
{% if request.form.get('category')=='mh' %}selected{% endif %}>
|
||||
Manhole Excavation
|
||||
</option>
|
||||
|
||||
<option value="dc"
|
||||
{% if request.form.get('category')=='dc' %}selected{% endif %}>
|
||||
Domestic Chamber
|
||||
</option>
|
||||
|
||||
<option value="laying"
|
||||
{% if request.form.get('category')=='laying' %}selected{% endif %}>
|
||||
Pipe Laying
|
||||
</option>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex justify-content-end gap-2">
|
||||
|
||||
<!-- Preview -->
|
||||
<button type="submit" name="action" value="preview"class="btn btn-primary">
|
||||
<i class="bi bi-search"></i>
|
||||
Preview Report
|
||||
</button>
|
||||
|
||||
<!-- Download -->
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-success dropdown-toggle"data-bs-toggle="dropdown">
|
||||
<i class="bi bi-download"></i>
|
||||
Download Report
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<button class="dropdown-item"type="submit" name="action" value="excel">
|
||||
<i class="bi bi-file-earmark-excel text-success"></i>
|
||||
Download Excel
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<button class="dropdown-item" type="submit" name="action"
|
||||
value="excel_all">
|
||||
<i class="bi bi-file-earmark-spreadsheet text-primary"></i>
|
||||
Download All Excel
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
|
||||
<li>
|
||||
<button class="dropdown-item"type="submit"name="action" value="pdf">
|
||||
<i class="bi bi-file-earmark-pdf text-danger"></i>
|
||||
PDF (Coming Soon)
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
<button type="reset" class="btn btn-secondary" id="resetBtn">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
Reset
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if tables %}
|
||||
{% set show_all = (not selected_category) or selected_category == 'all' %}
|
||||
<!-- Tabs -->
|
||||
<div class="card shadow-sm border-0 mt-4">
|
||||
<div class="card-header bg-light">
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link {% if show_all %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#abstract">
|
||||
<i class="bi bi-file-earmark-text"></i> Abstract
|
||||
</button>
|
||||
</li>
|
||||
|
||||
{% if show_all or selected_category == 'tr' %}
|
||||
<li class="nav-item">
|
||||
<button class="nav-link {% if selected_category == 'tr' %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#tr">
|
||||
<i class="bi bi-cone-striped"></i> Trench Excavation
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if show_all or selected_category == 'mh' %}
|
||||
<li class="nav-item">
|
||||
<button class="nav-link {% if selected_category == 'mh' %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#mh">
|
||||
<i class="bi bi-nut"></i> Manhole Excavation
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if show_all or selected_category == 'dc' %}
|
||||
<li class="nav-item">
|
||||
<button class="nav-link {% if selected_category == 'dc' %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#dc">
|
||||
<i class="bi bi-grid-3x3"></i> Manhole & Domestic Chambers Construction
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if show_all or selected_category == 'laying' %}
|
||||
<li class="nav-item">
|
||||
<button class="nav-link {% if selected_category == 'laying' %}active{% endif %}" data-bs-toggle="tab" data-bs-target="#laying">
|
||||
<i class="bi bi-bezier2"></i> Pipe Laying
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
|
||||
<div class="tab-pane fade {% if show_all %}show active{% endif %}" id="abstract">
|
||||
{{ abstract_html|safe }}
|
||||
</div>
|
||||
|
||||
{% if show_all or selected_category == 'tr' %}
|
||||
<!-- Trench -->
|
||||
<div class="tab-pane fade {% if selected_category == 'tr' %}show active{% endif %}" id="tr">
|
||||
<div class="mb-3">
|
||||
<button onclick="deleteSelected('tr')" class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i> Delete Selected
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive border rounded shadow-sm">
|
||||
{{ tables.tr|safe }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_all or selected_category == 'mh' %}
|
||||
<!-- MH -->
|
||||
<div class="tab-pane fade {% if selected_category == 'mh' %}show active{% endif %}" id="mh">
|
||||
<div class="mb-3">
|
||||
<button onclick="deleteSelected('mh')" class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i>Delete Selected
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive border rounded shadow-sm">
|
||||
{{ tables.mh|safe }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_all or selected_category == 'dc' %}
|
||||
<!-- DC -->
|
||||
<div class="tab-pane fade {% if selected_category == 'dc' %}show active{% endif %}" id="dc">
|
||||
<div class="mb-3">
|
||||
<button onclick="deleteSelected('dc')"class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
Delete Selected
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive border rounded shadow-sm">
|
||||
{{ tables.dc|safe }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_all or selected_category == 'laying' %}
|
||||
<!-- Laying -->
|
||||
<div class="tab-pane fade {% if selected_category == 'laying' %}show active{% endif %}" id="laying">
|
||||
<div class="mb-3">
|
||||
<button onclick="deleteSelected('laying')"
|
||||
class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
Delete Selected
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive border rounded shadow-sm">
|
||||
{{ tables.laying|safe }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
document.getElementById("resetBtn").addEventListener("click", function () {
|
||||
window.location.href = window.location.pathname;
|
||||
});
|
||||
|
||||
// DATATABLE
|
||||
$(document).ready(function () {
|
||||
|
||||
$('.datatable').each(function () {
|
||||
$(this).DataTable({
|
||||
pageLength: 10,
|
||||
dom: 'Bfrtip',
|
||||
buttons: ['copy', 'csv', 'excel', 'print'],
|
||||
initComplete: function () {
|
||||
|
||||
$(this).closest('.dataTables_wrapper')
|
||||
.find('thead tr th:first-child')
|
||||
.html('<input type="checkbox" class="select-all" title="Select All">');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// SELECT ALL — scoped to the checkbox's own table only
|
||||
$(document).on("change", ".select-all", function () {
|
||||
$(this).closest("table").find(".row-check").prop("checked", this.checked);
|
||||
});
|
||||
|
||||
// If a row checkbox is unchecked manually, uncheck that table's select-all
|
||||
$(document).on("change", ".row-check", function () {
|
||||
if (!this.checked) {
|
||||
$(this).closest("table").find(".select-all").prop("checked", false);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function getSelectedIds(model) {
|
||||
let ids = [];
|
||||
$("#" + model + " .row-check:checked").each(function () {
|
||||
ids.push($(this).data("id"));
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
||||
// BULK DELETE
|
||||
function deleteSelected(model) {
|
||||
let ids = getSelectedIds(model);
|
||||
if (ids.length === 0) return alert("Select records");
|
||||
|
||||
if (!confirm(`Delete ${ids.length} record(s)?`)) return;
|
||||
|
||||
fetch("/file/delete_records", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ model: model, ids: ids })
|
||||
})
|
||||
.then(res => res.json().then(data => ({ ok: res.ok, data })))
|
||||
.then(({ ok, data }) => {
|
||||
if (ok && data.status === "success") {
|
||||
alert("Deleted Successfully");
|
||||
location.reload();
|
||||
} else {
|
||||
alert("Delete failed: " + (data.message || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
alert("Delete request failed: " + err);
|
||||
});
|
||||
}
|
||||
|
||||
// SINGLE DELETE
|
||||
$(document).on("click", ".delete-btn", function () {
|
||||
let id = $(this).data("id");
|
||||
let model = $(this).data("model");
|
||||
|
||||
if (!confirm("Are you sure you want to delete this record?")) return;
|
||||
|
||||
fetch("/file/delete_records", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ model: model, ids: [id] })
|
||||
})
|
||||
.then(res => res.json().then(data => ({ ok: res.ok, data })))
|
||||
.then(({ ok, data }) => {
|
||||
if (ok && data.status === "success") {
|
||||
alert("Deleted Successfully");
|
||||
location.reload();
|
||||
} else {
|
||||
alert("Delete failed: " + (data.message || "Unknown error"));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
alert("Delete request failed: " + err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
</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,40 @@
|
||||
import os
|
||||
from app.config import Config
|
||||
from flask import current_app
|
||||
|
||||
|
||||
# file extension
|
||||
ALLOWED_EXTENSIONS = {"xlsx", "xls", "csv"}
|
||||
|
||||
# .log file allowed
|
||||
ALLOWED_LOG_FILE = {
|
||||
"app.log",
|
||||
"debug.log",
|
||||
"error.log"
|
||||
}
|
||||
|
||||
# Get path of format excel folder
|
||||
def get_download_format_folder():
|
||||
return os.path.join(
|
||||
current_app.root_path,
|
||||
"static",
|
||||
"downloads",
|
||||
"format"
|
||||
)
|
||||
|
||||
# Get path of uploads folder
|
||||
def get_uploads_folder():
|
||||
return os.path.join(
|
||||
current_app.root_path,
|
||||
"static",
|
||||
"uploads"
|
||||
)
|
||||
|
||||
def ensure_upload_folder():
|
||||
if not os.path.exists(Config.UPLOAD_FOLDER):
|
||||
os.makedirs(Config.UPLOAD_FOLDER)
|
||||
if not os.path.exists(get_uploads_folder()):
|
||||
os.makedirs(get_uploads_folder())
|
||||
|
||||
|
||||
# Get path of logs folder
|
||||
def get_logs_folder():
|
||||
project_root = os.path.dirname(current_app.root_path)
|
||||
return os.path.join(project_root, "logs")
|
||||
@@ -1,2 +1,14 @@
|
||||
def is_logged_in(session):
|
||||
return session.get("user_id") is not None
|
||||
# def is_logged_in(session):
|
||||
# return session.get("user_id") is not None
|
||||
|
||||
from functools import wraps
|
||||
from flask import session, redirect, url_for, flash
|
||||
|
||||
def login_required(view):
|
||||
@wraps(view)
|
||||
def wrapped_view(*args, **kwargs):
|
||||
if "user_id" not in session:
|
||||
flash("Please login first", "warning")
|
||||
return redirect(url_for("auth.login"))
|
||||
return view(*args, **kwargs)
|
||||
return wrapped_view
|
||||
|
||||
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.
324
logs/app.log
324
logs/app.log
@@ -1,200 +1,124 @@
|
||||
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
|
||||
2026-08-06 12:44:45 | INFO | User=System | IP=- | - | - | ======================================================================
|
||||
2026-08-06 12:44:45 | INFO | User=System | IP=- | - | - | Application Started Successfully
|
||||
2026-08-06 12:44:45 | INFO | User=System | IP=- | - | - | ======================================================================
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
2026-08-07 11:46:57 | INFO | User=System | IP=- | - | - | ======================================================================
|
||||
2026-08-07 11:46:57 | INFO | User=System | IP=- | - | - | Application Started Successfully
|
||||
2026-08-07 11:46:57 | INFO | User=System | IP=- | - | - | ======================================================================
|
||||
2026-08-07 11:47:12 | INFO | User=System | IP=- | - | - | ======================================================================
|
||||
2026-08-07 11:47:12 | INFO | User=System | IP=- | - | - | Application Started Successfully
|
||||
2026-08-07 11:47:12 | INFO | User=System | IP=- | - | - | ======================================================================
|
||||
2026-08-07 11:47:14 | INFO | User=System | IP=- | - | - | ======================================================================
|
||||
2026-08-07 11:47:14 | INFO | User=System | IP=- | - | - | Application Started Successfully
|
||||
2026-08-07 11:47:14 | INFO | User=System | IP=- | - | - | ======================================================================
|
||||
2026-08-07 11:47:18 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Started
|
||||
2026-08-07 11:47:18 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Completed | Status=200
|
||||
2026-08-07 11:47:19 | INFO | User=Laxmi | IP=192.168.0.142 | GET | http://192.168.0.142:5015/static/images/lcepl.png | Request Started
|
||||
2026-08-07 11:47:19 | INFO | User=Laxmi | IP=192.168.0.142 | GET | http://192.168.0.142:5015/static/images/lcepl.png | Request Completed | Status=200
|
||||
2026-08-07 11:47:22 | INFO | User=Laxmi | IP=192.168.0.142 | GET | http://192.168.0.142:5015/logout | Request Started
|
||||
2026-08-07 11:47:22 | INFO | User=Anonymous | IP=192.168.0.142 | GET | http://192.168.0.142:5015/logout | Logout successful. User=Laxmi
|
||||
2026-08-07 11:47:22 | INFO | User=Anonymous | IP=192.168.0.142 | GET | http://192.168.0.142:5015/logout | Request Completed | Status=302
|
||||
2026-08-07 11:47:22 | INFO | User=Anonymous | IP=192.168.0.142 | GET | http://192.168.0.142:5015/login | Request Started
|
||||
2026-08-07 11:47:22 | INFO | User=Anonymous | IP=192.168.0.142 | GET | http://192.168.0.142:5015/login | Request Completed | Status=200
|
||||
2026-08-07 11:47:22 | INFO | User=Anonymous | IP=192.168.0.142 | GET | http://192.168.0.142:5015/static/images/lcepl.png | Request Started
|
||||
2026-08-07 11:47:22 | INFO | User=Anonymous | IP=192.168.0.142 | GET | http://192.168.0.142:5015/static/images/lcepl.png | Request Completed | Status=304
|
||||
2026-08-07 11:47:30 | INFO | User=Anonymous | IP=192.168.0.142 | POST | http://192.168.0.142:5015/login | Request Started
|
||||
2026-08-07 11:47:30 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/login | Login successful. User=Laxmi
|
||||
2026-08-07 11:47:30 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/login | Request Completed | Status=302
|
||||
2026-08-07 11:47:30 | INFO | User=Laxmi | IP=192.168.0.142 | GET | http://192.168.0.142:5015/dashboard/ | Request Started
|
||||
2026-08-07 11:47:30 | INFO | User=Laxmi | IP=192.168.0.142 | GET | http://192.168.0.142:5015/dashboard/ | Request Completed | Status=200
|
||||
2026-08-07 11:47:30 | INFO | User=Laxmi | IP=192.168.0.142 | GET | http://192.168.0.142:5015/dashboard/api/live-stats | Request Started
|
||||
2026-08-07 11:47:30 | INFO | User=Laxmi | IP=192.168.0.142 | GET | http://192.168.0.142:5015/dashboard/api/live-stats | Request Completed | Status=200
|
||||
2026-08-07 11:47:35 | INFO | User=Laxmi | IP=192.168.0.142 | GET | http://192.168.0.142:5015/file/Subcontractor_report | Request Started
|
||||
2026-08-07 11:47:35 | INFO | User=Laxmi | IP=192.168.0.142 | GET | http://192.168.0.142:5015/file/Subcontractor_report | Request Completed | Status=200
|
||||
2026-08-07 11:47:40 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Started
|
||||
2026-08-07 11:47:40 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Completed | Status=200
|
||||
2026-08-07 11:47:41 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Started
|
||||
2026-08-07 11:47:42 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Completed | Status=200
|
||||
2026-08-07 11:47:44 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Started
|
||||
2026-08-07 11:47:44 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Completed | Status=200
|
||||
2026-08-07 11:47:46 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Started
|
||||
2026-08-07 11:47:46 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Completed | Status=200
|
||||
2026-08-07 11:47:49 | INFO | User=Laxmi | IP=192.168.0.142 | GET | http://192.168.0.142:5015/file/Subcontractor_report | Request Started
|
||||
2026-08-07 11:47:49 | INFO | User=Laxmi | IP=192.168.0.142 | GET | http://192.168.0.142:5015/file/Subcontractor_report | Request Completed | Status=200
|
||||
2026-08-07 11:47:54 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Started
|
||||
2026-08-07 11:47:54 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Completed | Status=200
|
||||
2026-08-07 11:47:57 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Started
|
||||
2026-08-07 11:47:57 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Completed | Status=200
|
||||
2026-08-07 11:48:07 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Started
|
||||
2026-08-07 11:48:07 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Completed | Status=200
|
||||
2026-08-07 11:48:11 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Started
|
||||
2026-08-07 11:48:11 | INFO | User=Laxmi | IP=192.168.0.142 | POST | http://192.168.0.142:5015/file/Subcontractor_report | Request Completed | Status=200
|
||||
2026-08-07 12:02:57 | INFO | User=System | IP=- | - | - | ======================================================================
|
||||
2026-08-07 12:02:57 | INFO | User=System | IP=- | - | - | Application Started Successfully
|
||||
2026-08-07 12:02:57 | INFO | User=System | IP=- | - | - | ======================================================================
|
||||
2026-08-07 12:03:04 | INFO | User=System | IP=- | - | - | ======================================================================
|
||||
2026-08-07 12:03:04 | INFO | User=System | IP=- | - | - | Application Started Successfully
|
||||
2026-08-07 12:03:04 | INFO | User=System | IP=- | - | - | ======================================================================
|
||||
2026-08-07 12:04:23 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/ | Request Started
|
||||
2026-08-07 12:04:23 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/ | Request Completed | Status=302
|
||||
2026-08-07 12:04:23 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/login | Request Started
|
||||
2026-08-07 12:04:23 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/login | User already logged in.
|
||||
2026-08-07 12:04:23 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/login | Request Completed | Status=302
|
||||
2026-08-07 12:04:23 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/dashboard/ | Request Started
|
||||
2026-08-07 12:04:23 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/dashboard/ | Request Completed | Status=200
|
||||
2026-08-07 12:04:23 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/dashboard/api/live-stats | Request Started
|
||||
2026-08-07 12:04:23 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/dashboard/api/live-stats | Request Completed | Status=200
|
||||
2026-08-07 12:04:33 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/dashboard/api/live-stats | Request Started
|
||||
2026-08-07 12:04:33 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/dashboard/api/live-stats | Request Completed | Status=200
|
||||
2026-08-07 12:04:37 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/engi/client-rate | Request Started
|
||||
2026-08-07 12:04:37 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/engi/client-rate | Request Completed | Status=200
|
||||
2026-08-07 12:04:39 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file_format | Request Started
|
||||
2026-08-07 12:04:39 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file_format | Request Completed | Status=200
|
||||
2026-08-07 12:04:40 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file_format | Request Started
|
||||
2026-08-07 12:04:40 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file_format | Request Completed | Status=200
|
||||
2026-08-07 12:04:41 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file_format | Request Started
|
||||
2026-08-07 12:04:41 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file_format | Request Completed | Status=200
|
||||
2026-08-07 12:04:42 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file_format | Request Started
|
||||
2026-08-07 12:04:42 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file_format | Request Completed | Status=200
|
||||
2026-08-07 12:04:42 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file_format | Request Started
|
||||
2026-08-07 12:04:42 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file_format | Request Completed | Status=200
|
||||
2026-08-07 12:04:44 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file/import_client | Request Started
|
||||
2026-08-07 12:04:44 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file/import_client | Request Completed | Status=200
|
||||
2026-08-07 12:04:46 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file/Subcontractor_report | Request Started
|
||||
2026-08-07 12:04:46 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file/Subcontractor_report | Request Completed | Status=200
|
||||
2026-08-07 12:04:49 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file/Subcontractor_report | Request Started
|
||||
2026-08-07 12:04:49 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file/Subcontractor_report | Request Completed | Status=200
|
||||
2026-08-07 12:04:52 | INFO | User=Admin | IP=192.168.0.118 | POST | http://192.168.0.118:5015/file/Subcontractor_report | Request Started
|
||||
2026-08-07 12:04:52 | INFO | User=Admin | IP=192.168.0.118 | POST | http://192.168.0.118:5015/file/Subcontractor_report | Request Completed | Status=200
|
||||
2026-08-07 12:04:54 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file/Subcontractor_report | Request Started
|
||||
2026-08-07 12:04:54 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file/Subcontractor_report | Request Completed | Status=200
|
||||
2026-08-07 12:05:01 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/dashboard/ | Request Started
|
||||
2026-08-07 12:05:01 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/dashboard/ | Request Completed | Status=200
|
||||
2026-08-07 12:05:01 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/dashboard/api/live-stats | Request Started
|
||||
2026-08-07 12:05:01 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/dashboard/api/live-stats | Request Completed | Status=200
|
||||
2026-08-07 12:05:02 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/activity/ | Request Started
|
||||
2026-08-07 12:05:02 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/activity/ | Request Completed | Status=200
|
||||
2026-08-07 12:07:06 | INFO | User=System | IP=- | - | - | ======================================================================
|
||||
2026-08-07 12:07:06 | INFO | User=System | IP=- | - | - | Application Started Successfully
|
||||
2026-08-07 12:07:06 | INFO | User=System | IP=- | - | - | ======================================================================
|
||||
2026-08-07 12:07:07 | INFO | User=System | IP=- | - | - | ======================================================================
|
||||
2026-08-07 12:07:07 | INFO | User=System | IP=- | - | - | Application Started Successfully
|
||||
2026-08-07 12:07:07 | INFO | User=System | IP=- | - | - | ======================================================================
|
||||
2026-08-07 12:07:09 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/activity/ | Request Started
|
||||
2026-08-07 12:07:09 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/activity/ | Request Completed | Status=200
|
||||
2026-08-07 12:07:10 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/engi/subcontractor-rate | Request Started
|
||||
2026-08-07 12:07:11 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/engi/subcontractor-rate | Request Completed | Status=200
|
||||
2026-08-07 12:07:11 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/static/images/lcepl.png | Request Started
|
||||
2026-08-07 12:07:11 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/static/images/lcepl.png | Request Completed | Status=304
|
||||
2026-08-07 12:07:14 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/engi/client-rate | Request Started
|
||||
2026-08-07 12:07:14 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/engi/client-rate | Request Completed | Status=200
|
||||
2026-08-07 12:07:16 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file_format | Request Started
|
||||
2026-08-07 12:07:16 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/file_format | Request Completed | Status=200
|
||||
2026-08-07 12:07:16 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/engi/subcontractor-rate | Request Started
|
||||
2026-08-07 12:07:16 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/engi/subcontractor-rate | Request Completed | Status=200
|
||||
2026-08-07 12:07:18 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/engi/subcontractor-rate | Request Started
|
||||
2026-08-07 12:07:18 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/engi/subcontractor-rate | Request Completed | Status=200
|
||||
2026-08-07 12:07:23 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/dashboard/ | Request Started
|
||||
2026-08-07 12:07:23 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/dashboard/ | Request Completed | Status=200
|
||||
2026-08-07 12:07:23 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/dashboard/api/live-stats | Request Started
|
||||
2026-08-07 12:07:23 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/dashboard/api/live-stats | Request Completed | Status=200
|
||||
2026-08-07 12:07:24 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/engi/subcontractor-rate | Request Started
|
||||
2026-08-07 12:07:24 | INFO | User=Admin | IP=192.168.0.118 | GET | http://192.168.0.118:5015/engi/subcontractor-rate | Request Completed | Status=200
|
||||
>>>>>>> pankaj-dev
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user