mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'inventree:master' into matmair/issue2385
This commit is contained in:
commit
41b75e4928
@ -12,11 +12,18 @@ import common.models
|
|||||||
INVENTREE_SW_VERSION = "0.7.0 dev"
|
INVENTREE_SW_VERSION = "0.7.0 dev"
|
||||||
|
|
||||||
# InvenTree API version
|
# 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
|
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
|
v34 -> 2022-03-25
|
||||||
- Change permissions for "plugin list" API endpoint (now allows any authenticated user)
|
- Change permissions for "plugin list" API endpoint (now allows any authenticated user)
|
||||||
|
|
||||||
|
@ -798,6 +798,20 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
return queryset
|
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()
|
is_template = rest_filters.BooleanFilter()
|
||||||
|
|
||||||
assembly = rest_filters.BooleanFilter()
|
assembly = rest_filters.BooleanFilter()
|
||||||
@ -1334,6 +1348,7 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
'creation_date',
|
'creation_date',
|
||||||
'IPN',
|
'IPN',
|
||||||
'in_stock',
|
'in_stock',
|
||||||
|
'unallocated_stock',
|
||||||
'category',
|
'category',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -38,3 +38,11 @@
|
|||||||
part: 1
|
part: 1
|
||||||
sub_part: 5
|
sub_part: 5
|
||||||
quantity: 3
|
quantity: 3
|
||||||
|
|
||||||
|
# Make "Assembly" from "Bob"
|
||||||
|
- model: part.bomitem
|
||||||
|
pk: 6
|
||||||
|
fields:
|
||||||
|
part: 101
|
||||||
|
sub_part: 100
|
||||||
|
quantity: 10
|
||||||
|
@ -108,6 +108,18 @@
|
|||||||
lft: 0
|
lft: 0
|
||||||
rght: 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
|
# A 'template' part
|
||||||
- model: part.part
|
- model: part.part
|
||||||
pk: 10000
|
pk: 10000
|
||||||
|
@ -1345,7 +1345,8 @@ class Part(MPTTModel):
|
|||||||
|
|
||||||
queryset = OrderModels.SalesOrderAllocation.objects.filter(item__part__id=self.id)
|
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:
|
if pending is True:
|
||||||
# Look only for 'open' orders which have not shipped
|
# 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
|
- 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):
|
def get_bom_item_filter(self, include_inherited=True):
|
||||||
"""
|
"""
|
||||||
|
@ -7,7 +7,7 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.db import models, transaction
|
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.db.models.functions import Coalesce
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
@ -24,7 +24,10 @@ from InvenTree.serializers import (DataFileUploadSerializer,
|
|||||||
InvenTreeAttachmentSerializer,
|
InvenTreeAttachmentSerializer,
|
||||||
InvenTreeMoneySerializer)
|
InvenTreeMoneySerializer)
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
|
from InvenTree.status_codes import (BuildStatus,
|
||||||
|
PurchaseOrderStatus,
|
||||||
|
SalesOrderStatus)
|
||||||
|
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
|
||||||
from .models import (BomItem, BomItemSubstitute,
|
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
|
return queryset
|
||||||
|
|
||||||
def get_starred(self, part):
|
def get_starred(self, part):
|
||||||
@ -376,9 +424,12 @@ class PartSerializer(InvenTreeModelSerializer):
|
|||||||
category_detail = CategorySerializer(source='category', many=False, read_only=True)
|
category_detail = CategorySerializer(source='category', many=False, read_only=True)
|
||||||
|
|
||||||
# Calculated fields
|
# 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)
|
in_stock = serializers.FloatField(read_only=True)
|
||||||
ordering = 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)
|
stock_item_count = serializers.IntegerField(read_only=True)
|
||||||
suppliers = serializers.IntegerField(read_only=True)
|
suppliers = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
@ -399,7 +450,8 @@ class PartSerializer(InvenTreeModelSerializer):
|
|||||||
partial = True
|
partial = True
|
||||||
fields = [
|
fields = [
|
||||||
'active',
|
'active',
|
||||||
|
'allocated_to_build_orders',
|
||||||
|
'allocated_to_sales_orders',
|
||||||
'assembly',
|
'assembly',
|
||||||
'category',
|
'category',
|
||||||
'category_detail',
|
'category_detail',
|
||||||
@ -430,6 +482,7 @@ class PartSerializer(InvenTreeModelSerializer):
|
|||||||
'suppliers',
|
'suppliers',
|
||||||
'thumbnail',
|
'thumbnail',
|
||||||
'trackable',
|
'trackable',
|
||||||
|
'unallocated_stock',
|
||||||
'units',
|
'units',
|
||||||
'variant_of',
|
'variant_of',
|
||||||
'virtual',
|
'virtual',
|
||||||
|
@ -9,7 +9,7 @@ from rest_framework import status
|
|||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from InvenTree.api_tester import InvenTreeAPITestCase
|
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 Part, PartCategory
|
||||||
from part.models import BomItem, BomItemSubstitute
|
from part.models import BomItem, BomItemSubstitute
|
||||||
@ -17,6 +17,9 @@ from stock.models import StockItem, StockLocation
|
|||||||
from company.models import Company
|
from company.models import Company
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
|
import build.models
|
||||||
|
import order.models
|
||||||
|
|
||||||
|
|
||||||
class PartOptionsAPITest(InvenTreeAPITestCase):
|
class PartOptionsAPITest(InvenTreeAPITestCase):
|
||||||
"""
|
"""
|
||||||
@ -247,7 +250,7 @@ class PartAPITest(InvenTreeAPITestCase):
|
|||||||
data = {'cascade': True}
|
data = {'cascade': True}
|
||||||
response = self.client.get(url, data, format='json')
|
response = self.client.get(url, data, format='json')
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
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):
|
def test_get_parts_by_cat(self):
|
||||||
url = reverse('api-part-list')
|
url = reverse('api-part-list')
|
||||||
@ -815,6 +818,10 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
|||||||
'location',
|
'location',
|
||||||
'bom',
|
'bom',
|
||||||
'test_templates',
|
'test_templates',
|
||||||
|
'build',
|
||||||
|
'location',
|
||||||
|
'stock',
|
||||||
|
'sales_order',
|
||||||
]
|
]
|
||||||
|
|
||||||
roles = [
|
roles = [
|
||||||
@ -826,6 +833,9 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
|
# Ensure the part "variant" tree is correctly structured
|
||||||
|
Part.objects.rebuild()
|
||||||
|
|
||||||
# Add a new part
|
# Add a new part
|
||||||
self.part = Part.objects.create(
|
self.part = Part.objects.create(
|
||||||
name='Banana',
|
name='Banana',
|
||||||
@ -880,6 +890,153 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
|
|||||||
self.assertEqual(data['in_stock'], 1100)
|
self.assertEqual(data['in_stock'], 1100)
|
||||||
self.assertEqual(data['stock_item_count'], 105)
|
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):
|
class BomItemTest(InvenTreeAPITestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -46,7 +46,7 @@ class BomItemTest(TestCase):
|
|||||||
# TODO: Tests for multi-level BOMs
|
# TODO: Tests for multi-level BOMs
|
||||||
|
|
||||||
def test_used_in(self):
|
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)
|
self.assertEqual(self.orphan.used_in_count, 1)
|
||||||
|
|
||||||
def test_self_reference(self):
|
def test_self_reference(self):
|
||||||
|
@ -251,3 +251,104 @@
|
|||||||
rght: 0
|
rght: 0
|
||||||
expiry_date: "1990-10-10"
|
expiry_date: "1990-10-10"
|
||||||
status: 70
|
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
|
||||||
|
|
@ -104,7 +104,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
|
|
||||||
response = self.get_stock()
|
response = self.get_stock()
|
||||||
|
|
||||||
self.assertEqual(len(response), 20)
|
self.assertEqual(len(response), 29)
|
||||||
|
|
||||||
def test_filter_by_part(self):
|
def test_filter_by_part(self):
|
||||||
"""
|
"""
|
||||||
@ -113,7 +113,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
|
|
||||||
response = self.get_stock(part=25)
|
response = self.get_stock(part=25)
|
||||||
|
|
||||||
self.assertEqual(len(response), 8)
|
self.assertEqual(len(response), 17)
|
||||||
|
|
||||||
response = self.get_stock(part=10004)
|
response = self.get_stock(part=10004)
|
||||||
|
|
||||||
@ -136,13 +136,13 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
self.assertEqual(len(response), 1)
|
self.assertEqual(len(response), 1)
|
||||||
|
|
||||||
response = self.get_stock(location=1, cascade=0)
|
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)
|
response = self.get_stock(location=1, cascade=1)
|
||||||
self.assertEqual(len(response), 2)
|
self.assertEqual(len(response), 9)
|
||||||
|
|
||||||
response = self.get_stock(location=7)
|
response = self.get_stock(location=7)
|
||||||
self.assertEqual(len(response), 16)
|
self.assertEqual(len(response), 18)
|
||||||
|
|
||||||
def test_filter_by_depleted(self):
|
def test_filter_by_depleted(self):
|
||||||
"""
|
"""
|
||||||
@ -153,7 +153,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
self.assertEqual(len(response), 1)
|
self.assertEqual(len(response), 1)
|
||||||
|
|
||||||
response = self.get_stock(depleted=0)
|
response = self.get_stock(depleted=0)
|
||||||
self.assertEqual(len(response), 19)
|
self.assertEqual(len(response), 28)
|
||||||
|
|
||||||
def test_filter_by_in_stock(self):
|
def test_filter_by_in_stock(self):
|
||||||
"""
|
"""
|
||||||
@ -161,7 +161,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
response = self.get_stock(in_stock=1)
|
response = self.get_stock(in_stock=1)
|
||||||
self.assertEqual(len(response), 17)
|
self.assertEqual(len(response), 26)
|
||||||
|
|
||||||
response = self.get_stock(in_stock=0)
|
response = self.get_stock(in_stock=0)
|
||||||
self.assertEqual(len(response), 3)
|
self.assertEqual(len(response), 3)
|
||||||
@ -172,7 +172,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
codes = {
|
codes = {
|
||||||
StockStatus.OK: 18,
|
StockStatus.OK: 27,
|
||||||
StockStatus.DESTROYED: 1,
|
StockStatus.DESTROYED: 1,
|
||||||
StockStatus.LOST: 1,
|
StockStatus.LOST: 1,
|
||||||
StockStatus.DAMAGED: 0,
|
StockStatus.DAMAGED: 0,
|
||||||
@ -205,7 +205,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
self.assertIsNotNone(item['serial'])
|
self.assertIsNotNone(item['serial'])
|
||||||
|
|
||||||
response = self.get_stock(serialized=0)
|
response = self.get_stock(serialized=0)
|
||||||
self.assertEqual(len(response), 8)
|
self.assertEqual(len(response), 17)
|
||||||
|
|
||||||
for item in response:
|
for item in response:
|
||||||
self.assertIsNone(item['serial'])
|
self.assertIsNone(item['serial'])
|
||||||
@ -217,7 +217,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
|
|
||||||
# First, we can assume that the 'stock expiry' feature is disabled
|
# First, we can assume that the 'stock expiry' feature is disabled
|
||||||
response = self.get_stock(expired=1)
|
response = self.get_stock(expired=1)
|
||||||
self.assertEqual(len(response), 20)
|
self.assertEqual(len(response), 29)
|
||||||
|
|
||||||
self.user.is_staff = True
|
self.user.is_staff = True
|
||||||
self.user.save()
|
self.user.save()
|
||||||
@ -232,7 +232,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
self.assertTrue(item['expired'])
|
self.assertTrue(item['expired'])
|
||||||
|
|
||||||
response = self.get_stock(expired=0)
|
response = self.get_stock(expired=0)
|
||||||
self.assertEqual(len(response), 19)
|
self.assertEqual(len(response), 28)
|
||||||
|
|
||||||
for item in response:
|
for item in response:
|
||||||
self.assertFalse(item['expired'])
|
self.assertFalse(item['expired'])
|
||||||
@ -249,7 +249,7 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
self.assertEqual(len(response), 4)
|
self.assertEqual(len(response), 4)
|
||||||
|
|
||||||
response = self.get_stock(expired=0)
|
response = self.get_stock(expired=0)
|
||||||
self.assertEqual(len(response), 16)
|
self.assertEqual(len(response), 25)
|
||||||
|
|
||||||
def test_paginate(self):
|
def test_paginate(self):
|
||||||
"""
|
"""
|
||||||
@ -290,7 +290,8 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
|
|
||||||
dataset = self.export_data({})
|
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
|
# Expected headers
|
||||||
headers = [
|
headers = [
|
||||||
@ -308,11 +309,11 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
# Now, add a filter to the results
|
# Now, add a filter to the results
|
||||||
dataset = self.export_data({'location': 1})
|
dataset = self.export_data({'location': 1})
|
||||||
|
|
||||||
self.assertEqual(len(dataset), 2)
|
self.assertEqual(len(dataset), 9)
|
||||||
|
|
||||||
dataset = self.export_data({'part': 25})
|
dataset = self.export_data({'part': 25})
|
||||||
|
|
||||||
self.assertEqual(len(dataset), 8)
|
self.assertEqual(len(dataset), 17)
|
||||||
|
|
||||||
|
|
||||||
class StockItemTest(StockAPITestCase):
|
class StockItemTest(StockAPITestCase):
|
||||||
|
@ -167,8 +167,8 @@ class StockTest(TestCase):
|
|||||||
self.assertFalse(self.drawer2.has_items())
|
self.assertFalse(self.drawer2.has_items())
|
||||||
|
|
||||||
# Drawer 3 should have three stock items
|
# Drawer 3 should have three stock items
|
||||||
self.assertEqual(self.drawer3.stock_items.count(), 16)
|
self.assertEqual(self.drawer3.stock_items.count(), 18)
|
||||||
self.assertEqual(self.drawer3.item_count, 16)
|
self.assertEqual(self.drawer3.item_count, 18)
|
||||||
|
|
||||||
def test_stock_count(self):
|
def test_stock_count(self):
|
||||||
part = Part.objects.get(pk=1)
|
part = Part.objects.get(pk=1)
|
||||||
|
@ -1746,7 +1746,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
|||||||
required: true,
|
required: true,
|
||||||
render_part_detail: true,
|
render_part_detail: true,
|
||||||
render_location_detail: true,
|
render_location_detail: true,
|
||||||
render_stock_id: false,
|
render_pk: false,
|
||||||
auto_fill: true,
|
auto_fill: true,
|
||||||
auto_fill_filters: auto_fill_filters,
|
auto_fill_filters: auto_fill_filters,
|
||||||
onSelect: function(data, field, opts) {
|
onSelect: function(data, field, opts) {
|
||||||
|
@ -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 `<span class='float-right'><small>${title}: ${pk}</small></span>`;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Renderer for "Company" model
|
// Renderer for "Company" model
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
function renderCompany(name, data, parameters={}, options={}) {
|
function renderCompany(name, data, parameters={}, options={}) {
|
||||||
@ -39,7 +57,7 @@ function renderCompany(name, data, parameters={}, options={}) {
|
|||||||
|
|
||||||
html += `<span><b>${data.name}</b></span> - <i>${data.description}</i>`;
|
html += `<span><b>${data.name}</b></span> - <i>${data.description}</i>`;
|
||||||
|
|
||||||
html += `<span class='float-right'><small>{% trans "Company ID" %}: ${data.pk}</small></span>`;
|
html += renderId('{% trans "Company ID" %}', data.pk, parameters);
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
@ -67,18 +85,6 @@ function renderStockItem(name, data, parameters={}, options={}) {
|
|||||||
part_detail = `<img src='${image}' class='select2-thumbnail'><span>${data.part_detail.full_name}</span> - `;
|
part_detail = `<img src='${image}' class='select2-thumbnail'><span>${data.part_detail.full_name}</span> - `;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = `<span class='float-right'><small>{% trans "Stock ID" %}: ${data.pk}</small></span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
var render_location_detail = false;
|
var render_location_detail = false;
|
||||||
|
|
||||||
if ('render_location_detail' in parameters) {
|
if ('render_location_detail' in parameters) {
|
||||||
@ -88,7 +94,7 @@ function renderStockItem(name, data, parameters={}, options={}) {
|
|||||||
var location_detail = '';
|
var location_detail = '';
|
||||||
|
|
||||||
if (render_location_detail && data.location_detail) {
|
if (render_location_detail && data.location_detail) {
|
||||||
location_detail = ` - (<em>${data.location_detail.name}</em>)`;
|
location_detail = ` <small>- (<em>${data.location_detail.name}</em>)</small>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
var stock_detail = '';
|
var stock_detail = '';
|
||||||
@ -103,7 +109,10 @@ function renderStockItem(name, data, parameters={}, options={}) {
|
|||||||
|
|
||||||
var html = `
|
var html = `
|
||||||
<span>
|
<span>
|
||||||
${part_detail}${stock_detail}${location_detail}${stock_id}
|
${part_detail}
|
||||||
|
${stock_detail}
|
||||||
|
${location_detail}
|
||||||
|
${renderId('{% trans "Stock ID" %}', data.pk, parameters)}
|
||||||
</span>
|
</span>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -183,7 +192,7 @@ function renderPart(name, data, parameters={}, options={}) {
|
|||||||
<small>
|
<small>
|
||||||
${stock_data}
|
${stock_data}
|
||||||
${extra}
|
${extra}
|
||||||
{% trans "Part ID" %}: ${data.pk}
|
${renderId('{% trans "Part ID" $}', data.pk, parameters)}
|
||||||
</small>
|
</small>
|
||||||
</span>`;
|
</span>`;
|
||||||
|
|
||||||
@ -245,13 +254,7 @@ function renderPurchaseOrder(name, data, parameters={}, options={}) {
|
|||||||
html += ` - <em>${data.description}</em>`;
|
html += ` - <em>${data.description}</em>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
html += `
|
html += renderId('{% trans "Order ID" %}', data.pk, parameters);
|
||||||
<span class='float-right'>
|
|
||||||
<small>
|
|
||||||
{% trans "Order ID" %}: ${data.pk}
|
|
||||||
</small>
|
|
||||||
</span>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
@ -277,12 +280,7 @@ function renderSalesOrder(name, data, parameters={}, options={}) {
|
|||||||
html += ` - <em>${data.description}</em>`;
|
html += ` - <em>${data.description}</em>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
html += `
|
html += renderId('{% trans "Order ID" %}', data.pk, parameters);
|
||||||
<span class='float-right'>
|
|
||||||
<small>
|
|
||||||
{% trans "Order ID" %}: ${data.pk}
|
|
||||||
</small>
|
|
||||||
</span>`;
|
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
@ -491,13 +491,50 @@ function duplicateBom(part_id, options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Construct a "badge" label showing stock information for this particular part
|
||||||
|
*/
|
||||||
function partStockLabel(part, options={}) {
|
function partStockLabel(part, options={}) {
|
||||||
|
|
||||||
if (part.in_stock) {
|
if (part.in_stock) {
|
||||||
return `<span class='badge rounded-pill bg-success ${options.classes}'>{% trans "Stock" %}: ${part.in_stock}</span>`;
|
// 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 `<span class='badge rounded-pill bg-warning ${options.classes}'>{% trans "Low stock" %}: ${part.in_stock}${part.units}</span>`;
|
||||||
|
} else if (part.unallocated_stock == 0) {
|
||||||
|
if (part.ordering) {
|
||||||
|
// There is no available stock, but stock is on order
|
||||||
|
return `<span class='badge rounded-pill bg-info ${options.classes}'>{% trans "On Order" %}: ${part.ordering}${part.units}</span>`;
|
||||||
|
} else if (part.building) {
|
||||||
|
// There is no available stock, but stock is being built
|
||||||
|
return `<span class='badge rounded-pill bg-info ${options.classes}'>{% trans "Building" %}: ${part.building}${part.units}</span>`;
|
||||||
|
} else {
|
||||||
|
// There is no available stock at all
|
||||||
|
return `<span class='badge rounded-pill bg-warning ${options.classes}'>{% trans "No stock available" %}</span>`;
|
||||||
|
}
|
||||||
|
} else if (part.unallocated_stock < part.in_stock) {
|
||||||
|
// Unallocated quanttiy is less than total quantity
|
||||||
|
return `<span class='badge rounded-pill bg-success ${options.classes}'>{% trans "Available" %}: ${part.unallocated_stock}/${part.in_stock}${part.units}</span>`;
|
||||||
|
} else {
|
||||||
|
// Stock is completely available
|
||||||
|
return `<span class='badge rounded-pill bg-success ${options.classes}'>{% trans "Available" %}: ${part.unallocated_stock}${part.units}</span>`;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return `<span class='badge rounded-pill bg-danger ${options.classes}'>{% trans "No Stock" %}</span>`;
|
// There IS NO stock available for this part
|
||||||
|
|
||||||
|
if (part.ordering) {
|
||||||
|
// There is no stock, but stock is on order
|
||||||
|
return `<span class='badge rounded-pill bg-info ${options.classes}'>{% trans "On Order" %}: ${part.ordering}${part.units}</span>`;
|
||||||
|
} else if (part.building) {
|
||||||
|
// There is no stock, but stock is being built
|
||||||
|
return `<span class='badge rounded-pill bg-info ${options.classes}'>{% trans "Building" %}: ${part.building}${part.units}</span>`;
|
||||||
|
} else {
|
||||||
|
// There is no stock
|
||||||
|
return `<span class='badge rounded-pill bg-danger ${options.classes}'>{% trans "No Stock" %}</span>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1160,12 +1197,14 @@ function partGridTile(part) {
|
|||||||
|
|
||||||
if (!part.in_stock) {
|
if (!part.in_stock) {
|
||||||
stock = `<span class='badge rounded-pill bg-danger'>{% trans "No Stock" %}</span>`;
|
stock = `<span class='badge rounded-pill bg-danger'>{% trans "No Stock" %}</span>`;
|
||||||
|
} else if (!part.unallocated_stock) {
|
||||||
|
stock = `<span class='badge rounded-pill bg-warning'>{% trans "Not available" %}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
rows += `<tr><td><b>{% trans "Stock" %}</b></td><td>${stock}</td></tr>`;
|
rows += `<tr><td><b>{% trans "Stock" %}</b></td><td>${stock}</td></tr>`;
|
||||||
|
|
||||||
if (part.on_order) {
|
if (part.ordering) {
|
||||||
rows += `<tr><td><b>{$ trans "On Order" %}</b></td><td>${part.on_order}</td></tr>`;
|
rows += `<tr><td><b>{% trans "On Order" %}</b></td><td>${part.ordering}</td></tr>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part.building) {
|
if (part.building) {
|
||||||
@ -1322,31 +1361,47 @@ function loadPartTable(table, url, options={}) {
|
|||||||
columns.push(col);
|
columns.push(col);
|
||||||
|
|
||||||
col = {
|
col = {
|
||||||
field: 'in_stock',
|
field: 'unallocated_stock',
|
||||||
title: '{% trans "Stock" %}',
|
title: '{% trans "Stock" %}',
|
||||||
searchable: false,
|
searchable: false,
|
||||||
formatter: function(value, row) {
|
formatter: function(value, row) {
|
||||||
var link = '?display=part-stock';
|
var link = '?display=part-stock';
|
||||||
|
|
||||||
if (value) {
|
if (row.in_stock) {
|
||||||
// There IS stock available for this part
|
// There IS stock available for this part
|
||||||
|
|
||||||
// Is stock "low" (below the 'minimum_stock' quantity)?
|
// Is stock "low" (below the 'minimum_stock' quantity)?
|
||||||
if (row.minimum_stock && row.minimum_stock > value) {
|
if (row.minimum_stock && row.minimum_stock > row.in_stock) {
|
||||||
value += `<span class='badge badge-right rounded-pill bg-warning'>{% trans "Low stock" %}</span>`;
|
value += `<span class='badge badge-right rounded-pill bg-warning'>{% trans "Low stock" %}</span>`;
|
||||||
|
} else if (value == 0) {
|
||||||
|
if (row.ordering) {
|
||||||
|
// There is no available stock, but stock is on order
|
||||||
|
value = `0<span class='badge badge-right rounded-pill bg-info'>{% trans "On Order" %}: ${row.ordering}</span>`;
|
||||||
|
link = '?display=purchase-orders';
|
||||||
|
} else if (row.building) {
|
||||||
|
// There is no available stock, but stock is being built
|
||||||
|
value = `0<span class='badge badge-right rounded-pill bg-info'>{% trans "Building" %}: ${row.building}</span>`;
|
||||||
|
link = '?display=build-orders';
|
||||||
|
} else {
|
||||||
|
// There is no available stock
|
||||||
|
value = `0<span class='badge badge-right rounded-pill bg-warning'>{% trans "No stock available" %}</span>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (row.on_order) {
|
|
||||||
// There is no stock available, but stock is on order
|
|
||||||
value = `0<span class='badge badge-right rounded-pill bg-info'>{% trans "On Order" %}: ${row.on_order}</span>`;
|
|
||||||
link = '?display=purchase-orders';
|
|
||||||
} else if (row.building) {
|
|
||||||
// There is no stock available, but stock is being built
|
|
||||||
value = `0<span class='badge badge-right rounded-pill bg-info'>{% trans "Building" %}: ${row.building}</span>`;
|
|
||||||
link = '?display=build-orders';
|
|
||||||
} else {
|
} else {
|
||||||
// There is no stock available
|
// There IS NO stock available for this part
|
||||||
value = `0<span class='badge badge-right rounded-pill bg-danger'>{% trans "No Stock" %}</span>`;
|
|
||||||
|
if (row.ordering) {
|
||||||
|
// There is no stock, but stock is on order
|
||||||
|
value = `0<span class='badge badge-right rounded-pill bg-info'>{% trans "On Order" %}: ${row.ordering}</span>`;
|
||||||
|
link = '?display=purchase-orders';
|
||||||
|
} else if (row.building) {
|
||||||
|
// There is no stock, but stock is being built
|
||||||
|
value = `0<span class='badge badge-right rounded-pill bg-info'>{% trans "Building" %}: ${row.building}</span>`;
|
||||||
|
link = '?display=build-orders';
|
||||||
|
} else {
|
||||||
|
// There is no stock
|
||||||
|
value = `0<span class='badge badge-right rounded-pill bg-danger'>{% trans "No Stock" %}</span>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderLink(value, `/part/${row.pk}/${link}`);
|
return renderLink(value, `/part/${row.pk}/${link}`);
|
||||||
|
@ -132,6 +132,7 @@ function updateSearch() {
|
|||||||
renderStockItem,
|
renderStockItem,
|
||||||
{
|
{
|
||||||
url: '/stock/item',
|
url: '/stock/item',
|
||||||
|
render_location_detail: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -232,6 +233,9 @@ function addSearchQuery(key, title, query_url, query_params, render_func, render
|
|||||||
query_params.offset = 0;
|
query_params.offset = 0;
|
||||||
query_params.limit = user_settings.SEARCH_PREVIEW_RESULTS;
|
query_params.limit = user_settings.SEARCH_PREVIEW_RESULTS;
|
||||||
|
|
||||||
|
// Do not display "pk" value for search results
|
||||||
|
render_params.render_pk = false;
|
||||||
|
|
||||||
// Add the result group to the panel
|
// Add the result group to the panel
|
||||||
$('#offcanvas-search').find('#search-results').append(`
|
$('#offcanvas-search').find('#search-results').append(`
|
||||||
<div class='search-result-group-wrapper' id='search-results-wrapper-${key}'></div>
|
<div class='search-result-group-wrapper' id='search-results-wrapper-${key}'></div>
|
||||||
|
@ -427,12 +427,16 @@ function getAvailableTableFilters(tableKey) {
|
|||||||
},
|
},
|
||||||
has_stock: {
|
has_stock: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Stock available" %}',
|
title: '{% trans "In stock" %}',
|
||||||
},
|
},
|
||||||
low_stock: {
|
low_stock: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Low stock" %}',
|
title: '{% trans "Low stock" %}',
|
||||||
},
|
},
|
||||||
|
unallocated_stock: {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "Available stock" %}',
|
||||||
|
},
|
||||||
assembly: {
|
assembly: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Assembly" %}',
|
title: '{% trans "Assembly" %}',
|
||||||
|
Loading…
Reference in New Issue
Block a user