mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 07:05:41 +00:00 
			
		
		
		
	[CI] docstrings (#6172)
* Squashed commit of the following: commit52d7ff0f65Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 23:03:20 2024 +0100 fixed lookup commit0d076eaea8Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 23:03:08 2024 +0100 switched to pathlib for lookup commit473e75eda2Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:52:30 2024 +0100 fix wrong url response commitfd74f8d703Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 21:14:38 2024 +0100 switched to ruff for import sorting commitf83fedbbb8Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 21:03:14 2024 +0100 switched to single quotes everywhere commita92442e60eAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:58:23 2024 +0100 added autofixes commitcc66c93136Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:56:47 2024 +0100 enable autoformat commit1f343606ecAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:42:14 2024 +0100 Squashed commit of the following: commitf5cf7b2e78Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:36:57 2024 +0100 fixed reqs commit9d845bee98Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:32:35 2024 +0100 disable autofix/format commitaff5f27148Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:50 2024 +0100 adjust checks commit47271cf1efAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:22 2024 +0100 reorder order of operations commite1bf178b40Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:01:09 2024 +0100 adapted ruff settings to better fit code base commitad7d88a6f4Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:59:45 2024 +0100 auto fixed docstring commita2e54a760eAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:46:35 2024 +0100 fix getattr useage commitcb80c73bc6Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:25:09 2024 +0100 fix requirements file commitb7780bbd21Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:42:28 2024 +0100 fix removed sections commit71f1681f55Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:41:21 2024 +0100 fix djlint syntax commita0bcf1bcceAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:35:28 2024 +0100 remove flake8 from code base commit22475b31ccAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:34:56 2024 +0100 remove flake8 from code base commit0413350f14Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:39 2024 +0100 moved ruff section commitd90c48a0bfAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:24 2024 +0100 move djlint config to pyproject commitc5ce55d511Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:20:39 2024 +0100 added isort again commit42a41d23afAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:19:02 2024 +0100 move config section commit8569233181Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:17:52 2024 +0100 fix codespell error commit2897c6704dAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 17:29:21 2024 +0100 replaced flake8 with ruff mostly for speed improvements * enable docstring checks * fix docstrings * fixed D417 Missing argument description * Squashed commit of the following: commitd3b795824bAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:56:17 2024 +0100 fixed source path commit0bac0c19b8Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:47:53 2024 +0100 fixed req commit9f61f01d9cAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:45:18 2024 +0100 added missing toml req commit91b71ed24aAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:49:50 2024 +0100 moved isort config commit12460b0419Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:43:22 2024 +0100 remove flake8 section from setup.cfg commitf5cf7b2e78Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:36:57 2024 +0100 fixed reqs commit9d845bee98Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:32:35 2024 +0100 disable autofix/format commitaff5f27148Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:50 2024 +0100 adjust checks commit47271cf1efAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:22 2024 +0100 reorder order of operations commite1bf178b40Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:01:09 2024 +0100 adapted ruff settings to better fit code base commitad7d88a6f4Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:59:45 2024 +0100 auto fixed docstring commita2e54a760eAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:46:35 2024 +0100 fix getattr useage commitcb80c73bc6Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:25:09 2024 +0100 fix requirements file commitb7780bbd21Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:42:28 2024 +0100 fix removed sections commit71f1681f55Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:41:21 2024 +0100 fix djlint syntax commita0bcf1bcceAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:35:28 2024 +0100 remove flake8 from code base commit22475b31ccAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:34:56 2024 +0100 remove flake8 from code base commit0413350f14Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:39 2024 +0100 moved ruff section commitd90c48a0bfAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:24 2024 +0100 move djlint config to pyproject commitc5ce55d511Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:20:39 2024 +0100 added isort again commit42a41d23afAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:19:02 2024 +0100 move config section commit8569233181Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:17:52 2024 +0100 fix codespell error commit2897c6704dAuthor: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 17:29:21 2024 +0100 replaced flake8 with ruff mostly for speed improvements * fix pyproject * make docstrings more uniform * auto-format * fix order * revert url change
This commit is contained in:
		@@ -256,7 +256,7 @@ class StockItemResource(InvenTreeResource):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def dehydrate_purchase_price(self, item):
 | 
			
		||||
        """Render purchase pric as float"""
 | 
			
		||||
        """Render purchase pric as float."""
 | 
			
		||||
        if item.purchase_price is not None:
 | 
			
		||||
            return float(item.purchase_price.amount)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -161,13 +161,13 @@ class StockItemUninstall(StockItemContextMixin, CreateAPI):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockItemConvert(StockItemContextMixin, CreateAPI):
 | 
			
		||||
    """API endpoint for converting a stock item to a variant part"""
 | 
			
		||||
    """API endpoint for converting a stock item to a variant part."""
 | 
			
		||||
 | 
			
		||||
    serializer_class = StockSerializers.ConvertStockItemSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockItemReturn(StockItemContextMixin, CreateAPI):
 | 
			
		||||
    """API endpoint for returning a stock item from a customer"""
 | 
			
		||||
    """API endpoint for returning a stock item from a customer."""
 | 
			
		||||
 | 
			
		||||
    serializer_class = StockSerializers.ReturnStockItemSerializer
 | 
			
		||||
 | 
			
		||||
@@ -262,7 +262,7 @@ class StockLocationFilter(rest_filters.FilterSet):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def filter_has_location_type(self, queryset, name, value):
 | 
			
		||||
        """Filter by whether or not the location has a location type"""
 | 
			
		||||
        """Filter by whether or not the location has a location type."""
 | 
			
		||||
        if str2bool(value):
 | 
			
		||||
            return queryset.exclude(location_type=None)
 | 
			
		||||
        return queryset.filter(location_type=None)
 | 
			
		||||
@@ -280,7 +280,7 @@ class StockLocationList(APIDownloadMixin, ListCreateAPI):
 | 
			
		||||
    filterset_class = StockLocationFilter
 | 
			
		||||
 | 
			
		||||
    def download_queryset(self, queryset, export_format):
 | 
			
		||||
        """Download the filtered queryset as a data file"""
 | 
			
		||||
        """Download the filtered queryset as a data file."""
 | 
			
		||||
        dataset = LocationResource().export(queryset=queryset)
 | 
			
		||||
        filedata = dataset.export(export_format)
 | 
			
		||||
        filename = f'InvenTree_Locations.{export_format}'
 | 
			
		||||
@@ -288,7 +288,7 @@ class StockLocationList(APIDownloadMixin, ListCreateAPI):
 | 
			
		||||
        return DownloadFile(filedata, filename)
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self, *args, **kwargs):
 | 
			
		||||
        """Return annotated queryset for the StockLocationList endpoint"""
 | 
			
		||||
        """Return annotated queryset for the StockLocationList endpoint."""
 | 
			
		||||
        queryset = super().get_queryset(*args, **kwargs)
 | 
			
		||||
        queryset = StockSerializers.LocationSerializer.annotate_queryset(queryset)
 | 
			
		||||
        return queryset
 | 
			
		||||
@@ -431,7 +431,7 @@ class StockFilter(rest_filters.FilterSet):
 | 
			
		||||
    """FilterSet for StockItem LIST API."""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass options for this filterset"""
 | 
			
		||||
        """Metaclass options for this filterset."""
 | 
			
		||||
 | 
			
		||||
        model = StockItem
 | 
			
		||||
 | 
			
		||||
@@ -505,7 +505,7 @@ class StockFilter(rest_filters.FilterSet):
 | 
			
		||||
    status = rest_filters.NumberFilter(label='Status Code', method='filter_status')
 | 
			
		||||
 | 
			
		||||
    def filter_status(self, queryset, name, value):
 | 
			
		||||
        """Filter by integer status code"""
 | 
			
		||||
        """Filter by integer status code."""
 | 
			
		||||
        return queryset.filter(status=value)
 | 
			
		||||
 | 
			
		||||
    allocated = rest_filters.BooleanFilter(
 | 
			
		||||
@@ -513,7 +513,7 @@ class StockFilter(rest_filters.FilterSet):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def filter_allocated(self, queryset, name, value):
 | 
			
		||||
        """Filter by whether or not the stock item is 'allocated'"""
 | 
			
		||||
        """Filter by whether or not the stock item is 'allocated'."""
 | 
			
		||||
        if str2bool(value):
 | 
			
		||||
            # Filter StockItem with either build allocations or sales order allocations
 | 
			
		||||
            return queryset.filter(
 | 
			
		||||
@@ -527,7 +527,7 @@ class StockFilter(rest_filters.FilterSet):
 | 
			
		||||
    expired = rest_filters.BooleanFilter(label='Expired', method='filter_expired')
 | 
			
		||||
 | 
			
		||||
    def filter_expired(self, queryset, name, value):
 | 
			
		||||
        """Filter by whether or not the stock item has expired"""
 | 
			
		||||
        """Filter by whether or not the stock item has expired."""
 | 
			
		||||
        if not common.settings.stock_expiry_enabled():
 | 
			
		||||
            return queryset
 | 
			
		||||
 | 
			
		||||
@@ -540,7 +540,7 @@ class StockFilter(rest_filters.FilterSet):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def filter_external(self, queryset, name, value):
 | 
			
		||||
        """Filter by whether or not the stock item is located in an external location"""
 | 
			
		||||
        """Filter by whether or not the stock item is located in an external location."""
 | 
			
		||||
        if str2bool(value):
 | 
			
		||||
            return queryset.filter(location__external=True)
 | 
			
		||||
        return queryset.exclude(location__external=True)
 | 
			
		||||
@@ -687,7 +687,7 @@ class StockFilter(rest_filters.FilterSet):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def filter_ancestor(self, queryset, name, ancestor):
 | 
			
		||||
        """Filter based on ancestor stock item"""
 | 
			
		||||
        """Filter based on ancestor stock item."""
 | 
			
		||||
        return queryset.filter(parent__in=ancestor.get_descendants(include_self=True))
 | 
			
		||||
 | 
			
		||||
    category = rest_filters.ModelChoiceFilter(
 | 
			
		||||
@@ -697,8 +697,7 @@ class StockFilter(rest_filters.FilterSet):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def filter_category(self, queryset, name, category):
 | 
			
		||||
        """Filter based on part category"""
 | 
			
		||||
 | 
			
		||||
        """Filter based on part category."""
 | 
			
		||||
        child_categories = category.get_descendants(include_self=True)
 | 
			
		||||
 | 
			
		||||
        return queryset.filter(part__category__in=child_categories)
 | 
			
		||||
@@ -708,8 +707,7 @@ class StockFilter(rest_filters.FilterSet):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def filter_bom_item(self, queryset, name, bom_item):
 | 
			
		||||
        """Filter based on BOM item"""
 | 
			
		||||
 | 
			
		||||
        """Filter based on BOM item."""
 | 
			
		||||
        return queryset.filter(bom_item.get_stock_filter())
 | 
			
		||||
 | 
			
		||||
    part_tree = rest_filters.ModelChoiceFilter(
 | 
			
		||||
@@ -717,7 +715,7 @@ class StockFilter(rest_filters.FilterSet):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def filter_part_tree(self, queryset, name, part_tree):
 | 
			
		||||
        """Filter based on part tree"""
 | 
			
		||||
        """Filter based on part tree."""
 | 
			
		||||
        return queryset.filter(part__tree_id=part_tree.tree_id)
 | 
			
		||||
 | 
			
		||||
    company = rest_filters.ModelChoiceFilter(
 | 
			
		||||
@@ -725,7 +723,7 @@ class StockFilter(rest_filters.FilterSet):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def filter_company(self, queryset, name, company):
 | 
			
		||||
        """Filter by company (either manufacturer or supplier)"""
 | 
			
		||||
        """Filter by company (either manufacturer or supplier)."""
 | 
			
		||||
        return queryset.filter(
 | 
			
		||||
            Q(supplier_part__supplier=company)
 | 
			
		||||
            | Q(supplier_part__manufacturer_part__manufacturer=company)
 | 
			
		||||
@@ -752,7 +750,6 @@ class StockFilter(rest_filters.FilterSet):
 | 
			
		||||
 | 
			
		||||
    def filter_stale(self, queryset, name, value):
 | 
			
		||||
        """Filter by stale stock items."""
 | 
			
		||||
 | 
			
		||||
        stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS')
 | 
			
		||||
 | 
			
		||||
        if stale_days <= 0:
 | 
			
		||||
@@ -1452,7 +1449,7 @@ class LocationDetail(CustomRetrieveUpdateDestroyAPI):
 | 
			
		||||
    serializer_class = StockSerializers.LocationSerializer
 | 
			
		||||
 | 
			
		||||
    def get_serializer(self, *args, **kwargs):
 | 
			
		||||
        """Add extra context to serializer based on provided query parameters"""
 | 
			
		||||
        """Add extra context to serializer based on provided query parameters."""
 | 
			
		||||
        try:
 | 
			
		||||
            params = self.request.query_params
 | 
			
		||||
 | 
			
		||||
@@ -1465,14 +1462,13 @@ class LocationDetail(CustomRetrieveUpdateDestroyAPI):
 | 
			
		||||
        return self.serializer_class(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self, *args, **kwargs):
 | 
			
		||||
        """Return annotated queryset for the StockLocationList endpoint"""
 | 
			
		||||
        """Return annotated queryset for the StockLocationList endpoint."""
 | 
			
		||||
        queryset = super().get_queryset(*args, **kwargs)
 | 
			
		||||
        queryset = StockSerializers.LocationSerializer.annotate_queryset(queryset)
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
    def destroy(self, request, *args, **kwargs):
 | 
			
		||||
        """Delete a Stock location instance via the API"""
 | 
			
		||||
 | 
			
		||||
        """Delete a Stock location instance via the API."""
 | 
			
		||||
        delete_stock_items = str(request.data.get('delete_stock_items', 0)) == '1'
 | 
			
		||||
        delete_sub_locations = str(request.data.get('delete_sub_locations', 0)) == '1'
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
"""Custom query filters for the Stock models"""
 | 
			
		||||
"""Custom query filters for the Stock models."""
 | 
			
		||||
 | 
			
		||||
from django.db.models import F, Func, IntegerField, OuterRef, Q, Subquery
 | 
			
		||||
from django.db.models.functions import Coalesce
 | 
			
		||||
 
 | 
			
		||||
@@ -119,7 +119,7 @@ class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree):
 | 
			
		||||
    objects = StockLocationManager()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass defines extra model properties"""
 | 
			
		||||
        """Metaclass defines extra model properties."""
 | 
			
		||||
 | 
			
		||||
        verbose_name = _('Stock Location')
 | 
			
		||||
        verbose_name_plural = _('Stock Locations')
 | 
			
		||||
@@ -131,7 +131,6 @@ class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree):
 | 
			
		||||
 | 
			
		||||
        This must be handled within a transaction.atomic(), otherwise the tree structure is damaged
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        super().delete(
 | 
			
		||||
            delete_children=kwargs.get('delete_sub_locations', False),
 | 
			
		||||
            delete_items=kwargs.get('delete_stock_items', False),
 | 
			
		||||
@@ -201,8 +200,9 @@ class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree):
 | 
			
		||||
 | 
			
		||||
    @icon.setter
 | 
			
		||||
    def icon(self, value):
 | 
			
		||||
        """Setter to keep model API compatibility. But be careful:
 | 
			
		||||
        """Setter to keep model API compatibility.
 | 
			
		||||
 | 
			
		||||
        But be careful:
 | 
			
		||||
        If the field gets loaded as default value by any form which is later saved,
 | 
			
		||||
        the location no longer inherits its icon from the location type.
 | 
			
		||||
        """
 | 
			
		||||
@@ -243,9 +243,9 @@ class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree):
 | 
			
		||||
        return owner.is_user_allowed(user, include_group=True)
 | 
			
		||||
 | 
			
		||||
    def clean(self):
 | 
			
		||||
        """Custom clean action for the StockLocation model:
 | 
			
		||||
        """Custom clean action for the StockLocation model.
 | 
			
		||||
 | 
			
		||||
        - Ensure stock location can't be made structural if stock items already located to them
 | 
			
		||||
        Ensure stock location can't be made structural if stock items already located to them
 | 
			
		||||
        """
 | 
			
		||||
        if self.pk and self.structural and self.stock_item_count(False) > 0:
 | 
			
		||||
            raise ValidationError(
 | 
			
		||||
@@ -288,7 +288,7 @@ class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree):
 | 
			
		||||
        return self.stock_item_count()
 | 
			
		||||
 | 
			
		||||
    def get_items(self, cascade=False):
 | 
			
		||||
        """Return a queryset for all stock items under this category"""
 | 
			
		||||
        """Return a queryset for all stock items under this category."""
 | 
			
		||||
        return self.get_stock_items(cascade=cascade)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -904,7 +904,7 @@ class StockItem(
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def status_text(self):
 | 
			
		||||
        """Return the text representation of the status field"""
 | 
			
		||||
        """Return the text representation of the status field."""
 | 
			
		||||
        return StockStatus.text(self.status)
 | 
			
		||||
 | 
			
		||||
    purchase_price = InvenTreeModelMoneyField(
 | 
			
		||||
@@ -1669,6 +1669,7 @@ class StockItem(
 | 
			
		||||
        Args:
 | 
			
		||||
            quantity: Number of stock items to remove from this entity, and pass to the next
 | 
			
		||||
            location: Where to move the new StockItem to
 | 
			
		||||
            user: User performing the action
 | 
			
		||||
 | 
			
		||||
        kwargs:
 | 
			
		||||
            notes: Optional notes for tracking
 | 
			
		||||
@@ -1752,7 +1753,7 @@ class StockItem(
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def optional_transfer_fields(cls):
 | 
			
		||||
        """Returns a list of optional fields for a stock transfer"""
 | 
			
		||||
        """Returns a list of optional fields for a stock transfer."""
 | 
			
		||||
        return ['batch', 'status', 'packaging']
 | 
			
		||||
 | 
			
		||||
    @transaction.atomic
 | 
			
		||||
 
 | 
			
		||||
@@ -218,7 +218,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def validate_part(self, part):
 | 
			
		||||
        """Ensure the provided Part instance is valid"""
 | 
			
		||||
        """Ensure the provided Part instance is valid."""
 | 
			
		||||
        if part.virtual:
 | 
			
		||||
            raise ValidationError(_('Stock item cannot be created for virtual parts'))
 | 
			
		||||
 | 
			
		||||
@@ -506,7 +506,6 @@ class InstallStockItemSerializer(serializers.Serializer):
 | 
			
		||||
 | 
			
		||||
    def validate_quantity(self, quantity):
 | 
			
		||||
        """Validate the quantity value."""
 | 
			
		||||
 | 
			
		||||
        if quantity < 1:
 | 
			
		||||
            raise ValidationError(_('Quantity to install must be at least 1'))
 | 
			
		||||
 | 
			
		||||
@@ -528,8 +527,7 @@ class InstallStockItemSerializer(serializers.Serializer):
 | 
			
		||||
        return stock_item
 | 
			
		||||
 | 
			
		||||
    def validate(self, data):
 | 
			
		||||
        """Ensure that the provided dataset is valid"""
 | 
			
		||||
 | 
			
		||||
        """Ensure that the provided dataset is valid."""
 | 
			
		||||
        stock_item = data['stock_item']
 | 
			
		||||
 | 
			
		||||
        quantity = data.get('quantity', stock_item.quantity)
 | 
			
		||||
@@ -596,10 +594,10 @@ class UninstallStockItemSerializer(serializers.Serializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ConvertStockItemSerializer(serializers.Serializer):
 | 
			
		||||
    """DRF serializer class for converting a StockItem to a valid variant part"""
 | 
			
		||||
    """DRF serializer class for converting a StockItem to a valid variant part."""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass options"""
 | 
			
		||||
        """Metaclass options."""
 | 
			
		||||
 | 
			
		||||
        fields = ['part']
 | 
			
		||||
 | 
			
		||||
@@ -613,7 +611,7 @@ class ConvertStockItemSerializer(serializers.Serializer):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def validate_part(self, part):
 | 
			
		||||
        """Ensure that the provided part is a valid option for the stock item"""
 | 
			
		||||
        """Ensure that the provided part is a valid option for the stock item."""
 | 
			
		||||
        stock_item = self.context['item']
 | 
			
		||||
        valid_options = stock_item.part.get_conversion_options()
 | 
			
		||||
 | 
			
		||||
@@ -625,8 +623,9 @@ class ConvertStockItemSerializer(serializers.Serializer):
 | 
			
		||||
        return part
 | 
			
		||||
 | 
			
		||||
    def validate(self, data):
 | 
			
		||||
        """Ensure that the stock item is valid for conversion:
 | 
			
		||||
        """Ensure that the stock item is valid for conversion.
 | 
			
		||||
 | 
			
		||||
        Rules:
 | 
			
		||||
        - If a SupplierPart is assigned, we cannot convert!
 | 
			
		||||
        """
 | 
			
		||||
        data = super().validate(data)
 | 
			
		||||
@@ -641,7 +640,7 @@ class ConvertStockItemSerializer(serializers.Serializer):
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
    def save(self):
 | 
			
		||||
        """Save the serializer to convert the StockItem to the selected Part"""
 | 
			
		||||
        """Save the serializer to convert the StockItem to the selected Part."""
 | 
			
		||||
        data = self.validated_data
 | 
			
		||||
 | 
			
		||||
        part = data['part']
 | 
			
		||||
@@ -653,10 +652,10 @@ class ConvertStockItemSerializer(serializers.Serializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ReturnStockItemSerializer(serializers.Serializer):
 | 
			
		||||
    """DRF serializer for returning a stock item from a customer"""
 | 
			
		||||
    """DRF serializer for returning a stock item from a customer."""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass options"""
 | 
			
		||||
        """Metaclass options."""
 | 
			
		||||
 | 
			
		||||
        fields = ['location', 'note']
 | 
			
		||||
 | 
			
		||||
@@ -677,7 +676,7 @@ class ReturnStockItemSerializer(serializers.Serializer):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def save(self):
 | 
			
		||||
        """Save the serialzier to return the item into stock"""
 | 
			
		||||
        """Save the serialzier to return the item into stock."""
 | 
			
		||||
        item = self.context['item']
 | 
			
		||||
        request = self.context['request']
 | 
			
		||||
 | 
			
		||||
@@ -690,10 +689,10 @@ class ReturnStockItemSerializer(serializers.Serializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockChangeStatusSerializer(serializers.Serializer):
 | 
			
		||||
    """Serializer for changing status of multiple StockItem objects"""
 | 
			
		||||
    """Serializer for changing status of multiple StockItem objects."""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass options"""
 | 
			
		||||
        """Metaclass options."""
 | 
			
		||||
 | 
			
		||||
        fields = ['items', 'status', 'note']
 | 
			
		||||
 | 
			
		||||
@@ -707,7 +706,7 @@ class StockChangeStatusSerializer(serializers.Serializer):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def validate_items(self, items):
 | 
			
		||||
        """Validate the selected stock items"""
 | 
			
		||||
        """Validate the selected stock items."""
 | 
			
		||||
        if len(items) == 0:
 | 
			
		||||
            raise ValidationError(_('No stock items selected'))
 | 
			
		||||
 | 
			
		||||
@@ -728,7 +727,7 @@ class StockChangeStatusSerializer(serializers.Serializer):
 | 
			
		||||
 | 
			
		||||
    @transaction.atomic
 | 
			
		||||
    def save(self):
 | 
			
		||||
        """Save the serializer to change the status of the selected stock items"""
 | 
			
		||||
        """Save the serializer to change the status of the selected stock items."""
 | 
			
		||||
        data = self.validated_data
 | 
			
		||||
 | 
			
		||||
        items = data['items']
 | 
			
		||||
@@ -837,7 +836,7 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
 | 
			
		||||
        read_only_fields = ['barcode_hash', 'icon']
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        """Optionally add or remove extra fields"""
 | 
			
		||||
        """Optionally add or remove extra fields."""
 | 
			
		||||
        path_detail = kwargs.pop('path_detail', False)
 | 
			
		||||
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
@@ -847,7 +846,7 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def annotate_queryset(queryset):
 | 
			
		||||
        """Annotate extra information to the queryset"""
 | 
			
		||||
        """Annotate extra information to the queryset."""
 | 
			
		||||
        # Annotate the number of stock items which exist in this category (including subcategories)
 | 
			
		||||
        queryset = queryset.annotate(items=stock.filters.annotate_location_items())
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -66,7 +66,7 @@ class StockLocationTest(StockAPITestCase):
 | 
			
		||||
        StockLocation.objects.create(name='top', description='top category')
 | 
			
		||||
 | 
			
		||||
    def test_list(self):
 | 
			
		||||
        """Test the StockLocationList API endpoint"""
 | 
			
		||||
        """Test the StockLocationList API endpoint."""
 | 
			
		||||
        test_cases = [
 | 
			
		||||
            ({}, 8, 'no parameters'),
 | 
			
		||||
            ({'parent': 1, 'cascade': False}, 2, 'Filter by parent, no cascading'),
 | 
			
		||||
@@ -165,7 +165,7 @@ class StockLocationTest(StockAPITestCase):
 | 
			
		||||
        self.post(self.list_url, data, expected_code=201)
 | 
			
		||||
 | 
			
		||||
    def test_stock_location_delete(self):
 | 
			
		||||
        """Test stock location deletion with different parameters"""
 | 
			
		||||
        """Test stock location deletion with different parameters."""
 | 
			
		||||
 | 
			
		||||
        class Target(IntEnum):
 | 
			
		||||
            move_sub_locations_to_parent_move_stockitems_to_parent = (0,)
 | 
			
		||||
@@ -294,7 +294,7 @@ class StockLocationTest(StockAPITestCase):
 | 
			
		||||
                    self.assertEqual(child.parent, parent_stock_location)
 | 
			
		||||
 | 
			
		||||
    def test_stock_location_structural(self):
 | 
			
		||||
        """Test the effectiveness of structural stock locations
 | 
			
		||||
        """Test the effectiveness of structural stock locations.
 | 
			
		||||
 | 
			
		||||
        Make sure:
 | 
			
		||||
        - Stock items cannot be created in structural locations
 | 
			
		||||
@@ -530,7 +530,7 @@ class StockItemListTest(StockAPITestCase):
 | 
			
		||||
        return response.data
 | 
			
		||||
 | 
			
		||||
    def test_top_level_filtering(self):
 | 
			
		||||
        """Test filtering against "top level" stock location"""
 | 
			
		||||
        """Test filtering against "top level" stock location."""
 | 
			
		||||
        # No filters, should return *all* items
 | 
			
		||||
        response = self.get(self.list_url, {}, expected_code=200)
 | 
			
		||||
        self.assertEqual(len(response.data), StockItem.objects.count())
 | 
			
		||||
@@ -628,7 +628,7 @@ class StockItemListTest(StockAPITestCase):
 | 
			
		||||
        self.assertEqual(len(response), 1)
 | 
			
		||||
 | 
			
		||||
    def test_filter_by_company(self):
 | 
			
		||||
        """Test that we can filter stock items by company"""
 | 
			
		||||
        """Test that we can filter stock items by company."""
 | 
			
		||||
        for cmp in company.models.Company.objects.all():
 | 
			
		||||
            self.get_stock(company=cmp.pk)
 | 
			
		||||
 | 
			
		||||
@@ -787,14 +787,14 @@ class StockItemListTest(StockAPITestCase):
 | 
			
		||||
        self.assertEqual(len(dataset), 17)
 | 
			
		||||
 | 
			
		||||
    def test_filter_by_allocated(self):
 | 
			
		||||
        """Test that we can filter by "allocated" status:
 | 
			
		||||
        """Test that we can filter by "allocated" status.
 | 
			
		||||
 | 
			
		||||
        Rules:
 | 
			
		||||
        - Only return stock items which are 'allocated'
 | 
			
		||||
        - Either to a build order or sales order
 | 
			
		||||
        - Test that the results are "distinct" (no duplicated results)
 | 
			
		||||
        - Ref: https://github.com/inventree/InvenTree/pull/5916
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        # Create a build order to allocate to
 | 
			
		||||
        assembly = part.models.Part.objects.create(
 | 
			
		||||
            name='F Assembly', description='Assembly for filter test', assembly=True
 | 
			
		||||
@@ -1284,7 +1284,7 @@ class StockItemTest(StockAPITestCase):
 | 
			
		||||
        self.assertEqual(sub_item.location.pk, 1)
 | 
			
		||||
 | 
			
		||||
    def test_return_from_customer(self):
 | 
			
		||||
        """Test that we can return a StockItem from a customer, via the API"""
 | 
			
		||||
        """Test that we can return a StockItem from a customer, via the API."""
 | 
			
		||||
        # Assign item to customer
 | 
			
		||||
        item = StockItem.objects.get(pk=521)
 | 
			
		||||
        customer = company.models.Company.objects.get(pk=4)
 | 
			
		||||
@@ -1316,7 +1316,7 @@ class StockItemTest(StockAPITestCase):
 | 
			
		||||
        self.assertIsNone(item.customer)
 | 
			
		||||
 | 
			
		||||
    def test_convert_to_variant(self):
 | 
			
		||||
        """Test that we can convert a StockItem to a variant part via the API"""
 | 
			
		||||
        """Test that we can convert a StockItem to a variant part via the API."""
 | 
			
		||||
        category = part.models.PartCategory.objects.get(pk=3)
 | 
			
		||||
 | 
			
		||||
        # First, construct a set of template / variant parts
 | 
			
		||||
@@ -1361,7 +1361,7 @@ class StockItemTest(StockAPITestCase):
 | 
			
		||||
            self.assertEqual(stock_item.part, variant)
 | 
			
		||||
 | 
			
		||||
    def test_set_status(self):
 | 
			
		||||
        """Test API endpoint for setting StockItem status"""
 | 
			
		||||
        """Test API endpoint for setting StockItem status."""
 | 
			
		||||
        url = reverse('api-stock-change-status')
 | 
			
		||||
 | 
			
		||||
        prt = Part.objects.first()
 | 
			
		||||
@@ -1612,7 +1612,7 @@ class StockTestResultTest(StockAPITestCase):
 | 
			
		||||
            self.assertIsNotNone(response.data['attachment'])
 | 
			
		||||
 | 
			
		||||
    def test_bulk_delete(self):
 | 
			
		||||
        """Test that the BulkDelete endpoint works for this model"""
 | 
			
		||||
        """Test that the BulkDelete endpoint works for this model."""
 | 
			
		||||
        n = StockItemTestResult.objects.count()
 | 
			
		||||
 | 
			
		||||
        tests = []
 | 
			
		||||
@@ -1849,7 +1849,7 @@ class StockMetadataAPITest(InvenTreeAPITestCase):
 | 
			
		||||
    roles = ['stock.change', 'stock_location.change']
 | 
			
		||||
 | 
			
		||||
    def metatester(self, apikey, model):
 | 
			
		||||
        """Generic tester"""
 | 
			
		||||
        """Generic tester."""
 | 
			
		||||
        modeldata = model.objects.first()
 | 
			
		||||
 | 
			
		||||
        # Useless test unless a model object is found
 | 
			
		||||
@@ -1875,7 +1875,7 @@ class StockMetadataAPITest(InvenTreeAPITestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_metadata(self):
 | 
			
		||||
        """Test all endpoints"""
 | 
			
		||||
        """Test all endpoints."""
 | 
			
		||||
        for apikey, model in {
 | 
			
		||||
            'api-location-metadata': StockLocation,
 | 
			
		||||
            'api-stock-test-result-metadata': StockItemTestResult,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
"""Unit tests for data migrations in the 'stock' app"""
 | 
			
		||||
"""Unit tests for data migrations in the 'stock' app."""
 | 
			
		||||
 | 
			
		||||
from django_test_migrations.contrib.unittest_case import MigratorTestCase
 | 
			
		||||
 | 
			
		||||
@@ -6,13 +6,13 @@ from InvenTree import unit_test
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestSerialNumberMigration(MigratorTestCase):
 | 
			
		||||
    """Test data migration which updates serial numbers"""
 | 
			
		||||
    """Test data migration which updates serial numbers."""
 | 
			
		||||
 | 
			
		||||
    migrate_from = ('stock', '0067_alter_stockitem_part')
 | 
			
		||||
    migrate_to = ('stock', unit_test.getNewestMigrationFile('stock'))
 | 
			
		||||
 | 
			
		||||
    def prepare(self):
 | 
			
		||||
        """Create initial data for this migration"""
 | 
			
		||||
        """Create initial data for this migration."""
 | 
			
		||||
        Part = self.old_state.apps.get_model('part', 'part')
 | 
			
		||||
        StockItem = self.old_state.apps.get_model('stock', 'stockitem')
 | 
			
		||||
 | 
			
		||||
@@ -48,7 +48,7 @@ class TestSerialNumberMigration(MigratorTestCase):
 | 
			
		||||
        self.big_ref_pk = item.pk
 | 
			
		||||
 | 
			
		||||
    def test_migrations(self):
 | 
			
		||||
        """Test that the migrations have been applied correctly"""
 | 
			
		||||
        """Test that the migrations have been applied correctly."""
 | 
			
		||||
        StockItem = self.new_state.apps.get_model('stock', 'stockitem')
 | 
			
		||||
 | 
			
		||||
        # Check that the serial number integer conversion has been applied correctly
 | 
			
		||||
@@ -68,13 +68,13 @@ class TestSerialNumberMigration(MigratorTestCase):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestScheduledForDeletionMigration(MigratorTestCase):
 | 
			
		||||
    """Test data migration for removing 'scheduled_for_deletion' field"""
 | 
			
		||||
    """Test data migration for removing 'scheduled_for_deletion' field."""
 | 
			
		||||
 | 
			
		||||
    migrate_from = ('stock', '0066_stockitem_scheduled_for_deletion')
 | 
			
		||||
    migrate_to = ('stock', unit_test.getNewestMigrationFile('stock'))
 | 
			
		||||
 | 
			
		||||
    def prepare(self):
 | 
			
		||||
        """Create some initial stock items"""
 | 
			
		||||
        """Create some initial stock items."""
 | 
			
		||||
        Part = self.old_state.apps.get_model('part', 'part')
 | 
			
		||||
        StockItem = self.old_state.apps.get_model('stock', 'stockitem')
 | 
			
		||||
 | 
			
		||||
@@ -128,7 +128,7 @@ class TestScheduledForDeletionMigration(MigratorTestCase):
 | 
			
		||||
        self.assertEqual(StockItem.objects.count(), 29)
 | 
			
		||||
 | 
			
		||||
    def test_migration(self):
 | 
			
		||||
        """Test that all stock items were actually removed"""
 | 
			
		||||
        """Test that all stock items were actually removed."""
 | 
			
		||||
        StockItem = self.new_state.apps.get_model('stock', 'stockitem')
 | 
			
		||||
 | 
			
		||||
        # All the "scheduled for deletion" items have been removed
 | 
			
		||||
 
 | 
			
		||||
@@ -28,10 +28,10 @@ class StockListTest(StockViewTestCase):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockDetailTest(StockViewTestCase):
 | 
			
		||||
    """Unit test for the 'stock detail' page"""
 | 
			
		||||
    """Unit test for the 'stock detail' page."""
 | 
			
		||||
 | 
			
		||||
    def test_basic_info(self):
 | 
			
		||||
        """Test that basic stock item info is rendered"""
 | 
			
		||||
        """Test that basic stock item info is rendered."""
 | 
			
		||||
        url = reverse('stock-item-detail', kwargs={'pk': 1})
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(url)
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ from .models import StockItem, StockItemTestResult, StockItemTracking, StockLoca
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockTestBase(InvenTreeTestCase):
 | 
			
		||||
    """Base class for running Stock tests"""
 | 
			
		||||
    """Base class for running Stock tests."""
 | 
			
		||||
 | 
			
		||||
    fixtures = [
 | 
			
		||||
        'category',
 | 
			
		||||
@@ -53,7 +53,7 @@ class StockTest(StockTestBase):
 | 
			
		||||
    """Tests to ensure that the stock location tree functions correctly."""
 | 
			
		||||
 | 
			
		||||
    def test_pathstring(self):
 | 
			
		||||
        """Check that pathstring updates occur as expected"""
 | 
			
		||||
        """Check that pathstring updates occur as expected."""
 | 
			
		||||
        a = StockLocation.objects.create(name='A')
 | 
			
		||||
        b = StockLocation.objects.create(name='B', parent=a)
 | 
			
		||||
        c = StockLocation.objects.create(name='C', parent=b)
 | 
			
		||||
@@ -128,7 +128,7 @@ class StockTest(StockTestBase):
 | 
			
		||||
        self.assertTrue(d.pathstring.endswith('DDDDDDDD'))
 | 
			
		||||
 | 
			
		||||
    def test_link(self):
 | 
			
		||||
        """Test the link URL field validation"""
 | 
			
		||||
        """Test the link URL field validation."""
 | 
			
		||||
        item = StockItem.objects.get(pk=1)
 | 
			
		||||
 | 
			
		||||
        # Check that invalid URLs fail
 | 
			
		||||
@@ -163,14 +163,14 @@ class StockTest(StockTestBase):
 | 
			
		||||
 | 
			
		||||
    @override_settings(EXTRA_URL_SCHEMES=['ssh'])
 | 
			
		||||
    def test_exteneded_schema(self):
 | 
			
		||||
        """Test that extended URL schemes are allowed"""
 | 
			
		||||
        """Test that extended URL schemes are allowed."""
 | 
			
		||||
        item = StockItem.objects.get(pk=1)
 | 
			
		||||
        item.link = 'ssh://user:pwd@deb.org:223'
 | 
			
		||||
        item.save()
 | 
			
		||||
        item.full_clean()
 | 
			
		||||
 | 
			
		||||
    def test_serial_numbers(self):
 | 
			
		||||
        """Test serial number uniqueness"""
 | 
			
		||||
        """Test serial number uniqueness."""
 | 
			
		||||
        # Ensure that 'global uniqueness' setting is enabled
 | 
			
		||||
        InvenTreeSetting.set_setting('SERIAL_NUMBER_GLOBALLY_UNIQUE', True, self.user)
 | 
			
		||||
 | 
			
		||||
@@ -456,7 +456,7 @@ class StockTest(StockTestBase):
 | 
			
		||||
        self.assertFalse(it.add_stock(-10, None))
 | 
			
		||||
 | 
			
		||||
    def test_allocate_to_customer(self):
 | 
			
		||||
        """Test allocating stock to a customer"""
 | 
			
		||||
        """Test allocating stock to a customer."""
 | 
			
		||||
        it = StockItem.objects.get(pk=2)
 | 
			
		||||
        n = it.quantity
 | 
			
		||||
        an = n - 10
 | 
			
		||||
@@ -486,7 +486,7 @@ class StockTest(StockTestBase):
 | 
			
		||||
        self.assertIn('Allocated some stock', track.notes)
 | 
			
		||||
 | 
			
		||||
    def test_return_from_customer(self):
 | 
			
		||||
        """Test removing previous allocated stock from customer"""
 | 
			
		||||
        """Test removing previous allocated stock from customer."""
 | 
			
		||||
        it = StockItem.objects.get(pk=2)
 | 
			
		||||
 | 
			
		||||
        # First establish total stock for this part
 | 
			
		||||
@@ -887,10 +887,10 @@ class StockTest(StockTestBase):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockBarcodeTest(StockTestBase):
 | 
			
		||||
    """Run barcode tests for the stock app"""
 | 
			
		||||
    """Run barcode tests for the stock app."""
 | 
			
		||||
 | 
			
		||||
    def test_stock_item_barcode_basics(self):
 | 
			
		||||
        """Simple tests for the StockItem barcode integration"""
 | 
			
		||||
        """Simple tests for the StockItem barcode integration."""
 | 
			
		||||
        item = StockItem.objects.get(pk=1)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(StockItem.barcode_model_type(), 'stockitem')
 | 
			
		||||
@@ -906,7 +906,7 @@ class StockBarcodeTest(StockTestBase):
 | 
			
		||||
        self.assertEqual(barcode, '{"stockitem": 1}')
 | 
			
		||||
 | 
			
		||||
    def test_location_barcode_basics(self):
 | 
			
		||||
        """Simple tests for the StockLocation barcode integration"""
 | 
			
		||||
        """Simple tests for the StockLocation barcode integration."""
 | 
			
		||||
        self.assertEqual(StockLocation.barcode_model_type(), 'stocklocation')
 | 
			
		||||
 | 
			
		||||
        loc = StockLocation.objects.get(pk=1)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user