566 lines
24 KiB
HTML
566 lines
24 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}Inventory Stock - 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" />
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- BEGIN breadcrumb -->
|
|
<ol class="breadcrumb float-xl-end">
|
|
<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 active">Stock Management</li>
|
|
</ol>
|
|
<!-- END breadcrumb -->
|
|
|
|
<!-- BEGIN page-header -->
|
|
<h1 class="page-header">
|
|
Inventory Stock
|
|
<small>Stock Level Management & Tracking</small>
|
|
</h1>
|
|
<!-- END page-header -->
|
|
|
|
<!-- BEGIN panel -->
|
|
<div class="panel panel-inverse">
|
|
<div class="panel-heading">
|
|
<h4 class="panel-title">Stock Management</h4>
|
|
<div class="panel-heading-btn">
|
|
<a href="{% url 'inventory:stock_create' %}" class="btn btn-xs btn-warning me-2">
|
|
<i class="fa fa-edit"></i> Stock Adjustment
|
|
</a>
|
|
<a href="{% url 'inventory:stock_create' %}" class="btn btn-xs btn-info me-2">
|
|
<i class="fa fa-clipboard-list"></i> Physical Count
|
|
</a>
|
|
<button class="btn btn-xs btn-primary me-2" data-bs-toggle="modal" data-bs-target="#stock-alerts-modal">
|
|
<i class="fa fa-exclamation-triangle"></i> Stock Alerts
|
|
</button>
|
|
<button class="btn btn-xs btn-success me-2" onclick="exportStock()">
|
|
<i class="fa fa-download"></i> Export
|
|
</button>
|
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
|
</div>
|
|
</div>
|
|
<div class="panel-body">
|
|
<!-- Stock Statistics -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card border-primary">
|
|
<div class="card-body text-center">
|
|
<div class="fs-24px fw-bold text-primary">{{ total_items }}</div>
|
|
<div class="small text-muted">Total Items</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-success">
|
|
<div class="card-body text-center">
|
|
<div class="fs-24px fw-bold text-success">{{ in_stock_items }}</div>
|
|
<div class="small text-muted">In Stock</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-warning">
|
|
<div class="card-body text-center">
|
|
<div class="fs-24px fw-bold text-warning">{{ low_stock_items }}</div>
|
|
<div class="small text-muted">Low Stock</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-danger">
|
|
<div class="card-body text-center">
|
|
<div class="fs-24px fw-bold text-danger">{{ out_of_stock_items }}</div>
|
|
<div class="small text-muted">Out of Stock</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Filters -->
|
|
<div class="row mb-3">
|
|
<div class="col-md-12">
|
|
<div class="btn-group me-2" role="group">
|
|
<button type="button" class="btn btn-outline-primary active" onclick="filterByStock('all')">All Stock</button>
|
|
<button type="button" class="btn btn-outline-primary" onclick="filterByStock('in_stock')">In Stock</button>
|
|
<button type="button" class="btn btn-outline-primary" onclick="filterByStock('low_stock')">Low Stock</button>
|
|
<button type="button" class="btn btn-outline-primary" onclick="filterByStock('out_of_stock')">Out of Stock</button>
|
|
<button type="button" class="btn btn-outline-primary" onclick="filterByStock('expired')">Expired</button>
|
|
</div>
|
|
|
|
<div class="btn-group me-2" role="group">
|
|
<button type="button" class="btn btn-outline-secondary active" onclick="filterByLocation('all')">All Locations</button>
|
|
{% for location in locations %}
|
|
<button type="button" class="btn btn-outline-secondary" onclick="filterByLocation('{{ location.id }}')">{{ location.name }}</button>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="btn-group" role="group">
|
|
<button type="button" class="btn btn-outline-info active" onclick="filterByCategory('all')">All Categories</button>
|
|
{% for category in categories %}
|
|
<button type="button" class="btn btn-outline-info" onclick="filterByCategory('{{ category }}')">{{ category }}</button>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search and Actions -->
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="search-input" placeholder="Search items, lot numbers, locations...">
|
|
<button class="btn btn-outline-secondary" type="button" onclick="searchStock()">
|
|
<i class="fa fa-search"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6 text-end">
|
|
<div class="form-check form-switch d-inline-block me-3">
|
|
<input class="form-check-input" type="checkbox" id="show-expired">
|
|
<label class="form-check-label" for="show-expired">
|
|
Show expired items
|
|
</label>
|
|
</div>
|
|
<div class="form-check form-switch d-inline-block">
|
|
<input class="form-check-input" type="checkbox" id="auto-refresh" checked>
|
|
<label class="form-check-label" for="auto-refresh">
|
|
Auto refresh
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stock Table -->
|
|
<div class="table-responsive">
|
|
<table id="stock-table" class="table table-striped table-bordered align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="select-all">
|
|
</div>
|
|
</th>
|
|
<th>Item</th>
|
|
<th>Location</th>
|
|
<th>Lot Number</th>
|
|
<th>Current Stock</th>
|
|
<th>Available</th>
|
|
<th>Expiry Date</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for stock in object_list %}
|
|
<tr data-stock-status="{{ stock.stock_status }}" data-location="{{ stock.location.id }}" data-category="{{ stock.inventory_item.category }}">
|
|
<td>
|
|
<div class="form-check">
|
|
<input class="form-check-input row-checkbox" type="checkbox" value="{{ stock.stock_id }}">
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
{% if stock.inventory_item.image %}
|
|
<img src="{{ stock.inventory_item.image.url }}" alt="{{ stock.inventory_item.name }}" class="rounded me-2" width="40" height="40">
|
|
{% else %}
|
|
<div class="bg-light rounded me-2 d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
|
<i class="fa fa-box text-muted"></i>
|
|
</div>
|
|
{% endif %}
|
|
<div>
|
|
<div class="fw-bold">{{ stock.inventory_item.item_name }}</div>
|
|
<div class="small text-muted">{{ stock.inventory_item.item_code }}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="fw-bold">{{ stock.location.name }}</div>
|
|
<div class="small text-muted">{{ stock.location.location_type }}</div>
|
|
</td>
|
|
<td>
|
|
<code class="small">{{ stock.lot_number|default:"--" }}</code>
|
|
</td>
|
|
<td>
|
|
<div class="fw-bold">{{ stock.quantity_on_hand }}</div>
|
|
<div class="small text-muted">{{ stock.inventory_item.unit_of_measure }}</div>
|
|
</td>
|
|
<td>
|
|
<div class="fw-bold">{{ stock.quantity_available }}</div>
|
|
<div class="small text-muted">Available</div>
|
|
</td>
|
|
<td>
|
|
{% if stock.expiration_date %}
|
|
<div class="{% if stock.is_expired %}text-danger{% elif stock.is_expiring_soon %}text-warning{% endif %}">
|
|
{{ stock.expiration_date|date:"M d, Y" }}
|
|
</div>
|
|
{% if stock.is_expired %}
|
|
<div class="small text-danger">Expired</div>
|
|
{% elif stock.is_expiring_soon %}
|
|
<div class="small text-warning">Expiring Soon</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<span class="text-muted">No expiry</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if stock.quantity_available >= stock.inventory_item.reorder_point %}
|
|
<span class="badge bg-success">AVAILABLE</span><br>
|
|
<small class="text-muted"> reorder point {{ stock.inventory_item.reorder_point }}</small>
|
|
{% elif stock.quantity_available <= stock.inventory_item.reorder_point %}
|
|
<span class="badge bg-warning">LOW STOCK</span><br>
|
|
<small class="text-muted"> reorder point {{ stock.inventory_item.reorder_point }}</small>
|
|
{% elif stock.quantity_available == 0 %}
|
|
<span class="badge bg-danger">OUT OF STOCK</span><br>
|
|
<small class="text-muted"> reorder point {{ stock.inventory_item.reorder_point }}</small>
|
|
|
|
{% else %}
|
|
<span class="badge bg-secondary">{{ stock.inventory_item.reorder_point }}</span>
|
|
{% endif %}
|
|
|
|
{% if stock.is_reserved %}
|
|
<div class="small mt-1">
|
|
<span class="badge bg-info badge-sm">{{ stock.quantity_reserved }} Reserved</span>
|
|
</div>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<a href="{% url 'inventory:stock_detail' stock.pk %}" class="btn btn-outline-primary btn-sm" title="View Details">
|
|
<i class="fa fa-eye"></i>
|
|
</a>
|
|
<button class="btn btn-outline-success btn-sm" onclick="quickAdjust({{ stock.id }})" title="Quick Adjust">
|
|
<i class="fa fa-edit"></i>
|
|
</button>
|
|
<button class="btn btn-outline-warning btn-sm" onclick="moveStock({{ stock.id }})" title="Move Stock">
|
|
<i class="fa fa-arrows-alt"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="9" class="text-center text-muted py-4">
|
|
<i class="fa fa-boxes fa-2x mb-2"></i>
|
|
<div>No stock records found</div>
|
|
<a href="{% url 'inventory:item_create' %}" class="btn btn-primary mt-2">
|
|
<i class="fa fa-plus me-2"></i>Add First Item
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Bulk Actions -->
|
|
<div id="bulk-actions" class="mt-3" style="display: none;">
|
|
<div class="alert alert-info">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<strong><span id="selected-count">0</span></strong> item(s) selected
|
|
</div>
|
|
<div>
|
|
<button class="btn btn-sm btn-warning me-2" onclick="bulkAdjust()">
|
|
<i class="fa fa-edit me-1"></i>Bulk Adjust
|
|
</button>
|
|
<button class="btn btn-sm btn-info me-2" onclick="bulkMove()">
|
|
<i class="fa fa-arrows-alt me-1"></i>Move Stock
|
|
</button>
|
|
<button class="btn btn-sm btn-success me-2" onclick="bulkReorder()">
|
|
<i class="fa fa-shopping-cart me-1"></i>Create Reorder
|
|
</button>
|
|
<button class="btn btn-sm btn-secondary" onclick="clearSelection()">
|
|
<i class="fa fa-times me-1"></i>Clear Selection
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- END panel -->
|
|
|
|
<!-- Stock Alerts Modal -->
|
|
<div class="modal fade" id="stock-alerts-modal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Stock Alerts</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
{% include 'inventory/stock_alerts.html' %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Adjust Modal -->
|
|
<div class="modal fade" id="quick-adjust-modal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Quick Stock Adjustment</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form id="quick-adjust-form">
|
|
<div class="modal-body">
|
|
{% csrf_token %}
|
|
<input type="hidden" id="adjust-stock-id" name="stock_id">
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Adjustment Type</label>
|
|
<select class="form-select" name="adjustment_type" required>
|
|
<option value="">Select type...</option>
|
|
<option value="ADD">Add Stock</option>
|
|
<option value="REMOVE">Remove Stock</option>
|
|
<option value="SET">Set Quantity</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Quantity</label>
|
|
<input type="number" class="form-control" name="quantity" min="0" step="0.01" required>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Reason</label>
|
|
<select class="form-select" name="reason" required>
|
|
<option value="">Select reason...</option>
|
|
<option value="RECEIVED">Stock Received</option>
|
|
<option value="DISPENSED">Dispensed</option>
|
|
<option value="EXPIRED">Expired</option>
|
|
<option value="DAMAGED">Damaged</option>
|
|
<option value="LOST">Lost</option>
|
|
<option value="CORRECTION">Correction</option>
|
|
<option value="OTHER">Other</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Notes</label>
|
|
<textarea class="form-control" name="notes" rows="2" placeholder="Optional notes..."></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Apply Adjustment</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% 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/dropzone/src/options.js' %}"></script>
|
|
|
|
<script>
|
|
|
|
$(document).ready(function() {
|
|
// Initialize DataTable
|
|
var table = $('#stock-table').DataTable({
|
|
responsive: true,
|
|
pageLength: 25,
|
|
order: [[1, 'asc']], // Sort by item name
|
|
columnDefs: [
|
|
{ orderable: false, targets: [0, 8] } // Disable sorting for checkbox and actions columns
|
|
]
|
|
});
|
|
|
|
// Select all checkbox
|
|
$('#select-all').on('change', function() {
|
|
$('.row-checkbox').prop('checked', this.checked);
|
|
updateBulkActions();
|
|
});
|
|
|
|
// Individual row checkboxes
|
|
$(document).on('change', '.row-checkbox', function() {
|
|
updateBulkActions();
|
|
|
|
// Update select all checkbox
|
|
var totalCheckboxes = $('.row-checkbox').length;
|
|
var checkedCheckboxes = $('.row-checkbox:checked').length;
|
|
$('#select-all').prop('checked', totalCheckboxes === checkedCheckboxes);
|
|
});
|
|
|
|
function updateBulkActions() {
|
|
var selectedCount = $('.row-checkbox:checked').length;
|
|
$('#selected-count').text(selectedCount);
|
|
|
|
if (selectedCount > 0) {
|
|
$('#bulk-actions').show();
|
|
} else {
|
|
$('#bulk-actions').hide();
|
|
}
|
|
}
|
|
|
|
// Auto refresh
|
|
setInterval(function() {
|
|
if ($('#auto-refresh').is(':checked')) {
|
|
location.reload();
|
|
}
|
|
}, 60000); // Every minute
|
|
|
|
// Quick adjust form submission
|
|
$('#quick-adjust-form').on('submit', function(e) {
|
|
e.preventDefault();
|
|
|
|
$.ajax({
|
|
url: '{% url "inventory:stock_create" %}',
|
|
method: 'POST',
|
|
data: $(this).serialize(),
|
|
success: function(response) {
|
|
if (response.success) {
|
|
toastr.success('Stock adjustment applied successfully');
|
|
$('#quick-adjust-modal').modal('hide');
|
|
location.reload();
|
|
} else {
|
|
toastr.error('Failed to apply adjustment: ' + response.error);
|
|
}
|
|
},
|
|
error: function() {
|
|
toastr.error('An error occurred while applying the adjustment');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
function filterByStock(status) {
|
|
$('.btn-group button').removeClass('active');
|
|
event.target.classList.add('active');
|
|
|
|
if (status === 'all') {
|
|
$('tr[data-stock-status]').show();
|
|
} else {
|
|
$('tr[data-stock-status]').hide();
|
|
$(`tr[data-stock-status="${status.toUpperCase()}"]`).show();
|
|
}
|
|
}
|
|
|
|
function filterByLocation(locationId) {
|
|
$('.btn-group button').removeClass('active');
|
|
event.target.classList.add('active');
|
|
|
|
if (locationId === 'all') {
|
|
$('tr[data-location]').show();
|
|
} else {
|
|
$('tr[data-location]').hide();
|
|
$(`tr[data-location="${locationId}"]`).show();
|
|
}
|
|
}
|
|
|
|
function filterByCategory(category) {
|
|
$('.btn-group button').removeClass('active');
|
|
event.target.classList.add('active');
|
|
|
|
if (category === 'all') {
|
|
$('tr[data-category]').show();
|
|
} else {
|
|
$('tr[data-category]').hide();
|
|
$(`tr[data-category="${category}"]`).show();
|
|
}
|
|
}
|
|
|
|
function searchStock() {
|
|
var searchTerm = $('#search-input').val().toLowerCase();
|
|
|
|
if (searchTerm === '') {
|
|
$('tbody tr').show();
|
|
return;
|
|
}
|
|
|
|
$('tbody tr').each(function() {
|
|
var rowText = $(this).text().toLowerCase();
|
|
if (rowText.includes(searchTerm)) {
|
|
$(this).show();
|
|
} else {
|
|
$(this).hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
function quickAdjust(stockId) {
|
|
$('#adjust-stock-id').val(stockId);
|
|
$('#quick-adjust-modal').modal('show');
|
|
}
|
|
|
|
{#function moveStock(stockId) {#}
|
|
{# // Implement stock movement functionality#}
|
|
{# window.location.href = '{% url "inventory:stock_move" 0 %}'.replace('0', stockId);#}
|
|
{# }#}
|
|
|
|
function bulkAdjust() {
|
|
var selectedIds = $('.row-checkbox:checked').map(function() {
|
|
return this.value;
|
|
}).get();
|
|
|
|
if (selectedIds.length === 0) {
|
|
toastr.warning('Please select items to adjust');
|
|
return;
|
|
}
|
|
|
|
// Redirect to bulk adjustment page
|
|
window.location.href = '?ids=' + selectedIds.join(',');
|
|
}
|
|
|
|
function bulkMove() {
|
|
var selectedIds = $('.row-checkbox:checked').map(function() {
|
|
return this.value;
|
|
}).get();
|
|
|
|
if (selectedIds.length === 0) {
|
|
toastr.warning('Please select items to move');
|
|
return;
|
|
}
|
|
|
|
// Redirect to bulk move page
|
|
window.location.href = '?ids=' + selectedIds.join(',');
|
|
}
|
|
|
|
function bulkReorder() {
|
|
var selectedIds = $('.row-checkbox:checked').map(function() {
|
|
return this.value;
|
|
}).get();
|
|
|
|
if (selectedIds.length === 0) {
|
|
toastr.warning('Please select items to reorder');
|
|
return;
|
|
}
|
|
|
|
// Create purchase order for selected items
|
|
window.location.href = '{% url "inventory:purchase_order_create" %}?stock_ids=' + selectedIds.join(',');
|
|
}
|
|
|
|
function clearSelection() {
|
|
$('.row-checkbox, #select-all').prop('checked', false);
|
|
$('#bulk-actions').hide();
|
|
}
|
|
|
|
{#function exportStock() {#}
|
|
{# window.open('{% url "inventory:stock_export" %}');#}
|
|
{# }#}
|
|
|
|
// Search on enter key
|
|
$('#search-input').on('keypress', function(e) {
|
|
if (e.which === 13) {
|
|
searchStock();
|
|
}
|
|
});
|
|
|
|
// Real-time search
|
|
$('#search-input').on('input', function() {
|
|
searchStock();
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
|