adding invoice

This commit is contained in:
Flatlogic Bot 2026-01-26 08:06:46 +00:00
parent 4020492307
commit 595ca7c1fe
8 changed files with 586 additions and 4 deletions

View File

@ -3,7 +3,12 @@
{% block content %}
<div class="container py-5">
<h1 class="mb-4">{% trans "Driver Dashboard" %}</h1>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="mb-0">{% trans "Driver Dashboard" %}</h1>
<a href="{% url 'scan_qr' %}" class="btn btn-primary rounded-pill px-4">
<i class="bi bi-qr-code-scan me-2"></i> {% trans "Scan Parcel" %}
</a>
</div>
<ul class="nav nav-pills mb-4" id="pills-tab" role="tablist">
<li class="nav-item" role="presentation">

View File

@ -0,0 +1,181 @@
{% load static %}
{% load i18n %}
<!DOCTYPE html>
<html lang="{{ request.LANGUAGE_CODE }}" dir="{% if request.LANGUAGE_CODE == 'ar' %}rtl{% else %}ltr{% endif %}">
<head>
<meta charset="UTF-8">
<title>{% trans "Tax Invoice" %} - {{ parcel.tracking_number }}</title>
<style>
@page {
size: A4;
margin: 2cm;
}
body {
font-family: sans-serif;
font-size: 14px;
color: #333;
line-height: 1.5;
}
.header {
width: 100%;
margin-bottom: 40px;
overflow: hidden;
}
.logo {
float: {% if request.LANGUAGE_CODE == 'ar' %}right{% else %}left{% endif %};
width: 150px;
}
.company-info {
float: {% if request.LANGUAGE_CODE == 'ar' %}left{% else %}right{% endif %};
text-align: {% if request.LANGUAGE_CODE == 'ar' %}left{% else %}right{% endif %};
}
.title {
clear: both;
text-align: center;
font-size: 24px;
font-weight: bold;
margin-bottom: 30px;
text-transform: uppercase;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}
.details-grid {
display: table;
width: 100%;
margin-bottom: 30px;
}
.details-col {
display: table-cell;
width: 50%;
vertical-align: top;
}
.label {
font-weight: bold;
color: #666;
margin-bottom: 2px;
}
.value {
margin-bottom: 10px;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
}
th, td {
padding: 10px;
border-bottom: 1px solid #eee;
}
th {
background-color: #f9f9f9;
text-align: {% if request.LANGUAGE_CODE == 'ar' %}right{% else %}left{% endif %};
font-weight: bold;
}
.text-right {
text-align: {% if request.LANGUAGE_CODE == 'ar' %}left{% else %}right{% endif %};
}
.total-row td {
font-weight: bold;
font-size: 16px;
border-top: 2px solid #333;
border-bottom: none;
}
.footer {
margin-top: 50px;
text-align: center;
font-size: 12px;
color: #888;
border-top: 1px solid #eee;
padding-top: 20px;
}
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
}
.paid { color: #0f9d58; border: 1px solid #0f9d58; }
.pending { color: #f4b400; border: 1px solid #f4b400; }
</style>
</head>
<body>
<div class="header">
<div class="logo">
{% if logo_base64 %}
<img src="data:image/jpeg;base64,{{ logo_base64 }}" style="max-width: 100%; max-height: 80px;">
{% else %}
<h2>{{ platform_profile.name }}</h2>
{% endif %}
</div>
<div class="company-info">
<strong>{{ platform_profile.name }}</strong><br>
{{ platform_profile.address|linebreaksbr }}<br>
{% if platform_profile.vat_number %}
{% trans "VAT Number" %}: {{ platform_profile.vat_number }}<br>
{% endif %}
{{ platform_profile.phone_number }}
</div>
</div>
<div class="title">{% trans "Tax Invoice" %}</div>
<div class="details-grid">
<div class="details-col">
<div class="label">{% trans "Invoice Details" %}</div>
<div class="value">
{% trans "Invoice No" %}: #{{ parcel.id }}<br>
{% trans "Date" %}: {{ parcel.created_at|date:"Y-m-d" }}<br>
{% trans "Status" %}:
<span class="status-badge {% if parcel.payment_status == 'paid' %}paid{% else %}pending{% endif %}">
{{ parcel.get_payment_status_display }}
</span>
</div>
</div>
<div class="details-col text-right">
<div class="label">{% trans "Bill To" %}</div>
<div class="value">
<strong>{{ parcel.shipper.first_name }} {{ parcel.shipper.last_name }}</strong><br>
{{ parcel.shipper.profile.address }}<br>
{{ parcel.shipper.email }}<br>
{{ parcel.shipper.profile.phone_number }}
</div>
</div>
</div>
<table>
<thead>
<tr>
<th>{% trans "Description" %}</th>
<th>{% trans "Tracking Number" %}</th>
<th>{% trans "Weight" %}</th>
<th class="text-right">{% trans "Amount" %} (OMR)</th>
</tr>
</thead>
<tbody>
<tr>
<td>{% trans "Delivery Service" %} - {{ parcel.description|truncatechars:50 }}</td>
<td>{{ parcel.tracking_number }}</td>
<td>{{ parcel.weight }} kg</td>
<td class="text-right">{{ parcel.price }}</td>
</tr>
</tbody>
<tfoot>
<tr class="total-row">
<td colspan="3" class="text-right">{% trans "Total" %}</td>
<td class="text-right">{{ parcel.price }} OMR</td>
</tr>
</tfoot>
</table>
<div class="footer">
<p>{% trans "Thank you for using" %} {{ platform_profile.name }}!</p>
{% if platform_profile.registration_number %}
<p>{% trans "CR No" %}: {{ platform_profile.registration_number }}</p>
{% endif %}
</div>
</body>
</html>

View File

@ -0,0 +1,240 @@
{% extends 'base.html' %}
{% load i18n static %}
{% block title %}masarX | {% trans "Scan Parcel" %}{% endblock %}
{% block head %}
<script src="https://unpkg.com/html5-qrcode" type="text/javascript"></script>
<style>
#reader {
width: 100%;
border-radius: 15px;
overflow: hidden;
}
.scan-result-card {
display: none;
transition: all 0.3s ease;
}
</style>
{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">{% trans "Scan Parcel QR" %}</h1>
<a href="{% url 'dashboard' %}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> {% trans "Back to Dashboard" %}
</a>
</div>
<!-- Scanner Section -->
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-0">
<div id="reader"></div>
</div>
<div class="card-footer bg-light text-center py-3">
<p class="mb-0 text-muted small">{% trans "Point your camera at the Parcel Label QR Code" %}</p>
</div>
</div>
<!-- Manual Entry Section -->
<div class="text-center mb-4">
<p class="text-muted mb-2">{% trans "Or enter tracking number manually" %}</p>
<form id="manual-form" class="d-flex gap-2 justify-content-center">
<input type="text" id="manual-input" class="form-control" placeholder="{% trans "e.g., TRK123456" %}" style="max-width: 200px;">
<button type="submit" class="btn btn-primary">{% trans "Search" %}</button>
</form>
</div>
<!-- Result Section -->
<div id="scan-result" class="scan-result-card card border-0 shadow rounded-4 border-start border-5 border-primary">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<span class="badge bg-primary mb-2" id="res-status-badge">Picked Up</span>
<h4 class="card-title mb-0" id="res-tracking">#TRK-12345</h4>
</div>
<div class="text-end">
<h5 class="text-primary fw-bold mb-0" id="res-price">5.000 OMR</h5>
<small class="text-muted">{% trans "Price" %}</small>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-6">
<small class="text-muted text-uppercase fw-bold">{% trans "From" %}</small>
<div class="fw-bold" id="res-from">Muscat / Seeb</div>
</div>
<div class="col-6">
<small class="text-muted text-uppercase fw-bold">{% trans "To" %}</small>
<div class="fw-bold" id="res-to">Sohar / Liwa</div>
</div>
<div class="col-12">
<small class="text-muted text-uppercase fw-bold">{% trans "Description" %}</small>
<div id="res-desc">Box of electronics</div>
</div>
</div>
<div id="action-buttons" class="d-grid gap-2">
<!-- Dynamic Buttons -->
</div>
</div>
</div>
<!-- Loading Spinner -->
<div id="loading" class="text-center py-4 d-none">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<!-- Error Message -->
<div id="error-msg" class="alert alert-danger d-none mt-3" role="alert"></div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const html5QrcodeScanner = new Html5QrcodeScanner(
"reader", { fps: 10, qrbox: {width: 250, height: 250} }
);
function onScanSuccess(decodedText, decodedResult) {
// Handle on success condition with the decoded text or result.
console.log(`Scan result: ${decodedText}`, decodedResult);
fetchParcelDetails(decodedText);
// Optional: Pause scanner
// html5QrcodeScanner.clear();
}
function onScanFailure(error) {
// handle scan failure, usually better to ignore and keep scanning.
// console.warn(`Code scan error = ${error}`);
}
html5QrcodeScanner.render(onScanSuccess, onScanFailure);
// Manual Search
document.getElementById('manual-form').addEventListener('submit', function(e) {
e.preventDefault();
const val = document.getElementById('manual-input').value.trim();
if (val) fetchParcelDetails(val);
});
function fetchParcelDetails(trackingNumber) {
const resultCard = document.getElementById('scan-result');
const loading = document.getElementById('loading');
const errorMsg = document.getElementById('error-msg');
// UI Reset
resultCard.style.display = 'none';
errorMsg.classList.add('d-none');
loading.classList.remove('d-none');
// API Call
fetch(`{% url 'get_parcel_details' %}?tracking_number=${trackingNumber}`)
.then(response => response.json())
.then(data => {
loading.classList.add('d-none');
if (data.success) {
renderParcel(data.parcel);
resultCard.style.display = 'block';
// Scroll to result
resultCard.scrollIntoView({ behavior: 'smooth' });
} else {
errorMsg.textContent = data.error || '{% trans "Parcel not found" %}';
errorMsg.classList.remove('d-none');
}
})
.catch(err => {
loading.classList.add('d-none');
errorMsg.textContent = '{% trans "Network error. Please try again." %}';
errorMsg.classList.remove('d-none');
});
}
function renderParcel(parcel) {
document.getElementById('res-tracking').textContent = parcel.tracking_number;
document.getElementById('res-status-badge').textContent = parcel.status_display;
document.getElementById('res-price').textContent = parcel.price + ' OMR';
document.getElementById('res-from').textContent = parcel.from;
document.getElementById('res-to').textContent = parcel.to;
document.getElementById('res-desc').textContent = parcel.description;
// Status Badge Color
const badge = document.getElementById('res-status-badge');
badge.className = 'badge mb-2 ' + (parcel.status === 'delivered' ? 'bg-success' : 'bg-primary');
// Action Buttons
const actionsDiv = document.getElementById('action-buttons');
actionsDiv.innerHTML = ''; // Clear previous
// Logic for buttons based on status
// Assuming current user is a driver (enforced by view)
// Only show actions if I am the carrier OR if it's pending (and I can accept it)
// But usually this scanner is for the assigned driver.
if (parcel.can_update) {
if (parcel.status === 'picked_up') {
const btn = createBtn('{% trans "Mark as Delivered" %}', 'success', 'delivered', parcel.id);
actionsDiv.appendChild(btn);
} else if (parcel.status === 'in_transit') {
const btn = createBtn('{% trans "Mark as Delivered" %}', 'success', 'delivered', parcel.id);
actionsDiv.appendChild(btn);
} else if (parcel.status === 'pending') {
// Maybe allow accepting via scan?
const btn = createBtn('{% trans "Accept Shipment" %}', 'primary', 'accept', parcel.id);
actionsDiv.appendChild(btn);
}
}
}
function createBtn(text, color, action, id) {
const btn = document.createElement('button');
btn.className = `btn btn-${color} fw-bold py-2`;
btn.innerHTML = `<i class="bi bi-check-lg me-2"></i> ${text}`;
btn.onclick = function() {
updateStatus(id, action);
};
return btn;
}
function updateStatus(id, action) {
if(!confirm('{% trans "Are you sure?" %}')) return;
const actionsDiv = document.getElementById('action-buttons');
actionsDiv.style.opacity = '0.5';
actionsDiv.style.pointerEvents = 'none';
fetch('{% url "update_parcel_status_ajax" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({
parcel_id: id,
action: action
})
})
.then(res => res.json())
.then(data => {
if(data.success) {
// Refresh details
fetchParcelDetails(document.getElementById('res-tracking').textContent);
} else {
alert(data.error);
}
})
.finally(() => {
actionsDiv.style.opacity = '1';
actionsDiv.style.pointerEvents = 'auto';
});
}
});
</script>
{% endblock %}

View File

@ -69,9 +69,16 @@
{% endif %}
{% endif %}
<a href="{% url 'generate_parcel_label' parcel.id %}" class="btn btn-sm btn-outline-dark w-100 mb-3" target="_blank">
<i class="fas fa-print me-1"></i> {% trans "Print Label" %}
<div class="d-flex gap-2 mb-3">
<a href="{% url 'generate_parcel_label' parcel.id %}" class="btn btn-sm btn-outline-dark w-100" target="_blank">
<i class="fas fa-print me-1"></i> {% trans "Label" %}
</a>
{% if parcel.payment_status == 'paid' %}
<a href="{% url 'generate_invoice' parcel.id %}" class="btn btn-sm btn-outline-secondary w-100" target="_blank">
<i class="fas fa-file-invoice me-1"></i> {% trans "Invoice" %}
</a>
{% endif %}
</div>
<hr>
<p class="card-text small mb-0"><strong>{% trans "Receiver" %}:</strong> {{ parcel.receiver_name }}</p>
@ -122,6 +129,11 @@
<a href="{% url 'generate_parcel_label' parcel.id %}" class="btn btn-sm btn-outline-dark" target="_blank" title="{% trans 'Print Label' %}">
<i class="fas fa-print"></i>
</a>
{% if parcel.payment_status == 'paid' %}
<a href="{% url 'generate_invoice' parcel.id %}" class="btn btn-sm btn-outline-secondary" target="_blank" title="{% trans 'Invoice' %}">
<i class="fas fa-file-invoice"></i>
</a>
{% endif %}
{% if parcel.payment_status == 'pending' and payments_enabled %}
<a href="{% url 'initiate_payment' parcel.id %}" class="btn btn-sm btn-outline-primary flex-grow-1 flex-md-grow-0">
@ -231,6 +243,11 @@
<a href="{% url 'generate_parcel_label' parcel.id %}" class="btn btn-sm btn-outline-dark me-2" target="_blank" title="{% trans 'Print Label' %}">
<i class="fas fa-print"></i>
</a>
{% if parcel.payment_status == 'paid' %}
<a href="{% url 'generate_invoice' parcel.id %}" class="btn btn-sm btn-outline-secondary me-2" target="_blank" title="{% trans 'Invoice' %}">
<i class="fas fa-file-invoice"></i>
</a>
{% endif %}
{% if parcel.status == 'delivered' and parcel.carrier %}
{% if not rating %}
<a href="{% url 'rate_driver' parcel.id %}" class="btn btn-sm btn-outline-warning">

View File

@ -32,11 +32,13 @@ urlpatterns = [
), name='password_reset_complete'),
path('dashboard/', views.dashboard, name='dashboard'),
path('scan-qr/', views.scan_qr_view, name='scan_qr'),
path('shipment-request/', views.shipment_request, name='shipment_request'),
path('accept-parcel/<int:parcel_id>/', views.accept_parcel, name='accept_parcel'),
path('update-status/<int:parcel_id>/', views.update_status, name='update_status'),
path('rate-driver/<int:parcel_id>/', views.rate_driver, name='rate_driver'),
path('parcel/<int:parcel_id>/label/', views.generate_parcel_label, name='generate_parcel_label'),
path('parcel/<int:parcel_id>/invoice/', views.generate_invoice, name='generate_invoice'),
path('initiate-payment/<int:parcel_id>/', views.initiate_payment, name='initiate_payment'),
path('payment-success/', views.payment_success, name='payment_success'),
path('payment-cancel/', views.payment_cancel, name='payment_cancel'),
@ -45,6 +47,9 @@ urlpatterns = [
path('ajax/get-governates/', views.get_governates, name='get_governates'),
path('ajax/get-cities/', views.get_cities, name='get_cities'),
path('ajax/chatbot/', views.chatbot, name='chatbot'),
path('ajax/get-parcel-details/', views.get_parcel_details, name='get_parcel_details'),
path('ajax/update-parcel-status/', views.update_parcel_status_ajax, name='update_parcel_status_ajax'),
path('privacy-policy/', views.privacy_policy, name='privacy_policy'),
path('terms-conditions/', views.terms_conditions, name='terms_conditions'),
path('contact/', views.contact, name='contact'),

View File

@ -705,3 +705,137 @@ def generate_parcel_label(request, parcel_id):
response = HttpResponse(pdf_file, content_type='application/pdf')
response['Content-Disposition'] = f'inline; filename="label_{parcel.tracking_number}.pdf"'
return response
@login_required
def generate_invoice(request, parcel_id):
parcel = get_object_or_404(Parcel, id=parcel_id)
# Security check: only shipper can view invoice (or admin)
if parcel.shipper != request.user and not request.user.is_staff:
messages.error(request, _("You are not authorized to view this invoice."))
return redirect('dashboard')
# Get Logo Base64
logo_base64 = None
platform_profile = PlatformProfile.objects.first()
if platform_profile and platform_profile.logo:
try:
with open(platform_profile.logo.path, "rb") as image_file:
logo_base64 = base64.b64encode(image_file.read()).decode()
except Exception:
pass
# Render Template
html_string = render_to_string('core/invoice.html', {
'parcel': parcel,
'logo_base64': logo_base64,
'platform_profile': platform_profile,
'request': request,
})
# Generate PDF
html = weasyprint.HTML(string=html_string, base_url=request.build_absolute_uri())
pdf_file = html.write_pdf()
response = HttpResponse(pdf_file, content_type='application/pdf')
response['Content-Disposition'] = f'inline; filename="invoice_{parcel.tracking_number}.pdf"'
return response
@login_required
def scan_qr_view(request):
"""Renders the QR Scanner page for drivers."""
# Optional: Restrict to drivers only
# if request.user.profile.role != 'car_owner':
# messages.error(request, _("Only drivers can access the scanner."))
# return redirect('dashboard')
return render(request, 'core/scan_qr.html')
@login_required
def get_parcel_details(request):
"""API to fetch parcel details by tracking number."""
tracking_number = request.GET.get('tracking_number')
if not tracking_number:
return JsonResponse({'success': False, 'error': _('Tracking number required')})
try:
parcel = Parcel.objects.get(tracking_number__iexact=tracking_number.strip())
# Check permissions: Is user the assigned driver? Or is it pending (for acceptance)?
is_driver = request.user.profile.role == 'car_owner'
is_assigned = parcel.carrier == request.user
can_update = False
if is_driver:
if is_assigned:
can_update = True
elif parcel.status == 'pending':
# Check if payments are enabled and paid
platform_profile = PlatformProfile.objects.first()
payments_enabled = platform_profile.enable_payment if platform_profile else True
if not payments_enabled or parcel.payment_status == 'paid':
can_update = True
data = {
'success': True,
'parcel': {
'id': parcel.id,
'tracking_number': parcel.tracking_number,
'status': parcel.status,
'status_display': parcel.get_status_display(),
'price': float(parcel.price),
'from': f"{parcel.pickup_governate.name} / {parcel.pickup_city.name}",
'to': f"{parcel.delivery_governate.name} / {parcel.delivery_city.name}",
'description': parcel.description,
'can_update': can_update
}
}
return JsonResponse(data)
except Parcel.DoesNotExist:
return JsonResponse({'success': False, 'error': _('Parcel not found')})
@login_required
@require_POST
def update_parcel_status_ajax(request):
"""API to update parcel status from scanner."""
try:
data = json.loads(request.body)
parcel_id = data.get('parcel_id')
action = data.get('action')
parcel = get_object_or_404(Parcel, id=parcel_id)
# Logic for actions
if action == 'accept':
# Similar to accept_parcel view
if request.user.profile.role != 'car_owner':
return JsonResponse({'success': False, 'error': _('Only drivers can accept shipments')})
if parcel.status != 'pending':
return JsonResponse({'success': False, 'error': _('Parcel is not available')})
# Check payment status if enabled
platform_profile = PlatformProfile.objects.first()
payments_enabled = platform_profile.enable_payment if platform_profile else True
if payments_enabled and parcel.payment_status != 'paid':
return JsonResponse({'success': False, 'error': _('Payment pending')})
parcel.carrier = request.user
parcel.status = 'picked_up' # Or 'assigned'? Logic says 'picked_up' in accept_parcel
parcel.save()
notify_driver_assigned(parcel)
elif action == 'delivered':
if parcel.carrier != request.user:
return JsonResponse({'success': False, 'error': _('Not authorized')})
parcel.status = 'delivered'
parcel.save()
notify_status_change(parcel)
else:
return JsonResponse({'success': False, 'error': _('Invalid action')})
return JsonResponse({'success': True})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)})