2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 03:00:54 +00:00

[CI] Enable python autoformat (#6169)

* Squashed commit of the following:

commit f5cf7b2e78
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:36:57 2024 +0100

    fixed reqs

commit 9d845bee98
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:32:35 2024 +0100

    disable autofix/format

commit aff5f27148
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:28:50 2024 +0100

    adjust checks

commit 47271cf1ef
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:28:22 2024 +0100

    reorder order of operations

commit e1bf178b40
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:01:09 2024 +0100

    adapted ruff settings to better fit code base

commit ad7d88a6f4
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 19:59:45 2024 +0100

    auto fixed docstring

commit a2e54a760e
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 19:46:35 2024 +0100

    fix getattr useage

commit cb80c73bc6
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 19:25:09 2024 +0100

    fix requirements file

commit b7780bbd21
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:42:28 2024 +0100

    fix removed sections

commit 71f1681f55
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:41:21 2024 +0100

    fix djlint syntax

commit a0bcf1bcce
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:35:28 2024 +0100

    remove flake8 from code base

commit 22475b31cc
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:34:56 2024 +0100

    remove flake8 from code base

commit 0413350f14
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:24:39 2024 +0100

    moved ruff section

commit d90c48a0bf
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:24:24 2024 +0100

    move djlint config to pyproject

commit c5ce55d511
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:20:39 2024 +0100

    added isort again

commit 42a41d23af
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:19:02 2024 +0100

    move config section

commit 8569233181
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:17:52 2024 +0100

    fix codespell error

commit 2897c6704d
Author: 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:

commit d3b795824b
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 22:56:17 2024 +0100

    fixed source path

commit 0bac0c19b8
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 22:47:53 2024 +0100

    fixed req

commit 9f61f01d9c
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 22:45:18 2024 +0100

    added missing toml req

commit 91b71ed24a
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:49:50 2024 +0100

    moved isort config

commit 12460b0419
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:43:22 2024 +0100

    remove flake8 section from setup.cfg

commit f5cf7b2e78
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:36:57 2024 +0100

    fixed reqs

commit 9d845bee98
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:32:35 2024 +0100

    disable autofix/format

commit aff5f27148
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:28:50 2024 +0100

    adjust checks

commit 47271cf1ef
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:28:22 2024 +0100

    reorder order of operations

commit e1bf178b40
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 20:01:09 2024 +0100

    adapted ruff settings to better fit code base

commit ad7d88a6f4
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 19:59:45 2024 +0100

    auto fixed docstring

commit a2e54a760e
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 19:46:35 2024 +0100

    fix getattr useage

commit cb80c73bc6
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 19:25:09 2024 +0100

    fix requirements file

commit b7780bbd21
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:42:28 2024 +0100

    fix removed sections

commit 71f1681f55
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:41:21 2024 +0100

    fix djlint syntax

commit a0bcf1bcce
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:35:28 2024 +0100

    remove flake8 from code base

commit 22475b31cc
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:34:56 2024 +0100

    remove flake8 from code base

commit 0413350f14
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:24:39 2024 +0100

    moved ruff section

commit d90c48a0bf
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:24:24 2024 +0100

    move djlint config to pyproject

commit c5ce55d511
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:20:39 2024 +0100

    added isort again

commit 42a41d23af
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:19:02 2024 +0100

    move config section

commit 8569233181
Author: Matthias Mair <code@mjmair.com>
Date:   Sun Jan 7 18:17:52 2024 +0100

    fix codespell error

commit 2897c6704d
Author: 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:
Matthias Mair
2024-01-11 01:28:58 +01:00
committed by GitHub
parent 9715af564f
commit 4b14986591
257 changed files with 13422 additions and 12200 deletions

View File

@ -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

View File

@ -5,4 +5,5 @@ from django.apps import AppConfig
class StockConfig(AppConfig):
"""AppConfig for stock app."""
name = 'stock'

View File

@ -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

View File

@ -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

View File

@ -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,
)

View File

@ -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

View File

@ -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

View File

@ -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'),
]

View File

@ -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