mirror of
https://github.com/inventree/InvenTree.git
synced 2025-10-29 20:30:39 +00:00
[UI] Part requirements (#10036)
* Tweak SalesOrderAllocations table * Refactor "include_variants" filter * Improved API filtering for "SalesOrderLineItem" endpoint * Fetch part detail for table * Fix email template - Referenced template which does not exist * Refactor the "requirements" endpoint - Include variant part requirements too * Updated starred notifications * Adjust column name * Update PartDetail - Extract information from partRequirements query first * Cache BOM items * Improve PartDetail page * Enhance isGeneratingSchema - Call inspect as late as possible * Adjust PartDetail * Improve BuildOrderAllocations table * Exclude common.newsfeedentry when exporting * Updated playwright tests * Bump API version
This commit is contained in:
@@ -1,12 +1,17 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 369
|
INVENTREE_API_VERSION = 370
|
||||||
|
|
||||||
"""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 = """
|
||||||
|
|
||||||
|
v370 -> 2025-07-17 : https://github.com/inventree/InvenTree/pull/10036
|
||||||
|
- Adds optional "assembly_detail" information to BuildLine API endpoint
|
||||||
|
- Adds "include_variants" filter to SalesOrderLineItem API endpoint
|
||||||
|
- Improves the "PartRequirements" API endpoint to include variant aggregations
|
||||||
|
|
||||||
v369 -> 2025-07-15 : https://github.com/inventree/InvenTree/pull/10023
|
v369 -> 2025-07-15 : https://github.com/inventree/InvenTree/pull/10023
|
||||||
- Adds "note", "updated", "updated_by" fields to the PartParameter API endpoints
|
- Adds "note", "updated", "updated_by" fields to the PartParameter API endpoints
|
||||||
|
|
||||||
|
|||||||
@@ -50,12 +50,19 @@ def isGeneratingSchema():
|
|||||||
if isInServerThread() or isInWorkerThread():
|
if isInServerThread() or isInWorkerThread():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if isRunningMigrations() or isRunningBackup() or isRebuildingData():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if isImportingData():
|
||||||
|
return False
|
||||||
|
|
||||||
if isInTestMode():
|
if isInTestMode():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if 'schema' in sys.argv:
|
if 'schema' in sys.argv:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# This is a very inefficient call - so we only use it as a last resort
|
||||||
return any('drf_spectacular' in frame.filename for frame in inspect.stack())
|
return any('drf_spectacular' in frame.filename for frame in inspect.stack())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -533,6 +533,7 @@ class BuildLineEndpoint:
|
|||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
|
|
||||||
kwargs['bom_item_detail'] = str2bool(params.get('bom_item_detail', True))
|
kwargs['bom_item_detail'] = str2bool(params.get('bom_item_detail', True))
|
||||||
|
kwargs['assembly_detail'] = str2bool(params.get('assembly_detail', True))
|
||||||
kwargs['part_detail'] = str2bool(params.get('part_detail', True))
|
kwargs['part_detail'] = str2bool(params.get('part_detail', True))
|
||||||
kwargs['build_detail'] = str2bool(params.get('build_detail', False))
|
kwargs['build_detail'] = str2bool(params.get('build_detail', False))
|
||||||
kwargs['allocations'] = str2bool(params.get('allocations', True))
|
kwargs['allocations'] = str2bool(params.get('allocations', True))
|
||||||
|
|||||||
@@ -1319,6 +1319,7 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
'part_category_name',
|
'part_category_name',
|
||||||
# Extra detail (related field) serializers
|
# Extra detail (related field) serializers
|
||||||
'bom_item_detail',
|
'bom_item_detail',
|
||||||
|
'assembly_detail',
|
||||||
'part_detail',
|
'part_detail',
|
||||||
'build_detail',
|
'build_detail',
|
||||||
]
|
]
|
||||||
@@ -1328,6 +1329,7 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Determine which extra details fields should be included."""
|
"""Determine which extra details fields should be included."""
|
||||||
part_detail = kwargs.pop('part_detail', True)
|
part_detail = kwargs.pop('part_detail', True)
|
||||||
|
assembly_detail = kwargs.pop('assembly_detail', True)
|
||||||
bom_item_detail = kwargs.pop('bom_item_detail', True)
|
bom_item_detail = kwargs.pop('bom_item_detail', True)
|
||||||
build_detail = kwargs.pop('build_detail', True)
|
build_detail = kwargs.pop('build_detail', True)
|
||||||
allocations = kwargs.pop('allocations', True)
|
allocations = kwargs.pop('allocations', True)
|
||||||
@@ -1349,6 +1351,9 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
if not allocations:
|
if not allocations:
|
||||||
self.fields.pop('allocations', None)
|
self.fields.pop('allocations', None)
|
||||||
|
|
||||||
|
if not assembly_detail:
|
||||||
|
self.fields.pop('assembly_detail', None)
|
||||||
|
|
||||||
# Build info fields
|
# Build info fields
|
||||||
build_reference = serializers.CharField(
|
build_reference = serializers.CharField(
|
||||||
source='build.reference', label=_('Build Reference'), read_only=True
|
source='build.reference', label=_('Build Reference'), read_only=True
|
||||||
@@ -1406,6 +1411,14 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali
|
|||||||
part_detail=False,
|
part_detail=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assembly_detail = part_serializers.PartBriefSerializer(
|
||||||
|
label=_('Assembly'),
|
||||||
|
source='bom_item.part',
|
||||||
|
many=False,
|
||||||
|
read_only=True,
|
||||||
|
pricing=False,
|
||||||
|
)
|
||||||
|
|
||||||
part_detail = part_serializers.PartBriefSerializer(
|
part_detail = part_serializers.PartBriefSerializer(
|
||||||
label=_('Part'),
|
label=_('Part'),
|
||||||
source='bom_item.sub_part',
|
source='bom_item.sub_part',
|
||||||
|
|||||||
@@ -894,10 +894,40 @@ class SalesOrderLineItemFilter(LineItemFilter):
|
|||||||
queryset=models.SalesOrder.objects.all(), field_name='order', label=_('Order')
|
queryset=models.SalesOrder.objects.all(), field_name='order', label=_('Order')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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(
|
part = rest_filters.ModelChoiceFilter(
|
||||||
queryset=Part.objects.all(), field_name='part', label=_('Part')
|
queryset=Part.objects.all(),
|
||||||
|
field_name='part',
|
||||||
|
label=_('Part'),
|
||||||
|
method='filter_part',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@extend_schema_field(OpenApiTypes.INT)
|
||||||
|
def filter_part(self, queryset, name, part):
|
||||||
|
"""Filter SalesOrderLineItem 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)
|
||||||
|
|
||||||
|
return queryset.filter(part__in=parts)
|
||||||
|
|
||||||
allocated = rest_filters.BooleanFilter(
|
allocated = rest_filters.BooleanFilter(
|
||||||
label=_('Allocated'), method='filter_allocated'
|
label=_('Allocated'), method='filter_allocated'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1330,29 +1330,62 @@ class Part(
|
|||||||
|
|
||||||
return max(total, 0)
|
return max(total, 0)
|
||||||
|
|
||||||
def requiring_build_orders(self):
|
def requiring_build_orders(self, include_variants: bool = True):
|
||||||
"""Return list of outstanding build orders which require this part."""
|
"""Return list of outstanding build orders which require this part.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
include_variants: If True, include variants of this part in the calculation
|
||||||
|
"""
|
||||||
# List parts that this part is required for
|
# List parts that this part is required for
|
||||||
|
|
||||||
|
if include_variants:
|
||||||
|
# If we are including variants, get all parts in the variant tree
|
||||||
|
parts = list(self.get_descendants(include_self=True))
|
||||||
|
else:
|
||||||
|
parts = [self]
|
||||||
|
|
||||||
|
used_in_parts = set()
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
# Get all assemblies which use this part
|
||||||
|
used_in_parts.update(part.get_used_in())
|
||||||
|
|
||||||
# Now, get a list of outstanding build orders which require this part
|
# Now, get a list of outstanding build orders which require this part
|
||||||
builds = BuildModels.Build.objects.filter(
|
builds = BuildModels.Build.objects.filter(
|
||||||
part__in=self.get_used_in(), status__in=BuildStatusGroups.ACTIVE_CODES
|
part__in=list(used_in_parts), status__in=BuildStatusGroups.ACTIVE_CODES
|
||||||
)
|
)
|
||||||
|
|
||||||
return builds
|
return builds
|
||||||
|
|
||||||
def required_build_order_quantity(self):
|
def required_build_order_quantity(self, include_variants: bool = True):
|
||||||
"""Return the quantity of this part required for active build orders."""
|
"""Return the quantity of this part required for active build orders.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
include_variants: If True, include variants of this part in the calculation
|
||||||
|
"""
|
||||||
# List active build orders which reference this part
|
# List active build orders which reference this part
|
||||||
builds = self.requiring_build_orders()
|
builds = self.requiring_build_orders(include_variants=include_variants)
|
||||||
|
|
||||||
quantity = 0
|
quantity = 0
|
||||||
|
|
||||||
for build in builds:
|
if include_variants:
|
||||||
bom_item = None
|
matching_parts = list(self.get_descendants(include_self=True))
|
||||||
|
else:
|
||||||
|
matching_parts = [self]
|
||||||
|
|
||||||
# List the bom lines required to make the build (including inherited ones!)
|
# Cache the BOM items that we query
|
||||||
bom_items = build.part.get_bom_items().filter(sub_part=self)
|
# Keep a dict of part ID to BOM items
|
||||||
|
cached_bom_items: dict = {}
|
||||||
|
|
||||||
|
for build in builds:
|
||||||
|
if build.part.pk not in cached_bom_items:
|
||||||
|
# Get the BOM items for this part
|
||||||
|
bom_items = build.part.get_bom_items().filter(
|
||||||
|
sub_part__in=matching_parts
|
||||||
|
)
|
||||||
|
cached_bom_items[build.part.pk] = bom_items
|
||||||
|
else:
|
||||||
|
bom_items = cached_bom_items[build.part.pk]
|
||||||
|
|
||||||
# Match BOM item to build
|
# Match BOM item to build
|
||||||
for bom_item in bom_items:
|
for bom_item in bom_items:
|
||||||
@@ -1362,13 +1395,22 @@ class Part(
|
|||||||
|
|
||||||
return quantity
|
return quantity
|
||||||
|
|
||||||
def requiring_sales_orders(self):
|
def requiring_sales_orders(self, include_variants: bool = True):
|
||||||
"""Return a list of sales orders which require this part."""
|
"""Return a list of sales orders which require this part.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
include_variants: If True, include variants of this part in the calculation
|
||||||
|
"""
|
||||||
orders = set()
|
orders = set()
|
||||||
|
|
||||||
|
if include_variants:
|
||||||
|
parts = list(self.get_descendants(include_self=True))
|
||||||
|
else:
|
||||||
|
parts = [self]
|
||||||
|
|
||||||
# Get a list of line items for open orders which match this part
|
# Get a list of line items for open orders which match this part
|
||||||
open_lines = OrderModels.SalesOrderLineItem.objects.filter(
|
open_lines = OrderModels.SalesOrderLineItem.objects.filter(
|
||||||
order__status__in=SalesOrderStatusGroups.OPEN, part=self
|
order__status__in=SalesOrderStatusGroups.OPEN, part__in=parts
|
||||||
)
|
)
|
||||||
|
|
||||||
for line in open_lines:
|
for line in open_lines:
|
||||||
@@ -1376,11 +1418,20 @@ class Part(
|
|||||||
|
|
||||||
return orders
|
return orders
|
||||||
|
|
||||||
def required_sales_order_quantity(self):
|
def required_sales_order_quantity(self, include_variants: bool = True):
|
||||||
"""Return the quantity of this part required for active sales orders."""
|
"""Return the quantity of this part required for active sales orders.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
include_variants: If True, include variants of this part in the calculation
|
||||||
|
"""
|
||||||
|
if include_variants:
|
||||||
|
parts = list(self.get_descendants(include_self=True))
|
||||||
|
else:
|
||||||
|
parts = [self]
|
||||||
|
|
||||||
# Get a list of line items for open orders which match this part
|
# Get a list of line items for open orders which match this part
|
||||||
open_lines = OrderModels.SalesOrderLineItem.objects.filter(
|
open_lines = OrderModels.SalesOrderLineItem.objects.filter(
|
||||||
order__status__in=SalesOrderStatusGroups.OPEN, part=self
|
order__status__in=SalesOrderStatusGroups.OPEN, part__in=parts
|
||||||
)
|
)
|
||||||
|
|
||||||
quantity = 0
|
quantity = 0
|
||||||
@@ -1392,11 +1443,11 @@ class Part(
|
|||||||
|
|
||||||
return quantity
|
return quantity
|
||||||
|
|
||||||
def required_order_quantity(self):
|
def required_order_quantity(self, include_variants: bool = True):
|
||||||
"""Return total required to fulfil orders."""
|
"""Return total required to fulfil orders."""
|
||||||
return (
|
return self.required_build_order_quantity(
|
||||||
self.required_build_order_quantity() + self.required_sales_order_quantity()
|
include_variants=include_variants
|
||||||
)
|
) + self.required_sales_order_quantity(include_variants=include_variants)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def quantity_to_order(self):
|
def quantity_to_order(self):
|
||||||
@@ -1626,13 +1677,25 @@ class Part(
|
|||||||
return self.builds.filter(status__in=BuildStatusGroups.ACTIVE_CODES)
|
return self.builds.filter(status__in=BuildStatusGroups.ACTIVE_CODES)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def quantity_being_built(self):
|
def quantity_being_built(self, include_variants: bool = True):
|
||||||
"""Return the current number of parts currently being built.
|
"""Return the current number of parts currently being built.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
include_variants: If True, include variants of this part in the calculation
|
||||||
|
|
||||||
Note: This is the total quantity of Build orders, *not* the number of build outputs.
|
Note: This is the total quantity of Build orders, *not* the number of build outputs.
|
||||||
In this fashion, it is the "projected" quantity of builds
|
In this fashion, it is the "projected" quantity of builds
|
||||||
"""
|
"""
|
||||||
builds = self.active_builds
|
builds = BuildModels.Build.objects.filter(
|
||||||
|
status__in=BuildStatusGroups.ACTIVE_CODES
|
||||||
|
)
|
||||||
|
|
||||||
|
if include_variants:
|
||||||
|
# If we are including variants, get all parts in the variant tree
|
||||||
|
builds = builds.filter(part__in=self.get_descendants(include_self=True))
|
||||||
|
else:
|
||||||
|
# Only look at this part
|
||||||
|
builds = builds.filter(part=self)
|
||||||
|
|
||||||
quantity = 0
|
quantity = 0
|
||||||
|
|
||||||
@@ -1643,17 +1706,27 @@ class Part(
|
|||||||
return quantity
|
return quantity
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def quantity_in_production(self):
|
def quantity_in_production(self, include_variants: bool = True):
|
||||||
"""Quantity of this part currently actively in production.
|
"""Quantity of this part currently actively in production.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
include_variants: If True, include variants of this part in the calculation
|
||||||
|
|
||||||
Note: This may return a different value to `quantity_being_built`
|
Note: This may return a different value to `quantity_being_built`
|
||||||
"""
|
"""
|
||||||
quantity = 0
|
quantity = 0
|
||||||
|
|
||||||
items = self.stock_items.filter(
|
items = StockModels.StockItem.objects.filter(
|
||||||
is_building=True, build__status__in=BuildStatusGroups.ACTIVE_CODES
|
is_building=True, build__status__in=BuildStatusGroups.ACTIVE_CODES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if include_variants:
|
||||||
|
# If we are including variants, get all parts in the variant tree
|
||||||
|
items = items.filter(part__in=self.get_descendants(include_self=True))
|
||||||
|
else:
|
||||||
|
# Only look at this part
|
||||||
|
items = items.filter(part=self)
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
# The remaining items in the build
|
# The remaining items in the build
|
||||||
quantity += item.quantity
|
quantity += item.quantity
|
||||||
@@ -1823,7 +1896,7 @@ class Part(
|
|||||||
self.get_bom_item_filter(include_inherited=include_inherited)
|
self.get_bom_item_filter(include_inherited=include_inherited)
|
||||||
)
|
)
|
||||||
|
|
||||||
return queryset.prefetch_related('sub_part')
|
return queryset.prefetch_related('part', 'sub_part')
|
||||||
|
|
||||||
def get_installed_part_options(
|
def get_installed_part_options(
|
||||||
self, include_inherited: bool = True, include_variants: bool = True
|
self, include_inherited: bool = True, include_variants: bool = True
|
||||||
|
|||||||
@@ -1277,7 +1277,7 @@ class PartRequirementsSerializer(InvenTree.serializers.InvenTreeModelSerializer)
|
|||||||
|
|
||||||
def get_allocated_to_sales_orders(self, part) -> float:
|
def get_allocated_to_sales_orders(self, part) -> float:
|
||||||
"""Return the allocated sales order quantity."""
|
"""Return the allocated sales order quantity."""
|
||||||
return part.sales_order_allocation_count(pending=True)
|
return part.sales_order_allocation_count(include_variants=True, pending=True)
|
||||||
|
|
||||||
|
|
||||||
class PartStocktakeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
class PartStocktakeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||||
|
|||||||
@@ -25,9 +25,9 @@
|
|||||||
<a href='{{ line.link }}'>{{ line.part.full_name }}</a>{% if line.part.description %} - <em>{{ line.part.description }}</em>{% endif %}
|
<a href='{{ line.link }}'>{{ line.part.full_name }}</a>{% if line.part.description %} - <em>{{ line.part.description }}</em>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;">
|
<td style="text-align: center;">
|
||||||
{% decimal line.required %} {% include "part/part_units.html" with part=line.part %}
|
{% decimal line.required %}
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: center;">{% decimal line.available %} {% include "part/part_units.html" with part=line.part %}</td>
|
<td style="text-align: center;">{% decimal line.available %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
|||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { showNotification } from '@mantine/notifications';
|
import { hideNotification, showNotification } from '@mantine/notifications';
|
||||||
import { IconBell } from '@tabler/icons-react';
|
import { IconBell } from '@tabler/icons-react';
|
||||||
import type { JSX } from 'react';
|
import type { JSX } from 'react';
|
||||||
import { useApi } from '../../contexts/ApiContext';
|
import { useApi } from '../../contexts/ApiContext';
|
||||||
@@ -31,8 +31,10 @@ export default function StarredToggleButton({
|
|||||||
{ starred: !starred }
|
{ starred: !starred }
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
hideNotification('subscription-update');
|
||||||
showNotification({
|
showNotification({
|
||||||
title: 'Subscription updated',
|
title: t`Subscription Updated`,
|
||||||
|
id: 'subscription-update',
|
||||||
message: `Subscription ${starred ? 'removed' : 'added'}`,
|
message: `Subscription ${starred ? 'removed' : 'added'}`,
|
||||||
autoClose: 5000,
|
autoClose: 5000,
|
||||||
color: 'blue'
|
color: 'blue'
|
||||||
|
|||||||
@@ -153,6 +153,16 @@ export default function PartDetail() {
|
|||||||
|
|
||||||
const data = { ...part };
|
const data = { ...part };
|
||||||
|
|
||||||
|
const fetching =
|
||||||
|
partRequirementsQuery.isFetching || instanceQuery.isFetching;
|
||||||
|
|
||||||
|
// Copy part requirements data into the main part data
|
||||||
|
data.total_in_stock =
|
||||||
|
partRequirements?.total_stock ?? part?.total_in_stock ?? 0;
|
||||||
|
data.unallocated =
|
||||||
|
partRequirements?.unallocated_stock ?? part?.unallocated_stock ?? 0;
|
||||||
|
data.ordering = partRequirements?.ordering ?? part?.ordering ?? 0;
|
||||||
|
|
||||||
data.required =
|
data.required =
|
||||||
(partRequirements?.required_for_build_orders ??
|
(partRequirements?.required_for_build_orders ??
|
||||||
part?.required_for_build_orders ??
|
part?.required_for_build_orders ??
|
||||||
@@ -273,26 +283,12 @@ export default function PartDetail() {
|
|||||||
label: t`In Stock`
|
label: t`In Stock`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'number',
|
type: 'progressbar',
|
||||||
name: 'unallocated_stock',
|
name: 'unallocated_stock',
|
||||||
unit: part.units,
|
total: data.total_in_stock,
|
||||||
|
progress: data.unallocated,
|
||||||
label: t`Available Stock`,
|
label: t`Available Stock`,
|
||||||
hidden: part.total_in_stock == part.unallocated_stock
|
hidden: data.total_in_stock == data.unallocated
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'number',
|
|
||||||
name: 'variant_stock',
|
|
||||||
unit: part.units,
|
|
||||||
label: t`Variant Stock`,
|
|
||||||
hidden: !part.variant_stock,
|
|
||||||
icon: 'stock'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'number',
|
|
||||||
name: 'minimum_stock',
|
|
||||||
unit: part.units,
|
|
||||||
label: t`Minimum Stock`,
|
|
||||||
hidden: part.minimum_stock <= 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'number',
|
type: 'number',
|
||||||
@@ -306,47 +302,56 @@ export default function PartDetail() {
|
|||||||
name: 'required',
|
name: 'required',
|
||||||
label: t`Required for Orders`,
|
label: t`Required for Orders`,
|
||||||
unit: part.units,
|
unit: part.units,
|
||||||
hidden: part.required <= 0,
|
hidden: data.required <= 0,
|
||||||
icon: 'stocktake'
|
icon: 'stocktake'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'progressbar',
|
type: 'progressbar',
|
||||||
name: 'allocated_to_build_orders',
|
name: 'allocated_to_build_orders',
|
||||||
icon: 'tick_off',
|
icon: 'manufacturers',
|
||||||
total: part.required_for_build_orders,
|
total: partRequirements.required_for_build_orders,
|
||||||
progress: part.allocated_to_build_orders,
|
progress: partRequirements.allocated_to_build_orders,
|
||||||
label: t`Allocated to Build Orders`,
|
label: t`Allocated to Build Orders`,
|
||||||
hidden:
|
hidden:
|
||||||
!part.component ||
|
fetching ||
|
||||||
(part.required_for_build_orders <= 0 &&
|
(partRequirements.required_for_build_orders <= 0 &&
|
||||||
part.allocated_to_build_orders <= 0)
|
partRequirements.allocated_to_build_orders <= 0)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'progressbar',
|
type: 'progressbar',
|
||||||
icon: 'tick_off',
|
icon: 'sales_orders',
|
||||||
name: 'allocated_to_sales_orders',
|
name: 'allocated_to_sales_orders',
|
||||||
total: part.required_for_sales_orders,
|
total: partRequirements.required_for_sales_orders,
|
||||||
progress: part.allocated_to_sales_orders,
|
progress: partRequirements.allocated_to_sales_orders,
|
||||||
label: t`Allocated to Sales Orders`,
|
label: t`Allocated to Sales Orders`,
|
||||||
hidden:
|
hidden:
|
||||||
!part.salable ||
|
fetching ||
|
||||||
(part.required_for_sales_orders <= 0 &&
|
(partRequirements.required_for_sales_orders <= 0 &&
|
||||||
part.allocated_to_sales_orders <= 0)
|
partRequirements.allocated_to_sales_orders <= 0)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'progressbar',
|
type: 'progressbar',
|
||||||
name: 'building',
|
name: 'building',
|
||||||
label: t`In Production`,
|
label: t`In Production`,
|
||||||
progress: part.building,
|
progress: partRequirements.building,
|
||||||
total: part.scheduled_to_build,
|
total: partRequirements.scheduled_to_build,
|
||||||
hidden: !part.assembly || (!part.building && !part.scheduled_to_build)
|
hidden:
|
||||||
|
fetching ||
|
||||||
|
(!partRequirements.building && !partRequirements.scheduled_to_build)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'number',
|
type: 'number',
|
||||||
name: 'can_build',
|
name: 'can_build',
|
||||||
unit: part.units,
|
unit: part.units,
|
||||||
label: t`Can Build`,
|
label: t`Can Build`,
|
||||||
hidden: !part.assembly || partRequirementsQuery.isFetching
|
hidden: !part.assembly || fetching
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
name: 'minimum_stock',
|
||||||
|
unit: part.units,
|
||||||
|
label: t`Minimum Stock`,
|
||||||
|
hidden: part.minimum_stock <= 0
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -768,30 +773,37 @@ export default function PartDetail() {
|
|||||||
}, [part]);
|
}, [part]);
|
||||||
|
|
||||||
const badges: ReactNode[] = useMemo(() => {
|
const badges: ReactNode[] = useMemo(() => {
|
||||||
if (instanceQuery.isFetching) {
|
if (partRequirementsQuery.isFetching) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const required =
|
const required =
|
||||||
part.required_for_build_orders + part.required_for_sales_orders;
|
partRequirements.required_for_build_orders +
|
||||||
|
partRequirements.required_for_sales_orders;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<DetailsBadge
|
<DetailsBadge
|
||||||
label={`${t`In Stock`}: ${part.total_in_stock}`}
|
label={`${t`In Stock`}: ${partRequirements.total_stock}`}
|
||||||
color={part.total_in_stock >= part.minimum_stock ? 'green' : 'orange'}
|
color={
|
||||||
visible={part.total_in_stock > 0}
|
partRequirements.total_stock >= part.minimum_stock
|
||||||
|
? 'green'
|
||||||
|
: 'orange'
|
||||||
|
}
|
||||||
|
visible={partRequirements.total_stock > 0}
|
||||||
key='in_stock'
|
key='in_stock'
|
||||||
/>,
|
/>,
|
||||||
<DetailsBadge
|
<DetailsBadge
|
||||||
label={`${t`Available`}: ${part.unallocated_stock}`}
|
label={`${t`Available`}: ${partRequirements.unallocated_stock}`}
|
||||||
color='yellow'
|
color='yellow'
|
||||||
key='available_stock'
|
key='available_stock'
|
||||||
visible={part.unallocated_stock != part.total_in_stock}
|
visible={
|
||||||
|
partRequirements.unallocated_stock != partRequirements.total_stock
|
||||||
|
}
|
||||||
/>,
|
/>,
|
||||||
<DetailsBadge
|
<DetailsBadge
|
||||||
label={t`No Stock`}
|
label={t`No Stock`}
|
||||||
color='orange'
|
color='orange'
|
||||||
visible={part.total_in_stock == 0}
|
visible={partRequirements.total_stock == 0}
|
||||||
key='no_stock'
|
key='no_stock'
|
||||||
/>,
|
/>,
|
||||||
<DetailsBadge
|
<DetailsBadge
|
||||||
@@ -801,15 +813,15 @@ export default function PartDetail() {
|
|||||||
key='required'
|
key='required'
|
||||||
/>,
|
/>,
|
||||||
<DetailsBadge
|
<DetailsBadge
|
||||||
label={`${t`On Order`}: ${part.ordering}`}
|
label={`${t`On Order`}: ${partRequirements.ordering}`}
|
||||||
color='blue'
|
color='blue'
|
||||||
visible={part.ordering > 0}
|
visible={partRequirements.ordering > 0}
|
||||||
key='on_order'
|
key='on_order'
|
||||||
/>,
|
/>,
|
||||||
<DetailsBadge
|
<DetailsBadge
|
||||||
label={`${t`In Production`}: ${part.building}`}
|
label={`${t`In Production`}: ${partRequirements.building}`}
|
||||||
color='blue'
|
color='blue'
|
||||||
visible={part.building > 0}
|
visible={partRequirements.building > 0}
|
||||||
key='in_production'
|
key='in_production'
|
||||||
/>,
|
/>,
|
||||||
<DetailsBadge
|
<DetailsBadge
|
||||||
@@ -819,7 +831,7 @@ export default function PartDetail() {
|
|||||||
key='inactive'
|
key='inactive'
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
}, [part, instanceQuery.isFetching]);
|
}, [partRequirements, partRequirementsQuery.isFetching, part]);
|
||||||
|
|
||||||
const partFields = usePartFields({ create: false });
|
const partFields = usePartFields({ create: false });
|
||||||
|
|
||||||
|
|||||||
@@ -249,6 +249,15 @@ export function HasProjectCodeFilter(): TableFilter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function IncludeVariantsFilter(): TableFilter {
|
||||||
|
return {
|
||||||
|
name: 'include_variants',
|
||||||
|
type: 'boolean',
|
||||||
|
label: t`Include Variants`,
|
||||||
|
description: t`Include results for part variants`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function OrderStatusFilter({
|
export function OrderStatusFilter({
|
||||||
model
|
model
|
||||||
}: { model: ModelType }): TableFilter {
|
}: { model: ModelType }): TableFilter {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
ReferenceColumn,
|
ReferenceColumn,
|
||||||
StatusColumn
|
StatusColumn
|
||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
import { StockLocationFilter } from '../Filter';
|
import { IncludeVariantsFilter, StockLocationFilter } from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,12 +66,7 @@ export default function BuildAllocatedStockTable({
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (!!partId) {
|
if (!!partId) {
|
||||||
filters.push({
|
filters.push(IncludeVariantsFilter());
|
||||||
name: 'include_variants',
|
|
||||||
type: 'boolean',
|
|
||||||
label: t`Include Variants`,
|
|
||||||
description: t`Include orders for part variants`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
filters.push(StockLocationFilter());
|
filters.push(StockLocationFilter());
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
CreatedAfterFilter,
|
CreatedAfterFilter,
|
||||||
CreatedBeforeFilter,
|
CreatedBeforeFilter,
|
||||||
HasProjectCodeFilter,
|
HasProjectCodeFilter,
|
||||||
|
IncludeVariantsFilter,
|
||||||
IssuedByFilter,
|
IssuedByFilter,
|
||||||
MaxDateFilter,
|
MaxDateFilter,
|
||||||
MinDateFilter,
|
MinDateFilter,
|
||||||
@@ -190,12 +191,7 @@ export function BuildOrderTable({
|
|||||||
|
|
||||||
// If we are filtering on a specific part, we can include the "include variants" filter
|
// If we are filtering on a specific part, we can include the "include variants" filter
|
||||||
if (!!partId) {
|
if (!!partId) {
|
||||||
filters.push({
|
filters.push(IncludeVariantsFilter());
|
||||||
name: 'include_variants',
|
|
||||||
type: 'boolean',
|
|
||||||
label: t`Include Variants`,
|
|
||||||
description: t`Include orders for part variants`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return filters;
|
return filters;
|
||||||
|
|||||||
@@ -285,6 +285,7 @@ export default function BuildOutputTable({
|
|||||||
title: t`Add Build Output`,
|
title: t`Add Build Output`,
|
||||||
modalId: 'add-build-output',
|
modalId: 'add-build-output',
|
||||||
fields: buildOutputFields,
|
fields: buildOutputFields,
|
||||||
|
successMessage: t`Build output created`,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
initialData: {
|
initialData: {
|
||||||
batch_code: build.batch,
|
batch_code: build.batch,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
|||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
|
import type { TableFilter } from '@lib/types/Filters';
|
||||||
import type { TableColumn } from '@lib/types/Tables';
|
import type { TableColumn } from '@lib/types/Tables';
|
||||||
import { useTable } from '../../hooks/UseTable';
|
import { useTable } from '../../hooks/UseTable';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
ProjectCodeColumn,
|
ProjectCodeColumn,
|
||||||
StatusColumn
|
StatusColumn
|
||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
|
import { IncludeVariantsFilter } from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
import RowExpansionIcon from '../RowExpansionIcon';
|
import RowExpansionIcon from '../RowExpansionIcon';
|
||||||
import { BuildLineSubTable } from '../build/BuildLineTable';
|
import { BuildLineSubTable } from '../build/BuildLineTable';
|
||||||
@@ -53,10 +55,26 @@ export default function PartBuildAllocationsTable({
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'part',
|
accessor: 'assembly_detail',
|
||||||
title: t`Assembly`,
|
title: t`Assembly`,
|
||||||
|
switchable: false,
|
||||||
|
render: (record: any) => <PartColumn part={record.assembly_detail} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'assembly_detail.IPN',
|
||||||
|
title: t`Assembly IPN`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'part_detail',
|
||||||
|
title: t`Part`,
|
||||||
|
defaultVisible: false,
|
||||||
render: (record: any) => <PartColumn part={record.part_detail} />
|
render: (record: any) => <PartColumn part={record.part_detail} />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessor: 'part_detail.IPN',
|
||||||
|
defaultVisible: false,
|
||||||
|
title: t`Part IPN`
|
||||||
|
},
|
||||||
DescriptionColumn({
|
DescriptionColumn({
|
||||||
accessor: 'build_detail.title'
|
accessor: 'build_detail.title'
|
||||||
}),
|
}),
|
||||||
@@ -114,6 +132,10 @@ export default function PartBuildAllocationsTable({
|
|||||||
};
|
};
|
||||||
}, [table.isRowExpanded]);
|
}, [table.isRowExpanded]);
|
||||||
|
|
||||||
|
const tableFilters: TableFilter[] = useMemo(() => {
|
||||||
|
return [IncludeVariantsFilter()];
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url={apiUrl(ApiEndpoints.build_line_list)}
|
url={apiUrl(ApiEndpoints.build_line_list)}
|
||||||
@@ -124,13 +146,16 @@ export default function PartBuildAllocationsTable({
|
|||||||
params: {
|
params: {
|
||||||
part: partId,
|
part: partId,
|
||||||
consumable: false,
|
consumable: false,
|
||||||
|
part_detail: true,
|
||||||
|
assembly_detail: true,
|
||||||
build_detail: true,
|
build_detail: true,
|
||||||
order_outstanding: true
|
order_outstanding: true
|
||||||
},
|
},
|
||||||
enableColumnSwitching: true,
|
enableColumnSwitching: true,
|
||||||
enableSearch: false,
|
enableSearch: false,
|
||||||
rowActions: rowActions,
|
rowActions: rowActions,
|
||||||
rowExpansion: rowExpansion
|
rowExpansion: rowExpansion,
|
||||||
|
tableFilters: tableFilters
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
NoteColumn,
|
NoteColumn,
|
||||||
PartColumn
|
PartColumn
|
||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
import { UserFilter } from '../Filter';
|
import { IncludeVariantsFilter, UserFilter } from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
import { TableHoverCard } from '../TableHoverCard';
|
import { TableHoverCard } from '../TableHoverCard';
|
||||||
|
|
||||||
@@ -142,11 +142,7 @@ export function PartParameterTable({
|
|||||||
|
|
||||||
const tableFilters: TableFilter[] = useMemo(() => {
|
const tableFilters: TableFilter[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
IncludeVariantsFilter(),
|
||||||
name: 'include_variants',
|
|
||||||
label: t`Include Variants`,
|
|
||||||
type: 'boolean'
|
|
||||||
},
|
|
||||||
UserFilter({
|
UserFilter({
|
||||||
name: 'updated_by',
|
name: 'updated_by',
|
||||||
label: t`Updated By`,
|
label: t`Updated By`,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type { TableColumn } from '@lib/types/Tables';
|
|||||||
import { formatCurrency } from '../../defaults/formatters';
|
import { formatCurrency } from '../../defaults/formatters';
|
||||||
import { useTable } from '../../hooks/UseTable';
|
import { useTable } from '../../hooks/UseTable';
|
||||||
import { DateColumn, ReferenceColumn, StatusColumn } from '../ColumnRenderers';
|
import { DateColumn, ReferenceColumn, StatusColumn } from '../ColumnRenderers';
|
||||||
import { StatusFilterOptions } from '../Filter';
|
import { IncludeVariantsFilter, StatusFilterOptions } from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
import { TableHoverCard } from '../TableHoverCard';
|
import { TableHoverCard } from '../TableHoverCard';
|
||||||
|
|
||||||
@@ -133,12 +133,7 @@ export default function PartPurchaseOrdersTable({
|
|||||||
description: t`Filter by order status`,
|
description: t`Filter by order status`,
|
||||||
choiceFunction: StatusFilterOptions(ModelType.purchaseorder)
|
choiceFunction: StatusFilterOptions(ModelType.purchaseorder)
|
||||||
},
|
},
|
||||||
{
|
IncludeVariantsFilter()
|
||||||
name: 'include_variants',
|
|
||||||
type: 'boolean',
|
|
||||||
label: t`Include Variants`,
|
|
||||||
description: t`Include orders for part variants`
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,17 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
|||||||
import { ModelType } from '@lib/enums/ModelType';
|
import { ModelType } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
|
import type { TableFilter } from '@lib/types/Filters';
|
||||||
import type { TableColumn } from '@lib/types/Tables';
|
import type { TableColumn } from '@lib/types/Tables';
|
||||||
import { useTable } from '../../hooks/UseTable';
|
import { useTable } from '../../hooks/UseTable';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import {
|
import {
|
||||||
DescriptionColumn,
|
DescriptionColumn,
|
||||||
|
PartColumn,
|
||||||
ProjectCodeColumn,
|
ProjectCodeColumn,
|
||||||
StatusColumn
|
StatusColumn
|
||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
|
import { IncludeVariantsFilter } from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
import RowExpansionIcon from '../RowExpansionIcon';
|
import RowExpansionIcon from '../RowExpansionIcon';
|
||||||
import SalesOrderAllocationTable from '../sales/SalesOrderAllocationTable';
|
import SalesOrderAllocationTable from '../sales/SalesOrderAllocationTable';
|
||||||
@@ -36,6 +39,7 @@ export default function PartSalesAllocationsTable({
|
|||||||
{
|
{
|
||||||
accessor: 'order',
|
accessor: 'order',
|
||||||
title: t`Sales Order`,
|
title: t`Sales Order`,
|
||||||
|
switchable: false,
|
||||||
render: (record: any) => (
|
render: (record: any) => (
|
||||||
<Group wrap='nowrap' gap='xs'>
|
<Group wrap='nowrap' gap='xs'>
|
||||||
<RowExpansionIcon
|
<RowExpansionIcon
|
||||||
@@ -49,6 +53,15 @@ export default function PartSalesAllocationsTable({
|
|||||||
DescriptionColumn({
|
DescriptionColumn({
|
||||||
accessor: 'order_detail.description'
|
accessor: 'order_detail.description'
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
accessor: 'part_detail',
|
||||||
|
title: t`Part`,
|
||||||
|
render: (record: any) => <PartColumn part={record.part_detail} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'part_detail.IPN',
|
||||||
|
title: t`IPN`
|
||||||
|
},
|
||||||
ProjectCodeColumn({
|
ProjectCodeColumn({
|
||||||
accessor: 'order_detail.project_code_detail'
|
accessor: 'order_detail.project_code_detail'
|
||||||
}),
|
}),
|
||||||
@@ -59,7 +72,8 @@ export default function PartSalesAllocationsTable({
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
accessor: 'allocated',
|
accessor: 'allocated',
|
||||||
title: t`Required Stock`,
|
title: t`Allocated Stock`,
|
||||||
|
switchable: false,
|
||||||
render: (record: any) => (
|
render: (record: any) => (
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
progressLabel
|
progressLabel
|
||||||
@@ -86,6 +100,10 @@ export default function PartSalesAllocationsTable({
|
|||||||
[user]
|
[user]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tableFilters: TableFilter[] = useMemo(() => {
|
||||||
|
return [IncludeVariantsFilter()];
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Control row expansion
|
// Control row expansion
|
||||||
const rowExpansion: DataTableRowExpansionProps<any> = useMemo(() => {
|
const rowExpansion: DataTableRowExpansionProps<any> = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@@ -117,11 +135,13 @@ export default function PartSalesAllocationsTable({
|
|||||||
minHeight: 200,
|
minHeight: 200,
|
||||||
params: {
|
params: {
|
||||||
part: partId,
|
part: partId,
|
||||||
|
part_detail: true,
|
||||||
order_detail: true,
|
order_detail: true,
|
||||||
order_outstanding: true
|
order_outstanding: true
|
||||||
},
|
},
|
||||||
|
tableFilters: tableFilters,
|
||||||
enableSearch: false,
|
enableSearch: false,
|
||||||
enableColumnSwitching: false,
|
enableColumnSwitching: true,
|
||||||
rowExpansion: rowExpansion,
|
rowExpansion: rowExpansion,
|
||||||
rowActions: rowActions
|
rowActions: rowActions
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
CreatedBeforeFilter,
|
CreatedBeforeFilter,
|
||||||
CreatedByFilter,
|
CreatedByFilter,
|
||||||
HasProjectCodeFilter,
|
HasProjectCodeFilter,
|
||||||
|
IncludeVariantsFilter,
|
||||||
MaxDateFilter,
|
MaxDateFilter,
|
||||||
MinDateFilter,
|
MinDateFilter,
|
||||||
OrderStatusFilter,
|
OrderStatusFilter,
|
||||||
@@ -93,12 +94,7 @@ export function ReturnOrderTable({
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (!!partId) {
|
if (!!partId) {
|
||||||
filters.push({
|
filters.push(IncludeVariantsFilter());
|
||||||
name: 'include_variants',
|
|
||||||
type: 'boolean',
|
|
||||||
label: t`Include Variants`,
|
|
||||||
description: t`Include orders for part variants`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return filters;
|
return filters;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
ReferenceColumn,
|
ReferenceColumn,
|
||||||
StatusColumn
|
StatusColumn
|
||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
import { StockLocationFilter } from '../Filter';
|
import { IncludeVariantsFilter, StockLocationFilter } from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
export default function SalesOrderAllocationTable({
|
export default function SalesOrderAllocationTable({
|
||||||
@@ -94,12 +94,7 @@ export default function SalesOrderAllocationTable({
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (!!partId) {
|
if (!!partId) {
|
||||||
filters.push({
|
filters.push(IncludeVariantsFilter());
|
||||||
name: 'include_variants',
|
|
||||||
type: 'boolean',
|
|
||||||
label: t`Include Variants`,
|
|
||||||
description: t`Include orders for part variants`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return filters;
|
return filters;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
CreatedBeforeFilter,
|
CreatedBeforeFilter,
|
||||||
CreatedByFilter,
|
CreatedByFilter,
|
||||||
HasProjectCodeFilter,
|
HasProjectCodeFilter,
|
||||||
|
IncludeVariantsFilter,
|
||||||
MaxDateFilter,
|
MaxDateFilter,
|
||||||
MinDateFilter,
|
MinDateFilter,
|
||||||
OrderStatusFilter,
|
OrderStatusFilter,
|
||||||
@@ -94,12 +95,7 @@ export function SalesOrderTable({
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (!!partId) {
|
if (!!partId) {
|
||||||
filters.push({
|
filters.push(IncludeVariantsFilter());
|
||||||
name: 'include_variants',
|
|
||||||
type: 'boolean',
|
|
||||||
label: t`Include Variants`,
|
|
||||||
description: t`Include orders for part variants`
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return filters;
|
return filters;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
BatchFilter,
|
BatchFilter,
|
||||||
HasBatchCodeFilter,
|
HasBatchCodeFilter,
|
||||||
|
IncludeVariantsFilter,
|
||||||
IsSerializedFilter,
|
IsSerializedFilter,
|
||||||
SerialFilter,
|
SerialFilter,
|
||||||
SerialGTEFilter,
|
SerialGTEFilter,
|
||||||
@@ -356,11 +357,7 @@ function stockItemTableFilters({
|
|||||||
label: t`In Production`,
|
label: t`In Production`,
|
||||||
description: t`Show items which are in production`
|
description: t`Show items which are in production`
|
||||||
},
|
},
|
||||||
{
|
IncludeVariantsFilter(),
|
||||||
name: 'include_variants',
|
|
||||||
label: t`Include Variants`,
|
|
||||||
description: t`Include stock items for variant parts`
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'consumed',
|
name: 'consumed',
|
||||||
label: t`Consumed`,
|
label: t`Consumed`,
|
||||||
|
|||||||
@@ -187,6 +187,45 @@ test('Parts - Details', async ({ browser }) => {
|
|||||||
await page.getByText('2022-04-29').waitFor();
|
await page.getByText('2022-04-29').waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Parts - Requirements', async ({ browser }) => {
|
||||||
|
// Navigate to the "Widget Assembly" part detail page
|
||||||
|
// This part has multiple "variants"
|
||||||
|
// We expect that the template page includes variant requirements
|
||||||
|
const page = await doCachedLogin(browser, { url: 'part/77/details' });
|
||||||
|
|
||||||
|
// Check top-level badges
|
||||||
|
await page.getByText('In Stock: 209').waitFor();
|
||||||
|
await page.getByText('Available: 204').waitFor();
|
||||||
|
await page.getByText('Required: 275').waitFor();
|
||||||
|
await page.getByText('In Production: 24').waitFor();
|
||||||
|
|
||||||
|
// Check requirements details
|
||||||
|
await page.getByText('204 / 209').waitFor(); // Available stock
|
||||||
|
await page.getByText('0 / 100').waitFor(); // Allocated to build orders
|
||||||
|
await page.getByText('5 / 175').waitFor(); // Allocated to sales orders
|
||||||
|
await page.getByText('24 / 214').waitFor(); // In production
|
||||||
|
|
||||||
|
// Let's check out the "variants" for this part, too
|
||||||
|
await navigate(page, 'part/81/details'); // WID-REV-A
|
||||||
|
await page.getByText('WID-REV-A', { exact: true }).first().waitFor();
|
||||||
|
await page.getByText('In Stock: 165').waitFor();
|
||||||
|
await page.getByText('Required: 75').waitFor();
|
||||||
|
|
||||||
|
await navigate(page, 'part/903/details'); // WID-REV-B
|
||||||
|
await page.getByText('WID-REV-B', { exact: true }).first().waitFor();
|
||||||
|
|
||||||
|
await page.getByText('In Stock: 44').waitFor();
|
||||||
|
await page.getByText('Available: 39').waitFor();
|
||||||
|
await page.getByText('Required: 100').waitFor();
|
||||||
|
await page.getByText('In Production: 10').waitFor();
|
||||||
|
|
||||||
|
await page.getByText('39 / 44').waitFor(); // Available stock
|
||||||
|
await page.getByText('5 / 100').waitFor(); // Allocated to sales orders
|
||||||
|
await page.getByText('10 / 125').waitFor(); // In production
|
||||||
|
|
||||||
|
await page.waitForTimeout(2500);
|
||||||
|
});
|
||||||
|
|
||||||
test('Parts - Allocations', async ({ browser }) => {
|
test('Parts - Allocations', async ({ browser }) => {
|
||||||
// Let's look at the allocations for a single stock item
|
// Let's look at the allocations for a single stock item
|
||||||
const page = await doCachedLogin(browser, { url: 'stock/item/324/' });
|
const page = await doCachedLogin(browser, { url: 'stock/item/324/' });
|
||||||
|
|||||||
1
tasks.py
1
tasks.py
@@ -296,6 +296,7 @@ def content_excludes(
|
|||||||
'exchange.rate',
|
'exchange.rate',
|
||||||
'exchange.exchangebackend',
|
'exchange.exchangebackend',
|
||||||
'common.dataoutput',
|
'common.dataoutput',
|
||||||
|
'common.newsfeedentry',
|
||||||
'common.notificationentry',
|
'common.notificationentry',
|
||||||
'common.notificationmessage',
|
'common.notificationmessage',
|
||||||
'importer.dataimportsession',
|
'importer.dataimportsession',
|
||||||
|
|||||||
Reference in New Issue
Block a user