1314 lines
43 KiB
HTML
1314 lines
43 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Purchase Order - {{ order.order_number }}{% endblock %}
|
|
|
|
{% block css %}
|
|
<style>
|
|
.order-header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border-radius: 0.5rem;
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.order-info-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.info-card {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
padding: 1.5rem;
|
|
text-align: center;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
}
|
|
|
|
.info-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.info-icon {
|
|
width: 50px;
|
|
height: 50px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 0 auto 1rem;
|
|
color: white;
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.info-value {
|
|
font-size: 1.5rem;
|
|
font-weight: bold;
|
|
color: #495057;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.info-label {
|
|
color: #6c757d;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.section-card {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
margin-bottom: 2rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.section-header {
|
|
background: #f8f9fa;
|
|
border-bottom: 1px solid #dee2e6;
|
|
padding: 1rem 1.5rem;
|
|
font-weight: 600;
|
|
color: #495057;
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
}
|
|
|
|
.section-content {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.status-badge {
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.status-draft { background: #f8f9fa; color: #6c757d; }
|
|
.status-pending { background: #fff3cd; color: #856404; }
|
|
.status-approved { background: #d1ecf1; color: #0c5460; }
|
|
.status-ordered { background: #d4edda; color: #155724; }
|
|
.status-received { background: #d4edda; color: #155724; }
|
|
.status-cancelled { background: #f8d7da; color: #721c24; }
|
|
|
|
.priority-badge {
|
|
padding: 0.375rem 0.75rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.priority-low { background: #d4edda; color: #155724; }
|
|
.priority-medium { background: #fff3cd; color: #856404; }
|
|
.priority-high { background: #f8d7da; color: #721c24; }
|
|
.priority-urgent { background: #f5c6cb; color: #721c24; }
|
|
|
|
.supplier-card {
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.supplier-avatar {
|
|
width: 60px;
|
|
height: 60px;
|
|
border-radius: 50%;
|
|
background: #007bff;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.items-table {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.item-row {
|
|
border-bottom: 1px solid #f1f3f4;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.item-row:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.item-image {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 0.25rem;
|
|
object-fit: cover;
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
}
|
|
|
|
.item-details {
|
|
flex: 1;
|
|
}
|
|
|
|
.item-name {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.item-description {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.item-specs {
|
|
font-size: 0.75rem;
|
|
color: #adb5bd;
|
|
}
|
|
|
|
.quantity-info {
|
|
text-align: center;
|
|
}
|
|
|
|
.quantity-number {
|
|
font-size: 1.25rem;
|
|
font-weight: bold;
|
|
color: #495057;
|
|
}
|
|
|
|
.quantity-unit {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.price-info {
|
|
text-align: right;
|
|
}
|
|
|
|
.unit-price {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.total-price {
|
|
font-size: 1.1rem;
|
|
font-weight: bold;
|
|
color: #28a745;
|
|
}
|
|
|
|
.order-totals {
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.total-row {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
padding: 0.5rem 0;
|
|
border-bottom: 1px solid #dee2e6;
|
|
}
|
|
|
|
.total-row:last-child {
|
|
border-bottom: none;
|
|
font-weight: bold;
|
|
font-size: 1.1rem;
|
|
color: #495057;
|
|
}
|
|
|
|
.timeline {
|
|
position: relative;
|
|
padding-left: 2rem;
|
|
}
|
|
|
|
.timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0.75rem;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 2px;
|
|
background: #dee2e6;
|
|
}
|
|
|
|
.timeline-item {
|
|
position: relative;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.timeline-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: -0.5rem;
|
|
top: 0.25rem;
|
|
width: 1rem;
|
|
height: 1rem;
|
|
border-radius: 50%;
|
|
background: white;
|
|
border: 3px solid #007bff;
|
|
}
|
|
|
|
.timeline-item.completed::before {
|
|
background: #28a745;
|
|
border-color: #28a745;
|
|
}
|
|
|
|
.timeline-item.current::before {
|
|
background: #ffc107;
|
|
border-color: #ffc107;
|
|
}
|
|
|
|
.timeline-content {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-left: 1rem;
|
|
}
|
|
|
|
.timeline-title {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.timeline-description {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.timeline-meta {
|
|
font-size: 0.75rem;
|
|
color: #adb5bd;
|
|
}
|
|
|
|
.documents-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
.document-card {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
text-align: center;
|
|
transition: all 0.2s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.document-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.document-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 0.25rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 0 auto 0.75rem;
|
|
color: white;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.doc-pdf { background: #dc3545; }
|
|
.doc-excel { background: #28a745; }
|
|
.doc-word { background: #007bff; }
|
|
.doc-image { background: #6f42c1; }
|
|
|
|
.document-name {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.25rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.document-size {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.notes-section {
|
|
background: #fff3cd;
|
|
border: 1px solid #ffeaa7;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.note-item {
|
|
background: white;
|
|
border: 1px solid #ffeaa7;
|
|
border-radius: 0.25rem;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.note-item:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.note-header {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.note-author {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
.note-date {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.note-content {
|
|
color: #495057;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.approval-workflow {
|
|
background: #e3f2fd;
|
|
border: 1px solid #bbdefb;
|
|
border-radius: 0.375rem;
|
|
padding: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.approval-step {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 1rem;
|
|
background: white;
|
|
border: 1px solid #bbdefb;
|
|
border-radius: 0.25rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.approval-step:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.approval-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 1rem;
|
|
color: white;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.step-pending { background: #6c757d; }
|
|
.step-approved { background: #28a745; }
|
|
.step-rejected { background: #dc3545; }
|
|
.step-current { background: #ffc107; }
|
|
|
|
.approval-details {
|
|
flex: 1;
|
|
}
|
|
|
|
.approval-title {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.approval-user {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.approval-date {
|
|
font-size: 0.75rem;
|
|
color: #adb5bd;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.order-header {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.order-info-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 1rem;
|
|
}
|
|
|
|
.section-content {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.action-buttons {
|
|
justify-content: center;
|
|
}
|
|
|
|
.timeline {
|
|
padding-left: 1.5rem;
|
|
}
|
|
|
|
.documents-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
}
|
|
|
|
@media print {
|
|
.action-buttons, .btn {
|
|
display: none !important;
|
|
}
|
|
|
|
.section-card {
|
|
break-inside: avoid;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.section-header {
|
|
background: none;
|
|
border-bottom: 2px solid #000;
|
|
color: #000;
|
|
}
|
|
|
|
.order-header {
|
|
background: none;
|
|
color: #000;
|
|
border: 2px solid #000;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div id="content" class="app-content">
|
|
<!-- Page Header -->
|
|
<div class="d-flex align-items-center mb-3">
|
|
<div>
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'inventory:dashboard' %}">Inventory</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'inventory:purchase_order_list' %}">Purchase Orders</a></li>
|
|
<li class="breadcrumb-item active">{{ order.order_number }}</li>
|
|
</ol>
|
|
<h1 class="page-header mb-0">
|
|
<i class="fas fa-file-invoice me-2"></i>Purchase Order Details
|
|
</h1>
|
|
</div>
|
|
<div class="ms-auto">
|
|
<div class="action-buttons">
|
|
<button type="button" class="btn btn-outline-secondary" onclick="printOrder()">
|
|
<i class="fas fa-print me-1"></i>Print
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info" onclick="exportOrder()">
|
|
<i class="fas fa-download me-1"></i>Export
|
|
</button>
|
|
<button type="button" class="btn btn-outline-warning" onclick="duplicateOrder()">
|
|
<i class="fas fa-copy me-1"></i>Duplicate
|
|
</button>
|
|
{% if order.can_edit %}
|
|
<a href="{% url 'inventory:purchase_order_edit' order.pk %}" class="btn btn-outline-primary">
|
|
<i class="fas fa-edit me-1"></i>Edit
|
|
</a>
|
|
{% endif %}
|
|
{% if order.can_approve %}
|
|
<button type="button" class="btn btn-success" onclick="approveOrder()">
|
|
<i class="fas fa-check me-1"></i>Approve
|
|
</button>
|
|
{% endif %}
|
|
{% if order.can_cancel %}
|
|
<button type="button" class="btn btn-outline-danger" onclick="cancelOrder()">
|
|
<i class="fas fa-times me-1"></i>Cancel
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Order Header -->
|
|
<div class="order-header">
|
|
<div class="row align-items-center">
|
|
<div class="col-md-8">
|
|
<h2 class="mb-2">{{ order.order_number }}</h2>
|
|
<p class="mb-2 fs-5">{{ order.title|default:"Purchase Order" }}</p>
|
|
<div class="d-flex align-items-center gap-3">
|
|
<span class="status-badge status-{{ order.status }}">
|
|
{{ order.get_status_display }}
|
|
</span>
|
|
<span class="priority-badge priority-{{ order.priority }}">
|
|
{{ order.get_priority_display }} Priority
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 text-md-end">
|
|
<div class="text-white-50 mb-1">Total Amount</div>
|
|
<div class="h3 mb-2"><span class="symbol">ê</span>{{ order.total_amount|floatformat:'2g' }}</div>
|
|
<div class="text-white-50">{{ order.total_items }} items</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Order Information Grid -->
|
|
<div class="order-info-grid">
|
|
<div class="info-card">
|
|
<div class="info-icon" style="background: #007bff;">
|
|
<i class="fas fa-calendar-plus"></i>
|
|
</div>
|
|
<div class="info-value">{{ order.created_at|date:"M d, Y" }}</div>
|
|
<div class="info-label">Date Created</div>
|
|
</div>
|
|
|
|
<div class="info-card">
|
|
<div class="info-icon" style="background: #28a745;">
|
|
<i class="fas fa-truck"></i>
|
|
</div>
|
|
<div class="info-value">
|
|
{% if order.expected_delivery_date %}
|
|
{{ order.expected_delivery_date|date:"M d, Y" }}
|
|
{% else %}
|
|
Not Set
|
|
{% endif %}
|
|
</div>
|
|
<div class="info-label">Expected Delivery</div>
|
|
</div>
|
|
|
|
<div class="info-card">
|
|
<div class="info-icon" style="background: #ffc107;">
|
|
<i class="fas fa-user"></i>
|
|
</div>
|
|
<div class="info-value">{{ order.created_by.get_full_name }}</div>
|
|
<div class="info-label">Created By</div>
|
|
</div>
|
|
|
|
<div class="info-card">
|
|
<div class="info-icon" style="background: #17a2b8;">
|
|
<i class="fas fa-building"></i>
|
|
</div>
|
|
<div class="info-value">{{ order.department.name|default:"General" }}</div>
|
|
<div class="info-label">Department</div>
|
|
</div>
|
|
|
|
<div class="info-card">
|
|
<div class="info-icon" style="background: #6f42c1;">
|
|
<i class="fas fa-hashtag"></i>
|
|
</div>
|
|
<div class="info-value">{{ order.reference_number|default:"N/A" }}</div>
|
|
<div class="info-label">Reference Number</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-lg-8">
|
|
<!-- Supplier Information -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<div>
|
|
<i class="fas fa-building me-2"></i>Supplier Information
|
|
</div>
|
|
<a href="{% url 'inventory:supplier_detail' order.supplier.pk %}" class="btn btn-outline-primary btn-sm">
|
|
<i class="fas fa-external-link-alt me-1"></i>View Supplier
|
|
</a>
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="supplier-card">
|
|
<div class="row">
|
|
<div class="col-md-2 text-center">
|
|
<div class="supplier-avatar">
|
|
{{ order.supplier.name.0|upper }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-5">
|
|
<h5 class="mb-2">{{ order.supplier.name }}</h5>
|
|
<p class="text-muted mb-1">
|
|
<i class="fas fa-user me-1"></i>{{ order.supplier.contact_person|default:"N/A" }}
|
|
</p>
|
|
<p class="text-muted mb-1">
|
|
<i class="fas fa-envelope me-1"></i>{{ order.supplier.email|default:"N/A" }}
|
|
</p>
|
|
<p class="text-muted mb-0">
|
|
<i class="fas fa-phone me-1"></i>{{ order.supplier.phone|default:"N/A" }}
|
|
</p>
|
|
</div>
|
|
<div class="col-md-5">
|
|
<h6 class="mb-2">Billing Address</h6>
|
|
<address class="text-muted mb-0">
|
|
{{ order.supplier.address|default:"Address not available" }}<br>
|
|
{% if order.supplier.city %}{{ order.supplier.city }}, {% endif %}
|
|
{% if order.supplier.state %}{{ order.supplier.state }} {% endif %}
|
|
{% if order.supplier.zip_code %}{{ order.supplier.zip_code }}{% endif %}
|
|
</address>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Order Items -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<div>
|
|
<i class="fas fa-list me-2"></i>Order Items ({{ order.items.count }})
|
|
</div>
|
|
<div class="btn-group btn-group-sm">
|
|
<button type="button" class="btn btn-outline-secondary" onclick="expandAllItems()">
|
|
<i class="fas fa-expand-alt me-1"></i>Expand All
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="collapseAllItems()">
|
|
<i class="fas fa-compress-alt me-1"></i>Collapse All
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="section-content p-0">
|
|
<div class="table-responsive">
|
|
<table class="table items-table">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th width="60">Image</th>
|
|
<th>Item Details</th>
|
|
<th width="100">Quantity</th>
|
|
<th width="120">Unit Price</th>
|
|
<th width="120">Total Price</th>
|
|
<th width="80">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for item in order.items.all %}
|
|
<tr class="item-row">
|
|
<td>
|
|
{% if item.product.image %}
|
|
<img src="{{ item.product.image.url }}" alt="{{ item.product.name }}" class="item-image">
|
|
{% else %}
|
|
<div class="item-image d-flex align-items-center justify-content-center">
|
|
<i class="fas fa-box text-muted"></i>
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="item-details">
|
|
<div class="item-name">{{ item.product.name }}</div>
|
|
<div class="item-description">{{ item.product.description|truncatechars:100 }}</div>
|
|
<div class="item-specs">
|
|
SKU: {{ item.product.sku|default:"N/A" }} |
|
|
Category: {{ item.product.category|default:"N/A" }}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="quantity-info">
|
|
<div class="quantity-number">{{ item.quantity }}</div>
|
|
<div class="quantity-unit">{{ item.unit|default:"pcs" }}</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="price-info">
|
|
<div class="unit-price"><span class="symbol">ê</span>{{ item.unit_price|floatformat:'2g' }}</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="price-info">
|
|
<div class="total-price"><span class="symbol">ê</span>{{ item.total_price|floatformat:'2g' }}</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-{{ item.status_color }}">
|
|
{{ item.get_status_display|default:"Pending" }}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Order Totals -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-calculator me-2"></i>Order Summary
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="order-totals">
|
|
<div class="total-row">
|
|
<span>Subtotal:</span>
|
|
<span><span class="symbol">ê</span>{{ order.subtotal|floatformat:'2g' }}</span>
|
|
</div>
|
|
{% if order.discount_amount %}
|
|
<div class="total-row">
|
|
<span>Discount ({{ order.discount_percentage }}%):</span>
|
|
<span class="text-success">-<span class="symbol">ê</span>{{ order.discount_amount|floatformat:'2g' }}</span>
|
|
</div>
|
|
{% endif %}
|
|
{% if order.tax_amount %}
|
|
<div class="total-row">
|
|
<span>Tax ({{ order.tax_percentage }}%):</span>
|
|
<span><span class="symbol">ê</span>{{ order.tax_amount|floatformat:'2g' }}</span>
|
|
</div>
|
|
{% endif %}
|
|
{% if order.shipping_cost %}
|
|
<div class="total-row">
|
|
<span>Shipping:</span>
|
|
<span><span class="symbol">ê</span>{{ order.shipping_cost|floatformat:'2g' }}</span>
|
|
</div>
|
|
{% endif %}
|
|
<div class="total-row">
|
|
<span>Total Amount:</span>
|
|
<span><span class="symbol">ê</span>{{ order.total_amount|floatformat:'2g' }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6 class="mb-3">Payment Terms</h6>
|
|
<p class="text-muted">{{ order.payment_terms|default:"Net 30 days" }}</p>
|
|
|
|
<h6 class="mb-3">Delivery Terms</h6>
|
|
<p class="text-muted">{{ order.delivery_terms|default:"FOB Destination" }}</p>
|
|
|
|
{% if order.special_instructions %}
|
|
<h6 class="mb-3">Special Instructions</h6>
|
|
<p class="text-muted">{{ order.special_instructions }}</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Documents -->
|
|
{% if order.documents.exists %}
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<div>
|
|
<i class="fas fa-paperclip me-2"></i>Attachments ({{ order.documents.count }})
|
|
</div>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="uploadDocument()">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="documents-grid">
|
|
{% for document in order.documents.all %}
|
|
<div class="document-card" onclick="viewDocument('{{ document.file.url }}')">
|
|
<div class="document-icon doc-{{ document.file_type }}">
|
|
<i class="fas fa-file-{{ document.icon }}"></i>
|
|
</div>
|
|
<div class="document-name">{{ document.name }}</div>
|
|
<div class="document-size">{{ document.file.size|filesizeformat }}</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="col-lg-4">
|
|
<!-- Approval Workflow -->
|
|
{% if order.approval_workflow %}
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-check-circle me-2"></i>Approval Workflow
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="approval-workflow">
|
|
{% for step in order.approval_steps %}
|
|
<div class="approval-step">
|
|
<div class="approval-icon step-{{ step.status }}">
|
|
{% if step.status == 'approved' %}
|
|
<i class="fas fa-check"></i>
|
|
{% elif step.status == 'rejected' %}
|
|
<i class="fas fa-times"></i>
|
|
{% elif step.status == 'current' %}
|
|
<i class="fas fa-clock"></i>
|
|
{% else %}
|
|
<i class="fas fa-circle"></i>
|
|
{% endif %}
|
|
</div>
|
|
<div class="approval-details">
|
|
<div class="approval-title">{{ step.title }}</div>
|
|
<div class="approval-user">{{ step.approver.get_full_name }}</div>
|
|
{% if step.approved_at %}
|
|
<div class="approval-date">{{ step.approved_at|date:"M d, Y g:i A" }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Order Timeline -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-history me-2"></i>Order Timeline
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="timeline">
|
|
{% for event in order.timeline %}
|
|
<div class="timeline-item {% if event.is_completed %}completed{% elif event.is_current %}current{% endif %}">
|
|
<div class="timeline-content">
|
|
<div class="timeline-title">{{ event.title }}</div>
|
|
<div class="timeline-description">{{ event.description }}</div>
|
|
<div class="timeline-meta">
|
|
{% if event.user %}{{ event.user.get_full_name }} • {% endif %}
|
|
{{ event.created_at|date:"M d, Y g:i A" }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notes -->
|
|
{% if order.notes.exists %}
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<div>
|
|
<i class="fas fa-sticky-note me-2"></i>Notes ({{ order.notes.count }})
|
|
</div>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addNote()">
|
|
<i class="fas fa-plus me-1"></i>Add Note
|
|
</button>
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="notes-section">
|
|
{% for note in order.notes.all %}
|
|
<div class="note-item">
|
|
<div class="note-header">
|
|
<div class="note-author">{{ note.created_by.get_full_name }}</div>
|
|
<div class="note-date">{{ note.created_at|date:"M d, Y g:i A" }}</div>
|
|
</div>
|
|
<div class="note-content">{{ note.content }}</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-bolt me-2"></i>Quick Actions
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="d-grid gap-2">
|
|
{% if order.can_send_to_supplier %}
|
|
<button type="button" class="btn btn-outline-primary" onclick="sendToSupplier()">
|
|
<i class="fas fa-paper-plane me-1"></i>Send to Supplier
|
|
</button>
|
|
{% endif %}
|
|
|
|
{% if order.can_mark_received %}
|
|
<button type="button" class="btn btn-outline-success" onclick="markReceived()">
|
|
<i class="fas fa-check-circle me-1"></i>Mark as Received
|
|
</button>
|
|
{% endif %}
|
|
|
|
<button type="button" class="btn btn-outline-info" onclick="trackDelivery()">
|
|
<i class="fas fa-truck me-1"></i>Track Delivery
|
|
</button>
|
|
|
|
<button type="button" class="btn btn-outline-warning" onclick="requestQuote()">
|
|
<i class="fas fa-file-invoice-dollar me-1"></i>Request Updated Quote
|
|
</button>
|
|
|
|
<button type="button" class="btn btn-outline-secondary" onclick="generateReport()">
|
|
<i class="fas fa-chart-bar me-1"></i>Generate Report
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Approval Modal -->
|
|
<div class="modal fade" id="approvalModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-check-circle me-2"></i>Approve Purchase Order
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
You are about to approve purchase order <strong>{{ order.order_number }}</strong> for <strong><span class="symbol">ê</span>{{ order.total_amount|floatformat:'2g' }}</strong>.
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Approval Notes</label>
|
|
<textarea class="form-control" id="approval-notes" rows="3"
|
|
placeholder="Add any notes about this approval..."></textarea>
|
|
</div>
|
|
|
|
<div class="form-check mb-3">
|
|
<input class="form-check-input" type="checkbox" id="send-to-supplier">
|
|
<label class="form-check-label" for="send-to-supplier">
|
|
Send approved order to supplier immediately
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="notify-requestor">
|
|
<label class="form-check-label" for="notify-requestor">
|
|
Notify order requestor of approval
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-1"></i>Cancel
|
|
</button>
|
|
<button type="button" class="btn btn-success" onclick="confirmApproval()">
|
|
<i class="fas fa-check me-1"></i>Approve Order
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Note Modal -->
|
|
<div class="modal fade" id="noteModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-sticky-note me-2"></i>Add Note
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Note Content</label>
|
|
<textarea class="form-control" id="note-content" rows="4"
|
|
placeholder="Enter your note here..." required></textarea>
|
|
</div>
|
|
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="note-important">
|
|
<label class="form-check-label" for="note-important">
|
|
Mark as important
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-1"></i>Cancel
|
|
</button>
|
|
<button type="button" class="btn btn-primary" onclick="saveNote()">
|
|
<i class="fas fa-save me-1"></i>Save Note
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Initialize tooltips
|
|
$('[data-bs-toggle="tooltip"]').tooltip();
|
|
});
|
|
|
|
function printOrder() {
|
|
window.print();
|
|
}
|
|
|
|
function exportOrder() {
|
|
window.open(`/inventory/orders/{{ order.pk }}/export/`, '_blank');
|
|
}
|
|
|
|
function duplicateOrder() {
|
|
if (confirm('Are you sure you want to create a duplicate of this purchase order?')) {
|
|
fetch(`/inventory/orders/{{ order.pk }}/duplicate/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Order duplicated successfully', 'success');
|
|
setTimeout(() => {
|
|
window.location.href = `/inventory/orders/${data.new_order_id}/`;
|
|
}, 1500);
|
|
} else {
|
|
showAlert('Error duplicating order', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error duplicating order', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function approveOrder() {
|
|
new bootstrap.Modal(document.getElementById('approvalModal')).show();
|
|
}
|
|
|
|
function confirmApproval() {
|
|
const notes = document.getElementById('approval-notes').value;
|
|
const sendToSupplier = document.getElementById('send-to-supplier').checked;
|
|
const notifyRequestor = document.getElementById('notify-requestor').checked;
|
|
|
|
fetch(`/inventory/orders/{{ order.pk }}/approve/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
notes: notes,
|
|
send_to_supplier: sendToSupplier,
|
|
notify_requestor: notifyRequestor
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Purchase order approved successfully', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error approving purchase order', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error approving purchase order', 'danger');
|
|
});
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('approvalModal')).hide();
|
|
}
|
|
|
|
function cancelOrder() {
|
|
if (confirm('Are you sure you want to cancel this purchase order? This action cannot be undone.')) {
|
|
fetch(`/inventory/orders/{{ order.pk }}/cancel/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Purchase order cancelled successfully', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error cancelling purchase order', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error cancelling purchase order', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function addNote() {
|
|
new bootstrap.Modal(document.getElementById('noteModal')).show();
|
|
}
|
|
|
|
function saveNote() {
|
|
const content = document.getElementById('note-content').value;
|
|
const important = document.getElementById('note-important').checked;
|
|
|
|
if (!content.trim()) {
|
|
showAlert('Please enter note content', 'warning');
|
|
return;
|
|
}
|
|
|
|
fetch(`/inventory/orders/{{ order.pk }}/notes/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
content: content,
|
|
important: important
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Note added successfully', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error adding note', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error adding note', 'danger');
|
|
});
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('noteModal')).hide();
|
|
}
|
|
|
|
function sendToSupplier() {
|
|
if (confirm('Send this purchase order to the supplier?')) {
|
|
fetch(`/inventory/orders/{{ order.pk }}/send-to-supplier/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Order sent to supplier successfully', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error sending order to supplier', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error sending order to supplier', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function markReceived() {
|
|
if (confirm('Mark this order as received? This will update inventory levels.')) {
|
|
fetch(`/inventory/orders/{{ order.pk }}/mark-received/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Order marked as received successfully', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error marking order as received', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error marking order as received', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function trackDelivery() {
|
|
// Open tracking modal or redirect to tracking page
|
|
window.open(`/inventory/orders/{{ order.pk }}/track/`, '_blank');
|
|
}
|
|
|
|
function requestQuote() {
|
|
// Open quote request modal or redirect to quote page
|
|
window.location.href = `/inventory/orders/{{ order.pk }}/request-quote/`;
|
|
}
|
|
|
|
function generateReport() {
|
|
window.open(`/inventory/orders/{{ order.pk }}/report/`, '_blank');
|
|
}
|
|
|
|
function uploadDocument() {
|
|
// Open file upload modal
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.multiple = true;
|
|
input.accept = '.pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png';
|
|
|
|
input.onchange = function(e) {
|
|
const files = e.target.files;
|
|
if (files.length > 0) {
|
|
const formData = new FormData();
|
|
for (let i = 0; i < files.length; i++) {
|
|
formData.append('documents', files[i]);
|
|
}
|
|
|
|
fetch(`/inventory/orders/{{ order.pk }}/upload-documents/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
},
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Documents uploaded successfully', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error uploading documents', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error uploading documents', 'danger');
|
|
});
|
|
}
|
|
};
|
|
|
|
input.click();
|
|
}
|
|
|
|
function viewDocument(url) {
|
|
window.open(url, '_blank');
|
|
}
|
|
|
|
function expandAllItems() {
|
|
// Expand all item details
|
|
$('.item-row').addClass('expanded');
|
|
}
|
|
|
|
function collapseAllItems() {
|
|
// Collapse all item details
|
|
$('.item-row').removeClass('expanded');
|
|
}
|
|
|
|
function showAlert(message, type) {
|
|
const alertDiv = document.createElement('div');
|
|
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
|
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 1060; min-width: 300px;';
|
|
alertDiv.innerHTML = `
|
|
${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
document.body.appendChild(alertDiv);
|
|
|
|
setTimeout(() => {
|
|
if (alertDiv.parentNode) {
|
|
alertDiv.remove();
|
|
}
|
|
}, 5000);
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|