mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-16 20:15:44 +00:00
Order table variants (#8295)
* BuildOrderTable: Show variants - Allow filtering of build orders by part variant * Add "include_variants" filter for SalesOrder table - A bit tricker! * Add "include_variants" filter to PartPurchaseOrdersTable * Enable filtering ReturnOrder by "part" attribute * Add similiar functionality for SalesOrderAllocation * Add similar filter for BuildAllocation table * Add migration file
This commit is contained in:
@ -1,13 +1,19 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 268
|
||||
INVENTREE_API_VERSION = 269
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v269 - 2024-10-16 : https://github.com/inventree/InvenTree/pull/8295
|
||||
- Adds "include_variants" filter to the BuildOrder API endpoint
|
||||
- Adds "include_variants" filter to the SalesOrder API endpoint
|
||||
- Adds "include_variants" filter to the PurchaseOrderLineItem API endpoint
|
||||
- Adds "include_variants" filter to the ReturnOrder API endpoint
|
||||
|
||||
268 - 2024-10-11 : https://github.com/inventree/InvenTree/pull/8274
|
||||
- Adds "in_stock" attribute to the StockItem serializer
|
||||
|
||||
|
@ -35,7 +35,6 @@ class BuildFilter(rest_filters.FilterSet):
|
||||
model = Build
|
||||
fields = [
|
||||
'sales_order',
|
||||
'part',
|
||||
]
|
||||
|
||||
status = rest_filters.NumberFilter(label='Status')
|
||||
@ -54,6 +53,39 @@ class BuildFilter(rest_filters.FilterSet):
|
||||
field_name='parent',
|
||||
)
|
||||
|
||||
include_variants = rest_filters.BooleanFilter(label=_('Include Variants'), method='filter_include_variants')
|
||||
|
||||
def filter_include_variants(self, queryset, name, value):
|
||||
"""Filter by whether or not to include variants of the selected part.
|
||||
|
||||
Note:
|
||||
- This filter does nothing by itself, and requires the 'part' filter to be set.
|
||||
- Refer to the 'filter_part' method for more information.
|
||||
"""
|
||||
|
||||
return queryset
|
||||
|
||||
part = rest_filters.ModelChoiceFilter(
|
||||
queryset=part.models.Part.objects.all(),
|
||||
field_name='part',
|
||||
method='filter_part'
|
||||
)
|
||||
|
||||
def filter_part(self, queryset, name, part):
|
||||
"""Filter by 'part' which is being built.
|
||||
|
||||
Note:
|
||||
- If "include_variants" is True, include all variants of the selected part.
|
||||
- Otherwise, just filter by the selected part.
|
||||
"""
|
||||
|
||||
include_variants = str2bool(self.data.get('include_variants', False))
|
||||
|
||||
if include_variants:
|
||||
return queryset.filter(part__in=part.get_descendants(include_self=True))
|
||||
else:
|
||||
return queryset.filter(part=part)
|
||||
|
||||
ancestor = rest_filters.ModelChoiceFilter(
|
||||
queryset=Build.objects.all(),
|
||||
label=_('Ancestor Build'),
|
||||
@ -581,13 +613,45 @@ class BuildItemFilter(rest_filters.FilterSet):
|
||||
'install_into',
|
||||
]
|
||||
|
||||
include_variants = rest_filters.BooleanFilter(
|
||||
label=_('Include Variants'), method='filter_include_variants'
|
||||
)
|
||||
|
||||
def filter_include_variants(self, queryset, name, value):
|
||||
"""Filter by whether or not to include variants of the selected part.
|
||||
|
||||
Note:
|
||||
- This filter does nothing by itself, and requires the 'part' filter to be set.
|
||||
- Refer to the 'filter_part' method for more information.
|
||||
"""
|
||||
|
||||
return queryset
|
||||
|
||||
part = rest_filters.ModelChoiceFilter(
|
||||
queryset=part.models.Part.objects.all(),
|
||||
label=_('Part'),
|
||||
method='filter_part',
|
||||
field_name='stock_item__part',
|
||||
)
|
||||
|
||||
def filter_part(self, queryset, name, part):
|
||||
"""Filter by 'part' which is being built.
|
||||
|
||||
Note:
|
||||
- If "include_variants" is True, include all variants of the selected part.
|
||||
- Otherwise, just filter by the selected part.
|
||||
"""
|
||||
|
||||
include_variants = str2bool(self.data.get('include_variants', False))
|
||||
|
||||
if include_variants:
|
||||
return queryset.filter(stock_item__part__in=part.get_descendants(include_self=True))
|
||||
else:
|
||||
return queryset.filter(stock_item__part=part)
|
||||
|
||||
build = rest_filters.ModelChoiceFilter(
|
||||
queryset=build.models.Build.objects.all(),
|
||||
label=_('Build Order'),
|
||||
field_name='build_line__build',
|
||||
)
|
||||
|
||||
|
@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.2.16 on 2024-10-16 06:19
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0130_alter_parttesttemplate_part'),
|
||||
('build', '0052_build_status_custom_key_alter_build_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='build',
|
||||
name='part',
|
||||
field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'assembly': True}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.part', verbose_name='Part'),
|
||||
),
|
||||
]
|
@ -270,8 +270,6 @@ class Build(
|
||||
related_name='builds',
|
||||
limit_choices_to={
|
||||
'assembly': True,
|
||||
'active': True,
|
||||
'virtual': False,
|
||||
},
|
||||
help_text=_('Select part to build'),
|
||||
)
|
||||
|
@ -390,12 +390,40 @@ class PurchaseOrderLineItemFilter(LineItemFilter):
|
||||
label=_('Supplier Part'),
|
||||
)
|
||||
|
||||
include_variants = rest_filters.BooleanFilter(
|
||||
label=_('Include Variants'), method='filter_include_variants'
|
||||
)
|
||||
|
||||
def filter_include_variants(self, queryset, name, value):
|
||||
"""Filter by whether or not to include variants of the selected part.
|
||||
|
||||
Note:
|
||||
- This filter does nothing by itself, and requires the 'base_part' filter to be set.
|
||||
- Refer to the 'filter_base_part' method for more information.
|
||||
"""
|
||||
return queryset
|
||||
|
||||
base_part = rest_filters.ModelChoiceFilter(
|
||||
queryset=Part.objects.filter(purchaseable=True),
|
||||
field_name='part__part',
|
||||
method='filter_base_part',
|
||||
label=_('Internal Part'),
|
||||
)
|
||||
|
||||
def filter_base_part(self, queryset, name, base_part):
|
||||
"""Filter by the 'base_part' attribute.
|
||||
|
||||
Note:
|
||||
- If "include_variants" is True, include all variants of the selected part
|
||||
- Otherwise, just filter by the selected part
|
||||
"""
|
||||
include_variants = str2bool(self.data.get('include_variants', False))
|
||||
|
||||
if include_variants:
|
||||
parts = base_part.get_descendants(include_self=True)
|
||||
return queryset.filter(part__part__in=parts)
|
||||
else:
|
||||
return queryset.filter(part__part=base_part)
|
||||
|
||||
pending = rest_filters.BooleanFilter(
|
||||
method='filter_pending', label=_('Order Pending')
|
||||
)
|
||||
@ -575,6 +603,47 @@ class SalesOrderFilter(OrderFilter):
|
||||
model = models.SalesOrder
|
||||
fields = ['customer']
|
||||
|
||||
include_variants = rest_filters.BooleanFilter(
|
||||
label=_('Include Variants'), method='filter_include_variants'
|
||||
)
|
||||
|
||||
def filter_include_variants(self, queryset, name, value):
|
||||
"""Filter by whether or not to include variants of the selected part.
|
||||
|
||||
Note:
|
||||
- This filter does nothing by itself, and requires the 'part' filter to be set.
|
||||
- Refer to the 'filter_part' method for more information.
|
||||
"""
|
||||
return queryset
|
||||
|
||||
part = rest_filters.ModelChoiceFilter(
|
||||
queryset=Part.objects.all(), field_name='part', method='filter_part'
|
||||
)
|
||||
|
||||
def filter_part(self, queryset, name, part):
|
||||
"""Filter SalesOrder by selected 'part'.
|
||||
|
||||
Note:
|
||||
- If 'include_variants' is set to True, then all variants of the selected part will be included.
|
||||
- Otherwise, just filter by the selected part.
|
||||
"""
|
||||
include_variants = str2bool(self.data.get('include_variants', False))
|
||||
|
||||
# Construct a queryset of parts to filter by
|
||||
if include_variants:
|
||||
parts = part.get_descendants(include_self=True)
|
||||
else:
|
||||
parts = Part.objects.filter(pk=part.pk)
|
||||
|
||||
# Now that we have a queryset of parts, find all the matching sales orders
|
||||
line_items = models.SalesOrderLineItem.objects.filter(part__in=parts)
|
||||
|
||||
# Generate a list of ID values for the matching sales orders
|
||||
sales_orders = line_items.values_list('order', flat=True).distinct()
|
||||
|
||||
# Now we have a list of matching IDs, filter the queryset
|
||||
return queryset.filter(pk__in=sales_orders)
|
||||
|
||||
|
||||
class SalesOrderMixin:
|
||||
"""Mixin class for SalesOrder endpoints."""
|
||||
@ -636,17 +705,6 @@ class SalesOrderList(SalesOrderMixin, DataExportViewMixin, ListCreateAPI):
|
||||
|
||||
params = self.request.query_params
|
||||
|
||||
# Filter by "Part"
|
||||
# Only return SalesOrder which have LineItem referencing the part
|
||||
part = params.get('part', None)
|
||||
|
||||
if part is not None:
|
||||
try:
|
||||
part = Part.objects.get(pk=part)
|
||||
queryset = queryset.filter(id__in=[so.id for so in part.sales_orders()])
|
||||
except (Part.DoesNotExist, ValueError):
|
||||
pass
|
||||
|
||||
# Filter by 'date range'
|
||||
min_date = params.get('min_date', None)
|
||||
max_date = params.get('max_date', None)
|
||||
@ -903,10 +961,38 @@ class SalesOrderAllocationFilter(rest_filters.FilterSet):
|
||||
label=_('Order'),
|
||||
)
|
||||
|
||||
part = rest_filters.ModelChoiceFilter(
|
||||
queryset=Part.objects.all(), field_name='item__part', label=_('Part')
|
||||
include_variants = rest_filters.BooleanFilter(
|
||||
label=_('Include Variants'), method='filter_include_variants'
|
||||
)
|
||||
|
||||
def filter_include_variants(self, queryset, name, value):
|
||||
"""Filter by whether or not to include variants of the selected part.
|
||||
|
||||
Note:
|
||||
- This filter does nothing by itself, and requires the 'part' filter to be set.
|
||||
- Refer to the 'filter_part' method for more information.
|
||||
"""
|
||||
return queryset
|
||||
|
||||
part = rest_filters.ModelChoiceFilter(
|
||||
queryset=Part.objects.all(), method='filter_part', label=_('Part')
|
||||
)
|
||||
|
||||
def filter_part(self, queryset, name, part):
|
||||
"""Filter by the 'part' attribute.
|
||||
|
||||
Note:
|
||||
- If "include_variants" is True, include all variants of the selected part
|
||||
- Otherwise, just filter by the selected part
|
||||
"""
|
||||
include_variants = str2bool(self.data.get('include_variants', False))
|
||||
|
||||
if include_variants:
|
||||
parts = part.get_descendants(include_self=True)
|
||||
return queryset.filter(item__part__in=parts)
|
||||
else:
|
||||
return queryset.filter(item__part=part)
|
||||
|
||||
outstanding = rest_filters.BooleanFilter(
|
||||
label=_('Outstanding'), method='filter_outstanding'
|
||||
)
|
||||
@ -1072,6 +1158,46 @@ class ReturnOrderFilter(OrderFilter):
|
||||
model = models.ReturnOrder
|
||||
fields = ['customer']
|
||||
|
||||
include_variants = rest_filters.BooleanFilter(
|
||||
label=_('Include Variants'), method='filter_include_variants'
|
||||
)
|
||||
|
||||
def filter_include_variants(self, queryset, name, value):
|
||||
"""Filter by whether or not to include variants of the selected part.
|
||||
|
||||
Note:
|
||||
- This filter does nothing by itself, and requires the 'part' filter to be set.
|
||||
- Refer to the 'filter_part' method for more information.
|
||||
"""
|
||||
return queryset
|
||||
|
||||
part = rest_filters.ModelChoiceFilter(
|
||||
queryset=Part.objects.all(), field_name='part', method='filter_part'
|
||||
)
|
||||
|
||||
def filter_part(self, queryset, name, part):
|
||||
"""Filter by selected 'part'.
|
||||
|
||||
Note:
|
||||
- If 'include_variants' is set to True, then all variants of the selected part will be included.
|
||||
- Otherwise, just filter by the selected part.
|
||||
"""
|
||||
include_variants = str2bool(self.data.get('include_variants', False))
|
||||
|
||||
if include_variants:
|
||||
parts = part.get_descendants(include_self=True)
|
||||
else:
|
||||
parts = Part.objects.filter(pk=part.pk)
|
||||
|
||||
# Now that we have a queryset of parts, find all the matching return orders
|
||||
line_items = models.ReturnOrderLineItem.objects.filter(item__part__in=parts)
|
||||
|
||||
# Generate a list of ID values for the matching return orders
|
||||
return_orders = line_items.values_list('order', flat=True).distinct()
|
||||
|
||||
# Now we have a list of matching IDs, filter the queryset
|
||||
return queryset.filter(pk__in=return_orders)
|
||||
|
||||
|
||||
class ReturnOrderMixin:
|
||||
"""Mixin class for ReturnOrder endpoints."""
|
||||
|
Reference in New Issue
Block a user