From 1017ff0605a37acd1e04976504d575cddbce885e Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 9 Jul 2024 08:13:21 +1000 Subject: [PATCH] Default location column (#7587) * Add "default_location_detail" serializer to part API * Add column to CUI table * Implement in PUI part table * Update API version --- .../InvenTree/InvenTree/api_version.py | 5 +++- src/backend/InvenTree/company/serializers.py | 10 ++++--- src/backend/InvenTree/part/api.py | 2 ++ src/backend/InvenTree/part/serializers.py | 26 +++++++++++++++++++ src/backend/InvenTree/stock/serializers.py | 14 ++++++---- .../InvenTree/templates/js/translated/part.js | 14 ++++++++++ src/frontend/src/tables/part/PartTable.tsx | 8 +++++- 7 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 87124db26a..185312b5eb 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,12 +1,15 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 213 +INVENTREE_API_VERSION = 214 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v214 - 2024-07-08 : https://github.com/inventree/InvenTree/pull/7587 + - Adds "default_location_detail" field to the Part API + v213 - 2024-07-06 : https://github.com/inventree/InvenTree/pull/7527 - Adds 'locked' field to Part API diff --git a/src/backend/InvenTree/company/serializers.py b/src/backend/InvenTree/company/serializers.py index 5ef5f248ff..68597d60a7 100644 --- a/src/backend/InvenTree/company/serializers.py +++ b/src/backend/InvenTree/company/serializers.py @@ -10,6 +10,7 @@ from sql_util.utils import SubqueryCount from taggit.serializers import TagListSerializerField import part.filters +import part.serializers as part_serializers from importer.mixins import DataImportExportSerializerMixin from importer.registry import register_importer from InvenTree.serializers import ( @@ -22,7 +23,6 @@ from InvenTree.serializers import ( NotesFieldMixin, RemoteImageMixin, ) -from part.serializers import PartBriefSerializer from .models import ( Address, @@ -254,7 +254,9 @@ class ManufacturerPartSerializer( if prettify is not True: self.fields.pop('pretty_name', None) - part_detail = PartBriefSerializer(source='part', many=False, read_only=True) + part_detail = part_serializers.PartBriefSerializer( + source='part', many=False, read_only=True + ) manufacturer_detail = CompanyBriefSerializer( source='manufacturer', many=False, read_only=True @@ -387,7 +389,9 @@ class SupplierPartSerializer( pack_quantity_native = serializers.FloatField(read_only=True) - part_detail = PartBriefSerializer(source='part', many=False, read_only=True) + part_detail = part_serializers.PartBriefSerializer( + source='part', many=False, read_only=True + ) supplier_detail = CompanyBriefSerializer( source='supplier', many=False, read_only=True diff --git a/src/backend/InvenTree/part/api.py b/src/backend/InvenTree/part/api.py index d8b6fbd190..95d0b5590a 100644 --- a/src/backend/InvenTree/part/api.py +++ b/src/backend/InvenTree/part/api.py @@ -1204,6 +1204,7 @@ class PartMixin: kwargs['parameters'] = str2bool(params.get('parameters', None)) kwargs['category_detail'] = str2bool(params.get('category_detail', False)) + kwargs['location_detail'] = str2bool(params.get('location_detail', False)) kwargs['path_detail'] = str2bool(params.get('path_detail', False)) except AttributeError: @@ -1354,6 +1355,7 @@ class PartList(PartMixin, DataExportViewMixin, ListCreateAPI): 'total_in_stock', 'unallocated_stock', 'category', + 'default_location', 'last_stocktake', 'units', 'pricing_min', diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index f89ce197d4..56c34bb5c3 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -591,6 +591,21 @@ class InitialSupplierSerializer(serializers.Serializer): return data +class DefaultLocationSerializer(InvenTree.serializers.InvenTreeModelSerializer): + """Brief serializer for a StockLocation object. + + Defined here, rather than stock.serializers, to negotiate circular imports. + """ + + class Meta: + """Metaclass options.""" + + import stock.models as stock_models + + model = stock_models.StockLocation + fields = ['pk', 'name', 'pathstring'] + + @register_importer() class PartSerializer( DataImportExportSerializerMixin, @@ -623,6 +638,7 @@ class PartSerializer( 'creation_user', 'default_expiry', 'default_location', + 'default_location_detail', 'default_supplier', 'description', 'full_name', @@ -687,6 +703,7 @@ class PartSerializer( """ self.starred_parts = kwargs.pop('starred_parts', []) category_detail = kwargs.pop('category_detail', False) + location_detail = kwargs.pop('location_detail', False) parameters = kwargs.pop('parameters', False) create = kwargs.pop('create', False) pricing = kwargs.pop('pricing', True) @@ -697,6 +714,9 @@ class PartSerializer( if not category_detail: self.fields.pop('category_detail', None) + if not location_detail: + self.fields.pop('default_location_detail', None) + if not parameters: self.fields.pop('parameters', None) @@ -740,6 +760,8 @@ class PartSerializer( Performing database queries as efficiently as possible, to reduce database trips. """ + queryset = queryset.prefetch_related('category', 'default_location') + # Annotate with the total number of stock items queryset = queryset.annotate(stock_item_count=SubqueryCount('stock_items')) @@ -833,6 +855,10 @@ class PartSerializer( child=serializers.DictField(), source='category.get_path', read_only=True ) + default_location_detail = DefaultLocationSerializer( + source='default_location', many=False, read_only=True + ) + category_name = serializers.CharField( source='category.name', read_only=True, label=_('Category Name') ) diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index 63e79b4981..7560bc2632 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -17,19 +17,19 @@ from taggit.serializers import TagListSerializerField import build.models import company.models +import company.serializers as company_serializers import InvenTree.helpers import InvenTree.serializers import order.models import part.filters as part_filters import part.models as part_models +import part.serializers as part_serializers import stock.filters import stock.status_codes from common.settings import get_global_setting -from company.serializers import SupplierPartSerializer from importer.mixins import DataImportExportSerializerMixin from importer.registry import register_importer from InvenTree.serializers import InvenTreeCurrencySerializer, InvenTreeDecimalField -from part.serializers import PartBriefSerializer, PartTestTemplateSerializer from .models import ( StockItem, @@ -233,7 +233,9 @@ class StockItemTestResultSerializer( label=_('Test template for this result'), ) - template_detail = PartTestTemplateSerializer(source='template', read_only=True) + template_detail = part_serializers.PartTestTemplateSerializer( + source='template', read_only=True + ) attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField( required=False @@ -560,7 +562,7 @@ class StockItemSerializer( sku = serializers.CharField(source='supplier_part.SKU', read_only=True) # Optional detail fields, which can be appended via query parameters - supplier_part_detail = SupplierPartSerializer( + supplier_part_detail = company_serializers.SupplierPartSerializer( source='supplier_part', supplier_detail=False, manufacturer_detail=False, @@ -568,7 +570,9 @@ class StockItemSerializer( many=False, read_only=True, ) - part_detail = PartBriefSerializer(source='part', many=False, read_only=True) + part_detail = part_serializers.PartBriefSerializer( + source='part', many=False, read_only=True + ) location_detail = LocationBriefSerializer( source='location', many=False, read_only=True diff --git a/src/backend/InvenTree/templates/js/translated/part.js b/src/backend/InvenTree/templates/js/translated/part.js index e4b4107d5b..ec438e1503 100644 --- a/src/backend/InvenTree/templates/js/translated/part.js +++ b/src/backend/InvenTree/templates/js/translated/part.js @@ -2276,6 +2276,7 @@ function loadPartTable(table, url, options={}) { // Ensure category detail is included options.params['category_detail'] = true; + options.params['location_detail'] = true; let filters = {}; @@ -2389,6 +2390,19 @@ function loadPartTable(table, url, options={}) { } }); + columns.push({ + field: 'default_location', + title: '{% trans "Default Location" %}', + sortable: true, + formatter: function(value, row) { + if (row.default_location && row.default_location_detail) { + let text = shortenString(row.default_location_detail.pathstring); + return withTitle(renderLink(text, `/stock/location/${row.default_location}/`), row.default_location_detail.pathstring); + } else { + return '-'; + } + } + }); columns.push({ field: 'total_in_stock', diff --git a/src/frontend/src/tables/part/PartTable.tsx b/src/frontend/src/tables/part/PartTable.tsx index e5e2dbb2dd..39119dfd7b 100644 --- a/src/frontend/src/tables/part/PartTable.tsx +++ b/src/frontend/src/tables/part/PartTable.tsx @@ -44,6 +44,11 @@ function partTableColumns(): TableColumn[] { sortable: true, render: (record: any) => record.category_detail?.pathstring }, + { + accessor: 'default_location', + sortable: true, + render: (record: any) => record.default_location_detail?.pathstring + }, { accessor: 'total_in_stock', sortable: true, @@ -327,7 +332,8 @@ export function PartListTable({ props }: { props: InvenTreeTableProps }) { tableActions: tableActions, params: { ...props.params, - category_detail: true + category_detail: true, + location_detail: true } }} />