Stock available (#3057)

* Fix display of stock labels

- If 'shipped' or 'installed', don't display 'allocated' flag

* Switch stock item data around

* Add 'available' and 'allocation' information to the StockItem detail page

- Cache some context data to the view renderer

* Stock table now also displays allocation informatoin
This commit is contained in:
Oliver 2022-05-24 15:35:04 +10:00 committed by GitHub
parent 1c6e5f0f20
commit ea9f2f81f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 223 additions and 185 deletions

View File

@ -156,158 +156,7 @@
{% endif %}
</td>
</tr>
{% if item.serialized %}
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Serial Number" %}</td>
<td>
{{ item.serial }}
<div class='btn-group float-right' role='group'>
{% if previous %}
<a class="btn btn-small btn-outline-secondary" aria-label="{% trans 'previous page' %}" href="{% url request.resolver_match.url_name previous.id %}" title='{% trans "Navigate to previous serial number" %}'>
<span class='fas fa-angle-left'></span>
<small>{{ previous.serial }}</small>
</a>
{% endif %}
<a class='btn btn-small btn-outline-secondary text-sm' href='#' id='serial-number-search' title='{% trans "Search for serial number" %}'>
<span class='fas fa-search'></span>
</a>
{% if next %}
<a class="btn btn-small btn-outline-secondary text-sm" aria-label="{% trans 'next page' %}" href="{% url request.resolver_match.url_name next.id %}" title='{% trans "Navigate to next serial number" %}'>
<small>{{ next.serial }}</small>
<span class='fas fa-angle-right'></span>
</a>
{% endif %}
</div>
</td>
</tr>
{% else %}
<tr>
<td></td>
<td>{% trans "Quantity" %}</td>
<td>{% decimal item.quantity %} {% if item.part.units %}{{ item.part.units }}{% endif %}</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</td>
<td>{% stock_status_label item.status %}</td>
</tr>
{% if item.expiry_date %}
<tr>
<td><span class='fas fa-calendar-alt{% if item.is_expired %} icon-red{% endif %}'></span></td>
<td>{% trans "Expiry Date" %}</td>
<td>
{% render_date item.expiry_date %}
{% if item.is_expired %}
<span title='{% blocktrans %}This StockItem expired on {{ item.expiry_date }}{% endblocktrans %}' class='badge rounded-pill bg-danger badge-right'>{% trans "Expired" %}</span>
{% elif item.is_stale %}
<span title='{% blocktrans %}This StockItem expires on {{ item.expiry_date }}{% endblocktrans %}' class='badge rounded-pill bg-warning badge-right'>{% trans "Stale" %}</span>
{% endif %}
</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Last Updated" %}</td>
<td>{{ item.updated }}</td>
</tr>
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Last Stocktake" %}</td>
{% if item.stocktake_date %}
<td>{% render_date item.stocktake_date %} <span class='badge badge-right rounded-pill bg-dark'>{{ item.stocktake_user }}</span></td>
{% else %}
<td><em>{% trans "No stocktake performed" %}</em></td>
{% endif %}
</tr>
</table>
<div class='info-messages'>
{% if item.is_building %}
<div class='alert alert-block alert-info'>
{% trans "This stock item is in production and cannot be edited." %}<br>
{% trans "Edit the stock item from the build view." %}<br>
{% if item.build %}
<a href="{% url 'build-detail' item.build.id %}">
<strong>{{ item.build }}</strong>
</a>
{% endif %}
</div>
{% endif %}
{% if item.hasRequiredTests and not item.passedAllRequiredTests %}
<div class='alert alert-block alert-danger'>
{% trans "This stock item has not passed all required tests" %}
</div>
{% endif %}
{% for allocation in item.sales_order_allocations.all %}
<div class='alert alert-block alert-info'>
{% object_link 'so-detail' allocation.line.order.id allocation.line.order as link %}
{% decimal allocation.quantity as qty %}
{% trans "This stock item is allocated to Sales Order" %} {{ link }} {% if qty < item.quantity %}({% trans "Quantity" %}: {{ qty }}){% endif %}
</div>
{% endfor %}
{% for allocation in item.allocations.all %}
<div class='alert alert-block alert-info'>
{% object_link 'build-detail' allocation.build.id allocation.build as link %}
{% decimal allocation.quantity as qty %}
{% trans "This stock item is allocated to Build Order" %} {{ link }} {% if qty < item.quantity %}({% trans "Quantity" %}: {{ qty }}){% endif %}
</div>
{% endfor %}
{% if item.serialized %}
<div class='alert alert-block alert-warning'>
{% trans "This stock item is serialized - it has a unique serial number and the quantity cannot be adjusted." %}
</div>
{% endif %}
</div>
{% endblock details %}
{% block details_right %}
<table class="table table-striped table-condensed">
<col width='25'>
{% if item.customer %}
<tr>
<td><span class='fas fa-user-tie'></span></td>
<td>{% trans "Customer" %}</td>
<td><a href="{% url 'company-detail' item.customer.id %}?display=assigned-stock">{{ item.customer.name }}</a></td>
</tr>
{% endif %}
{% if item.belongs_to %}
<tr>
<td><span class='fas fa-box'></span></td>
<td>
{% trans "Installed In" %}
</td>
<td>
<a href="{% url 'stock-item-detail' item.belongs_to.id %}">{{ item.belongs_to }}</a>
</td>
</tr>
{% elif item.sales_order %}
<tr>
<td><span class='fas fa-user-tie'></span></td>
<td>{% trans "Sales Order" %}</td>
<td><a href="{% url 'so-detail' item.sales_order.id %}">{{ item.sales_order.reference }}</a> - <a href="{% url 'company-detail' item.sales_order.customer.id %}">{{ item.sales_order.customer.name }}</a></td>
</tr>
{% else %}
<tr>
<td><span class='fas fa-map-marker-alt'></span></td>
<td>{% trans "Location" %}</td>
{% if item.location %}
<td><a href="{% url 'stock-location-detail' item.location.id %}">{{ item.location.name }}</a></td>
{% else %}
<td><em>{% trans "No location set" %}</em></td>
{% endif %}
</tr>
{% endif %}
{% if item.uid %}
<tr>
<td><span class='fas fa-barcode'></span></td>
@ -322,13 +171,6 @@
<td>{{ item.batch }}</td>
</tr>
{% endif %}
{% if item.packaging %}
<tr>
<td><span class='fas fa-cube'></span></td>
<td>{% trans "Packaging" %}</td>
<td>{{ item.packaging }}</td>
</tr>
{% endif %}
{% if item.build %}
<tr>
<td><span class='fas fa-tools'></span></td>
@ -397,18 +239,11 @@
<td><a href="{% url 'supplier-part-detail' item.supplier_part.id %}">{{ item.supplier_part.SKU }}</a></td>
</tr>
{% endif %}
{% if item.hasRequiredTests %}
{% if item.packaging %}
<tr>
<td><span class='fas fa-vial'></span></td>
<td>{% trans "Tests" %}</td>
<td>
{{ item.requiredTestStatus.passed }} / {{ item.requiredTestStatus.total }}
{% if item.passedAllRequiredTests %}
<span class='fas fa-check-circle float-right icon-green'></span>
{% else %}
<span class='fas fa-times-circle float-right icon-red'></span>
{% endif %}
</td>
<td><span class='fas fa-cube'></span></td>
<td>{% trans "Packaging" %}</td>
<td>{{ item.packaging }}</td>
</tr>
{% endif %}
{% if ownership_enabled and item_owner %}
@ -425,6 +260,199 @@
</td>
</tr>
{% endif %}
</table>
<div class='info-messages'>
{% if item.is_building %}
<div class='alert alert-block alert-info'>
{% trans "This stock item is in production and cannot be edited." %}<br>
{% trans "Edit the stock item from the build view." %}<br>
{% if item.build %}
<a href="{% url 'build-detail' item.build.id %}">
<strong>{{ item.build }}</strong>
</a>
{% endif %}
</div>
{% endif %}
{% if item.hasRequiredTests and not item.passedAllRequiredTests %}
<div class='alert alert-block alert-danger'>
{% trans "This stock item has not passed all required tests" %}
</div>
{% endif %}
{% for allocation in item.sales_order_allocations.all %}
<div class='alert alert-block alert-info'>
{% object_link 'so-detail' allocation.line.order.id allocation.line.order as link %}
{% decimal allocation.quantity as qty %}
{% trans "This stock item is allocated to Sales Order" %} {{ link }} {% if qty < item.quantity %}({% trans "Quantity" %}: {{ qty }}){% endif %}
</div>
{% endfor %}
{% for allocation in item.allocations.all %}
<div class='alert alert-block alert-info'>
{% object_link 'build-detail' allocation.build.id allocation.build as link %}
{% decimal allocation.quantity as qty %}
{% trans "This stock item is allocated to Build Order" %} {{ link }} {% if qty < item.quantity %}({% trans "Quantity" %}: {{ qty }}){% endif %}
</div>
{% endfor %}
{% if item.serialized %}
<div class='alert alert-block alert-warning'>
{% trans "This stock item is serialized - it has a unique serial number and the quantity cannot be adjusted." %}
</div>
{% endif %}
</div>
{% endblock details %}
{% block details_right %}
<table class="table table-striped table-condensed">
<col width='25'>
<tr>
{% if item.serialized %}
<td>
<h5><span class='fas fa-hashtag'></span></h5>
</td>
<td>
<h5>{% trans "Serial Number" %}</h5>
</td>
<td><h5>
{{ item.serial }}
<div class='btn-group float-right' role='group'>
{% if previous %}
<a class="btn btn-small btn-outline-secondary" aria-label="{% trans 'previous page' %}" href="{% url request.resolver_match.url_name previous.id %}" title='{% trans "Navigate to previous serial number" %}'>
<span class='fas fa-angle-left'></span>
<small>{{ previous.serial }}</small>
</a>
{% endif %}
<a class='btn btn-small btn-outline-secondary text-sm' href='#' id='serial-number-search' title='{% trans "Search for serial number" %}'>
<span class='fas fa-search'></span>
</a>
{% if next %}
<a class="btn btn-small btn-outline-secondary text-sm" aria-label="{% trans 'next page' %}" href="{% url request.resolver_match.url_name next.id %}" title='{% trans "Navigate to next serial number" %}'>
<small>{{ next.serial }}</small>
<span class='fas fa-angle-right'></span>
</a>
{% endif %}
</div>
</h5>
</td>
{% else %}
<td>
<h5><div class='fas fa-boxes'></div></h5>
</td>
<td>
<h5>{% trans "Available Quantity" %}</h5>
</td>
<td>
<h5>{% if item.quantity != available %}{% decimal available %} / {% endif %}{% decimal item.quantity %} {% if item.part.units %}{{ item.part.units }}{% endif %}</h5>
</td>
{% endif %}
</tr>
{% if item.belongs_to %}
<tr>
<td><span class='fas fa-box'></span></td>
<td>
{% trans "Installed In" %}
</td>
<td>
<a href="{% url 'stock-item-detail' item.belongs_to.id %}">{{ item.belongs_to }}</a>
</td>
</tr>
{% elif item.sales_order %}
<tr>
<td><span class='fas fa-user-tie'></span></td>
<td>{% trans "Sales Order" %}</td>
<td><a href="{% url 'so-detail' item.sales_order.id %}">{{ item.sales_order.reference }}</a> - <a href="{% url 'company-detail' item.sales_order.customer.id %}">{{ item.sales_order.customer.name }}</a></td>
</tr>
{% else %}
{% if allocated_to_sales_orders %}
<tr>
<td><span class='fas fa-truck'></span></td>
<td>{% trans "Allocated to Sales Orders" %}</td>
<td>{% decimal allocated_to_sales_orders %}</td>
</tr>
{% endif %}
{% if allocated_to_build_orders %}
<tr>
<td><span class='fas fa-tools'></span></td>
<td>{% trans "Allocated to Build Orders" %}</td>
<td>{% decimal allocated_to_build_orders %}</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-map-marker-alt'></span></td>
<td>{% trans "Location" %}</td>
{% if item.location %}
<td><a href="{% url 'stock-location-detail' item.location.id %}">{{ item.location.name }}</a></td>
{% elif not item.customer %}
<td><em>{% trans "No location set" %}</em></td>
{% endif %}
</tr>
{% endif %}
{% if item.customer %}
<tr>
<td><span class='fas fa-user-tie'></span></td>
<td>{% trans "Customer" %}</td>
<td><a href="{% url 'company-detail' item.customer.id %}?display=assigned-stock">{{ item.customer.name }}</a></td>
</tr>
{% endif %}
{% if item.hasRequiredTests %}
<tr>
<td><span class='fas fa-vial'></span></td>
<td>{% trans "Tests" %}</td>
<td>
{{ item.requiredTestStatus.passed }} / {{ item.requiredTestStatus.total }}
{% if item.passedAllRequiredTests %}
<span class='fas fa-check-circle float-right icon-green'></span>
{% else %}
<span class='fas fa-times-circle float-right icon-red'></span>
{% endif %}
</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-info'></span></td>
<td>{% trans "Status" %}</td>
<td>{% stock_status_label item.status %}</td>
</tr>
{% if item.expiry_date %}
<tr>
<td><span class='fas fa-calendar-alt{% if item.is_expired %} icon-red{% endif %}'></span></td>
<td>{% trans "Expiry Date" %}</td>
<td>
{% render_date item.expiry_date %}
{% if item.is_expired %}
<span title='{% blocktrans %}This StockItem expired on {{ item.expiry_date }}{% endblocktrans %}' class='badge rounded-pill bg-danger badge-right'>{% trans "Expired" %}</span>
{% elif item.is_stale %}
<span title='{% blocktrans %}This StockItem expires on {{ item.expiry_date }}{% endblocktrans %}' class='badge rounded-pill bg-warning badge-right'>{% trans "Stale" %}</span>
{% endif %}
</td>
</tr>
{% endif %}
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Last Updated" %}</td>
<td>{{ item.updated }}</td>
</tr>
<tr>
<td><span class='fas fa-calendar-alt'></span></td>
<td>{% trans "Last Stocktake" %}</td>
{% if item.stocktake_date %}
<td>{% render_date item.stocktake_date %} <span class='badge badge-right rounded-pill bg-dark'>{{ item.stocktake_user }}</span></td>
{% else %}
<td><em>{% trans "No stocktake performed" %}</em></td>
{% endif %}
</tr>
</table>
{% endblock details_right %}

View File

@ -94,6 +94,12 @@ class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
data['item_owner'] = self.object.get_item_owner()
data['user_owns_item'] = self.object.check_ownership(self.request.user)
# Allocation information
data['allocated_to_sales_orders'] = self.object.sales_order_allocation_count()
data['allocated_to_build_orders'] = self.object.build_allocation_count()
data['allocated_to_orders'] = data['allocated_to_sales_orders'] + data['allocated_to_build_orders']
data['available'] = max(0, self.object.quantity - data['allocated_to_orders'])
return data
def get(self, request, *args, **kwargs):

View File

@ -1698,13 +1698,22 @@ function loadStockTable(table, options) {
sortable: true,
formatter: function(value, row) {
var val = parseFloat(value);
var val = '';
var available = Math.max(0, (row.quantity || 0) - (row.allocated || 0));
// If there is a single unit with a serial number, use the serial number
if (row.serial && row.quantity == 1) {
// If there is a single unit with a serial number, use the serial number
val = '# ' + row.serial;
} else if (row.quantity != available) {
// Some quantity is available, show available *and* quantity
var ava = +parseFloat(available).toFixed(5);
var tot = +parseFloat(row.quantity).toFixed(5);
val = `${ava} / ${tot}`;
} else {
val = +val.toFixed(5);
// Format floating point numbers with this one weird trick
val = +parseFloat(value).toFixed(5);
}
var html = renderLink(val, `/stock/item/${row.pk}/`);
@ -1719,16 +1728,7 @@ function loadStockTable(table, options) {
} else if (row.customer) {
// StockItem has been assigned to a customer
html += makeIconBadge('fa-user', '{% trans "Stock item assigned to customer" %}');
}
if (row.expired) {
html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Stock item has expired" %}');
} else if (row.stale) {
html += makeIconBadge('fa-stopwatch', '{% trans "Stock item will expire soon" %}');
}
if (row.allocated) {
} else if (row.allocated) {
if (row.serial != null && row.quantity == 1) {
html += makeIconBadge('fa-bookmark icon-yellow', '{% trans "Serialized stock item has been allocated" %}');
} else if (row.allocated >= row.quantity) {
@ -1736,10 +1736,14 @@ function loadStockTable(table, options) {
} else {
html += makeIconBadge('fa-bookmark', '{% trans "Stock item has been partially allocated" %}');
}
} else if (row.belongs_to) {
html += makeIconBadge('fa-box', '{% trans "Stock item has been installed in another item" %}');
}
if (row.belongs_to) {
html += makeIconBadge('fa-box', '{% trans "Stock item has been installed in another item" %}');
if (row.expired) {
html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Stock item has expired" %}');
} else if (row.stale) {
html += makeIconBadge('fa-stopwatch', '{% trans "Stock item will expire soon" %}');
}
// Special stock status codes