From 0196dd2f600191583e3e0b7a255fe90b7bdec7ea Mon Sep 17 00:00:00 2001 From: Lavissa Date: Fri, 15 Mar 2024 02:06:18 +0100 Subject: [PATCH] [PUI/Feature] Integrate Part "Default Location" into UX (#5972) * Add default parts to location page * Fix name strings * Add Stock Transfer modal * Add ApiForm Table field * temp * Add stock transfer form to part, stock item and location * All stock operations for Item, Part, and Location added (except order new) * Add default_location category traversal, and initial PO Line Item Receive form * . * Remove debug values * Added PO line receive form * Add functionality to PO receive extra fields * . * Forgot to bump API version * Add Category Default to details panel * Fix stockItem query count * Fix reviewed issues * . * . * . * Prevent root category from checking parent for default location --- .pre-commit-config.yaml | 6 +- InvenTree/InvenTree/api_version.py | 10 +- InvenTree/order/serializers.py | 24 +- InvenTree/part/filters.py | 26 + InvenTree/part/serializers.py | 18 + InvenTree/stock/serializers.py | 12 +- src/frontend/src/components/forms/ApiForm.tsx | 2 +- .../components/forms/fields/ApiFormField.tsx | 13 +- .../forms/fields/RelatedModelField.tsx | 2 - .../components/forms/fields/TableField.tsx | 80 +++ .../src/components/items/ActionDropdown.tsx | 11 +- src/frontend/src/enums/ApiEndpoints.tsx | 9 + src/frontend/src/forms/PurchaseOrderForms.tsx | 509 ++++++++++++++ src/frontend/src/forms/StockForms.tsx | 665 +++++++++++++++++- src/frontend/src/functions/icons.tsx | 39 +- src/frontend/src/hooks/UseForm.tsx | 7 +- .../src/pages/part/CategoryDetail.tsx | 14 + src/frontend/src/pages/part/PartDetail.tsx | 49 +- .../src/pages/stock/LocationDetail.tsx | 114 ++- src/frontend/src/pages/stock/StockDetail.tsx | 65 +- .../purchasing/PurchaseOrderLineItemTable.tsx | 28 +- .../src/tables/stock/StockItemTable.tsx | 139 +++- 22 files changed, 1785 insertions(+), 57 deletions(-) create mode 100644 src/frontend/src/components/forms/fields/TableField.tsx diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b340ebe939a..bfc2314a9fbf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: check-yaml - id: mixed-line-ending - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.2 + rev: v0.3.0 hooks: - id: ruff-format args: [--preview] @@ -26,7 +26,7 @@ repos: --preview ] - repo: https://github.com/matmair/ruff-pre-commit - rev: 830893bf46db844d9c99b6c468e285199adf2de6 # uv-018 + rev: 8bed1087452bdf816b840ea7b6848b21d32b7419 # uv-018 hooks: - id: pip-compile name: pip-compile requirements-dev.in @@ -60,7 +60,7 @@ repos: - "prettier@^2.4.1" - "@trivago/prettier-plugin-sort-imports" - repo: https://github.com/pre-commit/mirrors-eslint - rev: "v9.0.0-beta.0" + rev: "v9.0.0-beta.1" hooks: - id: eslint additional_dependencies: diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index 7d62e9b909d5..1f12b4d7bcc1 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -1,12 +1,18 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 182 +INVENTREE_API_VERSION = 183 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ -v182 - 2024-03-15 : https://github.com/inventree/InvenTree/pull/6714 +v183 - 2024-03-14 : https://github.com/inventree/InvenTree/pull/5972 + - Adds "category_default_location" annotated field to part serializer + - Adds "part_detail.category_default_location" annotated field to stock item serializer + - Adds "part_detail.category_default_location" annotated field to purchase order line serializer + - Adds "parent_default_location" annotated field to category serializer + +v182 - 2024-03-13 : https://github.com/inventree/InvenTree/pull/6714 - Expose ReportSnippet model to the /report/snippet/ API endpoint - Expose ReportAsset model to the /report/asset/ API endpoint diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 3405e7b8b8ef..90f9aa0a0d0d 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -5,7 +5,16 @@ from django.core.exceptions import ValidationError as DjangoValidationError from django.db import models, transaction -from django.db.models import BooleanField, Case, ExpressionWrapper, F, Q, Value, When +from django.db.models import ( + BooleanField, + Case, + ExpressionWrapper, + F, + Prefetch, + Q, + Value, + When, +) from django.utils.translation import gettext_lazy as _ from rest_framework import serializers @@ -14,6 +23,8 @@ import order.models import part.filters +import part.filters as part_filters +import part.models as part_models import stock.models import stock.serializers from common.serializers import ProjectCodeSerializer @@ -375,6 +386,17 @@ def annotate_queryset(queryset): - "total_price" = purchase_price * quantity - "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.annotate( total_price=ExpressionWrapper( F('purchase_price') * F('quantity'), output_field=models.DecimalField() diff --git a/InvenTree/part/filters.py b/InvenTree/part/filters.py index 42a41eb92349..4d247529b9f9 100644 --- a/InvenTree/part/filters.py +++ b/InvenTree/part/filters.py @@ -287,6 +287,32 @@ def annotate_category_parts(): ) +def annotate_default_location(reference=''): + """Construct a queryset that finds the closest default location in the part's category tree. + + If the part's category has its own default_location, this is returned. + If not, the category tree is traversed until a value is found. + """ + subquery = part.models.PartCategory.objects.filter( + tree_id=OuterRef(f'{reference}tree_id'), + lft__lt=OuterRef(f'{reference}lft'), + rght__gt=OuterRef(f'{reference}rght'), + level__lte=OuterRef(f'{reference}level'), + parent__isnull=False, + ) + + return Coalesce( + F(f'{reference}default_location'), + Subquery( + subquery.order_by('-level') + .filter(default_location__isnull=False) + .values('default_location') + ), + Value(None), + output_field=IntegerField(), + ) + + def annotate_sub_categories(): """Construct a queryset annotation which returns the number of subcategories for each provided category.""" subquery = part.models.PartCategory.objects.filter( diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 38e2b7157d48..f02ade9ed942 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -81,6 +81,7 @@ class Meta: 'url', 'structural', 'icon', + 'parent_default_location', ] def __init__(self, *args, **kwargs): @@ -105,6 +106,10 @@ def annotate_queryset(queryset): subcategories=part.filters.annotate_sub_categories(), ) + queryset = queryset.annotate( + parent_default_location=part.filters.annotate_default_location('parent__') + ) + return queryset url = serializers.CharField(source='get_absolute_url', read_only=True) @@ -121,6 +126,8 @@ def annotate_queryset(queryset): child=serializers.DictField(), source='get_path', read_only=True ) + parent_default_location = serializers.IntegerField(read_only=True) + class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for PartCategory tree.""" @@ -283,6 +290,7 @@ class Meta: 'pk', 'IPN', 'barcode_hash', + 'category_default_location', 'default_location', 'name', 'revision', @@ -314,6 +322,8 @@ def __init__(self, *args, **kwargs): self.fields.pop('pricing_min') self.fields.pop('pricing_max') + category_default_location = serializers.IntegerField(read_only=True) + image = InvenTree.serializers.InvenTreeImageSerializerField(read_only=True) thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) @@ -611,6 +621,7 @@ class Meta: 'allocated_to_build_orders', 'allocated_to_sales_orders', 'building', + 'category_default_location', 'in_stock', 'ordering', 'required_for_build_orders', @@ -766,6 +777,12 @@ def annotate_queryset(queryset): required_for_sales_orders=part.filters.annotate_sales_order_requirements(), ) + queryset = queryset.annotate( + category_default_location=part.filters.annotate_default_location( + 'category__' + ) + ) + return queryset def get_starred(self, part) -> bool: @@ -805,6 +822,7 @@ def get_starred(self, part) -> bool: unallocated_stock = serializers.FloatField( read_only=True, label=_('Unallocated Stock') ) + category_default_location = serializers.IntegerField(read_only=True) variant_stock = serializers.FloatField(read_only=True, label=_('Variant Stock')) minimum_stock = serializers.FloatField() diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 88ed8e05a292..8d062f68559c 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -6,7 +6,7 @@ from django.core.exceptions import ValidationError as DjangoValidationError from django.db import transaction -from django.db.models import BooleanField, Case, Count, Q, Value, When +from django.db.models import BooleanField, Case, Count, Prefetch, Q, Value, When from django.db.models.functions import Coalesce from django.utils.translation import gettext_lazy as _ @@ -20,6 +20,7 @@ import InvenTree.helpers import InvenTree.serializers import InvenTree.status_codes +import part.filters as part_filters import part.models as part_models import stock.filters from company.serializers import SupplierPartSerializer @@ -289,7 +290,14 @@ def annotate_queryset(queryset): 'location', 'sales_order', 'purchase_order', - 'part', + Prefetch( + 'part', + queryset=part_models.Part.objects.annotate( + category_default_location=part_filters.annotate_default_location( + 'category__' + ) + ).prefetch_related(None), + ), 'part__category', 'part__pricing_data', 'supplier_part', diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 2b07aabf9fdb..53fdabe9c707 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -443,7 +443,7 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) { ))}