This commit is contained in:
Marwan Alwali 2025-08-31 12:21:16 +03:00
parent b9b8c69129
commit 1f0a6bff5f
11 changed files with 3145 additions and 684 deletions

Binary file not shown.

View File

@ -38,6 +38,7 @@ DJANGO_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
]
THIRD_PARTY_APPS = [

View File

@ -341,26 +341,26 @@ class InventoryLocationDetailView(LoginRequiredMixin, DetailView):
# Stock items at this location
context['stock_items'] = InventoryStock.objects.filter(
tenant=self.request.user.tenant,
inventory_item__tenant=self.request.user.tenant,
location=location
).select_related('item').order_by('item__item_name')
).select_related('inventory_item').order_by('inventory_item__item_name')
# Location statistics
context['total_items'] = InventoryStock.objects.filter(
tenant=self.request.user.tenant,
inventory_item__tenant=self.request.user.tenant,
location=location
).count()
context['total_quantity'] = InventoryStock.objects.filter(
tenant=self.request.user.tenant,
inventory_item__tenant=self.request.user.tenant,
location=location
).aggregate(total=Sum('quantity'))['total'] or 0
).aggregate(total=Sum('quantity_available'))['total'] or 0
context['total_value'] = InventoryStock.objects.filter(
tenant=self.request.user.tenant,
inventory_item__tenant=self.request.user.tenant,
location=location
).aggregate(
total_value=Sum(F('quantity') * F('unit_cost'))
total_value=Sum(F('quantity_available') * F('unit_cost'))
)['total_value'] or 0
return context
@ -503,27 +503,27 @@ class InventoryItemDetailView(LoginRequiredMixin, DetailView):
# Stock information across all locations
context['stock_locations'] = InventoryStock.objects.filter(
tenant=self.request.user.tenant,
item=item
inventory_item__tenant=self.request.user.tenant,
inventory_item=item
).select_related('location').order_by('location__location_name')
# Item statistics
context['total_stock'] = InventoryStock.objects.filter(
tenant=self.request.user.tenant,
item=item
).aggregate(total=Sum('quantity'))['total'] or 0
inventory_item__tenant=self.request.user.tenant,
inventory_item=item
).aggregate(total=Sum('quantity_available'))['total'] or 0
context['total_value'] = InventoryStock.objects.filter(
tenant=self.request.user.tenant,
item=item
inventory_item__tenant=self.request.user.tenant,
inventory_item=item
).aggregate(
total_value=Sum(F('quantity') * F('unit_cost'))
total_value=Sum(F('quantity_available') * F('unit_cost'))
)['total_value'] or 0
# Recent purchase orders for this item
context['recent_orders'] = PurchaseOrderItem.objects.filter(
purchase_order__tenant=self.request.user.tenant,
item=item
inventory_item=item
).select_related('purchase_order').order_by('-purchase_order__order_date')[:5]
return context

File diff suppressed because it is too large Load Diff

View File

@ -4,9 +4,9 @@
{% block title %}{{ object.item_name }} - Inventory Item Details{% endblock %}
{% block css %}
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/chart.js/dist/Chart.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/chart.js/dist/Chart.min.css' %}" rel="stylesheet" />
{% endblock %}
{% block content %}
@ -96,11 +96,11 @@
</tr>
<tr>
<td class="fw-bold">Unit Cost:</td>
<td class="fw-bold text-success">${{ object.unit_cost }}</td>
<td class="fw-bold text-success"><span class="symbol">&#xea;</span>{{ object.unit_cost|floatformat:'2g' }}</td>
</tr>
<tr>
<td class="fw-bold">List Price:</td>
<td>${{ object.list_price }}</td>
<td><span class="symbol">&#xea;</span>{{ object.list_price|floatformat:'2g' }}</td>
</tr>
</table>
</div>
@ -237,7 +237,7 @@
</div>
<div class="col-md-3">
<div class="text-center p-3 bg-success text-white rounded">
<div class="fs-24px fw-bold">${{ object.total_value|floatformat:2 }}</div>
<div class="fs-24px fw-bold"><span class="symbol">&#xea;</span>{{ object.total_value|floatformat:'2g' }}</div>
<div class="small">Total Value</div>
</div>
</div>
@ -303,10 +303,15 @@
<span class="text-muted">No expiration</span>
{% endif %}
</td>
<td>
<span class="badge bg-{{ stock.get_quality_status_color }}">
{{ stock.get_quality_status_display }}
</span>
{% if stock.quality_status == 'GOOD' %}
<span class="badge bg-success">{{ stock.get_quality_status_display }}</span>
{% elif stock.quality_status == 'QUARANTINE' %}
<span class="badge bg-warning">{{ stock.get_quality_status_display }}</span>
{% else %}
<span class="badge bg-danger">{{ stock.get_quality_status_display }}</span>
{% endif %}
</td>
<td>
<a href="{% url 'inventory:stock_detail' stock.pk %}" class="btn btn-xs btn-outline-primary">
@ -336,7 +341,7 @@
<div class="panel-heading">
<h4 class="panel-title">Recent Transactions</h4>
<div class="panel-heading-btn">
<a href="{% url 'inventory:transaction_list' %}?item={{ object.pk }}" class="btn btn-xs btn-outline-secondary me-2">
<a href="#" class="btn btn-xs btn-outline-secondary me-2">
<i class="fa fa-list"></i> View All
</a>
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
@ -405,7 +410,7 @@
<i class="fa fa-plus me-2"></i>Add Stock
</a>
<a href="{% url 'inventory:stock_adjustment_create' %}?item={{ object.pk }}" class="btn btn-warning">
<a href="#" class="btn btn-warning">
<i class="fa fa-adjust me-2"></i>Adjust Stock
</a>
@ -594,32 +599,32 @@
{% endblock %}
{% block js %}
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'assets/plugins/chart.js/dist/Chart.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'plugins/chart.js/dist/chart.js' %}"></script>
<script>
var stockChart;
function printItemLabel() {
window.open('{% url "inventory:item_label_print" object.pk %}', '_blank');
}
{#function printItemLabel() {#}
{# window.open('{% url "inventory:item_label_print" object.pk %}', '_blank');#}
{# }#}
function showStockChart() {
$('#stockChartModal').modal('show');
loadStockChart();
}
function loadStockChart() {
$.ajax({
url: '{% url "inventory:item_stock_history" object.pk %}',
success: function(data) {
renderStockChart(data);
},
error: function() {
toastr.error('Failed to load stock history data');
}
});
}
{#function loadStockChart() {#}
{# $.ajax({#}
{# url: '{% url "inventory:item_stock_history" object.pk %}',#}
{# success: function(data) {#}
{# renderStockChart(data);#}
{# },#}
{# error: function() {#}
{# toastr.error('Failed to load stock history data');#}
{# }#}
{# });#}
{# }#}
function renderStockChart(data) {
var ctx = document.getElementById('stock-chart').getContext('2d');

View File

@ -4,9 +4,9 @@
{% block title %}{% if object %}Edit {{ object.item_name }}{% else %}Add New Item{% endif %} - Inventory{% endblock %}
{% block css %}
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/jquery-file-upload/css/jquery.fileupload.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
{#<link href="{% static 'plugins/jquery-file-upload/css/jquery.fileupload.css' %}" rel="stylesheet" />#}
{% endblock %}
{% block content %}
@ -642,30 +642,30 @@ function toggleConditionalFields() {
}
}
function generateItemCode() {
var category = $('#id_category').val();
var itemType = $('#id_item_type').val();
if (!category) {
toastr.warning('Please select a category first');
return;
}
$.ajax({
url: '{% url "inventory:generate_item_code" %}',
data: {
'category': category,
'item_type': itemType
},
success: function(response) {
$('#id_item_code').val(response.item_code);
toastr.success('Item code generated: ' + response.item_code);
},
error: function() {
toastr.error('Failed to generate item code');
}
});
}
{#function generateItemCode() {#}
{# var category = $('#id_category').val();#}
{# var itemType = $('#id_item_type').val();#}
{# #}
{# if (!category) {#}
{# toastr.warning('Please select a category first');#}
{# return;#}
{# }#}
{# #}
{# $.ajax({#}
{# url: '{% url "inventory:generate_item_code" %}',#}
{# data: {#}
{# 'category': category,#}
{# 'item_type': itemType#}
{# },#}
{# success: function(response) {#}
{# $('#id_item_code').val(response.item_code);#}
{# toastr.success('Item code generated: ' + response.item_code);#}
{# },#}
{# error: function() {#}
{# toastr.error('Failed to generate item code');#}
{# }#}
{# });#}
{# }#}
function scanBarcode() {
// Implementation for barcode scanning

View File

@ -1,11 +1,10 @@
{% extends "base.html" %}
{% load static %}
{% load allauth %}
{% load static humanize %}
{% block title %}Inventory Items - Inventory Management{% endblock %}
{% block css %}
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
{% endblock %}
@ -39,71 +38,81 @@
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
</div>
</div>
<div class="panel-body">
<!-- Filters and Search -->
<div class="row mb-3">
<!-- Filters and Search (GET form) -->
<form id="filter-form" method="get" class="row g-3 mb-3 align-items-end">
<div class="col-md-3">
<label class="form-label">Search Items</label>
<div class="input-group">
<input type="text" class="form-control" id="search-input" placeholder="Item name, code, or description...">
<button class="btn btn-outline-secondary" type="button" id="search-btn">
<input type="text" class="form-control" name="q" id="search-input" value="{{ request.GET.q|default_if_none:'' }}" placeholder="Item name, code, or description...">
<button class="btn btn-outline-secondary" type="submit" id="search-btn">
<i class="fa fa-search"></i>
</button>
</div>
</div>
<div class="col-md-2">
<label class="form-label">Category</label>
<select class="form-select" id="category-filter">
<option value="">All Categories</option>
<option value="MEDICAL_SUPPLIES">Medical Supplies</option>
<option value="PHARMACEUTICALS">Pharmaceuticals</option>
<option value="SURGICAL_INSTRUMENTS">Surgical Instruments</option>
<option value="DIAGNOSTIC_EQUIPMENT">Diagnostic Equipment</option>
<option value="PATIENT_CARE">Patient Care Equipment</option>
<option value="LABORATORY_SUPPLIES">Laboratory Supplies</option>
<option value="RADIOLOGY_SUPPLIES">Radiology Supplies</option>
<option value="OTHER">Other</option>
<select class="form-select" name="category" id="category-filter">
<option value="" {% if not request.GET.category %}selected{% endif %}>All Categories</option>
<option value="MEDICAL_SUPPLIES" {% if request.GET.category == 'MEDICAL_SUPPLIES' %}selected{% endif %}>Medical Supplies</option>
<option value="PHARMACEUTICALS" {% if request.GET.category == 'PHARMACEUTICALS' %}selected{% endif %}>Pharmaceuticals</option>
<option value="SURGICAL_INSTRUMENTS" {% if request.GET.category == 'SURGICAL_INSTRUMENTS' %}selected{% endif %}>Surgical Instruments</option>
<option value="DIAGNOSTIC_EQUIPMENT" {% if request.GET.category == 'DIAGNOSTIC_EQUIPMENT' %}selected{% endif %}>Diagnostic Equipment</option>
<option value="PATIENT_CARE" {% if request.GET.category == 'PATIENT_CARE' %}selected{% endif %}>Patient Care Equipment</option>
<option value="LABORATORY_SUPPLIES" {% if request.GET.category == 'LABORATORY_SUPPLIES' %}selected{% endif %}>Laboratory Supplies</option>
<option value="RADIOLOGY_SUPPLIES" {% if request.GET.category == 'RADIOLOGY_SUPPLIES' %}selected{% endif %}>Radiology Supplies</option>
<option value="OTHER" {% if request.GET.category == 'OTHER' %}selected{% endif %}>Other</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Item Type</label>
<select class="form-select" id="type-filter">
<option value="">All Types</option>
<option value="CONSUMABLE">Consumable</option>
<option value="REUSABLE">Reusable</option>
<option value="EQUIPMENT">Equipment</option>
<option value="MEDICATION">Medication</option>
<option value="DEVICE">Medical Device</option>
<select class="form-select" name="item_type" id="type-filter">
<option value="" {% if not request.GET.item_type %}selected{% endif %}>All Types</option>
<option value="CONSUMABLE" {% if request.GET.item_type == 'CONSUMABLE' %}selected{% endif %}>Consumable</option>
<option value="REUSABLE" {% if request.GET.item_type == 'REUSABLE' %}selected{% endif %}>Reusable</option>
<option value="EQUIPMENT" {% if request.GET.item_type == 'EQUIPMENT' %}selected{% endif %}>Equipment</option>
<option value="MEDICATION" {% if request.GET.item_type == 'MEDICATION' %}selected{% endif %}>Medication</option>
<option value="DEVICE" {% if request.GET.item_type == 'DEVICE' %}selected{% endif %}>Medical Device</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Stock Status</label>
<select class="form-select" id="stock-filter">
<option value="">All Items</option>
<option value="in_stock">In Stock</option>
<option value="low_stock">Low Stock</option>
<option value="out_of_stock">Out of Stock</option>
<option value="needs_reorder">Needs Reorder</option>
<select class="form-select" name="stock_status" id="stock-filter">
<option value="" {% if not request.GET.stock_status %}selected{% endif %}>All Items</option>
<option value="in_stock" {% if request.GET.stock_status == 'in_stock' %}selected{% endif %}>In Stock</option>
<option value="low_stock" {% if request.GET.stock_status == 'low_stock' %}selected{% endif %}>Low Stock</option>
<option value="out_of_stock" {% if request.GET.stock_status == 'out_of_stock' %}selected{% endif %}>Out of Stock</option>
<option value="needs_reorder" {% if request.GET.stock_status == 'needs_reorder' %}selected{% endif %}>Needs Reorder</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Manufacturer</label>
<select class="form-select select2" id="manufacturer-filter">
<select class="form-select select2" name="manufacturer" id="manufacturer-filter" data-current="{{ request.GET.manufacturer|default_if_none:'' }}">
<option value="">All Manufacturers</option>
<!-- Options will be loaded dynamically -->
{% if manufacturers %}
{% for m in manufacturers %}
<option value="{{ m.id }}" {% if request.GET.manufacturer == m.id|stringformat:'s' %}selected{% endif %}>
{{ m.name }}
</option>
{% endfor %}
{% endif %}
</select>
</div>
</div>
</form>
<!-- Quick Stats -->
<div class="row mb-4">
<div class="col-lg-3 col-md-6 mb-3">
<div class="card border-0 bg-primary text-white">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="card-body d-flex align-items-center">
<div class="flex-fill">
<div class="fs-10px text-white-transparent-5 mb-1">TOTAL ITEMS</div>
<div class="fs-18px fw-900 text-white" id="total-items">-</div>
<div class="fs-18px fw-900 text-white" id="total-items">{{ stats.total_items|default:"-" }}</div>
</div>
<div class="w-50px h-50px bg-white-transparent-2 rounded-circle d-flex align-items-center justify-content-center">
<i class="fa fa-boxes fa-lg text-white"></i>
@ -111,14 +120,12 @@
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<div class="card border-0 bg-warning text-white">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="card-body d-flex align-items-center">
<div class="flex-fill">
<div class="fs-10px text-white-transparent-5 mb-1">LOW STOCK ITEMS</div>
<div class="fs-18px fw-900 text-white" id="low-stock-count">-</div>
<div class="fs-18px fw-900 text-white" id="low-stock-count">{{ stats.low_stock_count|default:"-" }}</div>
</div>
<div class="w-50px h-50px bg-white-transparent-2 rounded-circle d-flex align-items-center justify-content-center">
<i class="fa fa-exclamation-triangle fa-lg text-white"></i>
@ -126,14 +133,12 @@
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<div class="card border-0 bg-danger text-white">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="card-body d-flex align-items-center">
<div class="flex-fill">
<div class="fs-10px text-white-transparent-5 mb-1">OUT OF STOCK</div>
<div class="fs-18px fw-900 text-white" id="out-of-stock-count">-</div>
<div class="fs-18px fw-900 text-white" id="out-of-stock-count">{{ stats.out_of_stock_count|default:"-" }}</div>
</div>
<div class="w-50px h-50px bg-white-transparent-2 rounded-circle d-flex align-items-center justify-content-center">
<i class="fa fa-times-circle fa-lg text-white"></i>
@ -141,14 +146,14 @@
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<div class="card border-0 bg-success text-white">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="card-body d-flex align-items-center">
<div class="flex-fill">
<div class="fs-10px text-white-transparent-5 mb-1">TOTAL VALUE</div>
<div class="fs-18px fw-900 text-white" id="total-value">-</div>
<div class="fs-18px fw-900 text-white" id="total-value">
{% if stats.total_value %}${{ stats.total_value|floatformat:2|intcomma }}{% else %}-{% endif %}
</div>
</div>
<div class="w-50px h-50px bg-white-transparent-2 rounded-circle d-flex align-items-center justify-content-center">
<i class="fa fa-dollar-sign fa-lg text-white"></i>
@ -157,7 +162,6 @@
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="d-flex justify-content-between align-items-center mb-3">
@ -170,49 +174,49 @@
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="bulkUpdateCategory()">
<li><a class="dropdown-item" href="#" id="bulk-update-category">
<i class="fa fa-tag me-2"></i>Update Category
</a></li>
<li><a class="dropdown-item" href="#" onclick="bulkUpdateSupplier()">
<li><a class="dropdown-item" href="#" id="bulk-update-supplier">
<i class="fa fa-truck me-2"></i>Update Supplier
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="bulkExport()">
<i class="fa fa-download me-2"></i>Export Selected
</a></li>
<li><a class="dropdown-item text-danger" href="#" onclick="bulkDeactivate()">
{# <li><a class="dropdown-item" href="{% url 'inventory:item_export' %}?{{ request.GET.urlencode }}">#}
{# <i class="fa fa-download me-2"></i>Export Current View#}
{# </a></li>#}
<li><a class="dropdown-item text-danger" href="#" id="bulk-deactivate">
<i class="fa fa-ban me-2"></i>Deactivate Selected
</a></li>
</ul>
</div>
<div class="btn-group">
<button type="button" class="btn btn-outline-secondary" onclick="refreshTable()">
<a class="btn btn-outline-secondary" href="?{{ request.GET.urlencode }}">
<i class="fa fa-refresh me-2"></i>Refresh
</button>
</a>
<button type="button" class="btn btn-outline-secondary" onclick="showStockAlerts()">
<i class="fa fa-bell me-2"></i>Stock Alerts
</button>
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
<i class="fa fa-download me-2"></i>Export
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="exportData('csv')">
<i class="fa fa-file-csv me-2"></i>Export as CSV
</a></li>
<li><a class="dropdown-item" href="#" onclick="exportData('excel')">
<i class="fa fa-file-excel me-2"></i>Export as Excel
</a></li>
<li><a class="dropdown-item" href="#" onclick="exportData('pdf')">
<i class="fa fa-file-pdf me-2"></i>Export as PDF
</a></li>
</ul>
{# <ul class="dropdown-menu">#}
{# <li><a class="dropdown-item" href="{% url 'inventory:item_export' %}?format=csv&{{ request.GET.urlencode }}">#}
{# <i class="fa fa-file-csv me-2"></i>Export as CSV#}
{# </a></li>#}
{# <li><a class="dropdown-item" href="{% url 'inventory:item_export' %}?format=excel&{{ request.GET.urlencode }}">#}
{# <i class="fa fa-file-excel me-2"></i>Export as Excel#}
{# </a></li>#}
{# <li><a class="dropdown-item" href="{% url 'inventory:item_export' %}?format=pdf&{{ request.GET.urlencode }}">#}
{# <i class="fa fa-file-pdf me-2"></i>Export as PDF#}
{# </a></li>#}
{# </ul>#}
</div>
</div>
<!-- Items Table -->
<!-- Items Table (regular table) -->
<div class="table-responsive">
<table id="items-table" class="table table-striped table-bordered align-middle">
<table class="table table-striped table-bordered align-middle">
<thead>
<tr>
<th width="30">
@ -229,13 +233,98 @@
<th>Unit Cost</th>
<th>Total Value</th>
<th>Status</th>
<th width="120">Actions</th>
<th width="140">Actions</th>
</tr>
</thead>
<tbody>
<!-- Data will be loaded via AJAX -->
{% if items %}
{% for item in items %}
<tr>
<td>
<div class="form-check">
<input class="form-check-input row-checkbox" type="checkbox" value="{{ item.id }}">
</div>
</td>
<td><div class="fw-bold">{{ item.item_code }}</div></td>
<td>
<div class="fw-bold">{{ item.item_name }}</div>
{% if item.description %}<div class="text-muted small">{{ item.description|truncatechars:80 }}</div>{% endif %}
</td>
<td>
{% if item.get_category_display %}
<span class="badge bg-secondary">{{ item.get_category_display }}</span>
{% else %}-{% endif %}
</td>
<td>
<span class="badge
bg-{% if item.item_type == 'CONSUMABLE' %}primary
{% elif item.item_type == 'REUSABLE' %}success
{% elif item.item_type == 'EQUIPMENT' %}info
{% elif item.item_type == 'MEDICATION' %}warning
{% elif item.item_type == 'DEVICE' %}danger
{% elif item.item_type == 'REUSABLE' %}success
{% else %}secondary
{% endif %}">
{{ item.get_item_type_display }}</span>
</td>
<td>{{ item.manufacturer.name|default:item.manufacturer|default:"-" }}</td>
<td>
{% with cs=item.current_stock|default:0 rp=item.reorder_point|default:0 %}
{% if cs|floatformat:0 <= 0 %}
<span class="text-danger fw-bold">{{ cs }} {{ item.get_unit_of_measure_display }}</span>
{% elif cs|floatformat:0 <= rp|floatformat:0 %}
<span class="text-warning fw-bold">{{ cs }} {{ item.get_unit_of_measure_display }}</span>
{% else %}
<span>{{ cs }} {{ item.get_unit_of_measure_display }}</span>
{% endif %}
{% endwith %}
</td>
<td>${{ item.unit_cost|default:0|floatformat:2|intcomma }}</td>
<td>
{% with tv=item.total_value|default:item.current_stock|default:0|floatformat:2 %}
{% if item.total_value %}
${{ item.total_value|floatformat:2|intcomma }}
{% else %}
${{ item.current_stock|default:0|floatformat:0|add:"0"|floatformat:0|intcomma }}
{% endif %}
{% endwith %}
</td>
<td>
{# {% if item.get_stock_status_display %}#}
{# {% with s=item.stock_status %}#}
{# {% if s == 'in_stock' %}{% setvar 'success' as ss %}{% elif s == 'low_stock' %}{% setvar 'warning' as ss %}#}
{# {% elif s == 'out_of_stock' %}{% setvar 'danger' as ss %}{% elif s == 'needs_reorder' %}{% setvar 'info' as ss %}#}
{# {% else %}{% setvar 'secondary' as ss %}{% endif %}#}
{# <span class="badge bg-{{ ss|default:'secondary' }}">{{ item.get_stock_status_display }}</span>#}
{# {% endwith %}#}
{# {% else %}-{% endif %}#}
</td>
<td>
<div class="btn-group btn-group-sm">
<a class="btn btn-outline-primary" href="{% url 'inventory:item_detail' item.id %}" title="View">
<i class="fa fa-eye"></i>
</a>
<a class="btn btn-outline-secondary" href="{% url 'inventory:item_update' item.id %}" title="Edit">
<i class="fa fa-edit"></i>
</a>
<a class="btn btn-outline-info" href="{% url 'inventory:stock_list' %}?item={{ item.id }}" title="Stock">
<i class="fa fa-boxes"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="11" class="text-center">No items found.</td>
</tr>
{% endif %}
</tbody>
</table>
{% if is_paginated %}
{% include 'partial/pagination.html' %}
{% endif %}
</div>
</div>
</div>
@ -275,9 +364,7 @@
</button>
</div>
</div>
<div id="stock-alerts-content">
<!-- Content will be loaded dynamically -->
</div>
<div id="stock-alerts-content"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
@ -295,9 +382,7 @@
<h5 class="modal-title">Item Quick View</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="item-quick-view-content">
<!-- Content will be loaded dynamically -->
</div>
<div class="modal-body" id="item-quick-view-content"></div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<a href="#" class="btn btn-primary" id="view-full-item">View Full Details</a>
@ -308,323 +393,97 @@
{% endblock %}
{% block js %}
<script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
<script src="{% static 'plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
<script>
$(document).ready(function() {
var table;
(function() {
// Select2
$('.select2').select2({ theme: 'bootstrap-5', width: '100%' });
// Initialize DataTable
table = $('#items-table').DataTable({
processing: true,
serverSide: true,
ajax: {
url: '',
data: function(d) {
d.search_value = $('#search-input').val();
d.category = $('#category-filter').val();
d.item_type = $('#type-filter').val();
d.stock_status = $('#stock-filter').val();
d.manufacturer = $('#manufacturer-filter').val();
}
},
columns: [
{
data: 'id',
orderable: false,
render: function(data, type, row) {
return '<div class="form-check"><input class="form-check-input item-checkbox" type="checkbox" value="' + data + '"></div>';
}
},
{
data: 'item_code',
render: function(data, type, row) {
return '<div class="fw-bold">' + data + '</div>';
}
},
{
data: 'item_name',
render: function(data, type, row) {
return '<div>' +
'<div class="fw-bold">' + data + '</div>' +
(row.description ? '<div class="text-muted small">' + row.description.substring(0, 50) + '...</div>' : '') +
'</div>';
}
},
{
data: 'category',
render: function(data, type, row) {
return '<span class="badge bg-secondary">' + row.category_display + '</span>';
}
},
{
data: 'item_type',
render: function(data, type, row) {
var colors = {
'CONSUMABLE': 'primary',
'REUSABLE': 'success',
'EQUIPMENT': 'info',
'MEDICATION': 'warning',
'DEVICE': 'danger'
// Auto-submit filters on change (except search which uses the button, but still submit on Enter)
$('#category-filter, #type-filter, #stock-filter, #manufacturer-filter').on('change', function() {
document.getElementById('filter-form').submit();
});
// Select all / individual
const selectAll = document.getElementById('select-all');
const toggleBulk = () => {
const checkedCount = document.querySelectorAll('.row-checkbox:checked').length;
document.getElementById('bulk-actions-btn').disabled = checkedCount === 0;
document.getElementById('bulk-dropdown').disabled = checkedCount === 0;
};
return '<span class="badge bg-' + (colors[data] || 'secondary') + '">' + row.item_type_display + '</span>';
}
},
{
data: 'manufacturer',
render: function(data, type, row) {
return data || '<span class="text-muted">-</span>';
}
},
{
data: 'current_stock',
render: function(data, type, row) {
var className = '';
if (data <= 0) {
className = 'text-danger fw-bold';
} else if (data <= row.reorder_point) {
className = 'text-warning fw-bold';
}
return '<span class="' + className + '">' + data + ' ' + row.unit_of_measure_display + '</span>';
}
},
{
data: 'unit_cost',
render: function(data, type, row) {
return '$' + parseFloat(data).toFixed(2);
}
},
{
data: 'total_value',
render: function(data, type, row) {
return '$' + parseFloat(data).toFixed(2);
}
},
{
data: 'stock_status',
render: function(data, type, row) {
var badges = {
'in_stock': 'bg-success',
'low_stock': 'bg-warning',
'out_of_stock': 'bg-danger',
'needs_reorder': 'bg-info'
};
return '<span class="badge ' + (badges[data] || 'bg-secondary') + '">' + row.stock_status_display + '</span>';
}
},
{
data: 'id',
orderable: false,
render: function(data, type, row) {
return '<div class="btn-group btn-group-sm">' +
'<button class="btn btn-outline-primary" onclick="viewItem(' + data + ')" title="View">' +
'<i class="fa fa-eye"></i></button>' +
'<button class="btn btn-outline-secondary" onclick="editItem(' + data + ')" title="Edit">' +
'<i class="fa fa-edit"></i></button>' +
'<button class="btn btn-outline-info" onclick="viewStock(' + data + ')" title="Stock">' +
'<i class="fa fa-boxes"></i></button>' +
'</div>';
}
}
],
order: [[1, 'asc']],
pageLength: 25,
responsive: true,
language: {
processing: '<div class="d-flex justify-content-center"><div class="spinner-border" role="status"></div></div>'
}
});
// Initialize Select2
$('.select2').select2({
theme: 'bootstrap-5',
width: '100%'
});
// Load filter options and stats
loadFilterOptions();
loadStats();
// Filter event handlers
$('#search-input, #category-filter, #type-filter, #stock-filter, #manufacturer-filter').on('change keyup', function() {
table.draw();
});
// Select all checkbox
$('#select-all').on('change', function() {
$('.item-checkbox').prop('checked', this.checked);
updateBulkActions();
});
// Individual checkbox change
$(document).on('change', '.item-checkbox', function() {
updateBulkActions();
});
// Auto-refresh every 5 minutes
setInterval(function() {
table.ajax.reload(null, false);
loadStats();
}, 300000);
});
function loadFilterOptions() {
// Load manufacturer options
$.ajax({
url: '',
success: function(data) {
var select = $('#manufacturer-filter');
data.forEach(function(manufacturer) {
select.append('<option value="' + manufacturer + '">' + manufacturer + '</option>');
if (selectAll) {
selectAll.addEventListener('change', function() {
document.querySelectorAll('.row-checkbox').forEach(cb => cb.checked = selectAll.checked);
toggleBulk();
});
}
document.addEventListener('change', function(e) {
if (e.target.classList && e.target.classList.contains('row-checkbox')) toggleBulk();
});
}
function loadStats() {
$.ajax({
url: '{% url "inventory:inventory_stats" %}',
success: function(data) {
$('#total-items').text(data.total_items);
$('#low-stock-count').text(data.low_stock_count);
$('#out-of-stock-count').text(data.out_of_stock_count);
$('#total-value').text('$' + parseFloat(data.total_value).toLocaleString());
}
// Bulk actions (placeholders unless you wire endpoints)
const getSelectedIds = () => Array.from(document.querySelectorAll('.row-checkbox:checked')).map(x => x.value);
document.getElementById('bulk-update-category').addEventListener('click', function(e) {
e.preventDefault();
const ids = getSelectedIds();
if (!ids.length) { toastr && toastr.warning('Please select items to update'); return; }
toastr && toastr.info('Bulk category update functionality will be implemented');
});
}
function updateBulkActions() {
var checkedCount = $('.item-checkbox:checked').length;
$('#bulk-actions-btn, #bulk-dropdown').prop('disabled', checkedCount === 0);
}
document.getElementById('bulk-update-supplier').addEventListener('click', function(e) {
e.preventDefault();
const ids = getSelectedIds();
if (!ids.length) { toastr && toastr.warning('Please select items to update'); return; }
toastr && toastr.info('Bulk supplier update functionality will be implemented');
});
function viewItem(itemId) {
window.location.href = '{% url "inventory:item_detail" 0 %}'.replace('0', itemId);
}
function editItem(itemId) {
window.location.href = '{% url "inventory:item_update" 0 %}'.replace('0', itemId);
}
function viewStock(itemId) {
window.location.href = '{% url "inventory:stock_list" %}?item=' + itemId;
}
function bulkUpdateCategory() {
var selectedIds = $('.item-checkbox:checked').map(function() {
return this.value;
}).get();
if (selectedIds.length === 0) {
toastr.warning('Please select items to update');
return;
}
// Implementation for bulk category update
toastr.info('Bulk category update functionality will be implemented');
}
function bulkUpdateSupplier() {
var selectedIds = $('.item-checkbox:checked').map(function() {
return this.value;
}).get();
if (selectedIds.length === 0) {
toastr.warning('Please select items to update');
return;
}
// Implementation for bulk supplier update
toastr.info('Bulk supplier update functionality will be implemented');
}
{#function bulkExport() {#}
{# var selectedIds = $('.item-checkbox:checked').map(function() {#}
{# return this.value;#}
{# }).get();#}
{#document.getElementById('bulk-deactivate').addEventListener('click', function(e) {#}
{# e.preventDefault();#}
{# const ids = getSelectedIds();#}
{# if (!ids.length) { toastr && toastr.warning('Please select items to deactivate'); return; }#}
{# if (!confirm('Are you sure you want to deactivate ' + ids.length + ' item(s)?')) return;#}
{##}
{# if (selectedIds.length === 0) {#}
{# toastr.warning('Please select items to export');#}
{# return;#}
{# }#}
{# #}
{# window.location.href = '{% url "inventory:item_export" %}?ids=' + selectedIds.join(',');#}
{# }#}
function bulkDeactivate() {
var selectedIds = $('.item-checkbox:checked').map(function() {
return this.value;
}).get();
if (selectedIds.length === 0) {
toastr.warning('Please select items to deactivate');
return;
}
if (confirm('Are you sure you want to deactivate ' + selectedIds.length + ' item(s)?')) {
$.ajax({
url: '',
method: 'POST',
data: {
'item_ids': selectedIds,
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success: function(response) {
if (response.success) {
toastr.success(response.message);
$('#items-table').DataTable().ajax.reload();
$('#select-all').prop('checked', false);
updateBulkActions();
} else {
toastr.error('Failed to deactivate items');
}
},
error: function() {
toastr.error('An error occurred during bulk deactivation');
}
});
}
}
{#function exportData(format) {#}
{# window.location.href = '{% url "inventory:item_export" %}?format=' + format;#}
{# }#}
function refreshTable() {
$('#items-table').DataTable().ajax.reload();
loadStats();
toastr.info('Table refreshed');
}
function showStockAlerts() {
$('#stockAlertsModal').modal('show');
loadStockAlerts();
}
{#function loadStockAlerts() {#}
{# $.ajax({#}
{# url: '{% url "inventory:stock_alerts_api" %}',#}
{# success: function(data) {#}
{# $('#stock-alerts-content').html(data.html);#}
{# },#}
{# error: function() {#}
{# toastr.error('Failed to load stock alerts');#}
{# fetch("{% url 'inventory:item_bulk_deactivate' %}", {#}
{# method: 'POST',#}
{# headers: { 'Content-Type': 'application/json', 'X-CSRFToken': '{{ csrf_token }}' },#}
{# body: JSON.stringify({ item_ids: ids })#}
{# }).then(r => r.json()).then(data => {#}
{# if (data.success) {#}
{# toastr && toastr.success(data.message || 'Items deactivated');#}
{# window.location.reload();#}
{# } else {#}
{# toastr && toastr.error(data.message || 'Failed to deactivate items');#}
{# }#}
{# }).catch(() => toastr && toastr.error('An error occurred during bulk deactivation'));#}
{# });#}
{# } #}
{#function generateReorderReport() {#}
{# window.open('{% url "inventory:reorder_report" %}', '_blank');#}
{# }#}
function createPurchaseOrders() {
// Implementation for creating purchase orders from alerts
toastr.info('Purchase order creation functionality will be implemented');
// Stats (if you still want to refresh via API; otherwise you can remove this block)
function loadStats() {
fetch('{% url "inventory:inventory_stats" %}')
.then(r => r.json())
.then(data => {
document.getElementById('total-items').textContent = data.total_items ?? '-';
document.getElementById('low-stock-count').textContent = data.low_stock_count ?? '-';
document.getElementById('out-of-stock-count').textContent = data.out_of_stock_count ?? '-';
document.getElementById('total-value').textContent = data.total_value ? ('$' + Number(data.total_value).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })) : '-';
}).catch(() => {});
}
// Optional periodic refresh
// setInterval(loadStats, 300000);
{#window.showStockAlerts = function() {#}
{# const modal = new bootstrap.Modal(document.getElementById('stockAlertsModal'));#}
{# modal.show();#}
{# // TODO: fetch and render alerts if you have an API#}
{# // fetch('{% url "inventory:stock_alerts_api" %}').then(r => r.json()).then(...);#}
{# };#}
window.createPurchaseOrders = function() {
toastr && toastr.info('Purchase order creation functionality will be implemented');
};
})();
</script>
{% endblock %}

View File

@ -465,10 +465,10 @@
{% endif %}
</td>
<td>
<a class="me-3" href="{% url 'inventory:stock_detail' stock.id %}">
<a class="btn btn-xs btn-outline-primary me-1" href="{% url 'inventory:stock_detail' stock.id %}">
<i class="fas fa-eye"></i>
</a>
<a class="me-3" href="{% url 'inventory:stock_update' stock.id %}">
<a class="btn btn-xs btn-outline-secondary" href="{% url 'inventory:stock_update' stock.id %}">
<i class="fas fa-edit"></i>
</a>
</td>

View File

@ -301,44 +301,7 @@
<!-- Pagination -->
{% if is_paginated %}
<div class="pagination-wrapper">
<div class="row">
<div class="col-sm-12 col-md-5">
<div class="dataTables_info">
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ paginator.count }} entries
</div>
</div>
<div class="col-sm-12 col-md-7">
<div class="dataTables_paginate">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="paginate_button page-item previous">
<a href="?page={{ page_obj.previous_page_number }}" class="page-link">Previous</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="paginate_button page-item active">
<a href="#" class="page-link">{{ num }}</a>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="paginate_button page-item">
<a href="?page={{ num }}" class="page-link">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="paginate_button page-item next">
<a href="?page={{ page_obj.next_page_number }}" class="page-link">Next</a>
</li>
{% endif %}
</ul>
</div>
</div>
</div>
</div>
{% include 'partial/pagination.html' %}
{% endif %}
</div>
</div>