mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-17 01:38:19 +00:00
[refactor] Optional prefetch (#11012)
* Automatic prefetch of related fields for enable_filter - Allows us to *not* prefetch fields (expensive) when they are not going to be used - Enables re-usable components for common detail fields * Refactor "project_code_detail" filter into common component - Automatically apply correct prefetch fields * Refactor 'parameters' annotation - add 'enable_parameters_filter' function - Prefetch parameters only when needed - Refactor / consolidate code * Refactor SupplierPartSerializer - Make fields switchable - Ensure correct prefetch_related * Refactor serializer for ManufacturerPart * Refactor BuildSerializer * Refactor PurchaseOrderSerializer * Refactor SalesOrderSerializer * Refactor ReturnOrderSerializer * Remove debug statements * Tweaks * Simplify custom filterable fields * Bump API version * Fix for data export * Additional unit tests * Remove unused "prefetch_func" option * Refactor PurchaseOrderLineItemList * Refactor SalesOrderLineItemList * Refactor ReturnOrderLineItem * Cleanup "pretty_name" * Fix for build list * Refactoring StockItem API endpoint - Needs significant work still * Refactoring for BuildLineSerializer * Keep all optional fields when exporting data * Improve "UserRoles" API endpoint - Prefetch roles - Prevents significant number of db hits * Prefetch Parameter API list * Bug fix for exporting logic * Specify InvenTreeOutputOption * Optional prefetch for primary_address * Fix typing * Fix unit test * fixes for playwright tests * Update Part API - Improved prefetching * Fix for prefetch
This commit is contained in:
@@ -1,11 +1,17 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 431
|
INVENTREE_API_VERSION = 432
|
||||||
"""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."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v432 -> 2025-12-15 : https://github.com/inventree/InvenTree/pull/11012
|
||||||
|
- The "part_detail" field on the SupplierPart API endpoint is now optional
|
||||||
|
- The "supplier_detail" field on the SupplierPart API endpoint is now optional
|
||||||
|
- The "manufacturer_detail" field on the ManufacturerPart API endpoint is now optional
|
||||||
|
- The "part_detail" field on the StockItem API is now disabled by default
|
||||||
|
|
||||||
v431 -> 2025-12-14 : https://github.com/inventree/InvenTree/pull/11006
|
v431 -> 2025-12-14 : https://github.com/inventree/InvenTree/pull/11006
|
||||||
- Remove duplicate "address" field on the Company API endpoint
|
- Remove duplicate "address" field on the Company API endpoint
|
||||||
- Make "primary_address" field optional on the Company API endpoint
|
- Make "primary_address" field optional on the Company API endpoint
|
||||||
|
|||||||
@@ -258,6 +258,19 @@ class OutputOptionsMixin:
|
|||||||
|
|
||||||
return serializer
|
return serializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Return the queryset with output options applied.
|
||||||
|
|
||||||
|
This automatically applies any prefetching defined against the optional fields.
|
||||||
|
"""
|
||||||
|
queryset = super().get_queryset()
|
||||||
|
serializer = self.get_serializer()
|
||||||
|
|
||||||
|
if isinstance(serializer, FilterableSerializerMixin):
|
||||||
|
queryset = serializer.prefetch_queryset(queryset)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class SerializerContextMixin:
|
class SerializerContextMixin:
|
||||||
"""Mixin to add context to serializer."""
|
"""Mixin to add context to serializer."""
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from django.conf import settings
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import QuerySet
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from djmoney.contrib.django_rest_framework.fields import MoneyField
|
from djmoney.contrib.django_rest_framework.fields import MoneyField
|
||||||
@@ -44,11 +45,15 @@ class FilterableSerializerField:
|
|||||||
is_filterable = None
|
is_filterable = None
|
||||||
is_filterable_vals = {}
|
is_filterable_vals = {}
|
||||||
|
|
||||||
|
# Options for automatic queryset prefetching
|
||||||
|
prefetch_fields: Optional[list[str]] = None
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Initialize the serializer."""
|
"""Initialize the serializer."""
|
||||||
if self.is_filterable is None: # Materialize parameters for later usage
|
|
||||||
self.is_filterable = kwargs.pop('is_filterable', None)
|
self.is_filterable = kwargs.pop('is_filterable', None)
|
||||||
self.is_filterable_vals = kwargs.pop('is_filterable_vals', {})
|
self.is_filterable_vals = kwargs.pop('is_filterable_vals', {})
|
||||||
|
self.prefetch_fields = kwargs.pop('prefetch_fields', None)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@@ -57,6 +62,7 @@ def enable_filter(
|
|||||||
default_include: bool = False,
|
default_include: bool = False,
|
||||||
filter_name: Optional[str] = None,
|
filter_name: Optional[str] = None,
|
||||||
filter_by_query: bool = True,
|
filter_by_query: bool = True,
|
||||||
|
prefetch_fields: Optional[list[str]] = None,
|
||||||
):
|
):
|
||||||
"""Decorator for marking a serializer field as filterable.
|
"""Decorator for marking a serializer field as filterable.
|
||||||
|
|
||||||
@@ -67,6 +73,7 @@ def enable_filter(
|
|||||||
default_include (bool): If True, the field will be included by default unless explicitly excluded. If False, the field will be excluded by default unless explicitly included.
|
default_include (bool): If True, the field will be included by default unless explicitly excluded. If False, the field will be excluded by default unless explicitly included.
|
||||||
filter_name (str, optional): The name of the filter parameter to use in the URL. If None, the function name of the (decorated) function will be used.
|
filter_name (str, optional): The name of the filter parameter to use in the URL. If None, the function name of the (decorated) function will be used.
|
||||||
filter_by_query (bool): If True, also look for filter parameters in the request query parameters.
|
filter_by_query (bool): If True, also look for filter parameters in the request query parameters.
|
||||||
|
prefetch_fields (list of str, optional): List of related fields to prefetch when this field is included. This can be used to optimize database queries.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The decorated serializer field, marked as filterable.
|
The decorated serializer field, marked as filterable.
|
||||||
@@ -84,6 +91,10 @@ def enable_filter(
|
|||||||
'filter_name': filter_name if filter_name else func.field_name,
|
'filter_name': filter_name if filter_name else func.field_name,
|
||||||
'filter_by_query': filter_by_query,
|
'filter_by_query': filter_by_query,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Attach queryset prefetching information
|
||||||
|
func._kwargs['prefetch_fields'] = prefetch_fields
|
||||||
|
|
||||||
return func
|
return func
|
||||||
|
|
||||||
|
|
||||||
@@ -113,6 +124,34 @@ class FilterableSerializerMixin:
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.do_filtering()
|
self.do_filtering()
|
||||||
|
|
||||||
|
def prefetch_queryset(self, queryset: QuerySet) -> QuerySet:
|
||||||
|
"""Apply any prefetching to the queryset based on the optionally included fields.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
queryset: The original queryset.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The modified queryset with prefetching applied.
|
||||||
|
"""
|
||||||
|
# Gather up the set of simple 'prefetch' fields and functions
|
||||||
|
prefetch_fields = set()
|
||||||
|
|
||||||
|
filterable_fields = [
|
||||||
|
field
|
||||||
|
for field in self.fields.values()
|
||||||
|
if getattr(field, 'is_filterable', None)
|
||||||
|
]
|
||||||
|
|
||||||
|
for field in filterable_fields:
|
||||||
|
if prefetch_names := getattr(field, 'prefetch_fields', None):
|
||||||
|
for pf in prefetch_names:
|
||||||
|
prefetch_fields.add(pf)
|
||||||
|
|
||||||
|
if prefetch_fields and len(prefetch_fields) > 0:
|
||||||
|
queryset = queryset.prefetch_related(*list(prefetch_fields))
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
def gather_filters(self, kwargs) -> None:
|
def gather_filters(self, kwargs) -> None:
|
||||||
"""Gather filterable fields through introspection."""
|
"""Gather filterable fields through introspection."""
|
||||||
# Fast exit if this has already been done or would not have any effect
|
# Fast exit if this has already been done or would not have any effect
|
||||||
@@ -168,6 +207,10 @@ class FilterableSerializerMixin:
|
|||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Skip filtering when exporting data - leave all fields intact
|
||||||
|
if getattr(self, '_exporting_data', False):
|
||||||
|
return
|
||||||
|
|
||||||
# Throw out fields which are not requested (either by default or explicitly)
|
# Throw out fields which are not requested (either by default or explicitly)
|
||||||
for k, v in self.filter_target_values.items():
|
for k, v in self.filter_target_values.items():
|
||||||
# See `enable_filter` where` is_filterable and is_filterable_vals are set
|
# See `enable_filter` where` is_filterable and is_filterable_vals are set
|
||||||
|
|||||||
@@ -319,15 +319,6 @@ class BuildMixin:
|
|||||||
|
|
||||||
queryset = build.serializers.BuildSerializer.annotate_queryset(queryset)
|
queryset = build.serializers.BuildSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
queryset = queryset.prefetch_related(
|
|
||||||
'responsible',
|
|
||||||
'issued_by',
|
|
||||||
'build_lines',
|
|
||||||
'part',
|
|
||||||
'part__pricing_data',
|
|
||||||
'project_code',
|
|
||||||
)
|
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
@@ -568,26 +559,27 @@ class BuildLineOutputOptions(OutputConfiguration):
|
|||||||
InvenTreeOutputOption(
|
InvenTreeOutputOption(
|
||||||
'bom_item_detail',
|
'bom_item_detail',
|
||||||
description='Include detailed information about the BOM item linked to this build line.',
|
description='Include detailed information about the BOM item linked to this build line.',
|
||||||
default=True,
|
default=False,
|
||||||
),
|
),
|
||||||
InvenTreeOutputOption(
|
InvenTreeOutputOption(
|
||||||
'assembly_detail',
|
'assembly_detail',
|
||||||
description='Include brief details of the assembly (parent part) related to the BOM item in this build line.',
|
description='Include brief details of the assembly (parent part) related to the BOM item in this build line.',
|
||||||
default=True,
|
default=False,
|
||||||
),
|
),
|
||||||
InvenTreeOutputOption(
|
InvenTreeOutputOption(
|
||||||
'part_detail',
|
'part_detail',
|
||||||
description='Include detailed information about the specific part being built or consumed in this build line.',
|
description='Include detailed information about the specific part being built or consumed in this build line.',
|
||||||
default=True,
|
default=False,
|
||||||
),
|
),
|
||||||
InvenTreeOutputOption(
|
InvenTreeOutputOption(
|
||||||
'build_detail',
|
'build_detail',
|
||||||
description='Include detailed information about the associated build order.',
|
description='Include detailed information about the associated build order.',
|
||||||
|
default=False,
|
||||||
),
|
),
|
||||||
InvenTreeOutputOption(
|
InvenTreeOutputOption(
|
||||||
'allocations',
|
'allocations',
|
||||||
description='Include allocation details showing which stock items are allocated to this build line.',
|
description='Include allocation details showing which stock items are allocated to this build line.',
|
||||||
default=True,
|
default=False,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -905,6 +897,7 @@ class BuildItemOutputOptions(OutputConfiguration):
|
|||||||
InvenTreeOutputOption('location_detail'),
|
InvenTreeOutputOption('location_detail'),
|
||||||
InvenTreeOutputOption('stock_detail'),
|
InvenTreeOutputOption('stock_detail'),
|
||||||
InvenTreeOutputOption('build_detail'),
|
InvenTreeOutputOption('build_detail'),
|
||||||
|
InvenTreeOutputOption('supplier_part_detail'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -927,26 +920,9 @@ class BuildItemList(
|
|||||||
"""Override the queryset method, to perform custom prefetch."""
|
"""Override the queryset method, to perform custom prefetch."""
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
|
|
||||||
queryset = queryset.select_related(
|
queryset = queryset.select_related('install_into').prefetch_related(
|
||||||
'build_line',
|
'build_line', 'build_line__build', 'build_line__bom_item'
|
||||||
'build_line__build',
|
)
|
||||||
'build_line__build__part',
|
|
||||||
'build_line__build__responsible',
|
|
||||||
'build_line__build__issued_by',
|
|
||||||
'build_line__build__project_code',
|
|
||||||
'build_line__build__part__pricing_data',
|
|
||||||
'build_line__bom_item',
|
|
||||||
'build_line__bom_item__part',
|
|
||||||
'build_line__bom_item__sub_part',
|
|
||||||
'install_into',
|
|
||||||
'stock_item',
|
|
||||||
'stock_item__location',
|
|
||||||
'stock_item__part',
|
|
||||||
'stock_item__supplier_part__part',
|
|
||||||
'stock_item__supplier_part__supplier',
|
|
||||||
'stock_item__supplier_part__manufacturer_part',
|
|
||||||
'stock_item__supplier_part__manufacturer_part__manufacturer',
|
|
||||||
).prefetch_related('stock_item__location__tags', 'stock_item__tags')
|
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|||||||
@@ -22,18 +22,16 @@ from rest_framework import serializers
|
|||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
import build.tasks
|
import build.tasks
|
||||||
import common.serializers
|
import common.filters
|
||||||
import common.settings
|
import common.settings
|
||||||
import company.serializers
|
import company.serializers
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import part.filters
|
import part.filters
|
||||||
import part.serializers as part_serializers
|
import part.serializers as part_serializers
|
||||||
from common.serializers import ProjectCodeSerializer
|
|
||||||
from common.settings import get_global_setting
|
from common.settings import get_global_setting
|
||||||
from generic.states.fields import InvenTreeCustomStatusSerializerMixin
|
from generic.states.fields import InvenTreeCustomStatusSerializerMixin
|
||||||
from InvenTree.mixins import DataImportExportSerializerMixin
|
from InvenTree.mixins import DataImportExportSerializerMixin
|
||||||
from InvenTree.serializers import (
|
from InvenTree.serializers import (
|
||||||
FilterableCharField,
|
|
||||||
FilterableSerializerMixin,
|
FilterableSerializerMixin,
|
||||||
InvenTreeDecimalField,
|
InvenTreeDecimalField,
|
||||||
InvenTreeModelSerializer,
|
InvenTreeModelSerializer,
|
||||||
@@ -124,13 +122,10 @@ class BuildSerializer(
|
|||||||
part_detail = enable_filter(
|
part_detail = enable_filter(
|
||||||
part_serializers.PartBriefSerializer(source='part', many=False, read_only=True),
|
part_serializers.PartBriefSerializer(source='part', many=False, read_only=True),
|
||||||
True,
|
True,
|
||||||
|
prefetch_fields=['part', 'part__category', 'part__pricing_data'],
|
||||||
)
|
)
|
||||||
|
|
||||||
parameters = enable_filter(
|
parameters = common.filters.enable_parameters_filter()
|
||||||
common.serializers.ParameterSerializer(many=True, read_only=True),
|
|
||||||
False,
|
|
||||||
filter_name='parameters',
|
|
||||||
)
|
|
||||||
|
|
||||||
part_name = serializers.CharField(
|
part_name = serializers.CharField(
|
||||||
source='part.name', read_only=True, label=_('Part Name')
|
source='part.name', read_only=True, label=_('Part Name')
|
||||||
@@ -144,34 +139,21 @@ class BuildSerializer(
|
|||||||
UserSerializer(source='issued_by', read_only=True),
|
UserSerializer(source='issued_by', read_only=True),
|
||||||
True,
|
True,
|
||||||
filter_name='user_detail',
|
filter_name='user_detail',
|
||||||
|
prefetch_fields=['issued_by'],
|
||||||
)
|
)
|
||||||
|
|
||||||
responsible_detail = enable_filter(
|
responsible_detail = enable_filter(
|
||||||
OwnerSerializer(source='responsible', read_only=True, allow_null=True),
|
OwnerSerializer(source='responsible', read_only=True, allow_null=True),
|
||||||
True,
|
True,
|
||||||
filter_name='user_detail',
|
filter_name='user_detail',
|
||||||
|
prefetch_fields=['responsible'],
|
||||||
)
|
)
|
||||||
|
|
||||||
barcode_hash = serializers.CharField(read_only=True)
|
barcode_hash = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
project_code_label = enable_filter(
|
project_code_label = common.filters.enable_project_label_filter()
|
||||||
FilterableCharField(
|
|
||||||
source='project_code.code',
|
|
||||||
read_only=True,
|
|
||||||
label=_('Project Code Label'),
|
|
||||||
allow_null=True,
|
|
||||||
),
|
|
||||||
True,
|
|
||||||
filter_name='project_code_detail',
|
|
||||||
)
|
|
||||||
|
|
||||||
project_code_detail = enable_filter(
|
project_code_detail = common.filters.enable_project_code_filter()
|
||||||
ProjectCodeSerializer(
|
|
||||||
source='project_code', many=False, read_only=True, allow_null=True
|
|
||||||
),
|
|
||||||
True,
|
|
||||||
filter_name='project_code_detail',
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
@@ -192,8 +174,6 @@ class BuildSerializer(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
queryset = Build.annotate_parameters(queryset)
|
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -1240,6 +1220,7 @@ class BuildItemSerializer(
|
|||||||
pricing=False,
|
pricing=False,
|
||||||
),
|
),
|
||||||
True,
|
True,
|
||||||
|
prefetch_fields=['stock_item__part'],
|
||||||
)
|
)
|
||||||
|
|
||||||
stock_item_detail = enable_filter(
|
stock_item_detail = enable_filter(
|
||||||
@@ -1255,6 +1236,13 @@ class BuildItemSerializer(
|
|||||||
),
|
),
|
||||||
True,
|
True,
|
||||||
filter_name='stock_detail',
|
filter_name='stock_detail',
|
||||||
|
prefetch_fields=[
|
||||||
|
'stock_item',
|
||||||
|
'stock_item__tags',
|
||||||
|
'stock_item__part',
|
||||||
|
'stock_item__supplier_part',
|
||||||
|
'stock_item__supplier_part__manufacturer_part',
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
location = serializers.PrimaryKeyRelatedField(
|
location = serializers.PrimaryKeyRelatedField(
|
||||||
@@ -1269,6 +1257,7 @@ class BuildItemSerializer(
|
|||||||
allow_null=True,
|
allow_null=True,
|
||||||
),
|
),
|
||||||
True,
|
True,
|
||||||
|
prefetch_fields=['stock_item__location', 'stock_item__location__tags'],
|
||||||
)
|
)
|
||||||
|
|
||||||
build_detail = enable_filter(
|
build_detail = enable_filter(
|
||||||
@@ -1280,15 +1269,32 @@ class BuildItemSerializer(
|
|||||||
allow_null=True,
|
allow_null=True,
|
||||||
),
|
),
|
||||||
True,
|
True,
|
||||||
|
prefetch_fields=[
|
||||||
|
'build_line__build',
|
||||||
|
'build_line__build__part',
|
||||||
|
'build_line__build__responsible',
|
||||||
|
'build_line__build__issued_by',
|
||||||
|
'build_line__build__project_code',
|
||||||
|
'build_line__build__part__pricing_data',
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
supplier_part_detail = company.serializers.SupplierPartSerializer(
|
supplier_part_detail = enable_filter(
|
||||||
|
company.serializers.SupplierPartSerializer(
|
||||||
label=_('Supplier Part'),
|
label=_('Supplier Part'),
|
||||||
source='stock_item.supplier_part',
|
source='stock_item.supplier_part',
|
||||||
many=False,
|
many=False,
|
||||||
read_only=True,
|
read_only=True,
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
brief=True,
|
brief=True,
|
||||||
|
),
|
||||||
|
False,
|
||||||
|
prefetch_fields=[
|
||||||
|
'stock_item__supplier_part',
|
||||||
|
'stock_item__supplier_part__supplier',
|
||||||
|
'stock_item__supplier_part__manufacturer_part',
|
||||||
|
'stock_item__supplier_part__manufacturer_part__manufacturer',
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
quantity = InvenTreeDecimalField(label=_('Allocated Quantity'))
|
quantity = InvenTreeDecimalField(label=_('Allocated Quantity'))
|
||||||
@@ -1373,7 +1379,18 @@ class BuildLineSerializer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
allocations = enable_filter(
|
allocations = enable_filter(
|
||||||
BuildItemSerializer(many=True, read_only=True, build_detail=False), True
|
BuildItemSerializer(many=True, read_only=True, build_detail=False),
|
||||||
|
True,
|
||||||
|
prefetch_fields=[
|
||||||
|
'allocations',
|
||||||
|
'allocations__stock_item',
|
||||||
|
'allocations__stock_item__part',
|
||||||
|
'allocations__stock_item__part__pricing_data',
|
||||||
|
'allocations__stock_item__supplier_part',
|
||||||
|
'allocations__stock_item__supplier_part__manufacturer_part',
|
||||||
|
'allocations__stock_item__location',
|
||||||
|
'allocations__stock_item__tags',
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
# BOM item info fields
|
# BOM item info fields
|
||||||
@@ -1417,7 +1434,7 @@ class BuildLineSerializer(
|
|||||||
part_detail=False,
|
part_detail=False,
|
||||||
can_build=False,
|
can_build=False,
|
||||||
),
|
),
|
||||||
True,
|
False,
|
||||||
)
|
)
|
||||||
|
|
||||||
assembly_detail = enable_filter(
|
assembly_detail = enable_filter(
|
||||||
@@ -1429,7 +1446,8 @@ class BuildLineSerializer(
|
|||||||
allow_null=True,
|
allow_null=True,
|
||||||
pricing=False,
|
pricing=False,
|
||||||
),
|
),
|
||||||
True,
|
False,
|
||||||
|
prefetch_fields=['bom_item__part', 'bom_item__part__pricing_data'],
|
||||||
)
|
)
|
||||||
|
|
||||||
part_detail = enable_filter(
|
part_detail = enable_filter(
|
||||||
@@ -1440,7 +1458,8 @@ class BuildLineSerializer(
|
|||||||
read_only=True,
|
read_only=True,
|
||||||
pricing=False,
|
pricing=False,
|
||||||
),
|
),
|
||||||
True,
|
False,
|
||||||
|
prefetch_fields=['bom_item__sub_part', 'bom_item__sub_part__pricing_data'],
|
||||||
)
|
)
|
||||||
|
|
||||||
category_detail = enable_filter(
|
category_detail = enable_filter(
|
||||||
@@ -1452,6 +1471,7 @@ class BuildLineSerializer(
|
|||||||
allow_null=True,
|
allow_null=True,
|
||||||
),
|
),
|
||||||
False,
|
False,
|
||||||
|
prefetch_fields=['bom_item__sub_part__category'],
|
||||||
)
|
)
|
||||||
|
|
||||||
build_detail = enable_filter(
|
build_detail = enable_filter(
|
||||||
@@ -1517,77 +1537,16 @@ class BuildLineSerializer(
|
|||||||
'bom_item__sub_part__pricing_data',
|
'bom_item__sub_part__pricing_data',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Pre-fetch related fields
|
|
||||||
queryset = queryset.prefetch_related(
|
|
||||||
'allocations',
|
|
||||||
'allocations__stock_item',
|
|
||||||
'allocations__stock_item__part',
|
|
||||||
'allocations__stock_item__supplier_part',
|
|
||||||
'allocations__stock_item__supplier_part__manufacturer_part',
|
|
||||||
'allocations__stock_item__location',
|
|
||||||
'allocations__stock_item__tags',
|
|
||||||
'bom_item',
|
|
||||||
'bom_item__part',
|
|
||||||
'bom_item__sub_part',
|
|
||||||
'bom_item__sub_part__category',
|
|
||||||
'bom_item__sub_part__stock_items',
|
|
||||||
'bom_item__sub_part__stock_items__allocations',
|
|
||||||
'bom_item__sub_part__stock_items__sales_order_allocations',
|
|
||||||
'bom_item__substitutes',
|
|
||||||
'bom_item__substitutes__part__stock_items',
|
|
||||||
'bom_item__substitutes__part__stock_items__allocations',
|
|
||||||
'bom_item__substitutes__part__stock_items__sales_order_allocations',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Defer expensive fields which we do not need for this serializer
|
# Defer expensive fields which we do not need for this serializer
|
||||||
|
|
||||||
queryset = (
|
queryset = queryset.defer(
|
||||||
queryset.defer(
|
|
||||||
'build__lft',
|
|
||||||
'build__rght',
|
|
||||||
'build__level',
|
|
||||||
'build__tree_id',
|
|
||||||
'build__destination',
|
|
||||||
'build__take_from',
|
|
||||||
'build__completed_by',
|
|
||||||
'build__sales_order',
|
|
||||||
'build__parent',
|
|
||||||
'build__notes',
|
'build__notes',
|
||||||
'build__metadata',
|
'build__metadata',
|
||||||
'build__responsible',
|
'bom_item__metadata',
|
||||||
'build__barcode_data',
|
|
||||||
'build__barcode_hash',
|
|
||||||
'build__project_code',
|
|
||||||
)
|
|
||||||
.defer('bom_item__metadata')
|
|
||||||
.defer(
|
|
||||||
'bom_item__part__lft',
|
|
||||||
'bom_item__part__rght',
|
|
||||||
'bom_item__part__level',
|
|
||||||
'bom_item__part__tree_id',
|
|
||||||
'bom_item__part__tags',
|
|
||||||
'bom_item__part__notes',
|
'bom_item__part__notes',
|
||||||
'bom_item__part__variant_of',
|
'bom_item__part__metadata',
|
||||||
'bom_item__part__revision_of',
|
|
||||||
'bom_item__part__creation_user',
|
|
||||||
'bom_item__part__bom_checked_by',
|
|
||||||
'bom_item__part__default_supplier',
|
|
||||||
'bom_item__part__responsible_owner',
|
|
||||||
)
|
|
||||||
.defer(
|
|
||||||
'bom_item__sub_part__lft',
|
|
||||||
'bom_item__sub_part__rght',
|
|
||||||
'bom_item__sub_part__level',
|
|
||||||
'bom_item__sub_part__tree_id',
|
|
||||||
'bom_item__sub_part__tags',
|
|
||||||
'bom_item__sub_part__notes',
|
'bom_item__sub_part__notes',
|
||||||
'bom_item__sub_part__variant_of',
|
'bom_item__sub_part__metadata',
|
||||||
'bom_item__sub_part__revision_of',
|
|
||||||
'bom_item__sub_part__creation_user',
|
|
||||||
'bom_item__sub_part__bom_checked_by',
|
|
||||||
'bom_item__sub_part__default_supplier',
|
|
||||||
'bom_item__sub_part__responsible_owner',
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Annotate the "allocated" quantity
|
# Annotate the "allocated" quantity
|
||||||
|
|||||||
@@ -848,7 +848,9 @@ class ParameterTemplateFilter(FilterSet):
|
|||||||
class ParameterTemplateMixin:
|
class ParameterTemplateMixin:
|
||||||
"""Mixin class for ParameterTemplate views."""
|
"""Mixin class for ParameterTemplate views."""
|
||||||
|
|
||||||
queryset = common.models.ParameterTemplate.objects.all()
|
queryset = common.models.ParameterTemplate.objects.all().prefetch_related(
|
||||||
|
'model_type'
|
||||||
|
)
|
||||||
serializer_class = common.serializers.ParameterTemplateSerializer
|
serializer_class = common.serializers.ParameterTemplateSerializer
|
||||||
permission_classes = [IsAuthenticatedOrReadScope]
|
permission_classes = [IsAuthenticatedOrReadScope]
|
||||||
|
|
||||||
@@ -891,7 +893,9 @@ class ParameterFilter(FilterSet):
|
|||||||
class ParameterMixin:
|
class ParameterMixin:
|
||||||
"""Mixin class for Parameter views."""
|
"""Mixin class for Parameter views."""
|
||||||
|
|
||||||
queryset = common.models.Parameter.objects.all()
|
queryset = common.models.Parameter.objects.all().prefetch_related(
|
||||||
|
'model_type', 'updated_by', 'template', 'template__model_type'
|
||||||
|
)
|
||||||
serializer_class = common.serializers.ParameterSerializer
|
serializer_class = common.serializers.ParameterSerializer
|
||||||
permission_classes = [IsAuthenticatedOrReadScope]
|
permission_classes = [IsAuthenticatedOrReadScope]
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ from django.db.models import (
|
|||||||
When,
|
When,
|
||||||
)
|
)
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
import InvenTree.conversion
|
import InvenTree.conversion
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
|
import InvenTree.serializers
|
||||||
|
|
||||||
|
|
||||||
def determine_content_type(content_type: str | int | None) -> ContentType | None:
|
def determine_content_type(content_type: str | int | None) -> ContentType | None:
|
||||||
@@ -310,3 +312,71 @@ def order_by_parameter(
|
|||||||
f'{prefix}parameter_value_numeric',
|
f'{prefix}parameter_value_numeric',
|
||||||
f'{prefix}parameter_value',
|
f'{prefix}parameter_value',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def enable_project_code_filter(default: bool = True):
|
||||||
|
"""Add an optional 'project_code_detail' field to an API serializer.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
filter_name: The name of the filter field.
|
||||||
|
default: If True, enable the filter by default.
|
||||||
|
|
||||||
|
If applied, this field will automatically prefetch the 'project_code' relationship.
|
||||||
|
"""
|
||||||
|
from common.serializers import ProjectCodeSerializer
|
||||||
|
|
||||||
|
return InvenTree.serializers.enable_filter(
|
||||||
|
ProjectCodeSerializer(
|
||||||
|
source='project_code', many=False, read_only=True, allow_null=True
|
||||||
|
),
|
||||||
|
default,
|
||||||
|
filter_name='project_code_detail',
|
||||||
|
prefetch_fields=['project_code'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def enable_project_label_filter(default: bool = True):
|
||||||
|
"""Add an optional 'project_code_label' field to an API serializer.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
filter_name: The name of the filter field.
|
||||||
|
default: If True, enable the filter by default.
|
||||||
|
|
||||||
|
If applied, this field will automatically prefetch the 'project_code' relationship.
|
||||||
|
"""
|
||||||
|
return InvenTree.serializers.enable_filter(
|
||||||
|
InvenTree.serializers.FilterableCharField(
|
||||||
|
source='project_code.code',
|
||||||
|
read_only=True,
|
||||||
|
label=_('Project Code Label'),
|
||||||
|
allow_null=True,
|
||||||
|
),
|
||||||
|
default,
|
||||||
|
filter_name='project_code_detail',
|
||||||
|
prefetch_fields=['project_code'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def enable_parameters_filter():
|
||||||
|
"""Add an optional 'parameters' field to an API serializer.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
source: The source field for the serializer.
|
||||||
|
filter_name: The name of the filter field.
|
||||||
|
default: If True, enable the filter by default.
|
||||||
|
|
||||||
|
If applied, this field will automatically annotate the queryset with parameter data.
|
||||||
|
"""
|
||||||
|
from common.serializers import ParameterSerializer
|
||||||
|
|
||||||
|
return InvenTree.serializers.enable_filter(
|
||||||
|
ParameterSerializer(many=True, read_only=True, allow_null=True),
|
||||||
|
False,
|
||||||
|
filter_name='parameters',
|
||||||
|
prefetch_fields=[
|
||||||
|
'parameters_list',
|
||||||
|
'parameters_list__model_type',
|
||||||
|
'parameters_list__updated_by',
|
||||||
|
'parameters_list__template',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|||||||
@@ -178,11 +178,7 @@ class ManufacturerPartMixin(SerializerContextMixin):
|
|||||||
"""Return annotated queryset for the ManufacturerPart list endpoint."""
|
"""Return annotated queryset for the ManufacturerPart list endpoint."""
|
||||||
queryset = super().get_queryset(*args, **kwargs)
|
queryset = super().get_queryset(*args, **kwargs)
|
||||||
|
|
||||||
queryset = queryset.prefetch_related(
|
queryset = queryset.prefetch_related('supplier_parts', 'tags')
|
||||||
'part', 'manufacturer', 'supplier_parts', 'tags'
|
|
||||||
)
|
|
||||||
|
|
||||||
queryset = ManufacturerPart.annotate_parameters(queryset)
|
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
@@ -304,13 +300,18 @@ class SupplierPartOutputOptions(OutputConfiguration):
|
|||||||
InvenTreeOutputOption(
|
InvenTreeOutputOption(
|
||||||
description='Include detailed information about the Supplier in the response',
|
description='Include detailed information about the Supplier in the response',
|
||||||
flag='supplier_detail',
|
flag='supplier_detail',
|
||||||
default=True,
|
default=False,
|
||||||
),
|
),
|
||||||
InvenTreeOutputOption(
|
InvenTreeOutputOption(
|
||||||
description='Include detailed information about the Manufacturer in the response',
|
description='Include detailed information about the Manufacturer in the response',
|
||||||
flag='manufacturer_detail',
|
flag='manufacturer_detail',
|
||||||
default=False,
|
default=False,
|
||||||
),
|
),
|
||||||
|
InvenTreeOutputOption(
|
||||||
|
flag='manufacturer_part_detail',
|
||||||
|
description='Include detailed information about the linked ManufacturerPart in the response',
|
||||||
|
default=False,
|
||||||
|
),
|
||||||
InvenTreeOutputOption(
|
InvenTreeOutputOption(
|
||||||
description='Format the output with a more readable (pretty) name',
|
description='Format the output with a more readable (pretty) name',
|
||||||
flag='pretty',
|
flag='pretty',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from rest_framework import serializers
|
|||||||
from sql_util.utils import SubqueryCount
|
from sql_util.utils import SubqueryCount
|
||||||
from taggit.serializers import TagListSerializerField
|
from taggit.serializers import TagListSerializerField
|
||||||
|
|
||||||
import common.serializers
|
import common.filters
|
||||||
import company.filters
|
import company.filters
|
||||||
import part.filters
|
import part.filters
|
||||||
import part.serializers as part_serializers
|
import part.serializers as part_serializers
|
||||||
@@ -163,22 +163,19 @@ class CompanySerializer(
|
|||||||
|
|
||||||
queryset = queryset.annotate(parts_supplied=SubqueryCount('supplied_parts'))
|
queryset = queryset.annotate(parts_supplied=SubqueryCount('supplied_parts'))
|
||||||
|
|
||||||
queryset = queryset.prefetch_related(
|
return queryset
|
||||||
|
|
||||||
|
primary_address = enable_filter(
|
||||||
|
AddressBriefSerializer(read_only=True, allow_null=True),
|
||||||
|
False,
|
||||||
|
filter_name='address_detail',
|
||||||
|
prefetch_fields=[
|
||||||
Prefetch(
|
Prefetch(
|
||||||
'addresses',
|
'addresses',
|
||||||
queryset=Address.objects.filter(primary=True),
|
queryset=Address.objects.filter(primary=True),
|
||||||
to_attr='primary_address_list',
|
to_attr='primary_address_list',
|
||||||
)
|
)
|
||||||
)
|
],
|
||||||
|
|
||||||
queryset = Company.annotate_parameters(queryset)
|
|
||||||
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
primary_address = enable_filter(
|
|
||||||
AddressBriefSerializer(read_only=True, allow_null=True),
|
|
||||||
True,
|
|
||||||
filter_name='address_detail',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
image = InvenTreeImageSerializerField(required=False, allow_null=True)
|
image = InvenTreeImageSerializerField(required=False, allow_null=True)
|
||||||
@@ -195,11 +192,7 @@ class CompanySerializer(
|
|||||||
help_text=_('Default currency used for this supplier'), required=True
|
help_text=_('Default currency used for this supplier'), required=True
|
||||||
)
|
)
|
||||||
|
|
||||||
parameters = enable_filter(
|
parameters = common.filters.enable_parameters_filter()
|
||||||
common.serializers.ParameterSerializer(many=True, read_only=True),
|
|
||||||
False,
|
|
||||||
filter_name='parameters',
|
|
||||||
)
|
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Save the Company instance."""
|
"""Save the Company instance."""
|
||||||
@@ -269,24 +262,14 @@ class ManufacturerPartSerializer(
|
|||||||
|
|
||||||
tags = TagListSerializerField(required=False)
|
tags = TagListSerializerField(required=False)
|
||||||
|
|
||||||
parameters = enable_filter(
|
parameters = common.filters.enable_parameters_filter()
|
||||||
common.serializers.ParameterSerializer(many=True, read_only=True),
|
|
||||||
False,
|
|
||||||
filter_name='parameters',
|
|
||||||
)
|
|
||||||
|
|
||||||
part_detail = enable_filter(
|
part_detail = enable_filter(
|
||||||
part_serializers.PartBriefSerializer(
|
part_serializers.PartBriefSerializer(
|
||||||
source='part', many=False, read_only=True, allow_null=True
|
source='part', many=False, read_only=True, allow_null=True
|
||||||
),
|
),
|
||||||
True,
|
True,
|
||||||
)
|
prefetch_fields=['part'],
|
||||||
|
|
||||||
manufacturer_detail = enable_filter(
|
|
||||||
CompanyBriefSerializer(
|
|
||||||
source='manufacturer', many=False, read_only=True, allow_null=True
|
|
||||||
),
|
|
||||||
True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
pretty_name = enable_filter(
|
pretty_name = enable_filter(
|
||||||
@@ -297,6 +280,14 @@ class ManufacturerPartSerializer(
|
|||||||
queryset=Company.objects.filter(is_manufacturer=True)
|
queryset=Company.objects.filter(is_manufacturer=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
manufacturer_detail = enable_filter(
|
||||||
|
CompanyBriefSerializer(
|
||||||
|
source='manufacturer', many=False, read_only=True, allow_null=True
|
||||||
|
),
|
||||||
|
True,
|
||||||
|
prefetch_fields=['manufacturer'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SupplierPriceBreakBriefSerializer(
|
class SupplierPriceBreakBriefSerializer(
|
||||||
FilterableSerializerMixin, InvenTreeModelSerializer
|
FilterableSerializerMixin, InvenTreeModelSerializer
|
||||||
@@ -399,33 +390,13 @@ class SupplierPartSerializer(
|
|||||||
# Check if 'available' quantity was supplied
|
# Check if 'available' quantity was supplied
|
||||||
self.has_available_quantity = 'available' in kwargs.get('data', {})
|
self.has_available_quantity = 'available' in kwargs.get('data', {})
|
||||||
|
|
||||||
# TODO INVE-T1 support complex filters
|
|
||||||
brief = kwargs.pop('brief', False)
|
brief = kwargs.pop('brief', False)
|
||||||
detail_default = not brief
|
|
||||||
part_detail = kwargs.pop('part_detail', detail_default)
|
|
||||||
supplier_detail = kwargs.pop('supplier_detail', detail_default)
|
|
||||||
manufacturer_detail = kwargs.pop('manufacturer_detail', detail_default)
|
|
||||||
|
|
||||||
prettify = kwargs.pop('pretty', False)
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if isGeneratingSchema():
|
if isGeneratingSchema():
|
||||||
return
|
return
|
||||||
|
|
||||||
if part_detail is not True:
|
|
||||||
self.fields.pop('part_detail', None)
|
|
||||||
|
|
||||||
if supplier_detail is not True:
|
|
||||||
self.fields.pop('supplier_detail', None)
|
|
||||||
|
|
||||||
if manufacturer_detail is not True:
|
|
||||||
self.fields.pop('manufacturer_detail', None)
|
|
||||||
self.fields.pop('manufacturer_part_detail', None)
|
|
||||||
|
|
||||||
if brief or prettify is not True:
|
|
||||||
self.fields.pop('pretty_name', None)
|
|
||||||
|
|
||||||
if brief:
|
if brief:
|
||||||
self.fields.pop('tags')
|
self.fields.pop('tags')
|
||||||
self.fields.pop('available')
|
self.fields.pop('available')
|
||||||
@@ -455,46 +426,61 @@ class SupplierPartSerializer(
|
|||||||
),
|
),
|
||||||
False,
|
False,
|
||||||
filter_name='price_breaks',
|
filter_name='price_breaks',
|
||||||
|
prefetch_fields=['pricebreaks'],
|
||||||
)
|
)
|
||||||
|
|
||||||
parameters = enable_filter(
|
parameters = common.filters.enable_parameters_filter()
|
||||||
common.serializers.ParameterSerializer(many=True, read_only=True),
|
|
||||||
False,
|
|
||||||
filter_name='parameters',
|
|
||||||
)
|
|
||||||
|
|
||||||
part_detail = part_serializers.PartBriefSerializer(
|
part_detail = enable_filter(
|
||||||
|
part_serializers.PartBriefSerializer(
|
||||||
label=_('Part'), source='part', many=False, read_only=True, allow_null=True
|
label=_('Part'), source='part', many=False, read_only=True, allow_null=True
|
||||||
|
),
|
||||||
|
False,
|
||||||
|
prefetch_fields=['part'],
|
||||||
)
|
)
|
||||||
|
|
||||||
supplier_detail = CompanyBriefSerializer(
|
supplier_detail = enable_filter(
|
||||||
|
CompanyBriefSerializer(
|
||||||
label=_('Supplier'),
|
label=_('Supplier'),
|
||||||
source='supplier',
|
source='supplier',
|
||||||
many=False,
|
many=False,
|
||||||
read_only=True,
|
read_only=True,
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
|
),
|
||||||
|
False,
|
||||||
|
prefetch_fields=['supplier'],
|
||||||
)
|
)
|
||||||
|
|
||||||
manufacturer_detail = CompanyBriefSerializer(
|
manufacturer_detail = enable_filter(
|
||||||
|
CompanyBriefSerializer(
|
||||||
label=_('Manufacturer'),
|
label=_('Manufacturer'),
|
||||||
source='manufacturer_part.manufacturer',
|
source='manufacturer_part.manufacturer',
|
||||||
many=False,
|
many=False,
|
||||||
read_only=True,
|
read_only=True,
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
|
),
|
||||||
|
False,
|
||||||
|
prefetch_fields=['manufacturer_part__manufacturer'],
|
||||||
)
|
)
|
||||||
|
|
||||||
pretty_name = serializers.CharField(read_only=True, allow_null=True)
|
pretty_name = enable_filter(
|
||||||
|
FilterableCharField(read_only=True, allow_null=True), filter_name='pretty'
|
||||||
|
)
|
||||||
|
|
||||||
supplier = serializers.PrimaryKeyRelatedField(
|
supplier = serializers.PrimaryKeyRelatedField(
|
||||||
label=_('Supplier'), queryset=Company.objects.filter(is_supplier=True)
|
label=_('Supplier'), queryset=Company.objects.filter(is_supplier=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
manufacturer_part_detail = ManufacturerPartSerializer(
|
manufacturer_part_detail = enable_filter(
|
||||||
|
ManufacturerPartSerializer(
|
||||||
label=_('Manufacturer Part'),
|
label=_('Manufacturer Part'),
|
||||||
source='manufacturer_part',
|
source='manufacturer_part',
|
||||||
part_detail=False,
|
part_detail=False,
|
||||||
read_only=True,
|
read_only=True,
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
|
),
|
||||||
|
False,
|
||||||
|
prefetch_fields=['manufacturer_part'],
|
||||||
)
|
)
|
||||||
|
|
||||||
MPN = serializers.CharField(
|
MPN = serializers.CharField(
|
||||||
@@ -511,15 +497,13 @@ class SupplierPartSerializer(
|
|||||||
Fields:
|
Fields:
|
||||||
in_stock: Current stock quantity for each SupplierPart
|
in_stock: Current stock quantity for each SupplierPart
|
||||||
"""
|
"""
|
||||||
queryset = queryset.prefetch_related('part', 'pricebreaks')
|
|
||||||
|
|
||||||
queryset = queryset.annotate(in_stock=part.filters.annotate_total_stock())
|
queryset = queryset.annotate(in_stock=part.filters.annotate_total_stock())
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
on_order=company.filters.annotate_on_order_quantity()
|
on_order=company.filters.annotate_on_order_quantity()
|
||||||
)
|
)
|
||||||
|
|
||||||
queryset = SupplierPart.annotate_parameters(queryset)
|
queryset = queryset.prefetch_related('supplier', 'manufacturer_part')
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|||||||
@@ -210,6 +210,34 @@ class CompanyTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.data['notes'], note)
|
self.assertEqual(response.data['notes'], note)
|
||||||
|
|
||||||
|
def test_company_parameters(self):
|
||||||
|
"""Test for annotation of 'parameters' field in Company API."""
|
||||||
|
url = reverse('api-company-list')
|
||||||
|
|
||||||
|
response = self.get(url, expected_code=200)
|
||||||
|
|
||||||
|
self.assertGreater(len(response.data), 0)
|
||||||
|
|
||||||
|
# Default = not included
|
||||||
|
for result in response.data:
|
||||||
|
self.assertNotIn('parameters', result)
|
||||||
|
|
||||||
|
# Exclude parameters
|
||||||
|
response = self.get(url, {'parameters': 'false'}, expected_code=200)
|
||||||
|
|
||||||
|
self.assertGreater(len(response.data), 0)
|
||||||
|
|
||||||
|
for result in response.data:
|
||||||
|
self.assertNotIn('parameters', result)
|
||||||
|
|
||||||
|
# Include parameters
|
||||||
|
response = self.get(url, {'parameters': 'true'}, expected_code=200)
|
||||||
|
|
||||||
|
self.assertGreater(len(response.data), 0)
|
||||||
|
|
||||||
|
for result in response.data:
|
||||||
|
self.assertIn('parameters', result)
|
||||||
|
|
||||||
|
|
||||||
class ContactTest(InvenTreeAPITestCase):
|
class ContactTest(InvenTreeAPITestCase):
|
||||||
"""Tests for the Contact models."""
|
"""Tests for the Contact models."""
|
||||||
@@ -682,6 +710,42 @@ class SupplierPartTest(InvenTreeAPITestCase):
|
|||||||
for result in response.data:
|
for result in response.data:
|
||||||
self.assertEqual(result['supplier'], company.pk)
|
self.assertEqual(result['supplier'], company.pk)
|
||||||
|
|
||||||
|
def test_filterable_fields(self):
|
||||||
|
"""Test inclusion/exclusion of optional API fields."""
|
||||||
|
fields = {
|
||||||
|
'price_breaks': False,
|
||||||
|
'part_detail': False,
|
||||||
|
'supplier_detail': False,
|
||||||
|
'manufacturer_detail': False,
|
||||||
|
'manufacturer_part_detail': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
url = reverse('api-supplier-part-list')
|
||||||
|
|
||||||
|
for field, included in fields.items():
|
||||||
|
# Test default behavior
|
||||||
|
response = self.get(url, data={}, expected_code=200)
|
||||||
|
self.assertGreater(len(response.data), 0)
|
||||||
|
self.assertEqual(
|
||||||
|
included,
|
||||||
|
field in response.data[0],
|
||||||
|
f'Field: {field} failed default test',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test explicit inclusion
|
||||||
|
response = self.get(url, data={field: 'true'}, expected_code=200)
|
||||||
|
self.assertGreater(len(response.data), 0)
|
||||||
|
self.assertIn(
|
||||||
|
field, response.data[0], f'Field: {field} failed inclusion test'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test explicit exclusion
|
||||||
|
response = self.get(url, data={field: 'false'}, expected_code=200)
|
||||||
|
self.assertGreater(len(response.data), 0)
|
||||||
|
self.assertNotIn(
|
||||||
|
field, response.data[0], f'Field: {field} failed exclusion test'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CompanyMetadataAPITest(InvenTreeAPITestCase):
|
class CompanyMetadataAPITest(InvenTreeAPITestCase):
|
||||||
"""Unit tests for the various metadata endpoints of API."""
|
"""Unit tests for the various metadata endpoints of API."""
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class DataExportSerializerMixin:
|
|||||||
Determine if the serializer is being used for data export,
|
Determine if the serializer is being used for data export,
|
||||||
and if so, adjust the serializer fields accordingly.
|
and if so, adjust the serializer fields accordingly.
|
||||||
"""
|
"""
|
||||||
exporting = kwargs.pop('exporting', False)
|
self._exporting_data = exporting = kwargs.pop('exporting', False)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@@ -264,10 +264,8 @@ class DataExportViewMixin:
|
|||||||
exporting = kwargs.pop('exporting', None)
|
exporting = kwargs.pop('exporting', None)
|
||||||
|
|
||||||
if exporting is None:
|
if exporting is None:
|
||||||
exporting = (
|
method = str(getattr(self.request, 'method', '')).lower()
|
||||||
self.request.method.lower() in ['options', 'get']
|
exporting = method in ['options', 'get'] and self.is_exporting()
|
||||||
and self.is_exporting()
|
|
||||||
)
|
|
||||||
|
|
||||||
if exporting:
|
if exporting:
|
||||||
# Override kwargs when initializing the DataExportOptionsSerializer
|
# Override kwargs when initializing the DataExportOptionsSerializer
|
||||||
|
|||||||
@@ -369,12 +369,10 @@ class PurchaseOrderMixin(SerializerContextMixin):
|
|||||||
"""Return the annotated queryset for this endpoint."""
|
"""Return the annotated queryset for this endpoint."""
|
||||||
queryset = super().get_queryset(*args, **kwargs)
|
queryset = super().get_queryset(*args, **kwargs)
|
||||||
|
|
||||||
queryset = queryset.prefetch_related(
|
|
||||||
'supplier', 'project_code', 'lines', 'responsible'
|
|
||||||
)
|
|
||||||
|
|
||||||
queryset = serializers.PurchaseOrderSerializer.annotate_queryset(queryset)
|
queryset = serializers.PurchaseOrderSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
|
queryset = queryset.prefetch_related('supplier', 'created_by')
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
@@ -833,9 +831,7 @@ class SalesOrderMixin(SerializerContextMixin):
|
|||||||
"""Return annotated queryset for this endpoint."""
|
"""Return annotated queryset for this endpoint."""
|
||||||
queryset = super().get_queryset(*args, **kwargs)
|
queryset = super().get_queryset(*args, **kwargs)
|
||||||
|
|
||||||
queryset = queryset.prefetch_related(
|
queryset = queryset.prefetch_related('customer', 'created_by')
|
||||||
'customer', 'responsible', 'project_code', 'lines'
|
|
||||||
)
|
|
||||||
|
|
||||||
queryset = serializers.SalesOrderSerializer.annotate_queryset(queryset)
|
queryset = serializers.SalesOrderSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
@@ -1018,17 +1014,13 @@ class SalesOrderLineItemMixin(SerializerContextMixin):
|
|||||||
|
|
||||||
queryset = queryset.prefetch_related(
|
queryset = queryset.prefetch_related(
|
||||||
'part',
|
'part',
|
||||||
'part__stock_items',
|
|
||||||
'allocations',
|
'allocations',
|
||||||
'allocations__shipment',
|
'allocations__shipment',
|
||||||
'allocations__item__part',
|
'allocations__item__part',
|
||||||
'allocations__item__location',
|
'allocations__item__location',
|
||||||
'order',
|
'order',
|
||||||
'order__stock_items',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
queryset = queryset.select_related('part__pricing_data')
|
|
||||||
|
|
||||||
queryset = serializers.SalesOrderLineItemSerializer.annotate_queryset(queryset)
|
queryset = serializers.SalesOrderLineItemSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
@@ -1510,9 +1502,7 @@ class ReturnOrderMixin(SerializerContextMixin):
|
|||||||
"""Return annotated queryset for this endpoint."""
|
"""Return annotated queryset for this endpoint."""
|
||||||
queryset = super().get_queryset(*args, **kwargs)
|
queryset = super().get_queryset(*args, **kwargs)
|
||||||
|
|
||||||
queryset = queryset.prefetch_related(
|
queryset = queryset.prefetch_related('customer', 'created_by')
|
||||||
'customer', 'lines', 'project_code', 'responsible'
|
|
||||||
)
|
|
||||||
|
|
||||||
queryset = serializers.ReturnOrderSerializer.annotate_queryset(queryset)
|
queryset = serializers.ReturnOrderSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
|
|||||||
@@ -1989,17 +1989,16 @@ class PurchaseOrderLineItem(OrderLineItem):
|
|||||||
def get_destination(self):
|
def get_destination(self):
|
||||||
"""Show where the line item is or should be placed.
|
"""Show where the line item is or should be placed.
|
||||||
|
|
||||||
NOTE: If a line item gets split when received, only an arbitrary
|
1. If a destination is specified against this line item, return that.
|
||||||
stock items location will be reported as the location for the
|
2. If a destination is specified against the PurchaseOrderPart, return that.
|
||||||
entire line.
|
3. If a default location is specified against the linked Part, return that.
|
||||||
"""
|
"""
|
||||||
for item in stock.models.StockItem.objects.filter(
|
|
||||||
supplier_part=self.part, purchase_order=self.order
|
|
||||||
):
|
|
||||||
if item.location:
|
|
||||||
return item.location
|
|
||||||
if self.destination:
|
if self.destination:
|
||||||
return self.destination
|
return self.destination
|
||||||
|
|
||||||
|
if self.order.destination:
|
||||||
|
return self.order.destination
|
||||||
|
|
||||||
if self.part and self.part.part and self.part.part.default_location:
|
if self.part and self.part.part and self.part.part.default_location:
|
||||||
return self.part.part.default_location
|
return self.part.part.default_location
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,7 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import (
|
from django.db.models import BooleanField, Case, ExpressionWrapper, F, Q, Value, When
|
||||||
BooleanField,
|
|
||||||
Case,
|
|
||||||
ExpressionWrapper,
|
|
||||||
F,
|
|
||||||
Prefetch,
|
|
||||||
Q,
|
|
||||||
Value,
|
|
||||||
When,
|
|
||||||
)
|
|
||||||
from django.db.models.functions import Coalesce, Greatest
|
from django.db.models.functions import Coalesce, Greatest
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@@ -22,13 +13,12 @@ from rest_framework.serializers import ValidationError
|
|||||||
from sql_util.utils import SubqueryCount, SubquerySum
|
from sql_util.utils import SubqueryCount, SubquerySum
|
||||||
|
|
||||||
import build.serializers
|
import build.serializers
|
||||||
import common.serializers
|
import common.filters
|
||||||
import order.models
|
import order.models
|
||||||
import part.filters as part_filters
|
import part.filters as part_filters
|
||||||
import part.models as part_models
|
import part.models as part_models
|
||||||
import stock.models
|
import stock.models
|
||||||
import stock.serializers
|
import stock.serializers
|
||||||
from common.serializers import ProjectCodeSerializer
|
|
||||||
from company.serializers import (
|
from company.serializers import (
|
||||||
AddressBriefSerializer,
|
AddressBriefSerializer,
|
||||||
CompanyBriefSerializer,
|
CompanyBriefSerializer,
|
||||||
@@ -46,7 +36,6 @@ from InvenTree.helpers import (
|
|||||||
)
|
)
|
||||||
from InvenTree.mixins import DataImportExportSerializerMixin
|
from InvenTree.mixins import DataImportExportSerializerMixin
|
||||||
from InvenTree.serializers import (
|
from InvenTree.serializers import (
|
||||||
FilterableCharField,
|
|
||||||
FilterableSerializerMixin,
|
FilterableSerializerMixin,
|
||||||
InvenTreeCurrencySerializer,
|
InvenTreeCurrencySerializer,
|
||||||
InvenTreeDecimalField,
|
InvenTreeDecimalField,
|
||||||
@@ -141,6 +130,7 @@ class AbstractOrderSerializer(
|
|||||||
source='contact', many=False, read_only=True, allow_null=True
|
source='contact', many=False, read_only=True, allow_null=True
|
||||||
),
|
),
|
||||||
True,
|
True,
|
||||||
|
prefetch_fields=['contact'],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Detail for responsible field
|
# Detail for responsible field
|
||||||
@@ -149,26 +139,11 @@ class AbstractOrderSerializer(
|
|||||||
source='responsible', read_only=True, allow_null=True, many=False
|
source='responsible', read_only=True, allow_null=True, many=False
|
||||||
),
|
),
|
||||||
True,
|
True,
|
||||||
|
prefetch_fields=['responsible'],
|
||||||
)
|
)
|
||||||
|
|
||||||
project_code_label = enable_filter(
|
project_code_label = common.filters.enable_project_label_filter()
|
||||||
FilterableCharField(
|
project_code_detail = common.filters.enable_project_code_filter()
|
||||||
source='project_code.code',
|
|
||||||
read_only=True,
|
|
||||||
label='Project Code Label',
|
|
||||||
allow_null=True,
|
|
||||||
),
|
|
||||||
True,
|
|
||||||
filter_name='project_code_detail',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Detail for project code field
|
|
||||||
project_code_detail = enable_filter(
|
|
||||||
ProjectCodeSerializer(
|
|
||||||
source='project_code', read_only=True, many=False, allow_null=True
|
|
||||||
),
|
|
||||||
True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Detail for address field
|
# Detail for address field
|
||||||
address_detail = enable_filter(
|
address_detail = enable_filter(
|
||||||
@@ -176,13 +151,10 @@ class AbstractOrderSerializer(
|
|||||||
source='address', many=False, read_only=True, allow_null=True
|
source='address', many=False, read_only=True, allow_null=True
|
||||||
),
|
),
|
||||||
True,
|
True,
|
||||||
|
prefetch_fields=['address'],
|
||||||
)
|
)
|
||||||
|
|
||||||
parameters = enable_filter(
|
parameters = common.filters.enable_parameters_filter()
|
||||||
common.serializers.ParameterSerializer(many=True, read_only=True),
|
|
||||||
False,
|
|
||||||
filter_name='parameters',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Boolean field indicating if this order is overdue (Note: must be annotated)
|
# Boolean field indicating if this order is overdue (Note: must be annotated)
|
||||||
overdue = serializers.BooleanField(read_only=True, allow_null=True)
|
overdue = serializers.BooleanField(read_only=True, allow_null=True)
|
||||||
@@ -317,23 +289,9 @@ class AbstractLineItemSerializer(FilterableSerializerMixin, serializers.Serializ
|
|||||||
required=False, allow_null=True, label=_('Target Date')
|
required=False, allow_null=True, label=_('Target Date')
|
||||||
)
|
)
|
||||||
|
|
||||||
project_code_label = enable_filter(
|
project_code_label = common.filters.enable_project_label_filter()
|
||||||
FilterableCharField(
|
|
||||||
source='project_code.code',
|
|
||||||
read_only=True,
|
|
||||||
label='Project Code Label',
|
|
||||||
allow_null=True,
|
|
||||||
),
|
|
||||||
True,
|
|
||||||
filter_name='project_code_detail',
|
|
||||||
)
|
|
||||||
|
|
||||||
project_code_detail = enable_filter(
|
project_code_detail = common.filters.enable_project_code_filter()
|
||||||
ProjectCodeSerializer(
|
|
||||||
source='project_code', read_only=True, many=False, allow_null=True
|
|
||||||
),
|
|
||||||
True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractExtraLineSerializer(
|
class AbstractExtraLineSerializer(
|
||||||
@@ -369,24 +327,9 @@ class AbstractExtraLineSerializer(
|
|||||||
|
|
||||||
price_currency = InvenTreeCurrencySerializer()
|
price_currency = InvenTreeCurrencySerializer()
|
||||||
|
|
||||||
project_code_label = enable_filter(
|
project_code_label = common.filters.enable_project_label_filter()
|
||||||
FilterableCharField(
|
|
||||||
source='project_code.code',
|
|
||||||
read_only=True,
|
|
||||||
label='Project Code Label',
|
|
||||||
allow_null=True,
|
|
||||||
),
|
|
||||||
True,
|
|
||||||
filter_name='project_code_detail',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Detail for project code field
|
project_code_detail = common.filters.enable_project_code_filter()
|
||||||
project_code_detail = enable_filter(
|
|
||||||
ProjectCodeSerializer(
|
|
||||||
source='project_code', read_only=True, many=False, allow_null=True
|
|
||||||
),
|
|
||||||
True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractExtraLineMeta:
|
class AbstractExtraLineMeta:
|
||||||
@@ -452,9 +395,6 @@ class PurchaseOrderSerializer(
|
|||||||
"""
|
"""
|
||||||
queryset = AbstractOrderSerializer.annotate_queryset(queryset)
|
queryset = AbstractOrderSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
# Annotate parametric data
|
|
||||||
queryset = order.models.PurchaseOrder.annotate_parameters(queryset)
|
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
completed_lines=SubqueryCount(
|
completed_lines=SubqueryCount(
|
||||||
'lines', filter=Q(quantity__lte=F('received'))
|
'lines', filter=Q(quantity__lte=F('received'))
|
||||||
@@ -471,6 +411,8 @@ class PurchaseOrderSerializer(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
queryset = queryset.prefetch_related('created_by')
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
supplier_name = serializers.CharField(
|
supplier_name = serializers.CharField(
|
||||||
@@ -480,7 +422,8 @@ class PurchaseOrderSerializer(
|
|||||||
supplier_detail = enable_filter(
|
supplier_detail = enable_filter(
|
||||||
CompanyBriefSerializer(
|
CompanyBriefSerializer(
|
||||||
source='supplier', many=False, read_only=True, allow_null=True
|
source='supplier', many=False, read_only=True, allow_null=True
|
||||||
)
|
),
|
||||||
|
prefetch_fields=['supplier'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -612,27 +555,18 @@ class PurchaseOrderLineItemSerializer(
|
|||||||
- "total_price" = purchase_price * quantity
|
- "total_price" = purchase_price * quantity
|
||||||
- "overdue" status (boolean field)
|
- "overdue" status (boolean field)
|
||||||
"""
|
"""
|
||||||
queryset = queryset.prefetch_related(
|
|
||||||
Prefetch(
|
|
||||||
'part__part',
|
|
||||||
queryset=part_models.Part.objects.annotate(
|
|
||||||
category_default_location=part_filters.annotate_default_location(
|
|
||||||
'category__'
|
|
||||||
)
|
|
||||||
).prefetch_related(None),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
queryset = queryset.prefetch_related(
|
queryset = queryset.prefetch_related(
|
||||||
'order',
|
'order',
|
||||||
'order__responsible',
|
'order__responsible',
|
||||||
'order__stock_items',
|
'order__stock_items',
|
||||||
|
'part',
|
||||||
|
'part__part',
|
||||||
|
'part__part__pricing_data',
|
||||||
|
'part__part__default_location',
|
||||||
'part__tags',
|
'part__tags',
|
||||||
'part__supplier',
|
'part__supplier',
|
||||||
'part__manufacturer_part',
|
'part__manufacturer_part',
|
||||||
'part__manufacturer_part__manufacturer',
|
'part__manufacturer_part__manufacturer',
|
||||||
'part__part__pricing_data',
|
|
||||||
'part__part__tags',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
@@ -687,6 +621,7 @@ class PurchaseOrderLineItemSerializer(
|
|||||||
PartBriefSerializer(
|
PartBriefSerializer(
|
||||||
source='get_base_part', many=False, read_only=True, allow_null=True
|
source='get_base_part', many=False, read_only=True, allow_null=True
|
||||||
),
|
),
|
||||||
|
False,
|
||||||
filter_name='part_detail',
|
filter_name='part_detail',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -694,6 +629,7 @@ class PurchaseOrderLineItemSerializer(
|
|||||||
SupplierPartSerializer(
|
SupplierPartSerializer(
|
||||||
source='part', brief=True, many=False, read_only=True, allow_null=True
|
source='part', brief=True, many=False, read_only=True, allow_null=True
|
||||||
),
|
),
|
||||||
|
False,
|
||||||
filter_name='part_detail',
|
filter_name='part_detail',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -707,8 +643,12 @@ class PurchaseOrderLineItemSerializer(
|
|||||||
default=True,
|
default=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
destination_detail = stock.serializers.LocationBriefSerializer(
|
destination_detail = enable_filter(
|
||||||
|
stock.serializers.LocationBriefSerializer(
|
||||||
source='get_destination', read_only=True, allow_null=True
|
source='get_destination', read_only=True, allow_null=True
|
||||||
|
),
|
||||||
|
True,
|
||||||
|
prefetch_fields=['destination', 'order__destination'],
|
||||||
)
|
)
|
||||||
|
|
||||||
purchase_price_currency = InvenTreeCurrencySerializer(
|
purchase_price_currency = InvenTreeCurrencySerializer(
|
||||||
@@ -721,8 +661,16 @@ class PurchaseOrderLineItemSerializer(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
build_order_detail = build.serializers.BuildSerializer(
|
build_order_detail = enable_filter(
|
||||||
|
build.serializers.BuildSerializer(
|
||||||
source='build_order', read_only=True, allow_null=True, many=False
|
source='build_order', read_only=True, allow_null=True, many=False
|
||||||
|
),
|
||||||
|
True,
|
||||||
|
prefetch_fields=[
|
||||||
|
'build_order__responsible',
|
||||||
|
'build_order__issued_by',
|
||||||
|
'build_order__part',
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
merge_items = serializers.BooleanField(
|
merge_items = serializers.BooleanField(
|
||||||
@@ -1098,9 +1046,6 @@ class SalesOrderSerializer(
|
|||||||
"""
|
"""
|
||||||
queryset = AbstractOrderSerializer.annotate_queryset(queryset)
|
queryset = AbstractOrderSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
# Annotate parametric data
|
|
||||||
queryset = order.models.SalesOrder.annotate_parameters(queryset)
|
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
completed_lines=SubqueryCount('lines', filter=Q(quantity__lte=F('shipped')))
|
completed_lines=SubqueryCount('lines', filter=Q(quantity__lte=F('shipped')))
|
||||||
)
|
)
|
||||||
@@ -1128,7 +1073,8 @@ class SalesOrderSerializer(
|
|||||||
customer_detail = enable_filter(
|
customer_detail = enable_filter(
|
||||||
CompanyBriefSerializer(
|
CompanyBriefSerializer(
|
||||||
source='customer', many=False, read_only=True, allow_null=True
|
source='customer', many=False, read_only=True, allow_null=True
|
||||||
)
|
),
|
||||||
|
prefetch_fields=['customer'],
|
||||||
)
|
)
|
||||||
|
|
||||||
shipments_count = serializers.IntegerField(
|
shipments_count = serializers.IntegerField(
|
||||||
@@ -1277,15 +1223,26 @@ class SalesOrderLineItemSerializer(
|
|||||||
order_detail = enable_filter(
|
order_detail = enable_filter(
|
||||||
SalesOrderSerializer(
|
SalesOrderSerializer(
|
||||||
source='order', many=False, read_only=True, allow_null=True
|
source='order', many=False, read_only=True, allow_null=True
|
||||||
|
),
|
||||||
|
prefetch_fields=[
|
||||||
|
'order__created_by',
|
||||||
|
'order__responsible',
|
||||||
|
'order__address',
|
||||||
|
'order__project_code',
|
||||||
|
'order__contact',
|
||||||
|
],
|
||||||
)
|
)
|
||||||
)
|
|
||||||
part_detail = enable_filter(
|
part_detail = enable_filter(
|
||||||
PartBriefSerializer(source='part', many=False, read_only=True, allow_null=True)
|
PartBriefSerializer(source='part', many=False, read_only=True, allow_null=True),
|
||||||
|
prefetch_fields=['part__pricing_data'],
|
||||||
)
|
)
|
||||||
|
|
||||||
customer_detail = enable_filter(
|
customer_detail = enable_filter(
|
||||||
CompanyBriefSerializer(
|
CompanyBriefSerializer(
|
||||||
source='order.customer', many=False, read_only=True, allow_null=True
|
source='order.customer', many=False, read_only=True, allow_null=True
|
||||||
)
|
),
|
||||||
|
prefetch_fields=['order__customer'],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Annotated fields
|
# Annotated fields
|
||||||
@@ -1950,9 +1907,6 @@ class ReturnOrderSerializer(
|
|||||||
"""Custom annotation for the serializer queryset."""
|
"""Custom annotation for the serializer queryset."""
|
||||||
queryset = AbstractOrderSerializer.annotate_queryset(queryset)
|
queryset = AbstractOrderSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
# Annotate parametric data
|
|
||||||
queryset = order.models.ReturnOrder.annotate_parameters(queryset)
|
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
completed_lines=SubqueryCount(
|
completed_lines=SubqueryCount(
|
||||||
'lines', filter=~Q(outcome=ReturnOrderLineStatus.PENDING.value)
|
'lines', filter=~Q(outcome=ReturnOrderLineStatus.PENDING.value)
|
||||||
@@ -1974,7 +1928,8 @@ class ReturnOrderSerializer(
|
|||||||
customer_detail = enable_filter(
|
customer_detail = enable_filter(
|
||||||
CompanyBriefSerializer(
|
CompanyBriefSerializer(
|
||||||
source='customer', many=False, read_only=True, allow_null=True
|
source='customer', many=False, read_only=True, allow_null=True
|
||||||
)
|
),
|
||||||
|
prefetch_fields=['customer'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -2134,7 +2089,14 @@ class ReturnOrderLineItemSerializer(
|
|||||||
order_detail = enable_filter(
|
order_detail = enable_filter(
|
||||||
ReturnOrderSerializer(
|
ReturnOrderSerializer(
|
||||||
source='order', many=False, read_only=True, allow_null=True
|
source='order', many=False, read_only=True, allow_null=True
|
||||||
)
|
),
|
||||||
|
prefetch_fields=[
|
||||||
|
'order__created_by',
|
||||||
|
'order__responsible',
|
||||||
|
'order__address',
|
||||||
|
'order__project_code',
|
||||||
|
'order__contact',
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
quantity = serializers.FloatField(
|
quantity = serializers.FloatField(
|
||||||
@@ -2144,7 +2106,8 @@ class ReturnOrderLineItemSerializer(
|
|||||||
item_detail = enable_filter(
|
item_detail = enable_filter(
|
||||||
stock.serializers.StockItemSerializer(
|
stock.serializers.StockItemSerializer(
|
||||||
source='item', many=False, read_only=True, allow_null=True
|
source='item', many=False, read_only=True, allow_null=True
|
||||||
)
|
),
|
||||||
|
prefetch_fields=['item__supplier_part'],
|
||||||
)
|
)
|
||||||
|
|
||||||
part_detail = enable_filter(
|
part_detail = enable_filter(
|
||||||
|
|||||||
@@ -1009,7 +1009,7 @@ class PartMixin(SerializerContextMixin):
|
|||||||
"""Mixin class for Part API endpoints."""
|
"""Mixin class for Part API endpoints."""
|
||||||
|
|
||||||
serializer_class = part_serializers.PartSerializer
|
serializer_class = part_serializers.PartSerializer
|
||||||
queryset = Part.objects.all()
|
queryset = Part.objects.all().select_related('pricing_data')
|
||||||
|
|
||||||
starred_parts = None
|
starred_parts = None
|
||||||
is_create = False
|
is_create = False
|
||||||
@@ -1020,9 +1020,6 @@ class PartMixin(SerializerContextMixin):
|
|||||||
|
|
||||||
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
|
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
if str2bool(self.request.query_params.get('price_breaks', True)):
|
|
||||||
queryset = queryset.prefetch_related('salepricebreaks')
|
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from django.core.files.base import ContentFile
|
|||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import ExpressionWrapper, F, Q
|
from django.db.models import ExpressionWrapper, F, Q
|
||||||
from django.db.models.functions import Coalesce, Greatest
|
from django.db.models.functions import Greatest
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ from sql_util.utils import SubqueryCount
|
|||||||
from taggit.serializers import TagListSerializerField
|
from taggit.serializers import TagListSerializerField
|
||||||
|
|
||||||
import common.currency
|
import common.currency
|
||||||
|
import common.filters
|
||||||
import common.serializers
|
import common.serializers
|
||||||
import company.models
|
import company.models
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
@@ -619,7 +620,6 @@ class PartSerializer(
|
|||||||
'required_for_build_orders',
|
'required_for_build_orders',
|
||||||
'required_for_sales_orders',
|
'required_for_sales_orders',
|
||||||
'stock_item_count',
|
'stock_item_count',
|
||||||
'suppliers',
|
|
||||||
'total_in_stock',
|
'total_in_stock',
|
||||||
'external_stock',
|
'external_stock',
|
||||||
'unallocated_stock',
|
'unallocated_stock',
|
||||||
@@ -680,10 +680,6 @@ class PartSerializer(
|
|||||||
|
|
||||||
Performing database queries as efficiently as possible, to reduce database trips.
|
Performing database queries as efficiently as possible, to reduce database trips.
|
||||||
"""
|
"""
|
||||||
queryset = queryset.prefetch_related('category', 'default_location')
|
|
||||||
|
|
||||||
queryset = Part.annotate_parameters(queryset)
|
|
||||||
|
|
||||||
# Annotate with the total number of revisions
|
# Annotate with the total number of revisions
|
||||||
queryset = queryset.annotate(revision_count=SubqueryCount('revisions'))
|
queryset = queryset.annotate(revision_count=SubqueryCount('revisions'))
|
||||||
|
|
||||||
@@ -708,15 +704,6 @@ class PartSerializer(
|
|||||||
scheduled_to_build=part_filters.annotate_scheduled_to_build_quantity()
|
scheduled_to_build=part_filters.annotate_scheduled_to_build_quantity()
|
||||||
)
|
)
|
||||||
|
|
||||||
# Annotate with the number of 'suppliers'
|
|
||||||
queryset = queryset.annotate(
|
|
||||||
suppliers=Coalesce(
|
|
||||||
SubqueryCount('supplier_parts'),
|
|
||||||
Decimal(0),
|
|
||||||
output_field=models.DecimalField(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
ordering=part_filters.annotate_on_order_quantity(),
|
ordering=part_filters.annotate_on_order_quantity(),
|
||||||
in_stock=part_filters.annotate_total_stock(),
|
in_stock=part_filters.annotate_total_stock(),
|
||||||
@@ -775,7 +762,8 @@ class PartSerializer(
|
|||||||
category_detail = enable_filter(
|
category_detail = enable_filter(
|
||||||
CategorySerializer(
|
CategorySerializer(
|
||||||
source='category', many=False, read_only=True, allow_null=True
|
source='category', many=False, read_only=True, allow_null=True
|
||||||
)
|
),
|
||||||
|
prefetch_fields=['category'],
|
||||||
)
|
)
|
||||||
|
|
||||||
category_path = enable_filter(
|
category_path = enable_filter(
|
||||||
@@ -786,6 +774,7 @@ class PartSerializer(
|
|||||||
allow_null=True,
|
allow_null=True,
|
||||||
),
|
),
|
||||||
filter_name='path_detail',
|
filter_name='path_detail',
|
||||||
|
prefetch_fields=['category'],
|
||||||
)
|
)
|
||||||
|
|
||||||
default_location_detail = enable_filter(
|
default_location_detail = enable_filter(
|
||||||
@@ -793,6 +782,7 @@ class PartSerializer(
|
|||||||
source='default_location', many=False, read_only=True, allow_null=True
|
source='default_location', many=False, read_only=True, allow_null=True
|
||||||
),
|
),
|
||||||
filter_name='location_detail',
|
filter_name='location_detail',
|
||||||
|
prefetch_fields=['default_location'],
|
||||||
)
|
)
|
||||||
|
|
||||||
category_name = serializers.CharField(
|
category_name = serializers.CharField(
|
||||||
@@ -860,10 +850,6 @@ class PartSerializer(
|
|||||||
read_only=True, allow_null=True, label=_('Revisions')
|
read_only=True, allow_null=True, label=_('Revisions')
|
||||||
)
|
)
|
||||||
|
|
||||||
suppliers = serializers.IntegerField(
|
|
||||||
read_only=True, allow_null=True, label=_('Suppliers')
|
|
||||||
)
|
|
||||||
|
|
||||||
total_in_stock = serializers.FloatField(
|
total_in_stock = serializers.FloatField(
|
||||||
read_only=True, allow_null=True, label=_('Total Stock')
|
read_only=True, allow_null=True, label=_('Total Stock')
|
||||||
)
|
)
|
||||||
@@ -922,13 +908,7 @@ class PartSerializer(
|
|||||||
filter_name='pricing',
|
filter_name='pricing',
|
||||||
)
|
)
|
||||||
|
|
||||||
parameters = enable_filter(
|
parameters = common.filters.enable_parameters_filter()
|
||||||
common.serializers.ParameterSerializer(
|
|
||||||
many=True, read_only=True, allow_null=True
|
|
||||||
),
|
|
||||||
False,
|
|
||||||
filter_name='parameters',
|
|
||||||
)
|
|
||||||
|
|
||||||
price_breaks = enable_filter(
|
price_breaks = enable_filter(
|
||||||
PartSalePriceSerializer(
|
PartSalePriceSerializer(
|
||||||
@@ -936,6 +916,7 @@ class PartSerializer(
|
|||||||
),
|
),
|
||||||
False,
|
False,
|
||||||
filter_name='price_breaks',
|
filter_name='price_breaks',
|
||||||
|
prefetch_fields=['salepricebreaks'],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Extra fields used only for creation of a new Part instance
|
# Extra fields used only for creation of a new Part instance
|
||||||
@@ -963,6 +944,7 @@ class PartSerializer(
|
|||||||
copy_category_parameters = serializers.BooleanField(
|
copy_category_parameters = serializers.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
required=False,
|
required=False,
|
||||||
|
write_only=True,
|
||||||
label=_('Copy Category Parameters'),
|
label=_('Copy Category Parameters'),
|
||||||
help_text=_('Copy parameter templates from selected part category'),
|
help_text=_('Copy parameter templates from selected part category'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -475,9 +475,7 @@ class StockItemSerializer(
|
|||||||
"""Add some extra annotations to the queryset, performing database queries as efficiently as possible."""
|
"""Add some extra annotations to the queryset, performing database queries as efficiently as possible."""
|
||||||
queryset = queryset.prefetch_related(
|
queryset = queryset.prefetch_related(
|
||||||
'location',
|
'location',
|
||||||
'allocations',
|
|
||||||
'sales_order',
|
'sales_order',
|
||||||
'sales_order_allocations',
|
|
||||||
'purchase_order',
|
'purchase_order',
|
||||||
Prefetch(
|
Prefetch(
|
||||||
'part',
|
'part',
|
||||||
@@ -489,25 +487,15 @@ class StockItemSerializer(
|
|||||||
),
|
),
|
||||||
'parent',
|
'parent',
|
||||||
'part__category',
|
'part__category',
|
||||||
'part__supplier_parts',
|
|
||||||
'part__supplier_parts__purchase_order_line_items',
|
|
||||||
'part__pricing_data',
|
'part__pricing_data',
|
||||||
'part__tags',
|
|
||||||
'supplier_part',
|
'supplier_part',
|
||||||
'supplier_part__part',
|
|
||||||
'supplier_part__supplier',
|
|
||||||
'supplier_part__manufacturer_part',
|
'supplier_part__manufacturer_part',
|
||||||
'supplier_part__manufacturer_part__manufacturer',
|
|
||||||
'supplier_part__manufacturer_part__tags',
|
|
||||||
'supplier_part__purchase_order_line_items',
|
|
||||||
'supplier_part__tags',
|
|
||||||
'test_results',
|
|
||||||
'customer',
|
'customer',
|
||||||
'belongs_to',
|
'belongs_to',
|
||||||
'sales_order',
|
'sales_order',
|
||||||
'consumed_by',
|
'consumed_by',
|
||||||
'tags',
|
'tags',
|
||||||
)
|
).select_related('part')
|
||||||
|
|
||||||
# Annotate the queryset with the total allocated to sales orders
|
# Annotate the queryset with the total allocated to sales orders
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
@@ -586,7 +574,14 @@ class StockItemSerializer(
|
|||||||
read_only=True,
|
read_only=True,
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
),
|
),
|
||||||
True,
|
False,
|
||||||
|
prefetch_fields=[
|
||||||
|
'supplier_part__supplier',
|
||||||
|
'supplier_part__manufacturer_part__manufacturer',
|
||||||
|
'supplier_part__manufacturer_part__tags',
|
||||||
|
'supplier_part__purchase_order_line_items',
|
||||||
|
'supplier_part__tags',
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
part_detail = enable_filter(
|
part_detail = enable_filter(
|
||||||
@@ -604,13 +599,20 @@ class StockItemSerializer(
|
|||||||
read_only=True,
|
read_only=True,
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
),
|
),
|
||||||
True,
|
False,
|
||||||
|
prefetch_fields=['location'],
|
||||||
)
|
)
|
||||||
|
|
||||||
tests = enable_filter(
|
tests = enable_filter(
|
||||||
StockItemTestResultSerializer(
|
StockItemTestResultSerializer(
|
||||||
source='test_results', many=True, read_only=True, allow_null=True
|
source='test_results', many=True, read_only=True, allow_null=True
|
||||||
)
|
),
|
||||||
|
False,
|
||||||
|
prefetch_fields=[
|
||||||
|
'test_results',
|
||||||
|
'test_results__user',
|
||||||
|
'test_results__template',
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
quantity = InvenTreeDecimalField()
|
quantity = InvenTreeDecimalField()
|
||||||
|
|||||||
@@ -866,7 +866,9 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
|
|
||||||
excluded_headers = ['metadata']
|
excluded_headers = ['metadata']
|
||||||
|
|
||||||
with self.export_data(self.list_url) as data_file:
|
filters = {}
|
||||||
|
|
||||||
|
with self.export_data(self.list_url, filters) as data_file:
|
||||||
self.process_csv(
|
self.process_csv(
|
||||||
data_file,
|
data_file,
|
||||||
required_cols=required_headers,
|
required_cols=required_headers,
|
||||||
@@ -875,9 +877,10 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Now, add a filter to the results
|
# Now, add a filter to the results
|
||||||
with self.export_data(
|
filters['location'] = 1
|
||||||
self.list_url, {'location': 1, 'cascade': True}
|
filters['cascade'] = True
|
||||||
) as data_file:
|
|
||||||
|
with self.export_data(self.list_url, filters) as data_file:
|
||||||
data = self.process_csv(data_file, required_rows=9)
|
data = self.process_csv(data_file, required_rows=9)
|
||||||
|
|
||||||
for row in data:
|
for row in data:
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
"""Helper functions for user permission checks."""
|
"""Helper functions for user permission checks."""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models.query import Prefetch, QuerySet
|
||||||
|
|
||||||
import InvenTree.cache
|
import InvenTree.cache
|
||||||
from users.ruleset import RULESET_CHANGE_INHERIT, get_ruleset_ignore, get_ruleset_models
|
from users.ruleset import RULESET_CHANGE_INHERIT, get_ruleset_ignore, get_ruleset_models
|
||||||
@@ -55,8 +58,26 @@ def split_permission(app: str, perm: str) -> tuple[str, str]:
|
|||||||
return perm, model
|
return perm, model
|
||||||
|
|
||||||
|
|
||||||
|
def prefetch_rule_sets(user) -> QuerySet:
|
||||||
|
"""Return a queryset of groups with prefetched rule sets for the given user.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
user: The user object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
QuerySet: The queryset of groups with prefetched rule sets
|
||||||
|
"""
|
||||||
|
return user.groups.all().prefetch_related(
|
||||||
|
Prefetch('rule_sets', to_attr='prefetched_rule_sets')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_user_role(
|
def check_user_role(
|
||||||
user: User, role: str, permission: str, allow_inactive: bool = False
|
user: User,
|
||||||
|
role: str,
|
||||||
|
permission: str,
|
||||||
|
allow_inactive: bool = False,
|
||||||
|
groups: Optional[QuerySet] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Check if a user has a particular role:permission combination.
|
"""Check if a user has a particular role:permission combination.
|
||||||
|
|
||||||
@@ -65,6 +86,7 @@ def check_user_role(
|
|||||||
role: The role to check (e.g. 'part' / 'stock')
|
role: The role to check (e.g. 'part' / 'stock')
|
||||||
permission: The permission to check (e.g. 'view' / 'delete')
|
permission: The permission to check (e.g. 'view' / 'delete')
|
||||||
allow_inactive: If False, disallow inactive users from having permissions
|
allow_inactive: If False, disallow inactive users from having permissions
|
||||||
|
groups: Optional cached queryset of groups to check (defaults to user's groups)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the user has the specified role:permission combination
|
bool: True if the user has the specified role:permission combination
|
||||||
@@ -90,8 +112,10 @@ def check_user_role(
|
|||||||
# Default for no match
|
# Default for no match
|
||||||
result = False
|
result = False
|
||||||
|
|
||||||
for group in user.groups.all():
|
groups = groups or prefetch_rule_sets(user)
|
||||||
for rule in group.rule_sets.all():
|
|
||||||
|
for group in groups:
|
||||||
|
for rule in group.prefetched_rule_sets:
|
||||||
if rule.name == role:
|
if rule.name == role:
|
||||||
# Check if the rule has the specified permission
|
# Check if the rule has the specified permission
|
||||||
# e.g. "view" role maps to "can_view" attribute
|
# e.g. "view" role maps to "can_view" attribute
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from InvenTree.serializers import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .models import ApiToken, Owner, RuleSet, UserProfile
|
from .models import ApiToken, Owner, RuleSet, UserProfile
|
||||||
from .permissions import check_user_role
|
from .permissions import check_user_role, prefetch_rule_sets
|
||||||
from .ruleset import RULESET_CHOICES, RULESET_PERMISSIONS, RuleSetEnum
|
from .ruleset import RULESET_CHOICES, RULESET_PERMISSIONS, RuleSetEnum
|
||||||
|
|
||||||
|
|
||||||
@@ -83,13 +83,16 @@ class RoleSerializer(InvenTreeModelSerializer):
|
|||||||
"""Roles associated with the user."""
|
"""Roles associated with the user."""
|
||||||
roles = {}
|
roles = {}
|
||||||
|
|
||||||
|
# Cache the 'groups' queryset for the user
|
||||||
|
groups = prefetch_rule_sets(user)
|
||||||
|
|
||||||
for ruleset in RULESET_CHOICES:
|
for ruleset in RULESET_CHOICES:
|
||||||
role, _text = ruleset
|
role, _text = ruleset
|
||||||
|
|
||||||
permissions = []
|
permissions = []
|
||||||
|
|
||||||
for permission in RULESET_PERMISSIONS:
|
for permission in RULESET_PERMISSIONS:
|
||||||
if check_user_role(user, role, permission):
|
if check_user_role(user, role, permission, groups=groups):
|
||||||
permissions.append(permission)
|
permissions.append(permission)
|
||||||
|
|
||||||
if len(permissions) > 0:
|
if len(permissions) > 0:
|
||||||
|
|||||||
@@ -972,8 +972,10 @@ export default function BuildLineTable({
|
|||||||
...params,
|
...params,
|
||||||
build: build.pk,
|
build: build.pk,
|
||||||
assembly_detail: false,
|
assembly_detail: false,
|
||||||
|
bom_item_detail: true,
|
||||||
category_detail: true,
|
category_detail: true,
|
||||||
part_detail: true
|
part_detail: true,
|
||||||
|
allocations: true
|
||||||
},
|
},
|
||||||
tableActions: tableActions,
|
tableActions: tableActions,
|
||||||
tableFilters: tableFilters,
|
tableFilters: tableFilters,
|
||||||
|
|||||||
@@ -206,7 +206,9 @@ export default function BuildOutputTable({
|
|||||||
.get(apiUrl(ApiEndpoints.build_line_list), {
|
.get(apiUrl(ApiEndpoints.build_line_list), {
|
||||||
params: {
|
params: {
|
||||||
build: buildId,
|
build: buildId,
|
||||||
tracked: true
|
tracked: true,
|
||||||
|
bom_item_detail: true,
|
||||||
|
allocations: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
|
|||||||
@@ -165,7 +165,8 @@ export default function PartBuildAllocationsTable({
|
|||||||
project_code_detail: true,
|
project_code_detail: true,
|
||||||
assembly_detail: true,
|
assembly_detail: true,
|
||||||
build_detail: true,
|
build_detail: true,
|
||||||
order_outstanding: true
|
order_outstanding: true,
|
||||||
|
allocations: true
|
||||||
},
|
},
|
||||||
enableColumnSwitching: true,
|
enableColumnSwitching: true,
|
||||||
enableSearch: false,
|
enableSearch: false,
|
||||||
|
|||||||
@@ -439,7 +439,8 @@ export function PurchaseOrderLineItemTable({
|
|||||||
params: {
|
params: {
|
||||||
...params,
|
...params,
|
||||||
order: orderId,
|
order: orderId,
|
||||||
part_detail: true
|
part_detail: true,
|
||||||
|
destination_detail: true
|
||||||
},
|
},
|
||||||
rowActions: rowActions,
|
rowActions: rowActions,
|
||||||
tableActions: tableActions,
|
tableActions: tableActions,
|
||||||
|
|||||||
@@ -419,7 +419,8 @@ test('Purchase Orders - Receive Items', async ({ browser }) => {
|
|||||||
.getByRole('cell', { name: /Choose Location/ })
|
.getByRole('cell', { name: /Choose Location/ })
|
||||||
.getByText('Room 101')
|
.getByText('Room 101')
|
||||||
.waitFor();
|
.waitFor();
|
||||||
await page.getByText('Mechanical Lab').waitFor();
|
|
||||||
|
await page.getByText('Mechanical Lab').first().waitFor();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user