mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-02 13:28:49 +00:00
Cherry pick changes from fe0d9c19230013471a2100fbc1f41c7694f318fa (#5917)
* Cherry pick changes from fe0d9c19230013471a2100fbc1f41c7694f318fa * Bump version number * Quick fix for unit test - Sometimes the items are returned in a different order, maybe?
This commit is contained in:
parent
57e9497da1
commit
1d05e8d4e3
@ -18,7 +18,7 @@ from dulwich.repo import NotGitRepository, Repo
|
|||||||
from .api_version import INVENTREE_API_VERSION
|
from .api_version import INVENTREE_API_VERSION
|
||||||
|
|
||||||
# InvenTree software version
|
# InvenTree software version
|
||||||
INVENTREE_SW_VERSION = "0.12.9"
|
INVENTREE_SW_VERSION = "0.12.10"
|
||||||
|
|
||||||
# Discover git
|
# Discover git
|
||||||
try:
|
try:
|
||||||
|
@ -1006,7 +1006,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Filter out "serialized" stock items, these cannot be auto-allocated
|
# Filter out "serialized" stock items, these cannot be auto-allocated
|
||||||
available_stock = available_stock.filter(Q(serial=None) | Q(serial=''))
|
available_stock = available_stock.filter(Q(serial=None) | Q(serial='')).distinct()
|
||||||
|
|
||||||
if location:
|
if location:
|
||||||
# Filter only stock items located "below" the specified location
|
# Filter only stock items located "below" the specified location
|
||||||
|
@ -385,7 +385,7 @@ class SupplierPartList(ListCreateDestroyAPIView):
|
|||||||
company = params.get('company', None)
|
company = params.get('company', None)
|
||||||
|
|
||||||
if company is not None:
|
if company is not None:
|
||||||
queryset = queryset.filter(Q(manufacturer_part__manufacturer=company) | Q(supplier=company))
|
queryset = queryset.filter(Q(manufacturer_part__manufacturer=company) | Q(supplier=company)).distinct()
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@ -209,13 +209,13 @@ class Company(InvenTreeNotesMixin, MetadataMixin, models.Model):
|
|||||||
@property
|
@property
|
||||||
def parts(self):
|
def parts(self):
|
||||||
"""Return SupplierPart objects which are supplied or manufactured by this company."""
|
"""Return SupplierPart objects which are supplied or manufactured by this company."""
|
||||||
return SupplierPart.objects.filter(Q(supplier=self.id) | Q(manufacturer_part__manufacturer=self.id))
|
return SupplierPart.objects.filter(Q(supplier=self.id) | Q(manufacturer_part__manufacturer=self.id)).distinct()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stock_items(self):
|
def stock_items(self):
|
||||||
"""Return a list of all stock items supplied or manufactured by this company."""
|
"""Return a list of all stock items supplied or manufactured by this company."""
|
||||||
stock = apps.get_model('stock', 'StockItem')
|
stock = apps.get_model('stock', 'StockItem')
|
||||||
return stock.objects.filter(Q(supplier_part__supplier=self.id) | Q(supplier_part__manufacturer_part__manufacturer=self.id)).all()
|
return stock.objects.filter(Q(supplier_part__supplier=self.id) | Q(supplier_part__manufacturer_part__manufacturer=self.id)).distinct()
|
||||||
|
|
||||||
|
|
||||||
class CompanyAttachment(InvenTreeAttachment):
|
class CompanyAttachment(InvenTreeAttachment):
|
||||||
|
@ -935,8 +935,8 @@ class PartFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.exclude(q_a | q_b)
|
return queryset.exclude(q_a | q_b)
|
||||||
else:
|
|
||||||
return queryset.filter(q_a | q_b)
|
return queryset.filter(q_a | q_b).distinct()
|
||||||
|
|
||||||
stocktake = rest_filters.BooleanFilter(label="Has stocktake", method='filter_has_stocktake')
|
stocktake = rest_filters.BooleanFilter(label="Has stocktake", method='filter_has_stocktake')
|
||||||
|
|
||||||
@ -1153,7 +1153,7 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
|
|||||||
# Return any relationship which points to the part in question
|
# Return any relationship which points to the part in question
|
||||||
relation_filter = Q(part_1=related_part) | Q(part_2=related_part)
|
relation_filter = Q(part_1=related_part) | Q(part_2=related_part)
|
||||||
|
|
||||||
for relation in PartRelated.objects.filter(relation_filter):
|
for relation in PartRelated.objects.filter(relation_filter).distinct():
|
||||||
|
|
||||||
if relation.part_1.pk != pk:
|
if relation.part_1.pk != pk:
|
||||||
part_ids.add(relation.part_1.pk)
|
part_ids.add(relation.part_1.pk)
|
||||||
@ -1333,8 +1333,7 @@ class PartRelatedList(ListCreateAPI):
|
|||||||
if part is not None:
|
if part is not None:
|
||||||
try:
|
try:
|
||||||
part = Part.objects.get(pk=part)
|
part = Part.objects.get(pk=part)
|
||||||
|
queryset = queryset.filter(Q(part_1=part) | Q(part_2=part)).distinct()
|
||||||
queryset = queryset.filter(Q(part_1=part) | Q(part_2=part))
|
|
||||||
|
|
||||||
except (ValueError, Part.DoesNotExist):
|
except (ValueError, Part.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
@ -1373,8 +1372,8 @@ class PartParameterTemplateFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.exclude(Q(choices=None) | Q(choices=''))
|
return queryset.exclude(Q(choices=None) | Q(choices=''))
|
||||||
else:
|
|
||||||
return queryset.filter(Q(choices=None) | Q(choices=''))
|
return queryset.filter(Q(choices=None) | Q(choices='')).distinct()
|
||||||
|
|
||||||
has_units = rest_filters.BooleanFilter(
|
has_units = rest_filters.BooleanFilter(
|
||||||
method='filter_has_units',
|
method='filter_has_units',
|
||||||
@ -1386,8 +1385,8 @@ class PartParameterTemplateFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.exclude(Q(units=None) | Q(units=''))
|
return queryset.exclude(Q(units=None) | Q(units=''))
|
||||||
else:
|
|
||||||
return queryset.filter(Q(units=None) | Q(units=''))
|
return queryset.filter(Q(units=None) | Q(units='')).distinct()
|
||||||
|
|
||||||
|
|
||||||
class PartParameterTemplateList(ListCreateAPI):
|
class PartParameterTemplateList(ListCreateAPI):
|
||||||
@ -1653,8 +1652,8 @@ class BomFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.exclude(q_a | q_b)
|
return queryset.exclude(q_a | q_b)
|
||||||
else:
|
|
||||||
return queryset.filter(q_a | q_b)
|
return queryset.filter(q_a | q_b).distinct()
|
||||||
|
|
||||||
|
|
||||||
class BomMixin:
|
class BomMixin:
|
||||||
|
@ -405,10 +405,9 @@ class StockFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
# Filter StockItem with either build allocations or sales order allocations
|
# Filter StockItem with either build allocations or sales order allocations
|
||||||
return queryset.filter(Q(sales_order_allocations__isnull=False) | Q(allocations__isnull=False))
|
return queryset.filter(Q(sales_order_allocations__isnull=False) | Q(allocations__isnull=False)).distinct()
|
||||||
else:
|
# Filter StockItem without build allocations or sales order allocations
|
||||||
# Filter StockItem without build allocations or sales order allocations
|
return queryset.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True))
|
||||||
return queryset.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True))
|
|
||||||
|
|
||||||
expired = rest_filters.BooleanFilter(label='Expired', method='filter_expired')
|
expired = rest_filters.BooleanFilter(label='Expired', method='filter_expired')
|
||||||
|
|
||||||
@ -476,8 +475,8 @@ class StockFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.exclude(q)
|
return queryset.exclude(q)
|
||||||
else:
|
|
||||||
return queryset.filter(q)
|
return queryset.filter(q).distinct()
|
||||||
|
|
||||||
has_batch = rest_filters.BooleanFilter(label='Has batch code', method='filter_has_batch')
|
has_batch = rest_filters.BooleanFilter(label='Has batch code', method='filter_has_batch')
|
||||||
|
|
||||||
@ -487,8 +486,8 @@ class StockFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.exclude(q)
|
return queryset.exclude(q)
|
||||||
else:
|
|
||||||
return queryset.filter(q)
|
return queryset.filter(q).distinct()
|
||||||
|
|
||||||
tracked = rest_filters.BooleanFilter(label='Tracked', method='filter_tracked')
|
tracked = rest_filters.BooleanFilter(label='Tracked', method='filter_tracked')
|
||||||
|
|
||||||
@ -504,8 +503,8 @@ class StockFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
return queryset.exclude(q_batch & q_serial)
|
return queryset.exclude(q_batch & q_serial)
|
||||||
else:
|
|
||||||
return queryset.filter(q_batch & q_serial)
|
return queryset.filter(q_batch).filter(q_serial).distinct()
|
||||||
|
|
||||||
installed = rest_filters.BooleanFilter(label='Installed in other stock item', method='filter_installed')
|
installed = rest_filters.BooleanFilter(label='Installed in other stock item', method='filter_installed')
|
||||||
|
|
||||||
@ -996,7 +995,9 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
|
|||||||
company = params.get('company', None)
|
company = params.get('company', None)
|
||||||
|
|
||||||
if company is not None:
|
if company is not None:
|
||||||
queryset = queryset.filter(Q(supplier_part__supplier=company) | Q(supplier_part__manufacturer_part__manufacturer=company))
|
queryset = queryset.filter(
|
||||||
|
Q(supplier_part__supplier=company) | Q(supplier_part__manufacturer_part__manufacturer=company).distinct()
|
||||||
|
)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import tablib
|
|||||||
from djmoney.money import Money
|
from djmoney.money import Money
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
|
import build.models
|
||||||
import company.models
|
import company.models
|
||||||
import part.models
|
import part.models
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
@ -557,6 +558,103 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
|
|
||||||
self.assertEqual(len(dataset), 17)
|
self.assertEqual(len(dataset), 17)
|
||||||
|
|
||||||
|
def test_filter_by_allocated(self):
|
||||||
|
"""Test that we can filter by "allocated" status:
|
||||||
|
|
||||||
|
- Only return stock items which are 'allocated'
|
||||||
|
- Either to a build order or sales order
|
||||||
|
- Test that the results are "distinct" (no duplicated results)
|
||||||
|
- Ref: https://github.com/inventree/InvenTree/pull/5916
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a build order to allocate to
|
||||||
|
assembly = part.models.Part.objects.create(name='F Assembly', description='Assembly for filter test', assembly=True)
|
||||||
|
component = part.models.Part.objects.create(name='F Component', description='Component for filter test', component=True)
|
||||||
|
bom_item = part.models.BomItem.objects.create(part=assembly, sub_part=component, quantity=10)
|
||||||
|
|
||||||
|
# Create two build orders
|
||||||
|
bo_1 = build.models.Build.objects.create(part=assembly, quantity=10)
|
||||||
|
bo_2 = build.models.Build.objects.create(part=assembly, quantity=20)
|
||||||
|
|
||||||
|
# Test that two distinct build line items are created automatically
|
||||||
|
self.assertEqual(bo_1.build_lines.count(), 1)
|
||||||
|
self.assertEqual(bo_2.build_lines.count(), 1)
|
||||||
|
self.assertEqual(build.models.BuildLine.objects.filter(bom_item=bom_item).count(), 2)
|
||||||
|
|
||||||
|
build_line_1 = bo_1.build_lines.first()
|
||||||
|
build_line_2 = bo_2.build_lines.first()
|
||||||
|
|
||||||
|
# Allocate stock
|
||||||
|
location = StockLocation.objects.first()
|
||||||
|
stock_1 = StockItem.objects.create(part=component, quantity=100, location=location)
|
||||||
|
stock_2 = StockItem.objects.create(part=component, quantity=100, location=location)
|
||||||
|
stock_3 = StockItem.objects.create(part=component, quantity=100, location=location)
|
||||||
|
|
||||||
|
# Allocate stock_1 to two build orders
|
||||||
|
build.models.BuildItem.objects.create(
|
||||||
|
stock_item=stock_1,
|
||||||
|
build_line=build_line_1,
|
||||||
|
quantity=5
|
||||||
|
)
|
||||||
|
|
||||||
|
build.models.BuildItem.objects.create(
|
||||||
|
stock_item=stock_1,
|
||||||
|
build_line=build_line_2,
|
||||||
|
quantity=5
|
||||||
|
)
|
||||||
|
|
||||||
|
# Allocate stock_2 to 1 build orders
|
||||||
|
build.models.BuildItem.objects.create(
|
||||||
|
stock_item=stock_2,
|
||||||
|
build_line=build_line_1,
|
||||||
|
quantity=5
|
||||||
|
)
|
||||||
|
|
||||||
|
url = reverse('api-stock-list')
|
||||||
|
|
||||||
|
# 3 items when just filtering by part
|
||||||
|
response = self.get(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
"part": component.pk,
|
||||||
|
"in_stock": True
|
||||||
|
},
|
||||||
|
expected_code=200
|
||||||
|
)
|
||||||
|
self.assertEqual(len(response.data), 3)
|
||||||
|
|
||||||
|
# 1 item when filtering by "not allocated"
|
||||||
|
response = self.get(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
"part": component.pk,
|
||||||
|
"in_stock": True,
|
||||||
|
"allocated": False,
|
||||||
|
},
|
||||||
|
expected_code=200
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(response.data), 1)
|
||||||
|
self.assertEqual(response.data[0]["pk"], stock_3.pk)
|
||||||
|
|
||||||
|
# 2 items when filtering by "allocated"
|
||||||
|
response = self.get(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
"part": component.pk,
|
||||||
|
"in_stock": True,
|
||||||
|
"allocated": True,
|
||||||
|
},
|
||||||
|
expected_code=200
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(response.data), 2)
|
||||||
|
|
||||||
|
ids = [item["pk"] for item in response.data]
|
||||||
|
|
||||||
|
self.assertIn(stock_1.pk, ids)
|
||||||
|
self.assertIn(stock_2.pk, ids)
|
||||||
|
|
||||||
def test_query_count(self):
|
def test_query_count(self):
|
||||||
"""Test that the number of queries required to fetch stock items is reasonable."""
|
"""Test that the number of queries required to fetch stock items is reasonable."""
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user