diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py
index 8d9b0e8da1..0a9d39225b 100644
--- a/InvenTree/InvenTree/version.py
+++ b/InvenTree/InvenTree/version.py
@@ -12,11 +12,18 @@ import common.models
INVENTREE_SW_VERSION = "0.7.0 dev"
# InvenTree API version
-INVENTREE_API_VERSION = 34
+INVENTREE_API_VERSION = 36
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
+v36 -> 2022-04-03
+ - Adds ability to filter part list endpoint by unallocated_stock argument
+
+v35 -> 2022-04-01 : https://github.com/inventree/InvenTree/pull/2797
+ - Adds stock allocation information to the Part API
+ - Adds calculated field for "unallocated_quantity"
+
v34 -> 2022-03-25
- Change permissions for "plugin list" API endpoint (now allows any authenticated user)
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 3a2bb6eeb3..e1b0dda61f 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -798,6 +798,20 @@ class PartFilter(rest_filters.FilterSet):
return queryset
+ # unallocated_stock filter
+ unallocated_stock = rest_filters.BooleanFilter(label='Unallocated stock', method='filter_unallocated_stock')
+
+ def filter_unallocated_stock(self, queryset, name, value):
+
+ value = str2bool(value)
+
+ if value:
+ queryset = queryset.filter(Q(unallocated_stock__gt=0))
+ else:
+ queryset = queryset.filter(Q(unallocated_stock__lte=0))
+
+ return queryset
+
is_template = rest_filters.BooleanFilter()
assembly = rest_filters.BooleanFilter()
@@ -1334,6 +1348,7 @@ class PartList(generics.ListCreateAPIView):
'creation_date',
'IPN',
'in_stock',
+ 'unallocated_stock',
'category',
]
diff --git a/InvenTree/part/fixtures/bom.yaml b/InvenTree/part/fixtures/bom.yaml
index e879b8381f..facb7e76ae 100644
--- a/InvenTree/part/fixtures/bom.yaml
+++ b/InvenTree/part/fixtures/bom.yaml
@@ -38,3 +38,11 @@
part: 1
sub_part: 5
quantity: 3
+
+# Make "Assembly" from "Bob"
+- model: part.bomitem
+ pk: 6
+ fields:
+ part: 101
+ sub_part: 100
+ quantity: 10
diff --git a/InvenTree/part/fixtures/part.yaml b/InvenTree/part/fixtures/part.yaml
index 77e808fd7f..d0a2d949b1 100644
--- a/InvenTree/part/fixtures/part.yaml
+++ b/InvenTree/part/fixtures/part.yaml
@@ -108,6 +108,18 @@
lft: 0
rght: 0
+- model: part.part
+ pk: 101
+ fields:
+ name: 'Assembly'
+ description: 'A high level assembly'
+ salable: true
+ active: True
+ tree_id: 0
+ level: 0
+ lft: 0
+ rght: 0
+
# A 'template' part
- model: part.part
pk: 10000
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index fad0f12e28..c493028d71 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -1345,7 +1345,8 @@ class Part(MPTTModel):
queryset = OrderModels.SalesOrderAllocation.objects.filter(item__part__id=self.id)
- pending = kwargs.get('pending', None)
+ # Default behaviour is to only return *pending* allocations
+ pending = kwargs.get('pending', True)
if pending is True:
# Look only for 'open' orders which have not shipped
@@ -1433,7 +1434,7 @@ class Part(MPTTModel):
- If this part is a "template" (variants exist) then these are counted too
"""
- return self.get_stock_count()
+ return self.get_stock_count(include_variants=True)
def get_bom_item_filter(self, include_inherited=True):
"""
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index c46950adca..c352c59eab 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -7,7 +7,7 @@ from decimal import Decimal
from django.urls import reverse_lazy
from django.db import models, transaction
-from django.db.models import Q
+from django.db.models import ExpressionWrapper, F, Q
from django.db.models.functions import Coalesce
from django.utils.translation import ugettext_lazy as _
@@ -24,7 +24,10 @@ from InvenTree.serializers import (DataFileUploadSerializer,
InvenTreeAttachmentSerializer,
InvenTreeMoneySerializer)
-from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
+from InvenTree.status_codes import (BuildStatus,
+ PurchaseOrderStatus,
+ SalesOrderStatus)
+
from stock.models import StockItem
from .models import (BomItem, BomItemSubstitute,
@@ -363,6 +366,51 @@ class PartSerializer(InvenTreeModelSerializer):
),
)
+ """
+ Annotate with the number of stock items allocated to sales orders.
+ This annotation is modeled on Part.sales_order_allocations() method:
+
+ - Only look for "open" orders
+ - Stock items have not been "shipped"
+ """
+ so_allocation_filter = Q(
+ line__order__status__in=SalesOrderStatus.OPEN, # LineItem points to an OPEN order
+ shipment__shipment_date=None, # Allocated item has *not* been shipped out
+ )
+
+ queryset = queryset.annotate(
+ allocated_to_sales_orders=Coalesce(
+ SubquerySum('stock_items__sales_order_allocations__quantity', filter=so_allocation_filter),
+ Decimal(0),
+ output_field=models.DecimalField(),
+ )
+ )
+
+ """
+ Annotate with the number of stock items allocated to build orders.
+ This annotation is modeled on Part.build_order_allocations() method
+ """
+ bo_allocation_filter = Q(
+ build__status__in=BuildStatus.ACTIVE_CODES,
+ )
+
+ queryset = queryset.annotate(
+ allocated_to_build_orders=Coalesce(
+ SubquerySum('stock_items__allocations__quantity', filter=bo_allocation_filter),
+ Decimal(0),
+ 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'),
+ output_field=models.DecimalField(),
+ )
+ )
+
return queryset
def get_starred(self, part):
@@ -376,9 +424,12 @@ class PartSerializer(InvenTreeModelSerializer):
category_detail = CategorySerializer(source='category', many=False, read_only=True)
# Calculated fields
+ allocated_to_build_orders = serializers.FloatField(read_only=True)
+ allocated_to_sales_orders = serializers.FloatField(read_only=True)
+ unallocated_stock = serializers.FloatField(read_only=True)
+ building = serializers.FloatField(read_only=True)
in_stock = serializers.FloatField(read_only=True)
ordering = serializers.FloatField(read_only=True)
- building = serializers.FloatField(read_only=True)
stock_item_count = serializers.IntegerField(read_only=True)
suppliers = serializers.IntegerField(read_only=True)
@@ -399,7 +450,8 @@ class PartSerializer(InvenTreeModelSerializer):
partial = True
fields = [
'active',
-
+ 'allocated_to_build_orders',
+ 'allocated_to_sales_orders',
'assembly',
'category',
'category_detail',
@@ -430,6 +482,7 @@ class PartSerializer(InvenTreeModelSerializer):
'suppliers',
'thumbnail',
'trackable',
+ 'unallocated_stock',
'units',
'variant_of',
'virtual',
diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py
index 23f929bca0..bea7154612 100644
--- a/InvenTree/part/test_api.py
+++ b/InvenTree/part/test_api.py
@@ -9,7 +9,7 @@ from rest_framework import status
from rest_framework.test import APIClient
from InvenTree.api_tester import InvenTreeAPITestCase
-from InvenTree.status_codes import StockStatus
+from InvenTree.status_codes import BuildStatus, StockStatus
from part.models import Part, PartCategory
from part.models import BomItem, BomItemSubstitute
@@ -17,6 +17,9 @@ from stock.models import StockItem, StockLocation
from company.models import Company
from common.models import InvenTreeSetting
+import build.models
+import order.models
+
class PartOptionsAPITest(InvenTreeAPITestCase):
"""
@@ -247,7 +250,7 @@ class PartAPITest(InvenTreeAPITestCase):
data = {'cascade': True}
response = self.client.get(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(len(response.data), 13)
+ self.assertEqual(len(response.data), Part.objects.count())
def test_get_parts_by_cat(self):
url = reverse('api-part-list')
@@ -815,6 +818,10 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
'location',
'bom',
'test_templates',
+ 'build',
+ 'location',
+ 'stock',
+ 'sales_order',
]
roles = [
@@ -826,6 +833,9 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
super().setUp()
+ # Ensure the part "variant" tree is correctly structured
+ Part.objects.rebuild()
+
# Add a new part
self.part = Part.objects.create(
name='Banana',
@@ -880,6 +890,153 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
self.assertEqual(data['in_stock'], 1100)
self.assertEqual(data['stock_item_count'], 105)
+ def test_allocation_annotations(self):
+ """
+ Tests for query annotations which add allocation information.
+ Ref: https://github.com/inventree/InvenTree/pull/2797
+ """
+
+ # We are looking at Part ID 100 ("Bob")
+ url = reverse('api-part-detail', kwargs={'pk': 100})
+
+ part = Part.objects.get(pk=100)
+
+ response = self.get(url, expected_code=200)
+
+ # Check that the expected annotated fields exist in the data
+ data = response.data
+ self.assertEqual(data['allocated_to_build_orders'], 0)
+ self.assertEqual(data['allocated_to_sales_orders'], 0)
+
+ # The unallocated stock count should equal the 'in stock' coutn
+ in_stock = data['in_stock']
+ self.assertEqual(in_stock, 126)
+ self.assertEqual(data['unallocated_stock'], in_stock)
+
+ # Check that model functions return the same values
+ self.assertEqual(part.build_order_allocation_count(), 0)
+ self.assertEqual(part.sales_order_allocation_count(), 0)
+ self.assertEqual(part.total_stock, in_stock)
+ self.assertEqual(part.available_stock, in_stock)
+
+ # Now, let's create a sales order, and allocate some stock
+ so = order.models.SalesOrder.objects.create(
+ reference='001',
+ customer=Company.objects.get(pk=1),
+ )
+
+ # We wish to send 50 units of "Bob" against this sales order
+ line = order.models.SalesOrderLineItem.objects.create(
+ quantity=50,
+ order=so,
+ part=part,
+ )
+
+ # Create a shipment against the order
+ shipment_1 = order.models.SalesOrderShipment.objects.create(
+ order=so,
+ reference='001',
+ )
+
+ shipment_2 = order.models.SalesOrderShipment.objects.create(
+ order=so,
+ reference='002',
+ )
+
+ # Allocate stock items to this order, against multiple shipments
+ order.models.SalesOrderAllocation.objects.create(
+ line=line,
+ shipment=shipment_1,
+ item=StockItem.objects.get(pk=1007),
+ quantity=17
+ )
+
+ order.models.SalesOrderAllocation.objects.create(
+ line=line,
+ shipment=shipment_1,
+ item=StockItem.objects.get(pk=1008),
+ quantity=18
+ )
+
+ order.models.SalesOrderAllocation.objects.create(
+ line=line,
+ shipment=shipment_2,
+ item=StockItem.objects.get(pk=1006),
+ quantity=15,
+ )
+
+ # Submit the API request again - should show us the sales order allocation
+ data = self.get(url, expected_code=200).data
+
+ self.assertEqual(data['allocated_to_sales_orders'], 50)
+ self.assertEqual(data['in_stock'], 126)
+ self.assertEqual(data['unallocated_stock'], 76)
+
+ # Now, "ship" the first shipment (so the stock is not 'in stock' any more)
+ shipment_1.complete_shipment(None)
+
+ # Refresh the API data
+ data = self.get(url, expected_code=200).data
+
+ self.assertEqual(data['allocated_to_build_orders'], 0)
+ self.assertEqual(data['allocated_to_sales_orders'], 15)
+ self.assertEqual(data['in_stock'], 91)
+ self.assertEqual(data['unallocated_stock'], 76)
+
+ # Next, we create a build order and allocate stock against it
+ bo = build.models.Build.objects.create(
+ part=Part.objects.get(pk=101),
+ quantity=10,
+ title='Making some assemblies',
+ status=BuildStatus.PRODUCTION,
+ )
+
+ bom_item = BomItem.objects.get(pk=6)
+
+ # Allocate multiple stock items against this build order
+ build.models.BuildItem.objects.create(
+ build=bo,
+ bom_item=bom_item,
+ stock_item=StockItem.objects.get(pk=1000),
+ quantity=10,
+ )
+
+ # Request data once more
+ data = self.get(url, expected_code=200).data
+
+ self.assertEqual(data['allocated_to_build_orders'], 10)
+ self.assertEqual(data['allocated_to_sales_orders'], 15)
+ self.assertEqual(data['in_stock'], 91)
+ self.assertEqual(data['unallocated_stock'], 66)
+
+ # Again, check that the direct model functions return the same values
+ self.assertEqual(part.build_order_allocation_count(), 10)
+ self.assertEqual(part.sales_order_allocation_count(), 15)
+ self.assertEqual(part.total_stock, 91)
+ self.assertEqual(part.available_stock, 66)
+
+ # Allocate further stock against the build
+ build.models.BuildItem.objects.create(
+ build=bo,
+ bom_item=bom_item,
+ stock_item=StockItem.objects.get(pk=1001),
+ quantity=10,
+ )
+
+ # Request data once more
+ data = self.get(url, expected_code=200).data
+
+ self.assertEqual(data['allocated_to_build_orders'], 20)
+ self.assertEqual(data['allocated_to_sales_orders'], 15)
+ self.assertEqual(data['in_stock'], 91)
+ self.assertEqual(data['unallocated_stock'], 56)
+
+ # Again, check that the direct model functions return the same values
+ self.assertEqual(part.build_order_allocation_count(), 20)
+ self.assertEqual(part.sales_order_allocation_count(), 15)
+ self.assertEqual(part.total_stock, 91)
+ self.assertEqual(part.available_stock, 56)
+
class BomItemTest(InvenTreeAPITestCase):
"""
diff --git a/InvenTree/part/test_bom_item.py b/InvenTree/part/test_bom_item.py
index 7466277118..88548f3cf7 100644
--- a/InvenTree/part/test_bom_item.py
+++ b/InvenTree/part/test_bom_item.py
@@ -46,7 +46,7 @@ class BomItemTest(TestCase):
# TODO: Tests for multi-level BOMs
def test_used_in(self):
- self.assertEqual(self.bob.used_in_count, 0)
+ self.assertEqual(self.bob.used_in_count, 1)
self.assertEqual(self.orphan.used_in_count, 1)
def test_self_reference(self):
diff --git a/InvenTree/stock/fixtures/stock.yaml b/InvenTree/stock/fixtures/stock.yaml
index 0f44828d8e..2fd5b7eb92 100644
--- a/InvenTree/stock/fixtures/stock.yaml
+++ b/InvenTree/stock/fixtures/stock.yaml
@@ -251,3 +251,104 @@
rght: 0
expiry_date: "1990-10-10"
status: 70
+
+# Multiple stock items for "Bob" (PK 100)
+- model: stock.stockitem
+ pk: 1000
+ fields:
+ part: 100
+ location: 1
+ quantity: 10
+ level: 0
+ tree_id: 0
+ lft: 0
+ rght: 0
+
+- model: stock.stockitem
+ pk: 1001
+ fields:
+ part: 100
+ location: 1
+ quantity: 11
+ level: 0
+ tree_id: 0
+ lft: 0
+ rght: 0
+
+- model: stock.stockitem
+ pk: 1002
+ fields:
+ part: 100
+ location: 1
+ quantity: 12
+ level: 0
+ tree_id: 0
+ lft: 0
+ rght: 0
+
+- model: stock.stockitem
+ pk: 1003
+ fields:
+ part: 100
+ location: 1
+ quantity: 13
+ level: 0
+ tree_id: 0
+ lft: 0
+ rght: 0
+
+- model: stock.stockitem
+ pk: 1004
+ fields:
+ part: 100
+ location: 1
+ quantity: 14
+ level: 0
+ tree_id: 0
+ lft: 0
+ rght: 0
+
+- model: stock.stockitem
+ pk: 1005
+ fields:
+ part: 100
+ location: 1
+ quantity: 15
+ level: 0
+ tree_id: 0
+ lft: 0
+ rght: 0
+
+- model: stock.stockitem
+ pk: 1006
+ fields:
+ part: 100
+ location: 1
+ quantity: 16
+ level: 0
+ tree_id: 0
+ lft: 0
+ rght: 0
+
+- model: stock.stockitem
+ pk: 1007
+ fields:
+ part: 100
+ location: 7
+ quantity: 17
+ level: 0
+ tree_id: 0
+ lft: 0
+ rght: 0
+
+- model: stock.stockitem
+ pk: 1008
+ fields:
+ part: 100
+ location: 7
+ quantity: 18
+ level: 0
+ tree_id: 0
+ lft: 0
+ rght: 0
+
\ No newline at end of file
diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py
index 81973aed31..73bee54110 100644
--- a/InvenTree/stock/test_api.py
+++ b/InvenTree/stock/test_api.py
@@ -104,7 +104,7 @@ class StockItemListTest(StockAPITestCase):
response = self.get_stock()
- self.assertEqual(len(response), 20)
+ self.assertEqual(len(response), 29)
def test_filter_by_part(self):
"""
@@ -113,7 +113,7 @@ class StockItemListTest(StockAPITestCase):
response = self.get_stock(part=25)
- self.assertEqual(len(response), 8)
+ self.assertEqual(len(response), 17)
response = self.get_stock(part=10004)
@@ -136,13 +136,13 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), 1)
response = self.get_stock(location=1, cascade=0)
- self.assertEqual(len(response), 0)
+ self.assertEqual(len(response), 7)
response = self.get_stock(location=1, cascade=1)
- self.assertEqual(len(response), 2)
+ self.assertEqual(len(response), 9)
response = self.get_stock(location=7)
- self.assertEqual(len(response), 16)
+ self.assertEqual(len(response), 18)
def test_filter_by_depleted(self):
"""
@@ -153,7 +153,7 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), 1)
response = self.get_stock(depleted=0)
- self.assertEqual(len(response), 19)
+ self.assertEqual(len(response), 28)
def test_filter_by_in_stock(self):
"""
@@ -161,7 +161,7 @@ class StockItemListTest(StockAPITestCase):
"""
response = self.get_stock(in_stock=1)
- self.assertEqual(len(response), 17)
+ self.assertEqual(len(response), 26)
response = self.get_stock(in_stock=0)
self.assertEqual(len(response), 3)
@@ -172,7 +172,7 @@ class StockItemListTest(StockAPITestCase):
"""
codes = {
- StockStatus.OK: 18,
+ StockStatus.OK: 27,
StockStatus.DESTROYED: 1,
StockStatus.LOST: 1,
StockStatus.DAMAGED: 0,
@@ -205,7 +205,7 @@ class StockItemListTest(StockAPITestCase):
self.assertIsNotNone(item['serial'])
response = self.get_stock(serialized=0)
- self.assertEqual(len(response), 8)
+ self.assertEqual(len(response), 17)
for item in response:
self.assertIsNone(item['serial'])
@@ -217,7 +217,7 @@ class StockItemListTest(StockAPITestCase):
# First, we can assume that the 'stock expiry' feature is disabled
response = self.get_stock(expired=1)
- self.assertEqual(len(response), 20)
+ self.assertEqual(len(response), 29)
self.user.is_staff = True
self.user.save()
@@ -232,7 +232,7 @@ class StockItemListTest(StockAPITestCase):
self.assertTrue(item['expired'])
response = self.get_stock(expired=0)
- self.assertEqual(len(response), 19)
+ self.assertEqual(len(response), 28)
for item in response:
self.assertFalse(item['expired'])
@@ -249,7 +249,7 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), 4)
response = self.get_stock(expired=0)
- self.assertEqual(len(response), 16)
+ self.assertEqual(len(response), 25)
def test_paginate(self):
"""
@@ -290,7 +290,8 @@ class StockItemListTest(StockAPITestCase):
dataset = self.export_data({})
- self.assertEqual(len(dataset), 20)
+ # Check that *all* stock item objects have been exported
+ self.assertEqual(len(dataset), StockItem.objects.count())
# Expected headers
headers = [
@@ -308,11 +309,11 @@ class StockItemListTest(StockAPITestCase):
# Now, add a filter to the results
dataset = self.export_data({'location': 1})
- self.assertEqual(len(dataset), 2)
+ self.assertEqual(len(dataset), 9)
dataset = self.export_data({'part': 25})
- self.assertEqual(len(dataset), 8)
+ self.assertEqual(len(dataset), 17)
class StockItemTest(StockAPITestCase):
diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py
index 50f77a593b..97639b15bd 100644
--- a/InvenTree/stock/tests.py
+++ b/InvenTree/stock/tests.py
@@ -167,8 +167,8 @@ class StockTest(TestCase):
self.assertFalse(self.drawer2.has_items())
# Drawer 3 should have three stock items
- self.assertEqual(self.drawer3.stock_items.count(), 16)
- self.assertEqual(self.drawer3.item_count, 16)
+ self.assertEqual(self.drawer3.stock_items.count(), 18)
+ self.assertEqual(self.drawer3.item_count, 18)
def test_stock_count(self):
part = Part.objects.get(pk=1)
diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js
index 201d3c5c9f..d4db965ebd 100644
--- a/InvenTree/templates/js/translated/build.js
+++ b/InvenTree/templates/js/translated/build.js
@@ -1746,7 +1746,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
required: true,
render_part_detail: true,
render_location_detail: true,
- render_stock_id: false,
+ render_pk: false,
auto_fill: true,
auto_fill_filters: auto_fill_filters,
onSelect: function(data, field, opts) {
diff --git a/InvenTree/templates/js/translated/model_renderers.js b/InvenTree/templates/js/translated/model_renderers.js
index 7be6c954c2..8c98fa35de 100644
--- a/InvenTree/templates/js/translated/model_renderers.js
+++ b/InvenTree/templates/js/translated/model_renderers.js
@@ -31,6 +31,24 @@
*/
+// Should the ID be rendered for this string
+function renderId(title, pk, parameters={}) {
+
+ // Default = true
+ var render = true;
+
+ if ('render_pk' in parameters) {
+ render = parameters['render_pk'];
+ }
+
+ if (render) {
+ return `${title}: ${pk}`;
+ } else {
+ return '';
+ }
+}
+
+
// Renderer for "Company" model
// eslint-disable-next-line no-unused-vars
function renderCompany(name, data, parameters={}, options={}) {
@@ -39,7 +57,7 @@ function renderCompany(name, data, parameters={}, options={}) {
html += `${data.name} - ${data.description}`;
- html += `{% trans "Company ID" %}: ${data.pk}`;
+ html += renderId('{% trans "Company ID" %}', data.pk, parameters);
return html;
}
@@ -67,18 +85,6 @@ function renderStockItem(name, data, parameters={}, options={}) {
part_detail = `${data.part_detail.full_name} - `;
}
- var render_stock_id = true;
-
- if ('render_stock_id' in parameters) {
- render_stock_id = parameters['render_stock_id'];
- }
-
- var stock_id = '';
-
- if (render_stock_id) {
- stock_id = `{% trans "Stock ID" %}: ${data.pk}`;
- }
-
var render_location_detail = false;
if ('render_location_detail' in parameters) {
@@ -88,7 +94,7 @@ function renderStockItem(name, data, parameters={}, options={}) {
var location_detail = '';
if (render_location_detail && data.location_detail) {
- location_detail = ` - (${data.location_detail.name})`;
+ location_detail = ` - (${data.location_detail.name})`;
}
var stock_detail = '';
@@ -103,7 +109,10 @@ function renderStockItem(name, data, parameters={}, options={}) {
var html = `
- ${part_detail}${stock_detail}${location_detail}${stock_id}
+ ${part_detail}
+ ${stock_detail}
+ ${location_detail}
+ ${renderId('{% trans "Stock ID" %}', data.pk, parameters)}
`;
@@ -183,7 +192,7 @@ function renderPart(name, data, parameters={}, options={}) {
${stock_data}
${extra}
- {% trans "Part ID" %}: ${data.pk}
+ ${renderId('{% trans "Part ID" $}', data.pk, parameters)}
`;
@@ -245,13 +254,7 @@ function renderPurchaseOrder(name, data, parameters={}, options={}) {
html += ` - ${data.description}`;
}
- html += `
-
-
- {% trans "Order ID" %}: ${data.pk}
-
-
- `;
+ html += renderId('{% trans "Order ID" %}', data.pk, parameters);
return html;
}
@@ -277,12 +280,7 @@ function renderSalesOrder(name, data, parameters={}, options={}) {
html += ` - ${data.description}`;
}
- html += `
-
-
- {% trans "Order ID" %}: ${data.pk}
-
- `;
+ html += renderId('{% trans "Order ID" %}', data.pk, parameters);
return html;
}
diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js
index b0283a1b35..08b258fdc2 100644
--- a/InvenTree/templates/js/translated/part.js
+++ b/InvenTree/templates/js/translated/part.js
@@ -491,13 +491,50 @@ function duplicateBom(part_id, options={}) {
}
+/*
+ * Construct a "badge" label showing stock information for this particular part
+ */
function partStockLabel(part, options={}) {
if (part.in_stock) {
- return ` `;
+ // 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)) {
+ return ` `;
+ } else if (part.unallocated_stock == 0) {
+ if (part.ordering) {
+ // There is no available stock, but stock is on order
+ return ` `;
+ } else if (part.building) {
+ // There is no available stock, but stock is being built
+ return ` `;
+ } else {
+ // There is no available stock at all
+ return ` `;
+ }
+ } else if (part.unallocated_stock < part.in_stock) {
+ // Unallocated quanttiy is less than total quantity
+ return ` `;
+ } else {
+ // Stock is completely available
+ return ` `;
+ }
} else {
- return ` `;
+ // There IS NO stock available for this part
+
+ if (part.ordering) {
+ // There is no stock, but stock is on order
+ return ` `;
+ } else if (part.building) {
+ // There is no stock, but stock is being built
+ return ` `;
+ } else {
+ // There is no stock
+ return ` `;
+ }
}
+
}
@@ -1160,12 +1197,14 @@ function partGridTile(part) {
if (!part.in_stock) {
stock = `{% trans "No Stock" %}`;
+ } else if (!part.unallocated_stock) {
+ stock = `{% trans "Not available" %}`;
}
rows += `