Merge pull request 'pankaj-dev' (#27) from pankaj-dev into main

Reviewed-on: #27
This commit was merged in pull request #27.
This commit is contained in:
2026-08-08 07:30:36 +00:00
9 changed files with 549 additions and 42 deletions

5
.gitignore vendored
View File

@@ -11,8 +11,11 @@ app/static/uploads/
# Ignore env files # Ignore env files
venv venv
# Ignore Log files ss # Ignore Log files
logs/ logs/
logs/app.log
logs/debug.log
logs/error.log
*.log *.log

View File

@@ -0,0 +1,20 @@
from app import db
from datetime import datetime
class ClientRate(db.Model):
__tablename__ = "client_rates"
id = db.Column(db.Integer, primary_key=True)
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"< Client Rate {self.item_name}>"

View File

@@ -6,10 +6,11 @@ from flask import (
url_for, url_for,
flash, jsonify flash, jsonify
) )
from app.constants.messages import SuccessMessage, ErrorMessage
from app.models.subcontractor_model import Subcontractor from app.models.subcontractor_model import Subcontractor
from app.services.subcontractor_rate_service import SubcontractorRateService from app.services.subcontractor_rate_service import SubcontractorRateService
from app.constants.messages import SuccessMessage, ErrorMessage from app.services.client_rate_service import ClientRateService
engi_bp = Blueprint( engi_bp = Blueprint(
"engineering", "engineering",
@@ -27,17 +28,60 @@ def engineering_master():
title="Engineering Masters" title="Engineering Masters"
) )
# Client rate model #---------------------- Client rate model -------------------------------
@engi_bp.route("/client-rate") @engi_bp.route("/client-rate", methods=["GET", "POST"])
def client_rates(): def client_rate_master():
if request.method == "POST":
result = ClientRateService.save_or_update(request.form)
if result["success"]:
flash(result["message"], "success")
return redirect(url_for("engineering.client_rate_master"))
else:
flash(result["message"], "danger")
rates = ClientRateService.get_all_rates()
return render_template( return render_template(
"engineering/client_rate.html", "engineering/client_rate.html",
title="Client Rate Master" title="Client Rate Master",
rates=rates
) )
@engi_bp.route("/client-rate/edit/<int:rate_id>", methods=["GET", "POST"])
def client_edit_rate(rate_id):
if request.method == "POST":
result = ClientRateService.save_or_update(request.form)
if result["success"]:
flash(result["message"], "success")
return redirect(url_for("engineering.client-rate"))
flash(result["message"], "danger")
rate = ClientRateService.get_rate(rate_id)
rates = ClientRateService.get_all_rates()
return render_template(
"engineering/client_rate.html",
rate=rate,
rates=rates
)
@engi_bp.route("/client-rate/delete/<int:rate_id>")
def client_delete_rate(rate_id):
ClientRateService.delete_rate(rate_id)
flash("Rate deleted successfully.", "success")
return redirect(url_for("engineering.client-rate"))
# -------------------- Sub-Contractor -------------------------------------
@engi_bp.route("/subcontractor-rate", methods=["GET", "POST"]) @engi_bp.route("/subcontractor-rate", methods=["GET", "POST"])
def add_subcontractor_rates(): def subcontractor_rate_master():
subcontractors = Subcontractor.query.filter_by(status="Active").all() subcontractors = Subcontractor.query.filter_by(status="Active").all()
@@ -45,7 +89,7 @@ def add_subcontractor_rates():
result = SubcontractorRateService.save_or_update(request.form) result = SubcontractorRateService.save_or_update(request.form)
if result["success"]: if result["success"]:
flash(result["message"], "success") flash(result["message"], "success")
return redirect(url_for("engineering.add_subcontractor_rates")) return redirect(url_for("engineering.subcontractor_rate_master"))
else: else:
flash(result["message"], "danger") flash(result["message"], "danger")
@@ -59,7 +103,7 @@ def add_subcontractor_rates():
) )
@engi_bp.route("/subcontractor-rate/edit/<int:rate_id>", methods=["GET", "POST"]) @engi_bp.route("/subcontractor-rate/edit/<int:rate_id>", methods=["GET", "POST"])
def edit_rate(rate_id): def subcontractor_edit_rate(rate_id):
subcontractors = Subcontractor.query.filter_by(status="Active").all() subcontractors = Subcontractor.query.filter_by(status="Active").all()
@@ -69,7 +113,7 @@ def edit_rate(rate_id):
if result["success"]: if result["success"]:
flash(result["message"], "success") flash(result["message"], "success")
return redirect(url_for("engineering.add_subcontractor_rates")) return redirect(url_for("engineering.subcontractor_rate_master"))
flash(result["message"], "danger") flash(result["message"], "danger")
@@ -85,10 +129,10 @@ def edit_rate(rate_id):
) )
@engi_bp.route("/subcontractor-rate/delete/<int:rate_id>") @engi_bp.route("/subcontractor-rate/delete/<int:rate_id>")
def delete_rate(rate_id): def subcontractor_delete_rate(rate_id):
SubcontractorRateService.delete_rate(rate_id) SubcontractorRateService.delete_rate(rate_id)
flash("Rate deleted successfully.", "success") flash("Rate deleted successfully.", "success")
return redirect(url_for("engineering.add_subcontractor_rates")) return redirect(url_for("engineering.subcontractor_rate_master"))
@engi_bp.route("/check-rate") @engi_bp.route("/check-rate")

View File

@@ -0,0 +1,97 @@
from app.services.db_service import db
from app.models.client_rate_model import ClientRate
from sqlalchemy import func
class ClientRateService:
@staticmethod
def save_or_update(form):
rate_id = form.get("id")
category = form.get("category")
item_name = form.get("item_name").strip()
# -----------------------------
# Duplicate Validation
# -----------------------------
duplicate = (
ClientRate.query
.filter(
ClientRate.category == category,
func.lower(ClientRate.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 Client."
}
# -----------------------------
# Insert / Update
# -----------------------------
if rate_id:
rate = ClientRate.query.get_or_404(rate_id)
else:
rate = ClientRate()
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 (
ClientRate.query
.order_by(ClientRate.created_at.desc())
.all()
)
@staticmethod
def get_rate(rate_id):
return ClientRate.query.get_or_404(rate_id)
def delete_rate(rate_id):
rate = ClientRate.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 = ClientRate.query.filter(
ClientRate.category == category,
func.lower(ClientRate.item_name) == item_name.strip().lower()
)
if rate_id:
query = query.filter(ClientRate.id != int(rate_id))
return query.first() is not None

View File

@@ -14,7 +14,353 @@
</div> </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">
<!-- 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.client_rate_master') }}" 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> </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>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.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.client_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.client_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 %} {% endblock %}

View File

@@ -4,14 +4,12 @@
<div class="container-fluid mt-4"> <div class="container-fluid mt-4">
<div class="card shadow"> <div class="card shadow">
<!-- Heading -->
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center"> <div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h4 class="mb-0"> <h4 class="mb-0">
<i class="bi bi-currency-rupee"></i> <i class="bi bi-currency-rupee"></i>
Subcontractor Rate Master Subcontractor Rate Master
</h4> </h4>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -67,23 +65,6 @@
</select> </select>
</div> </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>
<div class="row"> <div class="row">
@@ -95,7 +76,7 @@
</div> </div>
<!-- Item Name --> <!-- Item Name -->
<div class="col-md-5 mb-3"> <div class="col-md-4 mb-3">
<label class="form-label fw-bold"> Item Name </label> <label class="form-label fw-bold"> Item Name </label>
<input type="text" <input type="text"
id="item_name" id="item_name"
@@ -116,6 +97,7 @@
<div class="col-md-2 mb-3"> <div class="col-md-2 mb-3">
<label class="form-label fw-bold">Unit</label> <label class="form-label fw-bold">Unit</label>
<select name="unit" class="form-select"> <select name="unit" class="form-select">
<option value=""> -- Select Unit -- </option>
<option value="Cum" <option value="Cum"
{% if not rate or rate.unit=="Cum" %}selected{% endif %}> {% if not rate or rate.unit=="Cum" %}selected{% endif %}>
Cum Cum
@@ -136,28 +118,46 @@
</div> </div>
<!-- Rate --> <!-- Rate -->
<div class="col-md-2 mb-3"> <div class="col-md-3 mb-3">
<label class="form-label fw-bold">Rate</label> <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> <input type="number" step="0.01" name="rate" class="form-control" value="{{ rate.rate if rate else '' }}" required>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<!-- Effective From -->
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<label class="form-label fw-bold"> Effective From </label> <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> <input type="date" name="effective_from" class="form-control" value="{{ rate.effective_from if rate else '' }}" required>
</div> </div>
<!-- Effective To -->
<div class="col-md-3 mb-3"> <div class="col-md-3 mb-3">
<label class="form-label fw-bold"> Effective To </label> <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 '' }}"> <input type="date" name="effective_to" class="form-control" value="{{ rate.effective_to if rate else '' }}">
</div> </div>
<!-- Status -->
<div class="col-md-3 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>
<hr> <hr>
<div class="text-end"> <div class="text-end">
<a href="{{ url_for('engineering.add_subcontractor_rates') }}" class="btn btn-secondary"> <a href="{{ url_for('engineering.subcontractor_rate_master') }}" class="btn btn-secondary">
<i class="bi bi-arrow-clockwise"></i> Reset <i class="bi bi-arrow-clockwise"></i> Reset
</a> </a>
@@ -222,11 +222,11 @@
</td> </td>
<td> <td>
<a href="{{ url_for('engineering.edit_rate', rate_id=row.id) }}" class="btn btn-warning btn-sm"> <a href="{{ url_for('engineering.subcontractor_edit_rate', rate_id=row.id) }}" class="btn btn-warning btn-sm">
<i class="bi bi-pencil-square"></i> <i class="bi bi-pencil-square"></i>
</a> </a>
<a href="{{ url_for('engineering.delete_rate', rate_id=row.id) }}" <a href="{{ url_for('engineering.subcontractor_delete_rate', rate_id=row.id) }}"
class="btn btn-danger btn-sm" onclick="return confirm('Delete this Item:{{row.item_name}} & Rate:{{row.rate}} ?')"> class="btn btn-danger btn-sm" onclick="return confirm('Delete this Item:{{row.item_name}} & Rate:{{row.rate}} ?')">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</a> </a>

View File

@@ -32,7 +32,7 @@
Manage subcontractor-wise rates. Manage subcontractor-wise rates.
</p> </p>
<a href="{{ url_for('engineering.add_subcontractor_rates') }}" <a href="{{ url_for('engineering.subcontractor_rate_master') }}"
class="btn btn-success"> class="btn btn-success">
<i class="bi bi-arrow-right-circle"></i> <i class="bi bi-arrow-right-circle"></i>
@@ -62,7 +62,7 @@
Manage client standard rates. Manage client standard rates.
</p> </p>
<a href="{{ url_for('engineering.client_rates') }}" <a href="{{ url_for('engineering.client_rate_master') }}"
class="btn btn-primary"> class="btn btn-primary">
<i class="bi bi-arrow-right-circle"></i> <i class="bi bi-arrow-right-circle"></i>

View File

@@ -1,3 +0,0 @@
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=- | - | - | ======================================================================