diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py
index 7b003d9406..5777f97afa 100644
--- a/InvenTree/InvenTree/api_version.py
+++ b/InvenTree/InvenTree/api_version.py
@@ -2,11 +2,14 @@
# InvenTree API version
-INVENTREE_API_VERSION = 100
+INVENTREE_API_VERSION = 101
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
+v101 -> 2023-03-07 : https://github.com/inventree/InvenTree/pull/4462
+ - Adds 'total_in_stock' to Part serializer, and supports API ordering
+
v100 -> 2023-03-04 : https://github.com/inventree/InvenTree/pull/4452
- Adds bulk delete of PurchaseOrderLineItems to API
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 4e9ab8f5ca..5c875a6c0f 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -1337,6 +1337,7 @@ class PartList(APIDownloadMixin, ListCreateAPI):
'creation_date',
'IPN',
'in_stock',
+ 'total_in_stock',
'unallocated_stock',
'category',
'last_stocktake',
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index a4b4105b6a..55b0d0622c 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -423,7 +423,6 @@ class PartSerializer(RemoteImageMixin, InvenTreeModelSerializer):
'full_name',
'image',
'in_stock',
- 'variant_stock',
'ordering',
'building',
'IPN',
@@ -444,10 +443,12 @@ class PartSerializer(RemoteImageMixin, InvenTreeModelSerializer):
'stock_item_count',
'suppliers',
'thumbnail',
+ 'total_in_stock',
'trackable',
'unallocated_stock',
'units',
'variant_of',
+ 'variant_stock',
'virtual',
'pricing_min',
'pricing_max',
@@ -554,11 +555,20 @@ class PartSerializer(RemoteImageMixin, InvenTreeModelSerializer):
allocated_to_build_orders=part.filters.annotate_build_order_allocations(),
)
+ # Annotate the queryset with the 'total_in_stock' quantity
+ # This is the 'in_stock' quantity summed with the 'variant_stock' quantity
+ queryset = queryset.annotate(
+ total_in_stock=ExpressionWrapper(
+ F('in_stock') + F('variant_stock'),
+ output_field=models.DecimalField(),
+ )
+ )
+
# Annotate with the total 'available stock' quantity
# This is the current stock, minus any allocations
queryset = queryset.annotate(
unallocated_stock=ExpressionWrapper(
- F('in_stock') - F('allocated_to_sales_orders') - F('allocated_to_build_orders'),
+ F('total_in_stock') - F('allocated_to_sales_orders') - F('allocated_to_build_orders'),
output_field=models.DecimalField(),
)
)
@@ -579,6 +589,7 @@ class PartSerializer(RemoteImageMixin, InvenTreeModelSerializer):
building = serializers.FloatField(read_only=True)
in_stock = serializers.FloatField(read_only=True)
variant_stock = serializers.FloatField(read_only=True)
+ total_in_stock = serializers.FloatField(read_only=True)
ordering = serializers.FloatField(read_only=True)
stock_item_count = serializers.IntegerField(read_only=True)
suppliers = serializers.IntegerField(read_only=True)
diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html
index e5c9d6f07d..91932e0545 100644
--- a/InvenTree/part/templates/part/part_base.html
+++ b/InvenTree/part/templates/part/part_base.html
@@ -215,7 +215,7 @@
{% endif %}
{% if part.component %}
- {% if required_build_order_quantity > 0 %}
+ {% if required_build_order_quantity > 0 or allocated_build_order_quantity > 0 %}
|
{% trans "Allocated to Build Orders" %} |
@@ -224,7 +224,7 @@
{% endif %}
{% endif %}
{% if part.salable %}
- {% if required_sales_order_quantity > 0 %}
+ {% if required_sales_order_quantity > 0 or allocated_sales_order_quantity > 0 %}
|
{% trans "Allocated to Sales Orders" %} |
diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py
index 9e5981aa09..c658267104 100644
--- a/InvenTree/part/templatetags/inventree_extras.py
+++ b/InvenTree/part/templatetags/inventree_extras.py
@@ -404,7 +404,10 @@ def progress_bar(val, max_val, *args, **kwargs):
else:
style = ''
- percent = float(val / max_val) * 100
+ if max_val != 0:
+ percent = float(val / max_val) * 100
+ else:
+ percent = 0
if percent > 100:
percent = 100
diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js
index 27b0545e2e..db75386118 100644
--- a/InvenTree/templates/js/translated/part.js
+++ b/InvenTree/templates/js/translated/part.js
@@ -589,68 +589,85 @@ function partStockLabel(part, options={}) {
// Prevent literal string 'null' from being displayed
var units = part.units || '';
- var text = '';
+ let elements = [];
// Check for stock
- if (part.in_stock) {
+ if (part.total_in_stock) {
// There IS stock available for this part
// Is stock "low" (below the 'minimum_stock' quantity)?
- if ((part.minimum_stock > 0) && (part.minimum_stock > part.in_stock)) {
- text += `{% trans "Low stock" %}: ${part.in_stock}`;
- } else if (part.unallocated_stock == 0) {
+ if ((part.minimum_stock > 0) && (part.minimum_stock > part.total_in_stock)) {
+ elements.push(`{% trans "Low stock" %}: ${part.total_in_stock}`);
+ } else if (part.unallocated_stock <= 0) {
// There is no available stock at all
- text += `{% trans "No stock available" %}`;
+ elements.push(`{% trans "No stock available" %}`);
} else if (part.unallocated_stock < part.in_stock) {
- // Unallocated quanttiy is less than total quantity
- text += `{% trans "Available" %}: ${part.unallocated_stock}/${part.in_stock}`;
+ // Unallocated quantity is less than total quantity
+ if (options.hideTotalStock) {
+ elements.push(`{% trans "Available" %}: ${part.unallocated_stock}`);
+ } else {
+ elements.push(`{% trans "Available" %}: ${part.unallocated_stock}/${part.in_stock}`);
+ }
} else {
// Stock is completely available
- text += `{% trans "Available" %}: ${part.unallocated_stock}`;
+ if (!options.hideTotalStock) {
+ elements.push(`{% trans "Available" %}: ${part.unallocated_stock}`);
+ }
}
} else {
// There IS NO stock available for this part
- text += `{% trans "No Stock" %}`;
+ elements.push(`{% trans "No Stock" %}`);
}
// Check for items on order
if (part.ordering) {
- text += ` | {% trans "On Order" %}: ${part.ordering}`;
+ elements.push(`{% trans "On Order" %}: ${part.ordering}`);
}
// Check for items beeing built
if (part.building) {
- text += ` | {% trans "Building" %}: ${part.building}`;
- }
-
- // Check for demand from unallocated build orders
- var required_build_order_quantity = null;
- var required_sales_order_quantity = null;
- inventreeGet(`/api/part/${part.pk}/requirements/`, {}, {
- async: false,
- success: function(response) {
- required_build_order_quantity = 0;
- if (response.required_build_order_quantity) {
- required_build_order_quantity = response.required_build_order_quantity;
- }
- required_sales_order_quantity = 0;
- if (response.required_sales_order_quantity) {
- required_sales_order_quantity = response.required_sales_order_quantity;
- }
- }
- });
- if ((required_build_order_quantity == null) || (required_sales_order_quantity == null)) {
- console.error(`Error loading part requirements for part ${part.pk}`);
- return;
- }
- var demand = (required_build_order_quantity - part.allocated_to_build_orders) + (required_sales_order_quantity - part.allocated_to_sales_orders);
- if (demand) {
- text += ` | {% trans "Demand" %}: ${demand}`;
+ elements.push(`{% trans "Building" %}: ${part.building}`);
}
// Determine badge color based on overall stock health
- var stock_health = part.in_stock + part.building + part.ordering - part.minimum_stock - required_build_order_quantity - required_sales_order_quantity;
+ var stock_health = part.unallocated_stock + part.building + part.ordering - part.minimum_stock;
+
+ // TODO: Refactor the API to include this information, so we don't have to request it!
+ if (!options.noDemandInfo) {
+
+ // Check for demand from unallocated build orders
+ var required_build_order_quantity = null;
+ var required_sales_order_quantity = null;
+
+ inventreeGet(`/api/part/${part.pk}/requirements/`, {}, {
+ async: false,
+ success: function(response) {
+ required_build_order_quantity = 0;
+ if (response.required_build_order_quantity) {
+ required_build_order_quantity = response.required_build_order_quantity;
+ }
+ required_sales_order_quantity = 0;
+ if (response.required_sales_order_quantity) {
+ required_sales_order_quantity = response.required_sales_order_quantity;
+ }
+ }
+ });
+
+ if ((required_build_order_quantity == null) || (required_sales_order_quantity == null)) {
+ console.error(`Error loading part requirements for part ${part.pk}`);
+ return;
+ }
+
+ var demand = (required_build_order_quantity - part.allocated_to_build_orders) + (required_sales_order_quantity - part.allocated_to_sales_orders);
+ if (demand) {
+ elements.push(`{% trans "Demand" %}: ${demand}`);
+ }
+
+ stock_health -= (required_build_order_quantity + required_sales_order_quantity);
+ }
+
var bg_class = '';
+
if (stock_health < 0) {
// Unsatisfied demand and/or below minimum stock
bg_class = 'bg-danger';
@@ -662,14 +679,19 @@ function partStockLabel(part, options={}) {
bg_class = 'bg-success';
}
- // show units next to stock badge
- var unit_badge = '';
+ // Display units next to stock badge
+ let unit_badge = '';
+
if (units && !options.no_units) {
unit_badge = `{% trans "Unit" %}: ${units} `;
}
- // return badge html
- return `${unit_badge}${text}`;
+ if (elements.length > 0) {
+ let text = elements.join(' | ');
+ return `${unit_badge}${text}`;
+ } else {
+ return '';
+ }
}
@@ -1150,24 +1172,25 @@ function loadPartVariantTable(table, partId, options={}) {
title: '{% trans "Description" %}',
},
{
- field: 'in_stock',
+ field: 'total_in_stock',
title: '{% trans "Stock" %}',
sortable: true,
formatter: function(value, row) {
- var base_stock = row.in_stock;
- var variant_stock = row.variant_stock || 0;
+ var text = renderLink(value, `/part/${row.pk}/?display=part-stock`);
- var total = base_stock + variant_stock;
+ text += partStockLabel(row, {
+ noDemandInfo: true,
+ hideTotalStock: true,
+ classes: 'float-right',
+ });
- var text = `${total}`;
-
- if (variant_stock > 0) {
+ if (row.variant_stock > 0) {
text = `${text}`;
text += ``;
}
- return renderLink(text, `/part/${row.pk}/?display=part-stock`);
+ return text;
}
},
{
@@ -1815,8 +1838,6 @@ function loadPartTable(table, url, options={}) {
var filters = {};
- var col = null;
-
if (!options.disableFilters) {
filters = loadTableFilters('parts');
}
@@ -1884,10 +1905,11 @@ function loadPartTable(table, url, options={}) {
}
});
- col = {
+ columns.push({
sortName: 'category',
field: 'category_detail',
title: '{% trans "Category" %}',
+ sortable: true,
formatter: function(value, row) {
if (row.category && row.category_detail) {
var text = shortenString(row.category_detail.pathstring);
@@ -1896,81 +1918,26 @@ function loadPartTable(table, url, options={}) {
return '{% trans "No category" %}';
}
}
- };
+ });
- if (!options.params.ordering) {
- col['sortable'] = true;
- }
- columns.push(col);
-
- col = {
- field: 'unallocated_stock',
+ columns.push({
+ field: 'total_in_stock',
title: '{% trans "Stock" %}',
- searchable: false,
+ sortable: true,
formatter: function(value, row) {
- var text = '';
+ var text = renderLink(value, `/part/${row.pk}/?display=part-stock`);
- var total_stock = row.in_stock;
-
- if (row.variant_stock) {
- total_stock += row.variant_stock;
- }
-
- var text = `${total_stock}`;
-
- // Construct extra informational badges
- var badges = '';
-
- if (total_stock == 0) {
- badges += ``;
- } else if (total_stock < row.minimum_stock) {
- badges += ``;
- }
-
- if (row.ordering && row.ordering > 0) {
- badges += renderLink(
- ``,
- `/part/${row.pk}/?display=purchase-orders`
- );
- }
-
- if (row.building && row.building > 0) {
- badges += renderLink(
- ``,
- `/part/${row.pk}/?display=build-orders`
- );
- }
-
- if (row.variant_stock && row.variant_stock > 0) {
- badges += ``;
- }
-
- if (row.allocated_to_build_orders > 0) {
- badges += ``;
- }
-
- if (row.allocated_to_sales_orders > 0) {
- badges += ``;
- }
-
- if (row.units) {
- text += ` ${row.units}`;
- }
-
- text = renderLink(text, `/part/${row.pk}/?display=part-stock`);
- text += badges;
+ text += partStockLabel(row, {
+ noDemandInfo: true,
+ hideTotalStock: true,
+ classes: 'float-right',
+ });
return text;
}
- };
-
- if (!options.params.ordering) {
- col['sortable'] = true;
- }
-
- columns.push(col);
+ });
// Pricing information
columns.push({
@@ -1985,6 +1952,7 @@ function loadPartTable(table, url, options={}) {
}
});
+ // External link / URL
columns.push({
field: 'link',
title: '{% trans "Link" %}',