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: commitf5cf7b2e78
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:36:57 2024 +0100 fixed reqs commit9d845bee98
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:32:35 2024 +0100 disable autofix/format commitaff5f27148
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:50 2024 +0100 adjust checks commit47271cf1ef
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:22 2024 +0100 reorder order of operations commite1bf178b40
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:01:09 2024 +0100 adapted ruff settings to better fit code base commitad7d88a6f4
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:59:45 2024 +0100 auto fixed docstring commita2e54a760e
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:46:35 2024 +0100 fix getattr useage commitcb80c73bc6
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:25:09 2024 +0100 fix requirements file commitb7780bbd21
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:42:28 2024 +0100 fix removed sections commit71f1681f55
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:41:21 2024 +0100 fix djlint syntax commita0bcf1bcce
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:35:28 2024 +0100 remove flake8 from code base commit22475b31cc
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:34:56 2024 +0100 remove flake8 from code base commit0413350f14
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:39 2024 +0100 moved ruff section commitd90c48a0bf
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:24 2024 +0100 move djlint config to pyproject commitc5ce55d511
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:20:39 2024 +0100 added isort again commit42a41d23af
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:19:02 2024 +0100 move config section commit8569233181
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:17:52 2024 +0100 fix codespell error commit2897c6704d
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: commitd3b795824b
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:56:17 2024 +0100 fixed source path commit0bac0c19b8
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:47:53 2024 +0100 fixed req commit9f61f01d9c
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 22:45:18 2024 +0100 added missing toml req commit91b71ed24a
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:49:50 2024 +0100 moved isort config commit12460b0419
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:43:22 2024 +0100 remove flake8 section from setup.cfg commitf5cf7b2e78
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:36:57 2024 +0100 fixed reqs commit9d845bee98
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:32:35 2024 +0100 disable autofix/format commitaff5f27148
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:50 2024 +0100 adjust checks commit47271cf1ef
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:28:22 2024 +0100 reorder order of operations commite1bf178b40
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 20:01:09 2024 +0100 adapted ruff settings to better fit code base commitad7d88a6f4
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:59:45 2024 +0100 auto fixed docstring commita2e54a760e
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:46:35 2024 +0100 fix getattr useage commitcb80c73bc6
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 19:25:09 2024 +0100 fix requirements file commitb7780bbd21
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:42:28 2024 +0100 fix removed sections commit71f1681f55
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:41:21 2024 +0100 fix djlint syntax commita0bcf1bcce
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:35:28 2024 +0100 remove flake8 from code base commit22475b31cc
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:34:56 2024 +0100 remove flake8 from code base commit0413350f14
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:39 2024 +0100 moved ruff section commitd90c48a0bf
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:24:24 2024 +0100 move djlint config to pyproject commitc5ce55d511
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:20:39 2024 +0100 added isort again commit42a41d23af
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:19:02 2024 +0100 move config section commit8569233181
Author: Matthias Mair <code@mjmair.com> Date: Sun Jan 7 18:17:52 2024 +0100 fix codespell error commit2897c6704d
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:
@ -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