mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	[CI] Enable python autoformat (#6169)
* 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 autoformat * added autofixes * switched to single quotes everywhere * switched to ruff for import sorting * fix wrong url response * switched to pathlib for lookup * fixed lookup * 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 coverage souce format --------- Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
This commit is contained in:
		@@ -14,8 +14,14 @@ from InvenTree.admin import InvenTreeResource
 | 
			
		||||
from order.models import PurchaseOrder, SalesOrder
 | 
			
		||||
from part.models import Part
 | 
			
		||||
 | 
			
		||||
from .models import (StockItem, StockItemAttachment, StockItemTestResult,
 | 
			
		||||
                     StockItemTracking, StockLocation, StockLocationType)
 | 
			
		||||
from .models import (
 | 
			
		||||
    StockItem,
 | 
			
		||||
    StockItemAttachment,
 | 
			
		||||
    StockItemTestResult,
 | 
			
		||||
    StockItemTracking,
 | 
			
		||||
    StockLocation,
 | 
			
		||||
    StockLocationType,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LocationResource(InvenTreeResource):
 | 
			
		||||
@@ -31,21 +37,39 @@ class LocationResource(InvenTreeResource):
 | 
			
		||||
 | 
			
		||||
        exclude = [
 | 
			
		||||
            # Exclude MPTT internal model fields
 | 
			
		||||
            'lft', 'rght', 'tree_id', 'level',
 | 
			
		||||
            'lft',
 | 
			
		||||
            'rght',
 | 
			
		||||
            'tree_id',
 | 
			
		||||
            'level',
 | 
			
		||||
            'metadata',
 | 
			
		||||
            'barcode_data', 'barcode_hash',
 | 
			
		||||
            'owner', 'icon',
 | 
			
		||||
            'barcode_data',
 | 
			
		||||
            'barcode_hash',
 | 
			
		||||
            'owner',
 | 
			
		||||
            'icon',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    id = Field(attribute='id', column_name=_('Location ID'), widget=widgets.IntegerWidget())
 | 
			
		||||
    id = Field(
 | 
			
		||||
        attribute='id', column_name=_('Location ID'), widget=widgets.IntegerWidget()
 | 
			
		||||
    )
 | 
			
		||||
    name = Field(attribute='name', column_name=_('Location Name'))
 | 
			
		||||
    description = Field(attribute='description', column_name=_('Description'))
 | 
			
		||||
    parent = Field(attribute='parent', column_name=_('Parent ID'), widget=widgets.ForeignKeyWidget(StockLocation))
 | 
			
		||||
    parent_name = Field(attribute='parent__name', column_name=_('Parent Name'), readonly=True)
 | 
			
		||||
    parent = Field(
 | 
			
		||||
        attribute='parent',
 | 
			
		||||
        column_name=_('Parent ID'),
 | 
			
		||||
        widget=widgets.ForeignKeyWidget(StockLocation),
 | 
			
		||||
    )
 | 
			
		||||
    parent_name = Field(
 | 
			
		||||
        attribute='parent__name', column_name=_('Parent Name'), readonly=True
 | 
			
		||||
    )
 | 
			
		||||
    pathstring = Field(attribute='pathstring', column_name=_('Location Path'))
 | 
			
		||||
 | 
			
		||||
    # Calculated fields
 | 
			
		||||
    items = Field(attribute='item_count', column_name=_('Stock Items'), widget=widgets.IntegerWidget(), readonly=True)
 | 
			
		||||
    items = Field(
 | 
			
		||||
        attribute='item_count',
 | 
			
		||||
        column_name=_('Stock Items'),
 | 
			
		||||
        widget=widgets.IntegerWidget(),
 | 
			
		||||
        readonly=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def after_import(self, dataset, result, using_transactions, dry_run, **kwargs):
 | 
			
		||||
        """Rebuild after import to keep tree intact."""
 | 
			
		||||
@@ -57,6 +81,7 @@ class LocationResource(InvenTreeResource):
 | 
			
		||||
 | 
			
		||||
class LocationInline(admin.TabularInline):
 | 
			
		||||
    """Inline for sub-locations."""
 | 
			
		||||
 | 
			
		||||
    model = StockLocation
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -69,25 +94,23 @@ class LocationAdmin(ImportExportModelAdmin):
 | 
			
		||||
 | 
			
		||||
    search_fields = ('name', 'description')
 | 
			
		||||
 | 
			
		||||
    inlines = [
 | 
			
		||||
        LocationInline,
 | 
			
		||||
    ]
 | 
			
		||||
    inlines = [LocationInline]
 | 
			
		||||
 | 
			
		||||
    autocomplete_fields = [
 | 
			
		||||
        'parent',
 | 
			
		||||
    ]
 | 
			
		||||
    autocomplete_fields = ['parent']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LocationTypeAdmin(admin.ModelAdmin):
 | 
			
		||||
    """Admin class for StockLocationType."""
 | 
			
		||||
 | 
			
		||||
    list_display = ('name', 'description', 'icon', 'location_count')
 | 
			
		||||
    readonly_fields = ('location_count', )
 | 
			
		||||
    readonly_fields = ('location_count',)
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self, request):
 | 
			
		||||
        """Annotate queryset to fetch location count."""
 | 
			
		||||
        return super().get_queryset(request).annotate(
 | 
			
		||||
            location_count=Count("stock_locations"),
 | 
			
		||||
        return (
 | 
			
		||||
            super()
 | 
			
		||||
            .get_queryset(request)
 | 
			
		||||
            .annotate(location_count=Count('stock_locations'))
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def location_count(self, obj):
 | 
			
		||||
@@ -108,45 +131,129 @@ class StockItemResource(InvenTreeResource):
 | 
			
		||||
 | 
			
		||||
        exclude = [
 | 
			
		||||
            # Exclude MPTT internal model fields
 | 
			
		||||
            'lft', 'rght', 'tree_id', 'level',
 | 
			
		||||
            'lft',
 | 
			
		||||
            'rght',
 | 
			
		||||
            'tree_id',
 | 
			
		||||
            'level',
 | 
			
		||||
            # Exclude internal fields
 | 
			
		||||
            'serial_int', 'metadata',
 | 
			
		||||
            'barcode_hash', 'barcode_data',
 | 
			
		||||
            'serial_int',
 | 
			
		||||
            'metadata',
 | 
			
		||||
            'barcode_hash',
 | 
			
		||||
            'barcode_data',
 | 
			
		||||
            'owner',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    id = Field(attribute='pk', column_name=_('Stock Item ID'), widget=widgets.IntegerWidget())
 | 
			
		||||
    part = Field(attribute='part', column_name=_('Part ID'), widget=widgets.ForeignKeyWidget(Part))
 | 
			
		||||
    part_name = Field(attribute='part__full_name', column_name=_('Part Name'), readonly=True)
 | 
			
		||||
    quantity = Field(attribute='quantity', column_name=_('Quantity'), widget=widgets.DecimalWidget())
 | 
			
		||||
    id = Field(
 | 
			
		||||
        attribute='pk', column_name=_('Stock Item ID'), widget=widgets.IntegerWidget()
 | 
			
		||||
    )
 | 
			
		||||
    part = Field(
 | 
			
		||||
        attribute='part',
 | 
			
		||||
        column_name=_('Part ID'),
 | 
			
		||||
        widget=widgets.ForeignKeyWidget(Part),
 | 
			
		||||
    )
 | 
			
		||||
    part_name = Field(
 | 
			
		||||
        attribute='part__full_name', column_name=_('Part Name'), readonly=True
 | 
			
		||||
    )
 | 
			
		||||
    quantity = Field(
 | 
			
		||||
        attribute='quantity', column_name=_('Quantity'), widget=widgets.DecimalWidget()
 | 
			
		||||
    )
 | 
			
		||||
    serial = Field(attribute='serial', column_name=_('Serial'))
 | 
			
		||||
    batch = Field(attribute='batch', column_name=_('Batch'))
 | 
			
		||||
    status_label = Field(attribute='status_label', column_name=_('Status'), readonly=True)
 | 
			
		||||
    status = Field(attribute='status', column_name=_('Status Code'), widget=widgets.IntegerWidget())
 | 
			
		||||
    location = Field(attribute='location', column_name=_('Location ID'), widget=widgets.ForeignKeyWidget(StockLocation))
 | 
			
		||||
    location_name = Field(attribute='location__name', column_name=_('Location Name'), readonly=True)
 | 
			
		||||
    supplier_part = Field(attribute='supplier_part', column_name=_('Supplier Part ID'), widget=widgets.ForeignKeyWidget(SupplierPart))
 | 
			
		||||
    supplier = Field(attribute='supplier_part__supplier__id', column_name=_('Supplier ID'), readonly=True, widget=widgets.IntegerWidget())
 | 
			
		||||
    supplier_name = Field(attribute='supplier_part__supplier__name', column_name=_('Supplier Name'), readonly=True)
 | 
			
		||||
    customer = Field(attribute='customer', column_name=_('Customer ID'), widget=widgets.ForeignKeyWidget(Company))
 | 
			
		||||
    belongs_to = Field(attribute='belongs_to', column_name=_('Installed In'), widget=widgets.ForeignKeyWidget(StockItem))
 | 
			
		||||
    build = Field(attribute='build', column_name=_('Build ID'), widget=widgets.ForeignKeyWidget(Build))
 | 
			
		||||
    parent = Field(attribute='parent', column_name=_('Parent ID'), widget=widgets.ForeignKeyWidget(StockItem))
 | 
			
		||||
    sales_order = Field(attribute='sales_order', column_name=_('Sales Order ID'), widget=widgets.ForeignKeyWidget(SalesOrder))
 | 
			
		||||
    purchase_order = Field(attribute='purchase_order', column_name=_('Purchase Order ID'), widget=widgets.ForeignKeyWidget(PurchaseOrder))
 | 
			
		||||
    status_label = Field(
 | 
			
		||||
        attribute='status_label', column_name=_('Status'), readonly=True
 | 
			
		||||
    )
 | 
			
		||||
    status = Field(
 | 
			
		||||
        attribute='status', column_name=_('Status Code'), widget=widgets.IntegerWidget()
 | 
			
		||||
    )
 | 
			
		||||
    location = Field(
 | 
			
		||||
        attribute='location',
 | 
			
		||||
        column_name=_('Location ID'),
 | 
			
		||||
        widget=widgets.ForeignKeyWidget(StockLocation),
 | 
			
		||||
    )
 | 
			
		||||
    location_name = Field(
 | 
			
		||||
        attribute='location__name', column_name=_('Location Name'), readonly=True
 | 
			
		||||
    )
 | 
			
		||||
    supplier_part = Field(
 | 
			
		||||
        attribute='supplier_part',
 | 
			
		||||
        column_name=_('Supplier Part ID'),
 | 
			
		||||
        widget=widgets.ForeignKeyWidget(SupplierPart),
 | 
			
		||||
    )
 | 
			
		||||
    supplier = Field(
 | 
			
		||||
        attribute='supplier_part__supplier__id',
 | 
			
		||||
        column_name=_('Supplier ID'),
 | 
			
		||||
        readonly=True,
 | 
			
		||||
        widget=widgets.IntegerWidget(),
 | 
			
		||||
    )
 | 
			
		||||
    supplier_name = Field(
 | 
			
		||||
        attribute='supplier_part__supplier__name',
 | 
			
		||||
        column_name=_('Supplier Name'),
 | 
			
		||||
        readonly=True,
 | 
			
		||||
    )
 | 
			
		||||
    customer = Field(
 | 
			
		||||
        attribute='customer',
 | 
			
		||||
        column_name=_('Customer ID'),
 | 
			
		||||
        widget=widgets.ForeignKeyWidget(Company),
 | 
			
		||||
    )
 | 
			
		||||
    belongs_to = Field(
 | 
			
		||||
        attribute='belongs_to',
 | 
			
		||||
        column_name=_('Installed In'),
 | 
			
		||||
        widget=widgets.ForeignKeyWidget(StockItem),
 | 
			
		||||
    )
 | 
			
		||||
    build = Field(
 | 
			
		||||
        attribute='build',
 | 
			
		||||
        column_name=_('Build ID'),
 | 
			
		||||
        widget=widgets.ForeignKeyWidget(Build),
 | 
			
		||||
    )
 | 
			
		||||
    parent = Field(
 | 
			
		||||
        attribute='parent',
 | 
			
		||||
        column_name=_('Parent ID'),
 | 
			
		||||
        widget=widgets.ForeignKeyWidget(StockItem),
 | 
			
		||||
    )
 | 
			
		||||
    sales_order = Field(
 | 
			
		||||
        attribute='sales_order',
 | 
			
		||||
        column_name=_('Sales Order ID'),
 | 
			
		||||
        widget=widgets.ForeignKeyWidget(SalesOrder),
 | 
			
		||||
    )
 | 
			
		||||
    purchase_order = Field(
 | 
			
		||||
        attribute='purchase_order',
 | 
			
		||||
        column_name=_('Purchase Order ID'),
 | 
			
		||||
        widget=widgets.ForeignKeyWidget(PurchaseOrder),
 | 
			
		||||
    )
 | 
			
		||||
    packaging = Field(attribute='packaging', column_name=_('Packaging'))
 | 
			
		||||
    link = Field(attribute='link', column_name=_('Link'))
 | 
			
		||||
    notes = Field(attribute='notes', column_name=_('Notes'))
 | 
			
		||||
 | 
			
		||||
    # Status fields (note that IntegerWidget exports better to excel than BooleanWidget)
 | 
			
		||||
    is_building = Field(attribute='is_building', column_name=_('Building'), widget=widgets.IntegerWidget())
 | 
			
		||||
    review_needed = Field(attribute='review_needed', column_name=_('Review Needed'), widget=widgets.IntegerWidget())
 | 
			
		||||
    delete_on_deplete = Field(attribute='delete_on_deplete', column_name=_('Delete on Deplete'), widget=widgets.IntegerWidget())
 | 
			
		||||
    is_building = Field(
 | 
			
		||||
        attribute='is_building',
 | 
			
		||||
        column_name=_('Building'),
 | 
			
		||||
        widget=widgets.IntegerWidget(),
 | 
			
		||||
    )
 | 
			
		||||
    review_needed = Field(
 | 
			
		||||
        attribute='review_needed',
 | 
			
		||||
        column_name=_('Review Needed'),
 | 
			
		||||
        widget=widgets.IntegerWidget(),
 | 
			
		||||
    )
 | 
			
		||||
    delete_on_deplete = Field(
 | 
			
		||||
        attribute='delete_on_deplete',
 | 
			
		||||
        column_name=_('Delete on Deplete'),
 | 
			
		||||
        widget=widgets.IntegerWidget(),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Date management
 | 
			
		||||
    updated = Field(attribute='updated', column_name=_('Last Updated'), widget=widgets.DateWidget())
 | 
			
		||||
    stocktake_date = Field(attribute='stocktake_date', column_name=_('Stocktake'), widget=widgets.DateWidget())
 | 
			
		||||
    expiry_date = Field(attribute='expiry_date', column_name=_('Expiry Date'), widget=widgets.DateWidget())
 | 
			
		||||
    updated = Field(
 | 
			
		||||
        attribute='updated', column_name=_('Last Updated'), widget=widgets.DateWidget()
 | 
			
		||||
    )
 | 
			
		||||
    stocktake_date = Field(
 | 
			
		||||
        attribute='stocktake_date',
 | 
			
		||||
        column_name=_('Stocktake'),
 | 
			
		||||
        widget=widgets.DateWidget(),
 | 
			
		||||
    )
 | 
			
		||||
    expiry_date = Field(
 | 
			
		||||
        attribute='expiry_date',
 | 
			
		||||
        column_name=_('Expiry Date'),
 | 
			
		||||
        widget=widgets.DateWidget(),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def dehydrate_purchase_price(self, item):
 | 
			
		||||
        """Render purchase pric as float"""
 | 
			
		||||
@@ -169,12 +276,7 @@ class StockItemAdmin(ImportExportModelAdmin):
 | 
			
		||||
    list_display = ('part', 'quantity', 'location', 'status', 'updated')
 | 
			
		||||
 | 
			
		||||
    # A list of search fields which can be used for lookup on matching 'autocomplete' fields
 | 
			
		||||
    search_fields = [
 | 
			
		||||
        'part__name',
 | 
			
		||||
        'part__description',
 | 
			
		||||
        'serial',
 | 
			
		||||
        'batch',
 | 
			
		||||
    ]
 | 
			
		||||
    search_fields = ['part__name', 'part__description', 'serial', 'batch']
 | 
			
		||||
 | 
			
		||||
    autocomplete_fields = [
 | 
			
		||||
        'belongs_to',
 | 
			
		||||
@@ -195,9 +297,7 @@ class StockAttachmentAdmin(admin.ModelAdmin):
 | 
			
		||||
 | 
			
		||||
    list_display = ('stock_item', 'attachment', 'comment')
 | 
			
		||||
 | 
			
		||||
    autocomplete_fields = [
 | 
			
		||||
        'stock_item',
 | 
			
		||||
    ]
 | 
			
		||||
    autocomplete_fields = ['stock_item']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockTrackingAdmin(ImportExportModelAdmin):
 | 
			
		||||
@@ -205,9 +305,7 @@ class StockTrackingAdmin(ImportExportModelAdmin):
 | 
			
		||||
 | 
			
		||||
    list_display = ('item', 'date', 'label')
 | 
			
		||||
 | 
			
		||||
    autocomplete_fields = [
 | 
			
		||||
        'item',
 | 
			
		||||
    ]
 | 
			
		||||
    autocomplete_fields = ['item']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockItemTestResultAdmin(admin.ModelAdmin):
 | 
			
		||||
@@ -215,9 +313,7 @@ class StockItemTestResultAdmin(admin.ModelAdmin):
 | 
			
		||||
 | 
			
		||||
    list_display = ('stock_item', 'test', 'result', 'value')
 | 
			
		||||
 | 
			
		||||
    autocomplete_fields = [
 | 
			
		||||
        'stock_item',
 | 
			
		||||
    ]
 | 
			
		||||
    autocomplete_fields = ['stock_item']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
admin.site.register(StockLocation, LocationAdmin)
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -5,4 +5,5 @@ from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
class StockConfig(AppConfig):
 | 
			
		||||
    """AppConfig for stock app."""
 | 
			
		||||
 | 
			
		||||
    name = 'stock'
 | 
			
		||||
 
 | 
			
		||||
@@ -31,5 +31,5 @@ def annotate_location_items(filter: Q = None):
 | 
			
		||||
            ).values('total')
 | 
			
		||||
        ),
 | 
			
		||||
        0,
 | 
			
		||||
        output_field=IntegerField()
 | 
			
		||||
        output_field=IntegerField(),
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -23,12 +23,17 @@ import part.models as part_models
 | 
			
		||||
import stock.filters
 | 
			
		||||
from company.serializers import SupplierPartSerializer
 | 
			
		||||
from InvenTree.models import extract_int
 | 
			
		||||
from InvenTree.serializers import (InvenTreeCurrencySerializer,
 | 
			
		||||
                                   InvenTreeDecimalField)
 | 
			
		||||
from InvenTree.serializers import InvenTreeCurrencySerializer, InvenTreeDecimalField
 | 
			
		||||
from part.serializers import PartBriefSerializer
 | 
			
		||||
 | 
			
		||||
from .models import (StockItem, StockItemAttachment, StockItemTestResult,
 | 
			
		||||
                     StockItemTracking, StockLocation, StockLocationType)
 | 
			
		||||
from .models import (
 | 
			
		||||
    StockItem,
 | 
			
		||||
    StockItemAttachment,
 | 
			
		||||
    StockItemTestResult,
 | 
			
		||||
    StockItemTracking,
 | 
			
		||||
    StockLocation,
 | 
			
		||||
    StockLocationType,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LocationBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
 | 
			
		||||
@@ -38,11 +43,7 @@ class LocationBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
 | 
			
		||||
        """Metaclass options."""
 | 
			
		||||
 | 
			
		||||
        model = StockLocation
 | 
			
		||||
        fields = [
 | 
			
		||||
            'pk',
 | 
			
		||||
            'name',
 | 
			
		||||
            'pathstring',
 | 
			
		||||
        ]
 | 
			
		||||
        fields = ['pk', 'name', 'pathstring']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockItemTestResultSerializer(InvenTree.serializers.InvenTreeModelSerializer):
 | 
			
		||||
@@ -64,15 +65,11 @@ class StockItemTestResultSerializer(InvenTree.serializers.InvenTreeModelSerializ
 | 
			
		||||
            'notes',
 | 
			
		||||
            'user',
 | 
			
		||||
            'user_detail',
 | 
			
		||||
            'date'
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        read_only_fields = [
 | 
			
		||||
            'pk',
 | 
			
		||||
            'user',
 | 
			
		||||
            'date',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        read_only_fields = ['pk', 'user', 'date']
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        """Add detail fields."""
 | 
			
		||||
        user_detail = kwargs.pop('user_detail', False)
 | 
			
		||||
@@ -86,7 +83,9 @@ class StockItemTestResultSerializer(InvenTree.serializers.InvenTreeModelSerializ
 | 
			
		||||
 | 
			
		||||
    key = serializers.CharField(read_only=True)
 | 
			
		||||
 | 
			
		||||
    attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField(required=False)
 | 
			
		||||
    attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField(
 | 
			
		||||
        required=False
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
 | 
			
		||||
@@ -107,9 +106,7 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
 | 
			
		||||
            'barcode_hash',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        read_only_fields = [
 | 
			
		||||
            'barcode_hash',
 | 
			
		||||
        ]
 | 
			
		||||
        read_only_fields = ['barcode_hash']
 | 
			
		||||
 | 
			
		||||
    part_name = serializers.CharField(source='part.full_name', read_only=True)
 | 
			
		||||
 | 
			
		||||
@@ -117,8 +114,8 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
 | 
			
		||||
 | 
			
		||||
    def validate_serial(self, value):
 | 
			
		||||
        """Make sure serial is not to big."""
 | 
			
		||||
        if abs(extract_int(value)) > 0x7fffffff:
 | 
			
		||||
            raise serializers.ValidationError(_("Serial number is too large"))
 | 
			
		||||
        if abs(extract_int(value)) > 0x7FFFFFFF:
 | 
			
		||||
            raise serializers.ValidationError(_('Serial number is too large'))
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -169,14 +166,12 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
 | 
			
		||||
            'purchase_price_currency',
 | 
			
		||||
            'use_pack_size',
 | 
			
		||||
            'tests',
 | 
			
		||||
 | 
			
		||||
            # Annotated fields
 | 
			
		||||
            'allocated',
 | 
			
		||||
            'expired',
 | 
			
		||||
            'installed_items',
 | 
			
		||||
            'stale',
 | 
			
		||||
            'tracking_items',
 | 
			
		||||
 | 
			
		||||
            'tags',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
@@ -195,21 +190,18 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
 | 
			
		||||
        """
 | 
			
		||||
        Fields used when creating a stock item
 | 
			
		||||
        """
 | 
			
		||||
        extra_kwargs = {
 | 
			
		||||
            'use_pack_size': {'write_only': True},
 | 
			
		||||
        }
 | 
			
		||||
        extra_kwargs = {'use_pack_size': {'write_only': True}}
 | 
			
		||||
 | 
			
		||||
    part = serializers.PrimaryKeyRelatedField(
 | 
			
		||||
        queryset=part_models.Part.objects.all(),
 | 
			
		||||
        many=False, allow_null=False,
 | 
			
		||||
        help_text=_("Base Part"),
 | 
			
		||||
        label=_("Part"),
 | 
			
		||||
        many=False,
 | 
			
		||||
        allow_null=False,
 | 
			
		||||
        help_text=_('Base Part'),
 | 
			
		||||
        label=_('Part'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    location_path = serializers.ListField(
 | 
			
		||||
        child=serializers.DictField(),
 | 
			
		||||
        source='location.get_path',
 | 
			
		||||
        read_only=True,
 | 
			
		||||
        child=serializers.DictField(), source='location.get_path', read_only=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
@@ -219,14 +211,16 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
 | 
			
		||||
        write_only=True,
 | 
			
		||||
        required=False,
 | 
			
		||||
        allow_null=True,
 | 
			
		||||
        help_text=_("Use pack size when adding: the quantity defined is the number of packs"),
 | 
			
		||||
        label=("Use pack size"),
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            'Use pack size when adding: the quantity defined is the number of packs'
 | 
			
		||||
        ),
 | 
			
		||||
        label=('Use pack size'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def validate_part(self, part):
 | 
			
		||||
        """Ensure the provided Part instance is valid"""
 | 
			
		||||
        if part.virtual:
 | 
			
		||||
            raise ValidationError(_("Stock item cannot be created for virtual parts"))
 | 
			
		||||
            raise ValidationError(_('Stock item cannot be created for virtual parts'))
 | 
			
		||||
 | 
			
		||||
        return part
 | 
			
		||||
 | 
			
		||||
@@ -257,54 +251,63 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
 | 
			
		||||
        queryset = queryset.annotate(
 | 
			
		||||
            allocated=Coalesce(
 | 
			
		||||
                SubquerySum('sales_order_allocations__quantity'), Decimal(0)
 | 
			
		||||
            ) + Coalesce(
 | 
			
		||||
                SubquerySum('allocations__quantity'), Decimal(0)
 | 
			
		||||
            )
 | 
			
		||||
            + Coalesce(SubquerySum('allocations__quantity'), Decimal(0))
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Annotate the queryset with the number of tracking items
 | 
			
		||||
        queryset = queryset.annotate(
 | 
			
		||||
            tracking_items=SubqueryCount('tracking_info')
 | 
			
		||||
        )
 | 
			
		||||
        queryset = queryset.annotate(tracking_items=SubqueryCount('tracking_info'))
 | 
			
		||||
 | 
			
		||||
        # Add flag to indicate if the StockItem has expired
 | 
			
		||||
        queryset = queryset.annotate(
 | 
			
		||||
            expired=Case(
 | 
			
		||||
                When(
 | 
			
		||||
                    StockItem.EXPIRED_FILTER, then=Value(True, output_field=BooleanField()),
 | 
			
		||||
                    StockItem.EXPIRED_FILTER,
 | 
			
		||||
                    then=Value(True, output_field=BooleanField()),
 | 
			
		||||
                ),
 | 
			
		||||
                default=Value(False, output_field=BooleanField())
 | 
			
		||||
                default=Value(False, output_field=BooleanField()),
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Add flag to indicate if the StockItem is stale
 | 
			
		||||
        stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS')
 | 
			
		||||
        stale_date = datetime.now().date() + timedelta(days=stale_days)
 | 
			
		||||
        stale_filter = StockItem.IN_STOCK_FILTER & ~Q(expiry_date=None) & Q(expiry_date__lt=stale_date)
 | 
			
		||||
        stale_filter = (
 | 
			
		||||
            StockItem.IN_STOCK_FILTER
 | 
			
		||||
            & ~Q(expiry_date=None)
 | 
			
		||||
            & Q(expiry_date__lt=stale_date)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        queryset = queryset.annotate(
 | 
			
		||||
            stale=Case(
 | 
			
		||||
                When(
 | 
			
		||||
                    stale_filter, then=Value(True, output_field=BooleanField()),
 | 
			
		||||
                ),
 | 
			
		||||
                When(stale_filter, then=Value(True, output_field=BooleanField())),
 | 
			
		||||
                default=Value(False, output_field=BooleanField()),
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Annotate with the total number of "installed items"
 | 
			
		||||
        queryset = queryset.annotate(
 | 
			
		||||
            installed_items=SubqueryCount('installed_parts')
 | 
			
		||||
        )
 | 
			
		||||
        queryset = queryset.annotate(installed_items=SubqueryCount('installed_parts'))
 | 
			
		||||
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
    status_text = serializers.CharField(source='get_status_display', read_only=True)
 | 
			
		||||
 | 
			
		||||
    # Optional detail fields, which can be appended via query parameters
 | 
			
		||||
    supplier_part_detail = SupplierPartSerializer(source='supplier_part', supplier_detail=False, manufacturer_detail=False, part_detail=False, many=False, read_only=True)
 | 
			
		||||
    supplier_part_detail = SupplierPartSerializer(
 | 
			
		||||
        source='supplier_part',
 | 
			
		||||
        supplier_detail=False,
 | 
			
		||||
        manufacturer_detail=False,
 | 
			
		||||
        part_detail=False,
 | 
			
		||||
        many=False,
 | 
			
		||||
        read_only=True,
 | 
			
		||||
    )
 | 
			
		||||
    part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
 | 
			
		||||
    location_detail = LocationBriefSerializer(source='location', many=False, read_only=True)
 | 
			
		||||
    tests = StockItemTestResultSerializer(source='test_results', many=True, read_only=True)
 | 
			
		||||
    location_detail = LocationBriefSerializer(
 | 
			
		||||
        source='location', many=False, read_only=True
 | 
			
		||||
    )
 | 
			
		||||
    tests = StockItemTestResultSerializer(
 | 
			
		||||
        source='test_results', many=True, read_only=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    quantity = InvenTreeDecimalField()
 | 
			
		||||
 | 
			
		||||
@@ -321,10 +324,16 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
 | 
			
		||||
        help_text=_('Purchase price of this stock item, per unit or pack'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    purchase_price_currency = InvenTreeCurrencySerializer(help_text=_('Purchase currency of this stock item'))
 | 
			
		||||
    purchase_price_currency = InvenTreeCurrencySerializer(
 | 
			
		||||
        help_text=_('Purchase currency of this stock item')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    purchase_order_reference = serializers.CharField(source='purchase_order.reference', read_only=True)
 | 
			
		||||
    sales_order_reference = serializers.CharField(source='sales_order.reference', read_only=True)
 | 
			
		||||
    purchase_order_reference = serializers.CharField(
 | 
			
		||||
        source='purchase_order.reference', read_only=True
 | 
			
		||||
    )
 | 
			
		||||
    sales_order_reference = serializers.CharField(
 | 
			
		||||
        source='sales_order.reference', read_only=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    tags = TagListSerializerField(required=False)
 | 
			
		||||
 | 
			
		||||
@@ -368,12 +377,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass options."""
 | 
			
		||||
 | 
			
		||||
        fields = [
 | 
			
		||||
            'quantity',
 | 
			
		||||
            'serial_numbers',
 | 
			
		||||
            'destination',
 | 
			
		||||
            'notes',
 | 
			
		||||
        ]
 | 
			
		||||
        fields = ['quantity', 'serial_numbers', 'destination', 'notes']
 | 
			
		||||
 | 
			
		||||
    quantity = serializers.IntegerField(
 | 
			
		||||
        min_value=0,
 | 
			
		||||
@@ -387,11 +391,13 @@ class SerializeStockItemSerializer(serializers.Serializer):
 | 
			
		||||
        item = self.context['item']
 | 
			
		||||
 | 
			
		||||
        if quantity < 0:
 | 
			
		||||
            raise ValidationError(_("Quantity must be greater than zero"))
 | 
			
		||||
            raise ValidationError(_('Quantity must be greater than zero'))
 | 
			
		||||
 | 
			
		||||
        if quantity > item.quantity:
 | 
			
		||||
            q = item.quantity
 | 
			
		||||
            raise ValidationError(_(f"Quantity must not exceed available stock quantity ({q})"))
 | 
			
		||||
            raise ValidationError(
 | 
			
		||||
                _(f'Quantity must not exceed available stock quantity ({q})')
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return quantity
 | 
			
		||||
 | 
			
		||||
@@ -414,8 +420,8 @@ class SerializeStockItemSerializer(serializers.Serializer):
 | 
			
		||||
    notes = serializers.CharField(
 | 
			
		||||
        required=False,
 | 
			
		||||
        allow_blank=True,
 | 
			
		||||
        label=_("Notes"),
 | 
			
		||||
        help_text=_("Optional note field")
 | 
			
		||||
        label=_('Notes'),
 | 
			
		||||
        help_text=_('Optional note field'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def validate(self, data):
 | 
			
		||||
@@ -425,7 +431,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
 | 
			
		||||
        item = self.context['item']
 | 
			
		||||
 | 
			
		||||
        if not item.part.trackable:
 | 
			
		||||
            raise ValidationError(_("Serial numbers cannot be assigned to this part"))
 | 
			
		||||
            raise ValidationError(_('Serial numbers cannot be assigned to this part'))
 | 
			
		||||
 | 
			
		||||
        # Ensure the serial numbers are valid!
 | 
			
		||||
        quantity = data['quantity']
 | 
			
		||||
@@ -433,24 +439,18 @@ class SerializeStockItemSerializer(serializers.Serializer):
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            serials = InvenTree.helpers.extract_serial_numbers(
 | 
			
		||||
                serial_numbers,
 | 
			
		||||
                quantity,
 | 
			
		||||
                item.part.get_latest_serial_number()
 | 
			
		||||
                serial_numbers, quantity, item.part.get_latest_serial_number()
 | 
			
		||||
            )
 | 
			
		||||
        except DjangoValidationError as e:
 | 
			
		||||
            raise ValidationError({
 | 
			
		||||
                'serial_numbers': e.messages,
 | 
			
		||||
            })
 | 
			
		||||
            raise ValidationError({'serial_numbers': e.messages})
 | 
			
		||||
 | 
			
		||||
        existing = item.part.find_conflicting_serial_numbers(serials)
 | 
			
		||||
 | 
			
		||||
        if len(existing) > 0:
 | 
			
		||||
            exists = ','.join([str(x) for x in existing])
 | 
			
		||||
            error = _('Serial numbers already exist') + ": " + exists
 | 
			
		||||
            error = _('Serial numbers already exist') + ': ' + exists
 | 
			
		||||
 | 
			
		||||
            raise ValidationError({
 | 
			
		||||
                'serial_numbers': error,
 | 
			
		||||
            })
 | 
			
		||||
            raise ValidationError({'serial_numbers': error})
 | 
			
		||||
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
@@ -465,7 +465,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
 | 
			
		||||
        serials = InvenTree.helpers.extract_serial_numbers(
 | 
			
		||||
            data['serial_numbers'],
 | 
			
		||||
            data['quantity'],
 | 
			
		||||
            item.part.get_latest_serial_number()
 | 
			
		||||
            item.part.get_latest_serial_number(),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        item.serializeStock(
 | 
			
		||||
@@ -508,7 +508,7 @@ class InstallStockItemSerializer(serializers.Serializer):
 | 
			
		||||
        """Validate the quantity value."""
 | 
			
		||||
 | 
			
		||||
        if quantity < 1:
 | 
			
		||||
            raise ValidationError(_("Quantity to install must be at least 1"))
 | 
			
		||||
            raise ValidationError(_('Quantity to install must be at least 1'))
 | 
			
		||||
 | 
			
		||||
        return quantity
 | 
			
		||||
 | 
			
		||||
@@ -516,14 +516,14 @@ class InstallStockItemSerializer(serializers.Serializer):
 | 
			
		||||
        """Validate the selected stock item."""
 | 
			
		||||
        if not stock_item.in_stock:
 | 
			
		||||
            # StockItem must be in stock to be "installed"
 | 
			
		||||
            raise ValidationError(_("Stock item is unavailable"))
 | 
			
		||||
            raise ValidationError(_('Stock item is unavailable'))
 | 
			
		||||
 | 
			
		||||
        parent_item = self.context['item']
 | 
			
		||||
        parent_part = parent_item.part
 | 
			
		||||
 | 
			
		||||
        # Check if the selected part is in the Bill of Materials of the parent item
 | 
			
		||||
        if not parent_part.check_if_part_in_bom(stock_item.part):
 | 
			
		||||
            raise ValidationError(_("Selected part is not in the Bill of Materials"))
 | 
			
		||||
            raise ValidationError(_('Selected part is not in the Bill of Materials'))
 | 
			
		||||
 | 
			
		||||
        return stock_item
 | 
			
		||||
 | 
			
		||||
@@ -535,7 +535,9 @@ class InstallStockItemSerializer(serializers.Serializer):
 | 
			
		||||
        quantity = data.get('quantity', stock_item.quantity)
 | 
			
		||||
 | 
			
		||||
        if quantity > stock_item.quantity:
 | 
			
		||||
            raise ValidationError(_("Quantity to install must not exceed available quantity"))
 | 
			
		||||
            raise ValidationError(
 | 
			
		||||
                _('Quantity to install must not exceed available quantity')
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
@@ -551,10 +553,7 @@ class InstallStockItemSerializer(serializers.Serializer):
 | 
			
		||||
        request = self.context['request']
 | 
			
		||||
 | 
			
		||||
        parent_item.installStockItem(
 | 
			
		||||
            stock_item,
 | 
			
		||||
            quantity_to_install,
 | 
			
		||||
            request.user,
 | 
			
		||||
            note,
 | 
			
		||||
            stock_item, quantity_to_install, request.user, note
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -564,22 +563,22 @@ class UninstallStockItemSerializer(serializers.Serializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass options."""
 | 
			
		||||
 | 
			
		||||
        fields = [
 | 
			
		||||
            'location',
 | 
			
		||||
            'note',
 | 
			
		||||
        ]
 | 
			
		||||
        fields = ['location', 'note']
 | 
			
		||||
 | 
			
		||||
    location = serializers.PrimaryKeyRelatedField(
 | 
			
		||||
        queryset=StockLocation.objects.all(),
 | 
			
		||||
        many=False, required=True, allow_null=False,
 | 
			
		||||
        many=False,
 | 
			
		||||
        required=True,
 | 
			
		||||
        allow_null=False,
 | 
			
		||||
        label=_('Location'),
 | 
			
		||||
        help_text=_('Destination location for uninstalled item')
 | 
			
		||||
        help_text=_('Destination location for uninstalled item'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    note = serializers.CharField(
 | 
			
		||||
        label=_('Notes'),
 | 
			
		||||
        help_text=_('Add transaction note (optional)'),
 | 
			
		||||
        required=False, allow_blank=True,
 | 
			
		||||
        required=False,
 | 
			
		||||
        allow_blank=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def save(self):
 | 
			
		||||
@@ -593,11 +592,7 @@ class UninstallStockItemSerializer(serializers.Serializer):
 | 
			
		||||
 | 
			
		||||
        note = data.get('note', '')
 | 
			
		||||
 | 
			
		||||
        item.uninstall_into_location(
 | 
			
		||||
            location,
 | 
			
		||||
            request.user,
 | 
			
		||||
            note
 | 
			
		||||
        )
 | 
			
		||||
        item.uninstall_into_location(location, request.user, note)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ConvertStockItemSerializer(serializers.Serializer):
 | 
			
		||||
@@ -605,15 +600,16 @@ class ConvertStockItemSerializer(serializers.Serializer):
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass options"""
 | 
			
		||||
        fields = [
 | 
			
		||||
            'part',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        fields = ['part']
 | 
			
		||||
 | 
			
		||||
    part = serializers.PrimaryKeyRelatedField(
 | 
			
		||||
        queryset=part_models.Part.objects.all(),
 | 
			
		||||
        label=_('Part'),
 | 
			
		||||
        help_text=_('Select part to convert stock item into'),
 | 
			
		||||
        many=False, required=True, allow_null=False
 | 
			
		||||
        many=False,
 | 
			
		||||
        required=True,
 | 
			
		||||
        allow_null=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def validate_part(self, part):
 | 
			
		||||
@@ -622,7 +618,9 @@ class ConvertStockItemSerializer(serializers.Serializer):
 | 
			
		||||
        valid_options = stock_item.part.get_conversion_options()
 | 
			
		||||
 | 
			
		||||
        if part not in valid_options:
 | 
			
		||||
            raise ValidationError(_("Selected part is not a valid option for conversion"))
 | 
			
		||||
            raise ValidationError(
 | 
			
		||||
                _('Selected part is not a valid option for conversion')
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return part
 | 
			
		||||
 | 
			
		||||
@@ -636,7 +634,9 @@ class ConvertStockItemSerializer(serializers.Serializer):
 | 
			
		||||
        stock_item = self.context['item']
 | 
			
		||||
 | 
			
		||||
        if stock_item.supplier_part is not None:
 | 
			
		||||
            raise ValidationError(_("Cannot convert stock item with assigned SupplierPart"))
 | 
			
		||||
            raise ValidationError(
 | 
			
		||||
                _('Cannot convert stock item with assigned SupplierPart')
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
@@ -658,14 +658,13 @@ class ReturnStockItemSerializer(serializers.Serializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass options"""
 | 
			
		||||
 | 
			
		||||
        fields = [
 | 
			
		||||
            'location',
 | 
			
		||||
            'note',
 | 
			
		||||
        ]
 | 
			
		||||
        fields = ['location', 'note']
 | 
			
		||||
 | 
			
		||||
    location = serializers.PrimaryKeyRelatedField(
 | 
			
		||||
        queryset=StockLocation.objects.all(),
 | 
			
		||||
        many=False, required=True, allow_null=False,
 | 
			
		||||
        many=False,
 | 
			
		||||
        required=True,
 | 
			
		||||
        allow_null=False,
 | 
			
		||||
        label=_('Location'),
 | 
			
		||||
        help_text=_('Destination location for returned item'),
 | 
			
		||||
    )
 | 
			
		||||
@@ -673,7 +672,8 @@ class ReturnStockItemSerializer(serializers.Serializer):
 | 
			
		||||
    notes = serializers.CharField(
 | 
			
		||||
        label=_('Notes'),
 | 
			
		||||
        help_text=_('Add transaction note (optional)'),
 | 
			
		||||
        required=False, allow_blank=True,
 | 
			
		||||
        required=False,
 | 
			
		||||
        allow_blank=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def save(self):
 | 
			
		||||
@@ -686,11 +686,7 @@ class ReturnStockItemSerializer(serializers.Serializer):
 | 
			
		||||
        location = data['location']
 | 
			
		||||
        notes = data.get('notes', '')
 | 
			
		||||
 | 
			
		||||
        item.return_from_customer(
 | 
			
		||||
            location,
 | 
			
		||||
            user=request.user,
 | 
			
		||||
            notes=notes
 | 
			
		||||
        )
 | 
			
		||||
        item.return_from_customer(location, user=request.user, notes=notes)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockChangeStatusSerializer(serializers.Serializer):
 | 
			
		||||
@@ -698,11 +694,8 @@ class StockChangeStatusSerializer(serializers.Serializer):
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass options"""
 | 
			
		||||
        fields = [
 | 
			
		||||
            'items',
 | 
			
		||||
            'status',
 | 
			
		||||
            'note',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        fields = ['items', 'status', 'note']
 | 
			
		||||
 | 
			
		||||
    items = serializers.PrimaryKeyRelatedField(
 | 
			
		||||
        queryset=StockItem.objects.all(),
 | 
			
		||||
@@ -716,7 +709,7 @@ class StockChangeStatusSerializer(serializers.Serializer):
 | 
			
		||||
    def validate_items(self, items):
 | 
			
		||||
        """Validate the selected stock items"""
 | 
			
		||||
        if len(items) == 0:
 | 
			
		||||
            raise ValidationError(_("No stock items selected"))
 | 
			
		||||
            raise ValidationError(_('No stock items selected'))
 | 
			
		||||
 | 
			
		||||
        return items
 | 
			
		||||
 | 
			
		||||
@@ -729,7 +722,8 @@ class StockChangeStatusSerializer(serializers.Serializer):
 | 
			
		||||
    note = serializers.CharField(
 | 
			
		||||
        label=_('Notes'),
 | 
			
		||||
        help_text=_('Add transaction note (optional)'),
 | 
			
		||||
        required=False, allow_blank=True,
 | 
			
		||||
        required=False,
 | 
			
		||||
        allow_blank=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @transaction.atomic
 | 
			
		||||
@@ -748,9 +742,7 @@ class StockChangeStatusSerializer(serializers.Serializer):
 | 
			
		||||
        items_to_update = []
 | 
			
		||||
        transaction_notes = []
 | 
			
		||||
 | 
			
		||||
        deltas = {
 | 
			
		||||
            'status': status,
 | 
			
		||||
        }
 | 
			
		||||
        deltas = {'status': status}
 | 
			
		||||
 | 
			
		||||
        now = datetime.now()
 | 
			
		||||
 | 
			
		||||
@@ -792,26 +784,16 @@ class StockLocationTypeSerializer(InvenTree.serializers.InvenTreeModelSerializer
 | 
			
		||||
        """Serializer metaclass."""
 | 
			
		||||
 | 
			
		||||
        model = StockLocationType
 | 
			
		||||
        fields = [
 | 
			
		||||
            "pk",
 | 
			
		||||
            "name",
 | 
			
		||||
            "description",
 | 
			
		||||
            "icon",
 | 
			
		||||
            "location_count",
 | 
			
		||||
        ]
 | 
			
		||||
        fields = ['pk', 'name', 'description', 'icon', 'location_count']
 | 
			
		||||
 | 
			
		||||
        read_only_fields = [
 | 
			
		||||
            "location_count",
 | 
			
		||||
        ]
 | 
			
		||||
        read_only_fields = ['location_count']
 | 
			
		||||
 | 
			
		||||
    location_count = serializers.IntegerField(read_only=True)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def annotate_queryset(queryset):
 | 
			
		||||
        """Add location count to each location type."""
 | 
			
		||||
        return queryset.annotate(
 | 
			
		||||
            location_count=Count("stock_locations")
 | 
			
		||||
        )
 | 
			
		||||
        return queryset.annotate(location_count=Count('stock_locations'))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
 | 
			
		||||
@@ -821,13 +803,7 @@ class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
 | 
			
		||||
        """Metaclass options."""
 | 
			
		||||
 | 
			
		||||
        model = StockLocation
 | 
			
		||||
        fields = [
 | 
			
		||||
            'pk',
 | 
			
		||||
            'name',
 | 
			
		||||
            'parent',
 | 
			
		||||
            'icon',
 | 
			
		||||
            'structural',
 | 
			
		||||
        ]
 | 
			
		||||
        fields = ['pk', 'name', 'parent', 'icon', 'structural']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
 | 
			
		||||
@@ -858,10 +834,7 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
 | 
			
		||||
            'tags',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        read_only_fields = [
 | 
			
		||||
            'barcode_hash',
 | 
			
		||||
            'icon',
 | 
			
		||||
        ]
 | 
			
		||||
        read_only_fields = ['barcode_hash', 'icon']
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        """Optionally add or remove extra fields"""
 | 
			
		||||
@@ -876,9 +849,7 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
 | 
			
		||||
    def annotate_queryset(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()
 | 
			
		||||
        )
 | 
			
		||||
        queryset = queryset.annotate(items=stock.filters.annotate_location_items())
 | 
			
		||||
 | 
			
		||||
        return queryset
 | 
			
		||||
 | 
			
		||||
@@ -891,19 +862,21 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
 | 
			
		||||
    tags = TagListSerializerField(required=False)
 | 
			
		||||
 | 
			
		||||
    path = serializers.ListField(
 | 
			
		||||
        child=serializers.DictField(),
 | 
			
		||||
        source='get_path',
 | 
			
		||||
        read_only=True,
 | 
			
		||||
        child=serializers.DictField(), source='get_path', read_only=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # explicitly set this field, so it gets included for AutoSchema
 | 
			
		||||
    icon = serializers.CharField(read_only=True)
 | 
			
		||||
 | 
			
		||||
    # Detail for location type
 | 
			
		||||
    location_type_detail = StockLocationTypeSerializer(source="location_type", read_only=True, many=False)
 | 
			
		||||
    location_type_detail = StockLocationTypeSerializer(
 | 
			
		||||
        source='location_type', read_only=True, many=False
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer):
 | 
			
		||||
class StockItemAttachmentSerializer(
 | 
			
		||||
    InvenTree.serializers.InvenTreeAttachmentSerializer
 | 
			
		||||
):
 | 
			
		||||
    """Serializer for StockItemAttachment model."""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
@@ -912,7 +885,7 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer
 | 
			
		||||
        model = StockItemAttachment
 | 
			
		||||
 | 
			
		||||
        fields = InvenTree.serializers.InvenTreeAttachmentSerializer.attachment_fields([
 | 
			
		||||
            'stock_item',
 | 
			
		||||
            'stock_item'
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -936,12 +909,7 @@ class StockTrackingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
 | 
			
		||||
            'user_detail',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        read_only_fields = [
 | 
			
		||||
            'date',
 | 
			
		||||
            'user',
 | 
			
		||||
            'label',
 | 
			
		||||
            'tracking_type',
 | 
			
		||||
        ]
 | 
			
		||||
        read_only_fields = ['date', 'user', 'label', 'tracking_type']
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        """Add detail fields."""
 | 
			
		||||
@@ -960,7 +928,9 @@ class StockTrackingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
 | 
			
		||||
 | 
			
		||||
    item_detail = StockItemSerializerBrief(source='item', many=False, read_only=True)
 | 
			
		||||
 | 
			
		||||
    user_detail = InvenTree.serializers.UserSerializer(source='user', many=False, read_only=True)
 | 
			
		||||
    user_detail = InvenTree.serializers.UserSerializer(
 | 
			
		||||
        source='user', many=False, read_only=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    deltas = serializers.JSONField(read_only=True)
 | 
			
		||||
 | 
			
		||||
@@ -977,9 +947,7 @@ class StockAssignmentItemSerializer(serializers.Serializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass options."""
 | 
			
		||||
 | 
			
		||||
        fields = [
 | 
			
		||||
            'item',
 | 
			
		||||
        ]
 | 
			
		||||
        fields = ['item']
 | 
			
		||||
 | 
			
		||||
    item = serializers.PrimaryKeyRelatedField(
 | 
			
		||||
        queryset=StockItem.objects.all(),
 | 
			
		||||
@@ -999,19 +967,19 @@ class StockAssignmentItemSerializer(serializers.Serializer):
 | 
			
		||||
        """
 | 
			
		||||
        # The item must currently be "in stock"
 | 
			
		||||
        if not item.in_stock:
 | 
			
		||||
            raise ValidationError(_("Item must be in stock"))
 | 
			
		||||
            raise ValidationError(_('Item must be in stock'))
 | 
			
		||||
 | 
			
		||||
        # The base part must be "salable"
 | 
			
		||||
        if not item.part.salable:
 | 
			
		||||
            raise ValidationError(_("Part must be salable"))
 | 
			
		||||
            raise ValidationError(_('Part must be salable'))
 | 
			
		||||
 | 
			
		||||
        # The item must not be allocated to a sales order
 | 
			
		||||
        if item.sales_order_allocations.count() > 0:
 | 
			
		||||
            raise ValidationError(_("Item is allocated to a sales order"))
 | 
			
		||||
            raise ValidationError(_('Item is allocated to a sales order'))
 | 
			
		||||
 | 
			
		||||
        # The item must not be allocated to a build order
 | 
			
		||||
        if item.allocations.count() > 0:
 | 
			
		||||
            raise ValidationError(_("Item is allocated to a build order"))
 | 
			
		||||
            raise ValidationError(_('Item is allocated to a build order'))
 | 
			
		||||
 | 
			
		||||
        return item
 | 
			
		||||
 | 
			
		||||
@@ -1025,16 +993,9 @@ class StockAssignmentSerializer(serializers.Serializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass options."""
 | 
			
		||||
 | 
			
		||||
        fields = [
 | 
			
		||||
            'items',
 | 
			
		||||
            'customer',
 | 
			
		||||
            'notes',
 | 
			
		||||
        ]
 | 
			
		||||
        fields = ['items', 'customer', 'notes']
 | 
			
		||||
 | 
			
		||||
    items = StockAssignmentItemSerializer(
 | 
			
		||||
        many=True,
 | 
			
		||||
        required=True,
 | 
			
		||||
    )
 | 
			
		||||
    items = StockAssignmentItemSerializer(many=True, required=True)
 | 
			
		||||
 | 
			
		||||
    customer = serializers.PrimaryKeyRelatedField(
 | 
			
		||||
        queryset=company.models.Company.objects.all(),
 | 
			
		||||
@@ -1066,7 +1027,7 @@ class StockAssignmentSerializer(serializers.Serializer):
 | 
			
		||||
        items = data.get('items', [])
 | 
			
		||||
 | 
			
		||||
        if len(items) == 0:
 | 
			
		||||
            raise ValidationError(_("A list of stock items must be provided"))
 | 
			
		||||
            raise ValidationError(_('A list of stock items must be provided'))
 | 
			
		||||
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
@@ -1084,14 +1045,9 @@ class StockAssignmentSerializer(serializers.Serializer):
 | 
			
		||||
 | 
			
		||||
        with transaction.atomic():
 | 
			
		||||
            for item in items:
 | 
			
		||||
 | 
			
		||||
                stock_item = item['item']
 | 
			
		||||
 | 
			
		||||
                stock_item.allocateToCustomer(
 | 
			
		||||
                    customer,
 | 
			
		||||
                    user=user,
 | 
			
		||||
                    notes=notes,
 | 
			
		||||
                )
 | 
			
		||||
                stock_item.allocateToCustomer(customer, user=user, notes=notes)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockMergeItemSerializer(serializers.Serializer):
 | 
			
		||||
@@ -1103,9 +1059,7 @@ class StockMergeItemSerializer(serializers.Serializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass options."""
 | 
			
		||||
 | 
			
		||||
        fields = [
 | 
			
		||||
            'item',
 | 
			
		||||
        ]
 | 
			
		||||
        fields = ['item']
 | 
			
		||||
 | 
			
		||||
    item = serializers.PrimaryKeyRelatedField(
 | 
			
		||||
        queryset=StockItem.objects.all(),
 | 
			
		||||
@@ -1137,10 +1091,7 @@ class StockMergeSerializer(serializers.Serializer):
 | 
			
		||||
            'allow_mismatched_status',
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    items = StockMergeItemSerializer(
 | 
			
		||||
        many=True,
 | 
			
		||||
        required=True,
 | 
			
		||||
    )
 | 
			
		||||
    items = StockMergeItemSerializer(many=True, required=True)
 | 
			
		||||
 | 
			
		||||
    location = serializers.PrimaryKeyRelatedField(
 | 
			
		||||
        queryset=StockLocation.objects.all(),
 | 
			
		||||
@@ -1203,7 +1154,9 @@ class StockMergeSerializer(serializers.Serializer):
 | 
			
		||||
            item.can_merge(
 | 
			
		||||
                raise_error=True,
 | 
			
		||||
                other=base_item,
 | 
			
		||||
                allow_mismatched_suppliers=data.get('allow_mismatched_suppliers', False),
 | 
			
		||||
                allow_mismatched_suppliers=data.get(
 | 
			
		||||
                    'allow_mismatched_suppliers', False
 | 
			
		||||
                ),
 | 
			
		||||
                allow_mismatched_status=data.get('allow_mismatched_status', False),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
@@ -1233,7 +1186,7 @@ class StockMergeSerializer(serializers.Serializer):
 | 
			
		||||
            allow_mismatched_status=data.get('allow_mismatched_status', False),
 | 
			
		||||
            user=user,
 | 
			
		||||
            location=data['location'],
 | 
			
		||||
            notes=data.get('notes', None)
 | 
			
		||||
            notes=data.get('notes', None),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -1255,10 +1208,7 @@ class StockAdjustmentItemSerializer(serializers.Serializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass options."""
 | 
			
		||||
 | 
			
		||||
        fields = [
 | 
			
		||||
            'item',
 | 
			
		||||
            'quantity'
 | 
			
		||||
        ]
 | 
			
		||||
        fields = ['item', 'quantity']
 | 
			
		||||
 | 
			
		||||
    pk = serializers.PrimaryKeyRelatedField(
 | 
			
		||||
        queryset=StockItem.objects.all(),
 | 
			
		||||
@@ -1266,19 +1216,17 @@ class StockAdjustmentItemSerializer(serializers.Serializer):
 | 
			
		||||
        allow_null=False,
 | 
			
		||||
        required=True,
 | 
			
		||||
        label='stock_item',
 | 
			
		||||
        help_text=_('StockItem primary key value')
 | 
			
		||||
        help_text=_('StockItem primary key value'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    quantity = serializers.DecimalField(
 | 
			
		||||
        max_digits=15,
 | 
			
		||||
        decimal_places=5,
 | 
			
		||||
        min_value=0,
 | 
			
		||||
        required=True
 | 
			
		||||
        max_digits=15, decimal_places=5, min_value=0, required=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    batch = serializers.CharField(
 | 
			
		||||
        max_length=100,
 | 
			
		||||
        required=False, allow_blank=True,
 | 
			
		||||
        required=False,
 | 
			
		||||
        allow_blank=True,
 | 
			
		||||
        label=_('Batch Code'),
 | 
			
		||||
        help_text=_('Batch code for this stock item'),
 | 
			
		||||
    )
 | 
			
		||||
@@ -1288,12 +1236,14 @@ class StockAdjustmentItemSerializer(serializers.Serializer):
 | 
			
		||||
        default=InvenTree.status_codes.StockStatus.OK.value,
 | 
			
		||||
        label=_('Status'),
 | 
			
		||||
        help_text=_('Stock item status code'),
 | 
			
		||||
        required=False, allow_blank=True,
 | 
			
		||||
        required=False,
 | 
			
		||||
        allow_blank=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    packaging = serializers.CharField(
 | 
			
		||||
        max_length=50,
 | 
			
		||||
        required=False, allow_blank=True,
 | 
			
		||||
        required=False,
 | 
			
		||||
        allow_blank=True,
 | 
			
		||||
        label=_('Packaging'),
 | 
			
		||||
        help_text=_('Packaging this stock item is stored in'),
 | 
			
		||||
    )
 | 
			
		||||
@@ -1305,18 +1255,15 @@ class StockAdjustmentSerializer(serializers.Serializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass options."""
 | 
			
		||||
 | 
			
		||||
        fields = [
 | 
			
		||||
            'items',
 | 
			
		||||
            'notes',
 | 
			
		||||
        ]
 | 
			
		||||
        fields = ['items', 'notes']
 | 
			
		||||
 | 
			
		||||
    items = StockAdjustmentItemSerializer(many=True)
 | 
			
		||||
 | 
			
		||||
    notes = serializers.CharField(
 | 
			
		||||
        required=False,
 | 
			
		||||
        allow_blank=True,
 | 
			
		||||
        label=_("Notes"),
 | 
			
		||||
        help_text=_("Stock transaction notes"),
 | 
			
		||||
        label=_('Notes'),
 | 
			
		||||
        help_text=_('Stock transaction notes'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def validate(self, data):
 | 
			
		||||
@@ -1326,7 +1273,7 @@ class StockAdjustmentSerializer(serializers.Serializer):
 | 
			
		||||
        items = data.get('items', [])
 | 
			
		||||
 | 
			
		||||
        if len(items) == 0:
 | 
			
		||||
            raise ValidationError(_("A list of stock items must be provided"))
 | 
			
		||||
            raise ValidationError(_('A list of stock items must be provided'))
 | 
			
		||||
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
@@ -1344,15 +1291,10 @@ class StockCountSerializer(StockAdjustmentSerializer):
 | 
			
		||||
 | 
			
		||||
        with transaction.atomic():
 | 
			
		||||
            for item in items:
 | 
			
		||||
 | 
			
		||||
                stock_item = item['pk']
 | 
			
		||||
                quantity = item['quantity']
 | 
			
		||||
 | 
			
		||||
                stock_item.stocktake(
 | 
			
		||||
                    quantity,
 | 
			
		||||
                    request.user,
 | 
			
		||||
                    notes=notes
 | 
			
		||||
                )
 | 
			
		||||
                stock_item.stocktake(quantity, request.user, notes=notes)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockAddSerializer(StockAdjustmentSerializer):
 | 
			
		||||
@@ -1367,15 +1309,10 @@ class StockAddSerializer(StockAdjustmentSerializer):
 | 
			
		||||
 | 
			
		||||
        with transaction.atomic():
 | 
			
		||||
            for item in data['items']:
 | 
			
		||||
 | 
			
		||||
                stock_item = item['pk']
 | 
			
		||||
                quantity = item['quantity']
 | 
			
		||||
 | 
			
		||||
                stock_item.add_stock(
 | 
			
		||||
                    quantity,
 | 
			
		||||
                    request.user,
 | 
			
		||||
                    notes=notes
 | 
			
		||||
                )
 | 
			
		||||
                stock_item.add_stock(quantity, request.user, notes=notes)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockRemoveSerializer(StockAdjustmentSerializer):
 | 
			
		||||
@@ -1390,15 +1327,10 @@ class StockRemoveSerializer(StockAdjustmentSerializer):
 | 
			
		||||
 | 
			
		||||
        with transaction.atomic():
 | 
			
		||||
            for item in data['items']:
 | 
			
		||||
 | 
			
		||||
                stock_item = item['pk']
 | 
			
		||||
                quantity = item['quantity']
 | 
			
		||||
 | 
			
		||||
                stock_item.take_stock(
 | 
			
		||||
                    quantity,
 | 
			
		||||
                    request.user,
 | 
			
		||||
                    notes=notes
 | 
			
		||||
                )
 | 
			
		||||
                stock_item.take_stock(quantity, request.user, notes=notes)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockTransferSerializer(StockAdjustmentSerializer):
 | 
			
		||||
@@ -1407,11 +1339,7 @@ class StockTransferSerializer(StockAdjustmentSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        """Metaclass options."""
 | 
			
		||||
 | 
			
		||||
        fields = [
 | 
			
		||||
            'items',
 | 
			
		||||
            'notes',
 | 
			
		||||
            'location',
 | 
			
		||||
        ]
 | 
			
		||||
        fields = ['items', 'notes', 'location']
 | 
			
		||||
 | 
			
		||||
    location = serializers.PrimaryKeyRelatedField(
 | 
			
		||||
        queryset=StockLocation.objects.all(),
 | 
			
		||||
@@ -1434,7 +1362,6 @@ class StockTransferSerializer(StockAdjustmentSerializer):
 | 
			
		||||
 | 
			
		||||
        with transaction.atomic():
 | 
			
		||||
            for item in items:
 | 
			
		||||
 | 
			
		||||
                # Required fields
 | 
			
		||||
                stock_item = item['pk']
 | 
			
		||||
                quantity = item['quantity']
 | 
			
		||||
@@ -1447,9 +1374,5 @@ class StockTransferSerializer(StockAdjustmentSerializer):
 | 
			
		||||
                        kwargs[field_name] = item[field_name]
 | 
			
		||||
 | 
			
		||||
                stock_item.move(
 | 
			
		||||
                    location,
 | 
			
		||||
                    notes,
 | 
			
		||||
                    request.user,
 | 
			
		||||
                    quantity=quantity,
 | 
			
		||||
                    **kwargs
 | 
			
		||||
                    location, notes, request.user, quantity=quantity, **kwargs
 | 
			
		||||
                )
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -24,18 +24,14 @@ class TestSerialNumberMigration(MigratorTestCase):
 | 
			
		||||
            trackable=True,
 | 
			
		||||
            level=0,
 | 
			
		||||
            tree_id=0,
 | 
			
		||||
            lft=0, rght=0
 | 
			
		||||
            lft=0,
 | 
			
		||||
            rght=0,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Create some serialized stock items
 | 
			
		||||
        for sn in range(10, 20):
 | 
			
		||||
            StockItem.objects.create(
 | 
			
		||||
                part=my_part,
 | 
			
		||||
                quantity=1,
 | 
			
		||||
                serial=sn,
 | 
			
		||||
                level=0,
 | 
			
		||||
                tree_id=0,
 | 
			
		||||
                lft=0, rght=0
 | 
			
		||||
                part=my_part, quantity=1, serial=sn, level=0, tree_id=0, lft=0, rght=0
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # Create a stock item with a very large serial number
 | 
			
		||||
@@ -45,7 +41,8 @@ class TestSerialNumberMigration(MigratorTestCase):
 | 
			
		||||
            serial='9999999999999999999999999999999999999999999999999999999999999',
 | 
			
		||||
            level=0,
 | 
			
		||||
            tree_id=0,
 | 
			
		||||
            lft=0, rght=0
 | 
			
		||||
            lft=0,
 | 
			
		||||
            rght=0,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.big_ref_pk = item.pk
 | 
			
		||||
@@ -63,8 +60,11 @@ class TestSerialNumberMigration(MigratorTestCase):
 | 
			
		||||
        big_ref_item = StockItem.objects.get(pk=self.big_ref_pk)
 | 
			
		||||
 | 
			
		||||
        # Check that the StockItem maximum serial number
 | 
			
		||||
        self.assertEqual(big_ref_item.serial, '9999999999999999999999999999999999999999999999999999999999999')
 | 
			
		||||
        self.assertEqual(big_ref_item.serial_int, 0x7fffffff)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            big_ref_item.serial,
 | 
			
		||||
            '9999999999999999999999999999999999999999999999999999999999999',
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(big_ref_item.serial_int, 0x7FFFFFFF)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestScheduledForDeletionMigration(MigratorTestCase):
 | 
			
		||||
@@ -83,17 +83,21 @@ class TestScheduledForDeletionMigration(MigratorTestCase):
 | 
			
		||||
                name=f'Part_{idx}',
 | 
			
		||||
                description='Just a part, nothing to see here',
 | 
			
		||||
                active=True,
 | 
			
		||||
                level=0, tree_id=0,
 | 
			
		||||
                lft=0, rght=0,
 | 
			
		||||
                level=0,
 | 
			
		||||
                tree_id=0,
 | 
			
		||||
                lft=0,
 | 
			
		||||
                rght=0,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            for jj in range(5):
 | 
			
		||||
                StockItem.objects.create(
 | 
			
		||||
                    part=part,
 | 
			
		||||
                    quantity=jj + 5,
 | 
			
		||||
                    level=0, tree_id=0,
 | 
			
		||||
                    lft=0, rght=0,
 | 
			
		||||
                    scheduled_for_deletion=True
 | 
			
		||||
                    level=0,
 | 
			
		||||
                    tree_id=0,
 | 
			
		||||
                    lft=0,
 | 
			
		||||
                    rght=0,
 | 
			
		||||
                    scheduled_for_deletion=True,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        # For extra points, create some parent-child relationships between stock items
 | 
			
		||||
@@ -102,8 +106,10 @@ class TestScheduledForDeletionMigration(MigratorTestCase):
 | 
			
		||||
        item_1 = StockItem.objects.create(
 | 
			
		||||
            part=part,
 | 
			
		||||
            quantity=100,
 | 
			
		||||
            level=0, tree_id=0,
 | 
			
		||||
            lft=0, rght=0,
 | 
			
		||||
            level=0,
 | 
			
		||||
            tree_id=0,
 | 
			
		||||
            lft=0,
 | 
			
		||||
            rght=0,
 | 
			
		||||
            scheduled_for_deletion=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -111,8 +117,10 @@ class TestScheduledForDeletionMigration(MigratorTestCase):
 | 
			
		||||
            StockItem.objects.create(
 | 
			
		||||
                part=part,
 | 
			
		||||
                quantity=200,
 | 
			
		||||
                level=0, tree_id=0,
 | 
			
		||||
                lft=0, rght=0,
 | 
			
		||||
                level=0,
 | 
			
		||||
                tree_id=0,
 | 
			
		||||
                lft=0,
 | 
			
		||||
                rght=0,
 | 
			
		||||
                scheduled_for_deletion=False,
 | 
			
		||||
                parent=item_1,
 | 
			
		||||
            )
 | 
			
		||||
 
 | 
			
		||||
@@ -13,14 +13,7 @@ from users.models import Owner
 | 
			
		||||
class StockViewTestCase(InvenTreeTestCase):
 | 
			
		||||
    """Mixin for Stockview tests."""
 | 
			
		||||
 | 
			
		||||
    fixtures = [
 | 
			
		||||
        'category',
 | 
			
		||||
        'part',
 | 
			
		||||
        'company',
 | 
			
		||||
        'location',
 | 
			
		||||
        'supplier_part',
 | 
			
		||||
        'stock',
 | 
			
		||||
    ]
 | 
			
		||||
    fixtures = ['category', 'part', 'company', 'location', 'supplier_part', 'stock']
 | 
			
		||||
 | 
			
		||||
    roles = 'all'
 | 
			
		||||
 | 
			
		||||
@@ -59,13 +52,13 @@ class StockDetailTest(StockViewTestCase):
 | 
			
		||||
 | 
			
		||||
        # Actions to check
 | 
			
		||||
        actions = [
 | 
			
		||||
            "id=\\\'stock-count\\\' title=\\\'Count stock\\\'",
 | 
			
		||||
            "id=\\\'stock-add\\\' title=\\\'Add stock\\\'",
 | 
			
		||||
            "id=\\\'stock-remove\\\' title=\\\'Remove stock\\\'",
 | 
			
		||||
            "id=\\\'stock-move\\\' title=\\\'Transfer stock\\\'",
 | 
			
		||||
            "id=\\\'stock-duplicate\\\'",
 | 
			
		||||
            "id=\\\'stock-edit\\\'",
 | 
			
		||||
            "id=\\\'stock-delete\\\'",
 | 
			
		||||
            "id=\\'stock-count\\' title=\\'Count stock\\'",
 | 
			
		||||
            "id=\\'stock-add\\' title=\\'Add stock\\'",
 | 
			
		||||
            "id=\\'stock-remove\\' title=\\'Remove stock\\'",
 | 
			
		||||
            "id=\\'stock-move\\' title=\\'Transfer stock\\'",
 | 
			
		||||
            "id=\\'stock-duplicate\\'",
 | 
			
		||||
            "id=\\'stock-edit\\'",
 | 
			
		||||
            "id=\\'stock-delete\\'",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        # Initially we should not have any of the required permissions
 | 
			
		||||
@@ -86,6 +79,7 @@ class StockDetailTest(StockViewTestCase):
 | 
			
		||||
 | 
			
		||||
class StockOwnershipTest(StockViewTestCase):
 | 
			
		||||
    """Tests for stock ownership views."""
 | 
			
		||||
 | 
			
		||||
    test_item_id = 11
 | 
			
		||||
    test_location_id = 1
 | 
			
		||||
 | 
			
		||||
@@ -135,9 +129,13 @@ class StockOwnershipTest(StockViewTestCase):
 | 
			
		||||
        location = StockLocation.objects.get(pk=self.test_location_id)
 | 
			
		||||
 | 
			
		||||
        # Check that user is not allowed to change item
 | 
			
		||||
        self.assertTrue(item.check_ownership(self.user))        # No owner -> True
 | 
			
		||||
        self.assertTrue(location.check_ownership(self.user))    # No owner -> True
 | 
			
		||||
        self.assertContains(self.assert_api_change(), 'You do not have permission to perform this action.', status_code=403)
 | 
			
		||||
        self.assertTrue(item.check_ownership(self.user))  # No owner -> True
 | 
			
		||||
        self.assertTrue(location.check_ownership(self.user))  # No owner -> True
 | 
			
		||||
        self.assertContains(
 | 
			
		||||
            self.assert_api_change(),
 | 
			
		||||
            'You do not have permission to perform this action.',
 | 
			
		||||
            status_code=403,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Adjust group rules
 | 
			
		||||
        group = Group.objects.get(name='my_test_group')
 | 
			
		||||
@@ -153,9 +151,13 @@ class StockOwnershipTest(StockViewTestCase):
 | 
			
		||||
        location.save()
 | 
			
		||||
 | 
			
		||||
        # Check that user is allowed to change item
 | 
			
		||||
        self.assertTrue(item.check_ownership(self.user))        # Owner is group -> True
 | 
			
		||||
        self.assertTrue(location.check_ownership(self.user))    # Owner is group -> True
 | 
			
		||||
        self.assertContains(self.assert_api_change(), f'"status":{StockStatus.DAMAGED.value}', status_code=200)
 | 
			
		||||
        self.assertTrue(item.check_ownership(self.user))  # Owner is group -> True
 | 
			
		||||
        self.assertTrue(location.check_ownership(self.user))  # Owner is group -> True
 | 
			
		||||
        self.assertContains(
 | 
			
		||||
            self.assert_api_change(),
 | 
			
		||||
            f'"status":{StockStatus.DAMAGED.value}',
 | 
			
		||||
            status_code=200,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Change group
 | 
			
		||||
        new_group = Group.objects.create(name='new_group')
 | 
			
		||||
@@ -166,5 +168,9 @@ class StockOwnershipTest(StockViewTestCase):
 | 
			
		||||
        location.save()
 | 
			
		||||
 | 
			
		||||
        # Check that user is not allowed to change item
 | 
			
		||||
        self.assertFalse(item.check_ownership(self.user))       # Owner is not in group -> False
 | 
			
		||||
        self.assertFalse(location.check_ownership(self.user))    # Owner is not in group -> False
 | 
			
		||||
        self.assertFalse(
 | 
			
		||||
            item.check_ownership(self.user)
 | 
			
		||||
        )  # Owner is not in group -> False
 | 
			
		||||
        self.assertFalse(
 | 
			
		||||
            location.check_ownership(self.user)
 | 
			
		||||
        )  # Owner is not in group -> False
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,7 @@ from InvenTree.unit_test import InvenTreeTestCase
 | 
			
		||||
from order.models import SalesOrder
 | 
			
		||||
from part.models import Part
 | 
			
		||||
 | 
			
		||||
from .models import (StockItem, StockItemTestResult, StockItemTracking,
 | 
			
		||||
                     StockLocation)
 | 
			
		||||
from .models import StockItem, StockItemTestResult, StockItemTracking, StockLocation
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StockTestBase(InvenTreeTestCase):
 | 
			
		||||
@@ -55,10 +54,10 @@ class StockTest(StockTestBase):
 | 
			
		||||
 | 
			
		||||
    def test_pathstring(self):
 | 
			
		||||
        """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)
 | 
			
		||||
        d = StockLocation.objects.create(name="D", parent=c)
 | 
			
		||||
        a = StockLocation.objects.create(name='A')
 | 
			
		||||
        b = StockLocation.objects.create(name='B', parent=a)
 | 
			
		||||
        c = StockLocation.objects.create(name='C', parent=b)
 | 
			
		||||
        d = StockLocation.objects.create(name='D', parent=c)
 | 
			
		||||
 | 
			
		||||
        def refresh():
 | 
			
		||||
            a.refresh_from_db()
 | 
			
		||||
@@ -67,56 +66,56 @@ class StockTest(StockTestBase):
 | 
			
		||||
            d.refresh_from_db()
 | 
			
		||||
 | 
			
		||||
        # Initial checks
 | 
			
		||||
        self.assertEqual(a.pathstring, "A")
 | 
			
		||||
        self.assertEqual(b.pathstring, "A/B")
 | 
			
		||||
        self.assertEqual(c.pathstring, "A/B/C")
 | 
			
		||||
        self.assertEqual(d.pathstring, "A/B/C/D")
 | 
			
		||||
        self.assertEqual(a.pathstring, 'A')
 | 
			
		||||
        self.assertEqual(b.pathstring, 'A/B')
 | 
			
		||||
        self.assertEqual(c.pathstring, 'A/B/C')
 | 
			
		||||
        self.assertEqual(d.pathstring, 'A/B/C/D')
 | 
			
		||||
 | 
			
		||||
        c.name = "Cc"
 | 
			
		||||
        c.name = 'Cc'
 | 
			
		||||
        c.save()
 | 
			
		||||
 | 
			
		||||
        refresh()
 | 
			
		||||
        self.assertEqual(a.pathstring, "A")
 | 
			
		||||
        self.assertEqual(b.pathstring, "A/B")
 | 
			
		||||
        self.assertEqual(c.pathstring, "A/B/Cc")
 | 
			
		||||
        self.assertEqual(d.pathstring, "A/B/Cc/D")
 | 
			
		||||
        self.assertEqual(a.pathstring, 'A')
 | 
			
		||||
        self.assertEqual(b.pathstring, 'A/B')
 | 
			
		||||
        self.assertEqual(c.pathstring, 'A/B/Cc')
 | 
			
		||||
        self.assertEqual(d.pathstring, 'A/B/Cc/D')
 | 
			
		||||
 | 
			
		||||
        b.name = "Bb"
 | 
			
		||||
        b.name = 'Bb'
 | 
			
		||||
        b.save()
 | 
			
		||||
 | 
			
		||||
        refresh()
 | 
			
		||||
        self.assertEqual(a.pathstring, "A")
 | 
			
		||||
        self.assertEqual(b.pathstring, "A/Bb")
 | 
			
		||||
        self.assertEqual(c.pathstring, "A/Bb/Cc")
 | 
			
		||||
        self.assertEqual(d.pathstring, "A/Bb/Cc/D")
 | 
			
		||||
        self.assertEqual(a.pathstring, 'A')
 | 
			
		||||
        self.assertEqual(b.pathstring, 'A/Bb')
 | 
			
		||||
        self.assertEqual(c.pathstring, 'A/Bb/Cc')
 | 
			
		||||
        self.assertEqual(d.pathstring, 'A/Bb/Cc/D')
 | 
			
		||||
 | 
			
		||||
        a.name = "Aa"
 | 
			
		||||
        a.name = 'Aa'
 | 
			
		||||
        a.save()
 | 
			
		||||
 | 
			
		||||
        refresh()
 | 
			
		||||
        self.assertEqual(a.pathstring, "Aa")
 | 
			
		||||
        self.assertEqual(b.pathstring, "Aa/Bb")
 | 
			
		||||
        self.assertEqual(c.pathstring, "Aa/Bb/Cc")
 | 
			
		||||
        self.assertEqual(d.pathstring, "Aa/Bb/Cc/D")
 | 
			
		||||
        self.assertEqual(a.pathstring, 'Aa')
 | 
			
		||||
        self.assertEqual(b.pathstring, 'Aa/Bb')
 | 
			
		||||
        self.assertEqual(c.pathstring, 'Aa/Bb/Cc')
 | 
			
		||||
        self.assertEqual(d.pathstring, 'Aa/Bb/Cc/D')
 | 
			
		||||
 | 
			
		||||
        d.name = "Dd"
 | 
			
		||||
        d.name = 'Dd'
 | 
			
		||||
        d.save()
 | 
			
		||||
 | 
			
		||||
        refresh()
 | 
			
		||||
        self.assertEqual(a.pathstring, "Aa")
 | 
			
		||||
        self.assertEqual(b.pathstring, "Aa/Bb")
 | 
			
		||||
        self.assertEqual(c.pathstring, "Aa/Bb/Cc")
 | 
			
		||||
        self.assertEqual(d.pathstring, "Aa/Bb/Cc/Dd")
 | 
			
		||||
        self.assertEqual(a.pathstring, 'Aa')
 | 
			
		||||
        self.assertEqual(b.pathstring, 'Aa/Bb')
 | 
			
		||||
        self.assertEqual(c.pathstring, 'Aa/Bb/Cc')
 | 
			
		||||
        self.assertEqual(d.pathstring, 'Aa/Bb/Cc/Dd')
 | 
			
		||||
 | 
			
		||||
        # Test a really long name
 | 
			
		||||
        # (it will be clipped to < 250 characters)
 | 
			
		||||
        a.name = "A" * 100
 | 
			
		||||
        a.name = 'A' * 100
 | 
			
		||||
        a.save()
 | 
			
		||||
        b.name = "B" * 100
 | 
			
		||||
        b.name = 'B' * 100
 | 
			
		||||
        b.save()
 | 
			
		||||
        c.name = "C" * 100
 | 
			
		||||
        c.name = 'C' * 100
 | 
			
		||||
        c.save()
 | 
			
		||||
        d.name = "D" * 100
 | 
			
		||||
        d.name = 'D' * 100
 | 
			
		||||
        d.save()
 | 
			
		||||
 | 
			
		||||
        refresh()
 | 
			
		||||
@@ -125,19 +124,15 @@ class StockTest(StockTestBase):
 | 
			
		||||
        self.assertEqual(len(c.pathstring), 249)
 | 
			
		||||
        self.assertEqual(len(d.pathstring), 249)
 | 
			
		||||
 | 
			
		||||
        self.assertTrue(d.pathstring.startswith("AAAAAAAA"))
 | 
			
		||||
        self.assertTrue(d.pathstring.endswith("DDDDDDDD"))
 | 
			
		||||
        self.assertTrue(d.pathstring.startswith('AAAAAAAA'))
 | 
			
		||||
        self.assertTrue(d.pathstring.endswith('DDDDDDDD'))
 | 
			
		||||
 | 
			
		||||
    def test_link(self):
 | 
			
		||||
        """Test the link URL field validation"""
 | 
			
		||||
        item = StockItem.objects.get(pk=1)
 | 
			
		||||
 | 
			
		||||
        # Check that invalid URLs fail
 | 
			
		||||
        for bad_url in [
 | 
			
		||||
            'test.com',
 | 
			
		||||
            'httpx://abc.xyz',
 | 
			
		||||
            'https:google.com',
 | 
			
		||||
        ]:
 | 
			
		||||
        for bad_url in ['test.com', 'httpx://abc.xyz', 'https:google.com']:
 | 
			
		||||
            with self.assertRaises(ValidationError):
 | 
			
		||||
                item.link = bad_url
 | 
			
		||||
                item.save()
 | 
			
		||||
@@ -179,41 +174,31 @@ class StockTest(StockTestBase):
 | 
			
		||||
        # Ensure that 'global uniqueness' setting is enabled
 | 
			
		||||
        InvenTreeSetting.set_setting('SERIAL_NUMBER_GLOBALLY_UNIQUE', True, self.user)
 | 
			
		||||
 | 
			
		||||
        part_a = Part.objects.create(name='A', description='A part with a description', trackable=True)
 | 
			
		||||
        part_b = Part.objects.create(name='B', description='B part with a description', trackable=True)
 | 
			
		||||
        part_a = Part.objects.create(
 | 
			
		||||
            name='A', description='A part with a description', trackable=True
 | 
			
		||||
        )
 | 
			
		||||
        part_b = Part.objects.create(
 | 
			
		||||
            name='B', description='B part with a description', trackable=True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Create a StockItem for part_a
 | 
			
		||||
        StockItem.objects.create(
 | 
			
		||||
            part=part_a,
 | 
			
		||||
            quantity=1,
 | 
			
		||||
            serial='ABCDE',
 | 
			
		||||
        )
 | 
			
		||||
        StockItem.objects.create(part=part_a, quantity=1, serial='ABCDE')
 | 
			
		||||
 | 
			
		||||
        # Create a StockItem for part_a (but, will error due to identical serial)
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            StockItem.objects.create(
 | 
			
		||||
                part=part_b,
 | 
			
		||||
                quantity=1,
 | 
			
		||||
                serial='ABCDE',
 | 
			
		||||
            )
 | 
			
		||||
            StockItem.objects.create(part=part_b, quantity=1, serial='ABCDE')
 | 
			
		||||
 | 
			
		||||
        # Now, allow serial numbers to be duplicated between different parts
 | 
			
		||||
        InvenTreeSetting.set_setting('SERIAL_NUMBER_GLOBALLY_UNIQUE', False, self.user)
 | 
			
		||||
 | 
			
		||||
        StockItem.objects.create(
 | 
			
		||||
            part=part_b,
 | 
			
		||||
            quantity=1,
 | 
			
		||||
            serial='ABCDE',
 | 
			
		||||
        )
 | 
			
		||||
        StockItem.objects.create(part=part_b, quantity=1, serial='ABCDE')
 | 
			
		||||
 | 
			
		||||
    def test_expiry(self):
 | 
			
		||||
        """Test expiry date functionality for StockItem model."""
 | 
			
		||||
        today = datetime.datetime.now().date()
 | 
			
		||||
 | 
			
		||||
        item = StockItem.objects.create(
 | 
			
		||||
            location=self.office,
 | 
			
		||||
            part=Part.objects.get(pk=1),
 | 
			
		||||
            quantity=10,
 | 
			
		||||
            location=self.office, part=Part.objects.get(pk=1), quantity=10
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Without an expiry_date set, item should not be "expired"
 | 
			
		||||
@@ -249,13 +234,14 @@ class StockTest(StockTestBase):
 | 
			
		||||
        # And there should be *no* items being build
 | 
			
		||||
        self.assertEqual(part.quantity_being_built, 0)
 | 
			
		||||
 | 
			
		||||
        build = Build.objects.create(reference='BO-4444', part=part, title='A test build', quantity=1)
 | 
			
		||||
        build = Build.objects.create(
 | 
			
		||||
            reference='BO-4444', part=part, title='A test build', quantity=1
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Add some stock items which are "building"
 | 
			
		||||
        for _ in range(10):
 | 
			
		||||
            StockItem.objects.create(
 | 
			
		||||
                part=part, build=build,
 | 
			
		||||
                quantity=10, is_building=True
 | 
			
		||||
                part=part, build=build, quantity=10, is_building=True
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # The "is_building" quantity should not be counted here
 | 
			
		||||
@@ -330,7 +316,10 @@ class StockTest(StockTestBase):
 | 
			
		||||
 | 
			
		||||
        # There should be 16 widgets "in stock"
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            StockItem.objects.filter(part=25).aggregate(Sum('quantity'))['quantity__sum'], 16
 | 
			
		||||
            StockItem.objects.filter(part=25).aggregate(Sum('quantity'))[
 | 
			
		||||
                'quantity__sum'
 | 
			
		||||
            ],
 | 
			
		||||
            16,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_delete_location(self):
 | 
			
		||||
@@ -339,7 +328,9 @@ class StockTest(StockTestBase):
 | 
			
		||||
        n_stock = StockItem.objects.count()
 | 
			
		||||
 | 
			
		||||
        # What parts are in drawer 3?
 | 
			
		||||
        stock_ids = [part.id for part in StockItem.objects.filter(location=self.drawer3.id)]
 | 
			
		||||
        stock_ids = [
 | 
			
		||||
            part.id for part in StockItem.objects.filter(location=self.drawer3.id)
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        # Delete location - parts should move to parent location
 | 
			
		||||
        self.drawer3.delete()
 | 
			
		||||
@@ -361,7 +352,9 @@ class StockTest(StockTestBase):
 | 
			
		||||
        self.assertEqual(it.location, self.bathroom)
 | 
			
		||||
 | 
			
		||||
        # There now should be 2 lots of screws in the bathroom
 | 
			
		||||
        self.assertEqual(StockItem.objects.filter(part=1, location=self.bathroom).count(), 2)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            StockItem.objects.filter(part=1, location=self.bathroom).count(), 2
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Check that a tracking item was added
 | 
			
		||||
        track = StockItemTracking.objects.filter(item=it).latest('id')
 | 
			
		||||
@@ -467,9 +460,11 @@ class StockTest(StockTestBase):
 | 
			
		||||
        it = StockItem.objects.get(pk=2)
 | 
			
		||||
        n = it.quantity
 | 
			
		||||
        an = n - 10
 | 
			
		||||
        customer = Company.objects.create(name="MyTestCompany")
 | 
			
		||||
        order = SalesOrder.objects.create(description="Test order")
 | 
			
		||||
        ait = it.allocateToCustomer(customer, quantity=an, order=order, user=None, notes='Allocated some stock')
 | 
			
		||||
        customer = Company.objects.create(name='MyTestCompany')
 | 
			
		||||
        order = SalesOrder.objects.create(description='Test order')
 | 
			
		||||
        ait = it.allocateToCustomer(
 | 
			
		||||
            customer, quantity=an, order=order, user=None, notes='Allocated some stock'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Check if new stockitem is created
 | 
			
		||||
        self.assertTrue(ait)
 | 
			
		||||
@@ -485,7 +480,9 @@ class StockTest(StockTestBase):
 | 
			
		||||
        # Check that a tracking item was added
 | 
			
		||||
        track = StockItemTracking.objects.filter(item=ait).latest('id')
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(track.tracking_type, StockHistoryCode.SHIPPED_AGAINST_SALES_ORDER)
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            track.tracking_type, StockHistoryCode.SHIPPED_AGAINST_SALES_ORDER
 | 
			
		||||
        )
 | 
			
		||||
        self.assertIn('Allocated some stock', track.notes)
 | 
			
		||||
 | 
			
		||||
    def test_return_from_customer(self):
 | 
			
		||||
@@ -493,21 +490,29 @@ class StockTest(StockTestBase):
 | 
			
		||||
        it = StockItem.objects.get(pk=2)
 | 
			
		||||
 | 
			
		||||
        # First establish total stock for this part
 | 
			
		||||
        allstock_before = StockItem.objects.filter(part=it.part).aggregate(Sum("quantity"))["quantity__sum"]
 | 
			
		||||
        allstock_before = StockItem.objects.filter(part=it.part).aggregate(
 | 
			
		||||
            Sum('quantity')
 | 
			
		||||
        )['quantity__sum']
 | 
			
		||||
 | 
			
		||||
        n = it.quantity
 | 
			
		||||
        an = n - 10
 | 
			
		||||
        customer = Company.objects.create(name="MyTestCompany")
 | 
			
		||||
        order = SalesOrder.objects.create(description="Test order")
 | 
			
		||||
        customer = Company.objects.create(name='MyTestCompany')
 | 
			
		||||
        order = SalesOrder.objects.create(description='Test order')
 | 
			
		||||
 | 
			
		||||
        ait = it.allocateToCustomer(customer, quantity=an, order=order, user=None, notes='Allocated some stock')
 | 
			
		||||
        ait.return_from_customer(it.location, None, notes="Stock removed from customer")
 | 
			
		||||
        ait = it.allocateToCustomer(
 | 
			
		||||
            customer, quantity=an, order=order, user=None, notes='Allocated some stock'
 | 
			
		||||
        )
 | 
			
		||||
        ait.return_from_customer(it.location, None, notes='Stock removed from customer')
 | 
			
		||||
 | 
			
		||||
        # When returned stock is returned to its original (parent) location, check that the parent has correct quantity
 | 
			
		||||
        self.assertEqual(it.quantity, n)
 | 
			
		||||
 | 
			
		||||
        ait = it.allocateToCustomer(customer, quantity=an, order=order, user=None, notes='Allocated some stock')
 | 
			
		||||
        ait.return_from_customer(self.drawer3, None, notes="Stock removed from customer")
 | 
			
		||||
        ait = it.allocateToCustomer(
 | 
			
		||||
            customer, quantity=an, order=order, user=None, notes='Allocated some stock'
 | 
			
		||||
        )
 | 
			
		||||
        ait.return_from_customer(
 | 
			
		||||
            self.drawer3, None, notes='Stock removed from customer'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Check correct assignment of the new location
 | 
			
		||||
        self.assertEqual(ait.location, self.drawer3)
 | 
			
		||||
@@ -527,7 +532,9 @@ class StockTest(StockTestBase):
 | 
			
		||||
        self.assertIn('Stock removed from customer', track.notes)
 | 
			
		||||
 | 
			
		||||
        # Establish total stock for the part after remove from customer to check that we still have the correct quantity in stock
 | 
			
		||||
        allstock_after = StockItem.objects.filter(part=it.part).aggregate(Sum("quantity"))["quantity__sum"]
 | 
			
		||||
        allstock_after = StockItem.objects.filter(part=it.part).aggregate(
 | 
			
		||||
            Sum('quantity')
 | 
			
		||||
        )['quantity__sum']
 | 
			
		||||
        self.assertEqual(allstock_before, allstock_after)
 | 
			
		||||
 | 
			
		||||
    def test_take_stock(self):
 | 
			
		||||
@@ -578,10 +585,7 @@ class StockTest(StockTestBase):
 | 
			
		||||
        # Ensure we do not have unique serials enabled
 | 
			
		||||
        InvenTreeSetting.set_setting('SERIAL_NUMBER_GLOBALLY_UNIQUE', False, None)
 | 
			
		||||
 | 
			
		||||
        item = StockItem.objects.create(
 | 
			
		||||
            part=p,
 | 
			
		||||
            quantity=1,
 | 
			
		||||
        )
 | 
			
		||||
        item = StockItem.objects.create(part=p, quantity=1)
 | 
			
		||||
 | 
			
		||||
        self.assertFalse(item.serialized)
 | 
			
		||||
 | 
			
		||||
@@ -609,10 +613,7 @@ class StockTest(StockTestBase):
 | 
			
		||||
            trackable=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        item = StockItem.objects.create(
 | 
			
		||||
            part=p,
 | 
			
		||||
            quantity=1,
 | 
			
		||||
        )
 | 
			
		||||
        item = StockItem.objects.create(part=p, quantity=1)
 | 
			
		||||
 | 
			
		||||
        for sn in [12345, '12345', ' 12345 ']:
 | 
			
		||||
            item.serial = sn
 | 
			
		||||
@@ -620,7 +621,7 @@ class StockTest(StockTestBase):
 | 
			
		||||
 | 
			
		||||
            self.assertEqual(item.serial_int, 12345)
 | 
			
		||||
 | 
			
		||||
        item.serial = "-123"
 | 
			
		||||
        item.serial = '-123'
 | 
			
		||||
        item.save()
 | 
			
		||||
 | 
			
		||||
        # Negative number should map to positive value
 | 
			
		||||
@@ -631,7 +632,7 @@ class StockTest(StockTestBase):
 | 
			
		||||
        item.save()
 | 
			
		||||
 | 
			
		||||
        # The 'integer' portion has been clipped to a maximum value
 | 
			
		||||
        self.assertEqual(item.serial_int, 0x7fffffff)
 | 
			
		||||
        self.assertEqual(item.serial_int, 0x7FFFFFFF)
 | 
			
		||||
 | 
			
		||||
        # Non-numeric values should encode to zero
 | 
			
		||||
        for sn in ['apple', 'banana', 'carrot']:
 | 
			
		||||
@@ -644,30 +645,18 @@ class StockTest(StockTestBase):
 | 
			
		||||
        item.serial = 100
 | 
			
		||||
        item.save()
 | 
			
		||||
 | 
			
		||||
        item_next = StockItem.objects.create(
 | 
			
		||||
            part=p,
 | 
			
		||||
            serial=150,
 | 
			
		||||
            quantity=1
 | 
			
		||||
        )
 | 
			
		||||
        item_next = StockItem.objects.create(part=p, serial=150, quantity=1)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(item.get_next_serialized_item(), item_next)
 | 
			
		||||
 | 
			
		||||
        item_prev = StockItem.objects.create(
 | 
			
		||||
            part=p,
 | 
			
		||||
            serial=' 57',
 | 
			
		||||
            quantity=1,
 | 
			
		||||
        )
 | 
			
		||||
        item_prev = StockItem.objects.create(part=p, serial=' 57', quantity=1)
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(item.get_next_serialized_item(reverse=True), item_prev)
 | 
			
		||||
 | 
			
		||||
        # Create a number of serialized stock items around the current item
 | 
			
		||||
        for i in range(75, 125):
 | 
			
		||||
            try:
 | 
			
		||||
                StockItem.objects.create(
 | 
			
		||||
                    part=p,
 | 
			
		||||
                    serial=i,
 | 
			
		||||
                    quantity=1,
 | 
			
		||||
                )
 | 
			
		||||
                StockItem.objects.create(part=p, serial=i, quantity=1)
 | 
			
		||||
            except Exception:
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
@@ -696,14 +685,14 @@ class StockTest(StockTestBase):
 | 
			
		||||
 | 
			
		||||
        # Try an invalid quantity
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            item.serializeStock("k", [], self.user)
 | 
			
		||||
            item.serializeStock('k', [], self.user)
 | 
			
		||||
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            item.serializeStock(-1, [], self.user)
 | 
			
		||||
 | 
			
		||||
        # Not enough serial numbers for all stock items.
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            item.serializeStock(3, "hello", self.user)
 | 
			
		||||
            item.serializeStock(3, 'hello', self.user)
 | 
			
		||||
 | 
			
		||||
    def test_serialize_stock_valid(self):
 | 
			
		||||
        """Perform valid stock serializations."""
 | 
			
		||||
@@ -755,55 +744,25 @@ class StockTest(StockTestBase):
 | 
			
		||||
        """
 | 
			
		||||
        # First, we will create a stock location structure
 | 
			
		||||
 | 
			
		||||
        A = StockLocation.objects.create(
 | 
			
		||||
            name='A',
 | 
			
		||||
            description='Top level location'
 | 
			
		||||
        )
 | 
			
		||||
        A = StockLocation.objects.create(name='A', description='Top level location')
 | 
			
		||||
 | 
			
		||||
        B1 = StockLocation.objects.create(
 | 
			
		||||
            name='B1',
 | 
			
		||||
            parent=A
 | 
			
		||||
        )
 | 
			
		||||
        B1 = StockLocation.objects.create(name='B1', parent=A)
 | 
			
		||||
 | 
			
		||||
        B2 = StockLocation.objects.create(
 | 
			
		||||
            name='B2',
 | 
			
		||||
            parent=A
 | 
			
		||||
        )
 | 
			
		||||
        B2 = StockLocation.objects.create(name='B2', parent=A)
 | 
			
		||||
 | 
			
		||||
        B3 = StockLocation.objects.create(
 | 
			
		||||
            name='B3',
 | 
			
		||||
            parent=A
 | 
			
		||||
        )
 | 
			
		||||
        B3 = StockLocation.objects.create(name='B3', parent=A)
 | 
			
		||||
 | 
			
		||||
        C11 = StockLocation.objects.create(
 | 
			
		||||
            name='C11',
 | 
			
		||||
            parent=B1,
 | 
			
		||||
        )
 | 
			
		||||
        C11 = StockLocation.objects.create(name='C11', parent=B1)
 | 
			
		||||
 | 
			
		||||
        C12 = StockLocation.objects.create(
 | 
			
		||||
            name='C12',
 | 
			
		||||
            parent=B1,
 | 
			
		||||
        )
 | 
			
		||||
        C12 = StockLocation.objects.create(name='C12', parent=B1)
 | 
			
		||||
 | 
			
		||||
        C21 = StockLocation.objects.create(
 | 
			
		||||
            name='C21',
 | 
			
		||||
            parent=B2,
 | 
			
		||||
        )
 | 
			
		||||
        C21 = StockLocation.objects.create(name='C21', parent=B2)
 | 
			
		||||
 | 
			
		||||
        C22 = StockLocation.objects.create(
 | 
			
		||||
            name='C22',
 | 
			
		||||
            parent=B2,
 | 
			
		||||
        )
 | 
			
		||||
        C22 = StockLocation.objects.create(name='C22', parent=B2)
 | 
			
		||||
 | 
			
		||||
        C31 = StockLocation.objects.create(
 | 
			
		||||
            name='C31',
 | 
			
		||||
            parent=B3,
 | 
			
		||||
        )
 | 
			
		||||
        C31 = StockLocation.objects.create(name='C31', parent=B3)
 | 
			
		||||
 | 
			
		||||
        C32 = StockLocation.objects.create(
 | 
			
		||||
            name='C32',
 | 
			
		||||
            parent=B3
 | 
			
		||||
        )
 | 
			
		||||
        C32 = StockLocation.objects.create(name='C32', parent=B3)
 | 
			
		||||
 | 
			
		||||
        # Check that the tree_id is correct for each sublocation
 | 
			
		||||
        for loc in [B1, B2, B3, C11, C12, C21, C22, C31, C32]:
 | 
			
		||||
@@ -850,9 +809,7 @@ class StockTest(StockTestBase):
 | 
			
		||||
        # Add some stock items to B3
 | 
			
		||||
        for _ in range(10):
 | 
			
		||||
            StockItem.objects.create(
 | 
			
		||||
                part=Part.objects.get(pk=1),
 | 
			
		||||
                quantity=10,
 | 
			
		||||
                location=B3
 | 
			
		||||
                part=Part.objects.get(pk=1), quantity=10, location=B3
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(StockItem.objects.filter(location=B3).count(), 10)
 | 
			
		||||
@@ -982,7 +939,10 @@ class VariantTest(StockTestBase):
 | 
			
		||||
        chair = Part.objects.get(pk=10000)
 | 
			
		||||
 | 
			
		||||
        # Operations on the top-level object
 | 
			
		||||
        [self.assertFalse(chair.validate_serial_number(i)) for i in [1, 2, 3, 4, 5, 20, 21, 22]]
 | 
			
		||||
        [
 | 
			
		||||
            self.assertFalse(chair.validate_serial_number(i))
 | 
			
		||||
            for i in [1, 2, 3, 4, 5, 20, 21, 22]
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        self.assertFalse(chair.validate_serial_number(20))
 | 
			
		||||
        self.assertFalse(chair.validate_serial_number(21))
 | 
			
		||||
@@ -1006,11 +966,7 @@ class VariantTest(StockTestBase):
 | 
			
		||||
        # Create a new serial number
 | 
			
		||||
        n = variant.get_latest_serial_number()
 | 
			
		||||
 | 
			
		||||
        item = StockItem(
 | 
			
		||||
            part=variant,
 | 
			
		||||
            quantity=1,
 | 
			
		||||
            serial=n
 | 
			
		||||
        )
 | 
			
		||||
        item = StockItem(part=variant, quantity=1, serial=n)
 | 
			
		||||
 | 
			
		||||
        # This should fail
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
@@ -1040,7 +996,7 @@ class TestResultTest(StockTestBase):
 | 
			
		||||
        tests = item.test_results
 | 
			
		||||
        self.assertEqual(tests.count(), 4)
 | 
			
		||||
 | 
			
		||||
        results = item.getTestResults(test="Temperature Test")
 | 
			
		||||
        results = item.getTestResults(test='Temperature Test')
 | 
			
		||||
        self.assertEqual(results.count(), 2)
 | 
			
		||||
 | 
			
		||||
        # Passing tests
 | 
			
		||||
@@ -1074,9 +1030,7 @@ class TestResultTest(StockTestBase):
 | 
			
		||||
        test.save()
 | 
			
		||||
 | 
			
		||||
        StockItemTestResult.objects.create(
 | 
			
		||||
            stock_item=item,
 | 
			
		||||
            test='sew cushion',
 | 
			
		||||
            result=True
 | 
			
		||||
            stock_item=item, test='sew cushion', result=True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Still should be failing at this point,
 | 
			
		||||
@@ -1088,7 +1042,7 @@ class TestResultTest(StockTestBase):
 | 
			
		||||
            stock_item=item,
 | 
			
		||||
            test='apply paint',
 | 
			
		||||
            date=datetime.datetime(2022, 12, 12),
 | 
			
		||||
            result=True
 | 
			
		||||
            result=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertTrue(item.passedAllRequiredTests())
 | 
			
		||||
@@ -1103,32 +1057,25 @@ class TestResultTest(StockTestBase):
 | 
			
		||||
        item.quantity = 50
 | 
			
		||||
 | 
			
		||||
        # Try with an invalid batch code (according to sample validatoin plugin)
 | 
			
		||||
        item.batch = "X234"
 | 
			
		||||
        item.batch = 'X234'
 | 
			
		||||
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            item.save()
 | 
			
		||||
 | 
			
		||||
        item.batch = "B123"
 | 
			
		||||
        item.batch = 'B123'
 | 
			
		||||
        item.save()
 | 
			
		||||
 | 
			
		||||
        # Do some tests!
 | 
			
		||||
        StockItemTestResult.objects.create(
 | 
			
		||||
            stock_item=item,
 | 
			
		||||
            test="Firmware",
 | 
			
		||||
            result=True
 | 
			
		||||
            stock_item=item, test='Firmware', result=True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        StockItemTestResult.objects.create(
 | 
			
		||||
            stock_item=item,
 | 
			
		||||
            test="Paint Color",
 | 
			
		||||
            result=True,
 | 
			
		||||
            value="Red"
 | 
			
		||||
            stock_item=item, test='Paint Color', result=True, value='Red'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        StockItemTestResult.objects.create(
 | 
			
		||||
            stock_item=item,
 | 
			
		||||
            test="Applied Sticker",
 | 
			
		||||
            result=False
 | 
			
		||||
            stock_item=item, test='Applied Sticker', result=False
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(item.test_results.count(), 3)
 | 
			
		||||
@@ -1142,10 +1089,7 @@ class TestResultTest(StockTestBase):
 | 
			
		||||
        self.assertEqual(item.test_results.count(), 3)
 | 
			
		||||
        self.assertEqual(item2.test_results.count(), 3)
 | 
			
		||||
 | 
			
		||||
        StockItemTestResult.objects.create(
 | 
			
		||||
            stock_item=item2,
 | 
			
		||||
            test='A new test'
 | 
			
		||||
        )
 | 
			
		||||
        StockItemTestResult.objects.create(stock_item=item2, test='A new test')
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(item.test_results.count(), 3)
 | 
			
		||||
        self.assertEqual(item2.test_results.count(), 4)
 | 
			
		||||
@@ -1154,10 +1098,7 @@ class TestResultTest(StockTestBase):
 | 
			
		||||
        item2.serializeStock(1, [100], self.user)
 | 
			
		||||
 | 
			
		||||
        # Add a test result to the parent *after* serialization
 | 
			
		||||
        StockItemTestResult.objects.create(
 | 
			
		||||
            stock_item=item2,
 | 
			
		||||
            test='abcde'
 | 
			
		||||
        )
 | 
			
		||||
        StockItemTestResult.objects.create(stock_item=item2, test='abcde')
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(item2.test_results.count(), 5)
 | 
			
		||||
 | 
			
		||||
@@ -1182,10 +1123,7 @@ class TestResultTest(StockTestBase):
 | 
			
		||||
 | 
			
		||||
        # Create a stock item which is installed *inside* the master item
 | 
			
		||||
        sub_item = StockItem.objects.create(
 | 
			
		||||
            part=item.part,
 | 
			
		||||
            quantity=1,
 | 
			
		||||
            belongs_to=item,
 | 
			
		||||
            location=None
 | 
			
		||||
            part=item.part, quantity=1, belongs_to=item, location=None
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Now, create some test results against the sub item
 | 
			
		||||
@@ -1195,7 +1133,7 @@ class TestResultTest(StockTestBase):
 | 
			
		||||
            stock_item=sub_item,
 | 
			
		||||
            test='firmware version',
 | 
			
		||||
            date=datetime.datetime.now().date(),
 | 
			
		||||
            result=True
 | 
			
		||||
            result=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Should return the same number of tests as before
 | 
			
		||||
 
 | 
			
		||||
@@ -5,26 +5,29 @@ from django.urls import include, path, re_path
 | 
			
		||||
from stock import views
 | 
			
		||||
 | 
			
		||||
location_urls = [
 | 
			
		||||
 | 
			
		||||
    path(r'<int:pk>/', include([
 | 
			
		||||
        # Anything else - direct to the location detail view
 | 
			
		||||
        re_path('^.*$', views.StockLocationDetail.as_view(), name='stock-location-detail'),
 | 
			
		||||
    ])),
 | 
			
		||||
 | 
			
		||||
    path(
 | 
			
		||||
        r'<int:pk>/',
 | 
			
		||||
        include([
 | 
			
		||||
            # Anything else - direct to the location detail view
 | 
			
		||||
            re_path(
 | 
			
		||||
                '^.*$',
 | 
			
		||||
                views.StockLocationDetail.as_view(),
 | 
			
		||||
                name='stock-location-detail',
 | 
			
		||||
            )
 | 
			
		||||
        ]),
 | 
			
		||||
    )
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
stock_item_detail_urls = [
 | 
			
		||||
    # Anything else - direct to the item detail view
 | 
			
		||||
    re_path('^.*$', views.StockItemDetail.as_view(), name='stock-item-detail'),
 | 
			
		||||
    re_path('^.*$', views.StockItemDetail.as_view(), name='stock-item-detail')
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
stock_urls = [
 | 
			
		||||
    # Stock location
 | 
			
		||||
    re_path(r'^location/', include(location_urls)),
 | 
			
		||||
 | 
			
		||||
    # Individual stock items
 | 
			
		||||
    re_path(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)),
 | 
			
		||||
 | 
			
		||||
    # Default to the stock index page
 | 
			
		||||
    re_path(r'^.*$', views.StockIndex.as_view(), name='stock-index'),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,9 @@ class StockIndex(InvenTreeRoleMixin, InvenTreePluginViewMixin, ListView):
 | 
			
		||||
        # No 'ownership' checks are necessary for the top-level StockLocation view
 | 
			
		||||
        context['user_owns_location'] = True
 | 
			
		||||
        context['location_owner'] = None
 | 
			
		||||
        context['ownership_enabled'] = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
 | 
			
		||||
        context['ownership_enabled'] = common.models.InvenTreeSetting.get_setting(
 | 
			
		||||
            'STOCK_OWNERSHIP_CONTROL'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
@@ -51,9 +53,13 @@ class StockLocationDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailVi
 | 
			
		||||
        """Extend template context."""
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
        context['ownership_enabled'] = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
 | 
			
		||||
        context['ownership_enabled'] = common.models.InvenTreeSetting.get_setting(
 | 
			
		||||
            'STOCK_OWNERSHIP_CONTROL'
 | 
			
		||||
        )
 | 
			
		||||
        context['location_owner'] = context['location'].get_location_owner()
 | 
			
		||||
        context['user_owns_location'] = context['location'].check_ownership(self.request.user)
 | 
			
		||||
        context['user_owns_location'] = context['location'].check_ownership(
 | 
			
		||||
            self.request.user
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
@@ -74,14 +80,18 @@ class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
 | 
			
		||||
            data['previous'] = self.object.get_next_serialized_item(reverse=True)
 | 
			
		||||
            data['next'] = self.object.get_next_serialized_item()
 | 
			
		||||
 | 
			
		||||
        data['ownership_enabled'] = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
 | 
			
		||||
        data['ownership_enabled'] = common.models.InvenTreeSetting.get_setting(
 | 
			
		||||
            'STOCK_OWNERSHIP_CONTROL'
 | 
			
		||||
        )
 | 
			
		||||
        data['item_owner'] = self.object.get_item_owner()
 | 
			
		||||
        data['user_owns_item'] = self.object.check_ownership(self.request.user)
 | 
			
		||||
 | 
			
		||||
        # Allocation information
 | 
			
		||||
        data['allocated_to_sales_orders'] = self.object.sales_order_allocation_count()
 | 
			
		||||
        data['allocated_to_build_orders'] = self.object.build_allocation_count()
 | 
			
		||||
        data['allocated_to_orders'] = data['allocated_to_sales_orders'] + data['allocated_to_build_orders']
 | 
			
		||||
        data['allocated_to_orders'] = (
 | 
			
		||||
            data['allocated_to_sales_orders'] + data['allocated_to_build_orders']
 | 
			
		||||
        )
 | 
			
		||||
        data['available'] = max(0, self.object.quantity - data['allocated_to_orders'])
 | 
			
		||||
 | 
			
		||||
        return data
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user