adding invoice
This commit is contained in:
parent
4020492307
commit
595ca7c1fe
Binary file not shown.
Binary file not shown.
@ -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">
|
||||
|
||||
181
core/templates/core/invoice.html
Normal file
181
core/templates/core/invoice.html
Normal 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>
|
||||
240
core/templates/core/scan_qr.html
Normal file
240
core/templates/core/scan_qr.html
Normal 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 %}
|
||||
@ -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" %}
|
||||
</a>
|
||||
<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">
|
||||
|
||||
@ -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'),
|
||||
|
||||
134
core/views.py
134
core/views.py
@ -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)})
|
||||
Loading…
x
Reference in New Issue
Block a user