mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-01 11:10:54 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
@ -12,8 +12,8 @@ from .models import StockLocation, StockItem
|
||||
from .models import StockItemTracking
|
||||
|
||||
from build.models import Build
|
||||
from company.models import Company, SupplierPart
|
||||
from order.models import PurchaseOrder
|
||||
from company.models import SupplierPart
|
||||
from order.models import PurchaseOrder, SalesOrder
|
||||
from part.models import Part
|
||||
|
||||
|
||||
@ -74,10 +74,12 @@ class StockItemResource(ModelResource):
|
||||
|
||||
belongs_to = Field(attribute='belongs_to', widget=widgets.ForeignKeyWidget(StockItem))
|
||||
|
||||
customer = Field(attribute='customer', widget=widgets.ForeignKeyWidget(Company))
|
||||
|
||||
build = Field(attribute='build', widget=widgets.ForeignKeyWidget(Build))
|
||||
|
||||
sales_order = Field(attribute='sales_order', widget=widgets.ForeignKeyWidget(SalesOrder))
|
||||
|
||||
build_order = Field(attribute='build_order', widget=widgets.ForeignKeyWidget(Build))
|
||||
|
||||
purchase_order = Field(attribute='purchase_order', widget=widgets.ForeignKeyWidget(PurchaseOrder))
|
||||
|
||||
# Date management
|
||||
|
@ -314,7 +314,6 @@ class StockList(generics.ListCreateAPIView):
|
||||
- POST: Create a new StockItem
|
||||
|
||||
Additional query parameters are available:
|
||||
- aggregate: If 'true' then stock items are aggregated by Part and Location
|
||||
- location: Filter stock by location
|
||||
- category: Filter by parts belonging to a certain category
|
||||
- supplier: Filter by supplier
|
||||
@ -363,9 +362,31 @@ class StockList(generics.ListCreateAPIView):
|
||||
# Start with all objects
|
||||
stock_list = super().filter_queryset(queryset)
|
||||
|
||||
# Filter out parts which are not actually "in stock"
|
||||
stock_list = stock_list.filter(customer=None, belongs_to=None)
|
||||
in_stock = self.request.query_params.get('in_stock', None)
|
||||
|
||||
if in_stock is not None:
|
||||
in_stock = str2bool(in_stock)
|
||||
|
||||
if in_stock:
|
||||
# Filter out parts which are not actually "in stock"
|
||||
stock_list = stock_list.filter(StockItem.IN_STOCK_FILTER)
|
||||
else:
|
||||
# Only show parts which are not in stock
|
||||
stock_list = stock_list.exclude(StockItem.IN_STOCK_FILTER)
|
||||
|
||||
# Filter by 'allocated' patrs?
|
||||
allocated = self.request.query_params.get('allocated', None)
|
||||
|
||||
if allocated is not None:
|
||||
allocated = str2bool(allocated)
|
||||
|
||||
if allocated:
|
||||
# Filter StockItem with either build allocations or sales order allocations
|
||||
stock_list = stock_list.filter(Q(sales_order_allocations__isnull=False) | Q(allocations__isnull=False))
|
||||
else:
|
||||
# Filter StockItem without build allocations or sales order allocations
|
||||
stock_list = stock_list.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True))
|
||||
|
||||
# Do we wish to filter by "active parts"
|
||||
active = self.request.query_params.get('active', None)
|
||||
|
||||
@ -387,7 +408,7 @@ class StockList(generics.ListCreateAPIView):
|
||||
stock_list = stock_list.filter(part=part_id)
|
||||
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
pass
|
||||
raise ValidationError({"part": "Invalid Part ID specified"})
|
||||
|
||||
# Does the client wish to filter by the 'ancestor'?
|
||||
anc_id = self.request.query_params.get('ancestor', None)
|
||||
@ -400,12 +421,12 @@ class StockList(generics.ListCreateAPIView):
|
||||
stock_list = stock_list.filter(id__in=[item.pk for item in ancestor.children.all()])
|
||||
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
pass
|
||||
raise ValidationError({"ancestor": "Invalid ancestor ID specified"})
|
||||
|
||||
# Does the client wish to filter by stock location?
|
||||
loc_id = self.request.query_params.get('location', None)
|
||||
|
||||
cascade = str2bool(self.request.query_params.get('cascade', False))
|
||||
cascade = str2bool(self.request.query_params.get('cascade', True))
|
||||
|
||||
if loc_id is not None:
|
||||
|
||||
@ -433,7 +454,7 @@ class StockList(generics.ListCreateAPIView):
|
||||
stock_list = stock_list.filter(part__category__in=category.getUniqueChildren())
|
||||
|
||||
except (ValueError, PartCategory.DoesNotExist):
|
||||
pass
|
||||
raise ValidationError({"category": "Invalid category id specified"})
|
||||
|
||||
# Filter by StockItem status
|
||||
status = self.request.query_params.get('status', None)
|
||||
@ -490,9 +511,11 @@ class StockList(generics.ListCreateAPIView):
|
||||
|
||||
filter_fields = [
|
||||
'supplier_part',
|
||||
'customer',
|
||||
'belongs_to',
|
||||
'build',
|
||||
'build_order',
|
||||
'sales_order',
|
||||
'build_order',
|
||||
]
|
||||
|
||||
|
||||
|
20
InvenTree/stock/migrations/0027_stockitem_sales_order.py
Normal file
20
InvenTree/stock/migrations/0027_stockitem_sales_order.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.0.5 on 2020-04-21 05:03
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0023_auto_20200420_2309'),
|
||||
('stock', '0026_stockitem_uid'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='stockitem',
|
||||
name='sales_order',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='order.SalesOrderLineItem'),
|
||||
),
|
||||
]
|
18
InvenTree/stock/migrations/0028_auto_20200421_0724.py
Normal file
18
InvenTree/stock/migrations/0028_auto_20200421_0724.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.0.5 on 2020-04-21 07:24
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stock', '0027_stockitem_sales_order'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='stockitem',
|
||||
old_name='sales_order',
|
||||
new_name='sales_order_line',
|
||||
),
|
||||
]
|
19
InvenTree/stock/migrations/0029_auto_20200421_2359.py
Normal file
19
InvenTree/stock/migrations/0029_auto_20200421_2359.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.0.5 on 2020-04-21 23:59
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stock', '0028_auto_20200421_0724'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='status',
|
||||
field=models.PositiveIntegerField(choices=[(10, 'OK'), (50, 'Attention needed'), (55, 'Damaged'), (60, 'Destroyed'), (70, 'Lost'), (110, 'Shipped'), (85, 'Returned')], default=10, validators=[django.core.validators.MinValueValidator(0)]),
|
||||
),
|
||||
]
|
20
InvenTree/stock/migrations/0030_auto_20200422_0015.py
Normal file
20
InvenTree/stock/migrations/0030_auto_20200422_0015.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.0.5 on 2020-04-22 00:15
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0023_auto_20200420_2309'),
|
||||
('stock', '0029_auto_20200421_2359'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='sales_order_line',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='order.SalesOrderLineItem'),
|
||||
),
|
||||
]
|
24
InvenTree/stock/migrations/0031_auto_20200422_0209.py
Normal file
24
InvenTree/stock/migrations/0031_auto_20200422_0209.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.0.5 on 2020-04-22 02:09
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0024_salesorderallocation'),
|
||||
('stock', '0030_auto_20200422_0015'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='stockitem',
|
||||
name='sales_order_line',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='stockitem',
|
||||
name='sales_order',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='order.SalesOrder'),
|
||||
),
|
||||
]
|
20
InvenTree/stock/migrations/0032_stockitem_build_order.py
Normal file
20
InvenTree/stock/migrations/0032_stockitem_build_order.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.0.5 on 2020-04-25 14:02
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('build', '0015_auto_20200425_1350'),
|
||||
('stock', '0031_auto_20200422_0209'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='stockitem',
|
||||
name='build_order',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='build.Build'),
|
||||
),
|
||||
]
|
19
InvenTree/stock/migrations/0033_auto_20200426_0539.py
Normal file
19
InvenTree/stock/migrations/0033_auto_20200426_0539.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.0.5 on 2020-04-26 05:39
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stock', '0032_stockitem_build_order'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='status',
|
||||
field=models.PositiveIntegerField(choices=[(10, 'OK'), (50, 'Attention needed'), (55, 'Damaged'), (60, 'Destroyed'), (70, 'Lost'), (85, 'Returned'), (110, 'Shipped'), (120, 'Used for Build'), (130, 'Installed in Stock Item')], default=10, validators=[django.core.validators.MinValueValidator(0)]),
|
||||
),
|
||||
]
|
96
InvenTree/stock/migrations/0034_auto_20200426_0602.py
Normal file
96
InvenTree/stock/migrations/0034_auto_20200426_0602.py
Normal file
@ -0,0 +1,96 @@
|
||||
# Generated by Django 3.0.5 on 2020-04-26 06:02
|
||||
|
||||
import InvenTree.fields
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import markdownx.models
|
||||
import mptt.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('order', '0030_auto_20200426_0551'),
|
||||
('build', '0016_auto_20200426_0551'),
|
||||
('part', '0035_auto_20200406_0045'),
|
||||
('company', '0021_remove_supplierpart_manufacturer_name'),
|
||||
('stock', '0033_auto_20200426_0539'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='stockitem',
|
||||
name='customer',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='batch',
|
||||
field=models.CharField(blank=True, help_text='Batch code for this stock item', max_length=100, null=True, verbose_name='Batch Code'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='belongs_to',
|
||||
field=models.ForeignKey(blank=True, help_text='Is this item installed in another item?', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='owned_parts', to='stock.StockItem', verbose_name='Installed In'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='build',
|
||||
field=models.ForeignKey(blank=True, help_text='Build for this stock item', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='build_outputs', to='build.Build', verbose_name='Source Build'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='build_order',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='build.Build', verbose_name='Destination Build Order'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='link',
|
||||
field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', max_length=125, verbose_name='External Link'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='location',
|
||||
field=mptt.fields.TreeForeignKey(blank=True, help_text='Where is this stock item located?', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='stock_items', to='stock.StockLocation', verbose_name='Stock Location'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='notes',
|
||||
field=markdownx.models.MarkdownxField(blank=True, help_text='Stock Item Notes', null=True, verbose_name='Notes'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='parent',
|
||||
field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='stock.StockItem', verbose_name='Parent Stock Item'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='part',
|
||||
field=models.ForeignKey(help_text='Base part', limit_choices_to={'active': True, 'is_template': False, 'virtual': False}, on_delete=django.db.models.deletion.CASCADE, related_name='stock_items', to='part.Part', verbose_name='Base Part'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='purchase_order',
|
||||
field=models.ForeignKey(blank=True, help_text='Purchase order for this stock item', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='order.PurchaseOrder', verbose_name='Source Purchase Order'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='quantity',
|
||||
field=models.DecimalField(decimal_places=5, default=1, max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Stock Quantity'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='sales_order',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_items', to='order.SalesOrder', verbose_name='Destination Sales Order'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='serial',
|
||||
field=models.PositiveIntegerField(blank=True, help_text='Serial number for this item', null=True, verbose_name='Serial Number'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='supplier_part',
|
||||
field=models.ForeignKey(blank=True, help_text='Select a matching supplier part for this stock item', null=True, on_delete=django.db.models.deletion.SET_NULL, to='company.SupplierPart', verbose_name='Supplier Part'),
|
||||
),
|
||||
]
|
@ -11,6 +11,8 @@ from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Sum, Q
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models.signals import pre_delete
|
||||
@ -28,7 +30,8 @@ from InvenTree.status_codes import StockStatus
|
||||
from InvenTree.models import InvenTreeTree
|
||||
from InvenTree.fields import InvenTreeURLField
|
||||
|
||||
from part.models import Part
|
||||
from part import models as PartModels
|
||||
from order.models import PurchaseOrder, SalesOrder
|
||||
|
||||
|
||||
class StockLocation(InvenTreeTree):
|
||||
@ -126,8 +129,13 @@ class StockItem(MPTTModel):
|
||||
build: Link to a Build (if this stock item was created from a build)
|
||||
purchase_order: Link to a PurchaseOrder (if this stock item was created from a PurchaseOrder)
|
||||
infinite: If True this StockItem can never be exhausted
|
||||
sales_order: Link to a SalesOrder object (if the StockItem has been assigned to a SalesOrder)
|
||||
build_order: Link to a BuildOrder object (if the StockItem has been assigned to a BuildOrder)
|
||||
"""
|
||||
|
||||
# A Query filter which will be re-used in multiple places to determine if a StockItem is actually "in stock"
|
||||
IN_STOCK_FILTER = Q(sales_order=None, build_order=None, belongs_to=None)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.pk:
|
||||
add_note = True
|
||||
@ -210,7 +218,7 @@ class StockItem(MPTTModel):
|
||||
raise ValidationError({
|
||||
'serial': _('A stock item with this serial number already exists')
|
||||
})
|
||||
except Part.DoesNotExist:
|
||||
except PartModels.Part.DoesNotExist:
|
||||
pass
|
||||
|
||||
def clean(self):
|
||||
@ -223,6 +231,18 @@ class StockItem(MPTTModel):
|
||||
- Quantity must be 1 if the StockItem has a serial number
|
||||
"""
|
||||
|
||||
if self.status == StockStatus.SHIPPED and self.sales_order is None:
|
||||
raise ValidationError({
|
||||
'sales_order': "SalesOrder must be specified as status is marked as SHIPPED",
|
||||
'status': "Status cannot be marked as SHIPPED if the Customer is not set",
|
||||
})
|
||||
|
||||
if self.status == StockStatus.ASSIGNED_TO_OTHER_ITEM and self.belongs_to is None:
|
||||
raise ValidationError({
|
||||
'belongs_to': "Belongs_to field must be specified as statis is marked as ASSIGNED_TO_OTHER_ITEM",
|
||||
'status': 'Status cannot be marked as ASSIGNED_TO_OTHER_ITEM if the belongs_to field is not set',
|
||||
})
|
||||
|
||||
# The 'supplier_part' field must point to the same part!
|
||||
try:
|
||||
if self.supplier_part is not None:
|
||||
@ -256,7 +276,7 @@ class StockItem(MPTTModel):
|
||||
if self.part.is_template:
|
||||
raise ValidationError({'part': _('Stock item cannot be created for a template Part')})
|
||||
|
||||
except Part.DoesNotExist:
|
||||
except PartModels.Part.DoesNotExist:
|
||||
# This gets thrown if self.supplier_part is null
|
||||
# TODO - Find a test than can be perfomed...
|
||||
pass
|
||||
@ -298,61 +318,104 @@ class StockItem(MPTTModel):
|
||||
|
||||
uid = models.CharField(blank=True, max_length=128, help_text=("Unique identifier field"))
|
||||
|
||||
parent = TreeForeignKey('self',
|
||||
on_delete=models.DO_NOTHING,
|
||||
blank=True, null=True,
|
||||
related_name='children')
|
||||
parent = TreeForeignKey(
|
||||
'self',
|
||||
verbose_name=_('Parent Stock Item'),
|
||||
on_delete=models.DO_NOTHING,
|
||||
blank=True, null=True,
|
||||
related_name='children'
|
||||
)
|
||||
|
||||
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
|
||||
related_name='stock_items', help_text=_('Base part'),
|
||||
limit_choices_to={
|
||||
'is_template': False,
|
||||
'active': True,
|
||||
'virtual': False
|
||||
})
|
||||
part = models.ForeignKey(
|
||||
'part.Part', on_delete=models.CASCADE,
|
||||
verbose_name=_('Base Part'),
|
||||
related_name='stock_items', help_text=_('Base part'),
|
||||
limit_choices_to={
|
||||
'is_template': False,
|
||||
'active': True,
|
||||
'virtual': False
|
||||
})
|
||||
|
||||
supplier_part = models.ForeignKey('company.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL,
|
||||
help_text=_('Select a matching supplier part for this stock item'))
|
||||
supplier_part = models.ForeignKey(
|
||||
'company.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL,
|
||||
verbose_name=_('Supplier Part'),
|
||||
help_text=_('Select a matching supplier part for this stock item')
|
||||
)
|
||||
|
||||
location = TreeForeignKey(StockLocation, on_delete=models.DO_NOTHING,
|
||||
related_name='stock_items', blank=True, null=True,
|
||||
help_text=_('Where is this stock item located?'))
|
||||
location = TreeForeignKey(
|
||||
StockLocation, on_delete=models.DO_NOTHING,
|
||||
verbose_name=_('Stock Location'),
|
||||
related_name='stock_items',
|
||||
blank=True, null=True,
|
||||
help_text=_('Where is this stock item located?')
|
||||
)
|
||||
|
||||
belongs_to = models.ForeignKey('self', on_delete=models.DO_NOTHING,
|
||||
related_name='owned_parts', blank=True, null=True,
|
||||
help_text=_('Is this item installed in another item?'))
|
||||
belongs_to = models.ForeignKey(
|
||||
'self',
|
||||
verbose_name=_('Installed In'),
|
||||
on_delete=models.DO_NOTHING,
|
||||
related_name='owned_parts', blank=True, null=True,
|
||||
help_text=_('Is this item installed in another item?')
|
||||
)
|
||||
|
||||
customer = models.ForeignKey('company.Company', on_delete=models.SET_NULL,
|
||||
related_name='stockitems', blank=True, null=True,
|
||||
help_text=_('Item assigned to customer?'))
|
||||
|
||||
serial = models.PositiveIntegerField(blank=True, null=True,
|
||||
help_text=_('Serial number for this item'))
|
||||
serial = models.PositiveIntegerField(
|
||||
verbose_name=_('Serial Number'),
|
||||
blank=True, null=True,
|
||||
help_text=_('Serial number for this item')
|
||||
)
|
||||
|
||||
link = InvenTreeURLField(max_length=125, blank=True, help_text=_("Link to external URL"))
|
||||
link = InvenTreeURLField(
|
||||
verbose_name=_('External Link'),
|
||||
max_length=125, blank=True,
|
||||
help_text=_("Link to external URL")
|
||||
)
|
||||
|
||||
batch = models.CharField(max_length=100, blank=True, null=True,
|
||||
help_text=_('Batch code for this stock item'))
|
||||
batch = models.CharField(
|
||||
verbose_name=_('Batch Code'),
|
||||
max_length=100, blank=True, null=True,
|
||||
help_text=_('Batch code for this stock item')
|
||||
)
|
||||
|
||||
quantity = models.DecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1)
|
||||
quantity = models.DecimalField(
|
||||
verbose_name=_("Stock Quantity"),
|
||||
max_digits=15, decimal_places=5, validators=[MinValueValidator(0)],
|
||||
default=1
|
||||
)
|
||||
|
||||
updated = models.DateField(auto_now=True, null=True)
|
||||
|
||||
build = models.ForeignKey(
|
||||
'build.Build', on_delete=models.SET_NULL,
|
||||
verbose_name=_('Source Build'),
|
||||
blank=True, null=True,
|
||||
help_text=_('Build for this stock item'),
|
||||
related_name='build_outputs',
|
||||
)
|
||||
|
||||
purchase_order = models.ForeignKey(
|
||||
'order.PurchaseOrder',
|
||||
PurchaseOrder,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_('Source Purchase Order'),
|
||||
related_name='stock_items',
|
||||
blank=True, null=True,
|
||||
help_text=_('Purchase order for this stock item')
|
||||
)
|
||||
|
||||
sales_order = models.ForeignKey(
|
||||
SalesOrder,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_("Destination Sales Order"),
|
||||
related_name='stock_items',
|
||||
null=True, blank=True)
|
||||
|
||||
build_order = models.ForeignKey(
|
||||
'build.Build',
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_("Destination Build Order"),
|
||||
related_name='stock_items',
|
||||
null=True, blank=True
|
||||
)
|
||||
|
||||
# last time the stock was checked / counted
|
||||
stocktake_date = models.DateField(blank=True, null=True)
|
||||
|
||||
@ -368,19 +431,73 @@ class StockItem(MPTTModel):
|
||||
choices=StockStatus.items(),
|
||||
validators=[MinValueValidator(0)])
|
||||
|
||||
notes = MarkdownxField(blank=True, null=True, help_text=_('Stock Item Notes'))
|
||||
notes = MarkdownxField(
|
||||
blank=True, null=True,
|
||||
verbose_name=_("Notes"),
|
||||
help_text=_('Stock Item Notes')
|
||||
)
|
||||
|
||||
# If stock item is incoming, an (optional) ETA field
|
||||
# expected_arrival = models.DateField(null=True, blank=True)
|
||||
|
||||
infinite = models.BooleanField(default=False)
|
||||
|
||||
def is_allocated(self):
|
||||
"""
|
||||
Return True if this StockItem is allocated to a SalesOrder or a Build
|
||||
"""
|
||||
|
||||
# TODO - For now this only checks if the StockItem is allocated to a SalesOrder
|
||||
# TODO - In future, once the "build" is working better, check this too
|
||||
|
||||
if self.allocations.count() > 0:
|
||||
return True
|
||||
|
||||
if self.sales_order_allocations.count() > 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def build_allocation_count(self):
|
||||
"""
|
||||
Return the total quantity allocated to builds
|
||||
"""
|
||||
|
||||
query = self.allocations.aggregate(q=Coalesce(Sum('quantity'), Decimal(0)))
|
||||
|
||||
return query['q']
|
||||
|
||||
def sales_order_allocation_count(self):
|
||||
"""
|
||||
Return the total quantity allocated to SalesOrders
|
||||
"""
|
||||
|
||||
query = self.sales_order_allocations.aggregate(q=Coalesce(Sum('quantity'), Decimal(0)))
|
||||
|
||||
return query['q']
|
||||
|
||||
def allocation_count(self):
|
||||
"""
|
||||
Return the total quantity allocated to builds or orders
|
||||
"""
|
||||
|
||||
return self.build_allocation_count() + self.sales_order_allocation_count()
|
||||
|
||||
def unallocated_quantity(self):
|
||||
"""
|
||||
Return the quantity of this StockItem which is *not* allocated
|
||||
"""
|
||||
|
||||
return max(self.quantity - self.allocation_count(), 0)
|
||||
|
||||
def can_delete(self):
|
||||
""" Can this stock item be deleted? It can NOT be deleted under the following circumstances:
|
||||
|
||||
- Has child StockItems
|
||||
- Has a serial number and is tracked
|
||||
- Is installed inside another StockItem
|
||||
- It has been assigned to a SalesOrder
|
||||
- It has been assigned to a BuildOrder
|
||||
"""
|
||||
|
||||
if self.child_count > 0:
|
||||
@ -389,6 +506,12 @@ class StockItem(MPTTModel):
|
||||
if self.part.trackable and self.serial is not None:
|
||||
return False
|
||||
|
||||
if self.sales_order is not None:
|
||||
return False
|
||||
|
||||
if self.build_order is not None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
@ -406,7 +529,20 @@ class StockItem(MPTTModel):
|
||||
@property
|
||||
def in_stock(self):
|
||||
|
||||
if self.belongs_to or self.customer:
|
||||
# Not 'in stock' if it has been installed inside another StockItem
|
||||
if self.belongs_to is not None:
|
||||
return False
|
||||
|
||||
# Not 'in stock' if it has been sent to a customer
|
||||
if self.sales_order is not None:
|
||||
return False
|
||||
|
||||
# Not 'in stock' if it has been allocated to a BuildOrder
|
||||
if self.build_order is not None:
|
||||
return False
|
||||
|
||||
# Not 'in stock' if the status code makes it unavailable
|
||||
if self.status in StockStatus.UNAVAILABLE_CODES:
|
||||
return False
|
||||
|
||||
return True
|
||||
@ -583,6 +719,9 @@ class StockItem(MPTTModel):
|
||||
# Remove the specified quantity from THIS stock item
|
||||
self.take_stock(quantity, user, 'Split {n} items into new stock item'.format(n=quantity))
|
||||
|
||||
# Return a copy of the "new" stock item
|
||||
return new_stock
|
||||
|
||||
@transaction.atomic
|
||||
def move(self, location, notes, user, **kwargs):
|
||||
""" Move part to a new location.
|
||||
@ -605,6 +744,9 @@ class StockItem(MPTTModel):
|
||||
except InvalidOperation:
|
||||
return False
|
||||
|
||||
if not self.in_stock:
|
||||
raise ValidationError(_("StockItem cannot be moved as it is not in stock"))
|
||||
|
||||
if quantity <= 0:
|
||||
return False
|
||||
|
||||
|
@ -34,6 +34,7 @@ class StockItemSerializerBrief(InvenTreeModelSerializer):
|
||||
|
||||
location_name = serializers.CharField(source='location', read_only=True)
|
||||
part_name = serializers.CharField(source='part.full_name', read_only=True)
|
||||
quantity = serializers.FloatField()
|
||||
|
||||
class Meta:
|
||||
model = StockItem
|
||||
@ -67,6 +68,8 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
||||
'supplier_part',
|
||||
'supplier_part__supplier',
|
||||
'supplier_part__manufacturer',
|
||||
'allocations',
|
||||
'sales_order_allocations',
|
||||
'location',
|
||||
'part',
|
||||
'tracking_info',
|
||||
@ -90,6 +93,9 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True)
|
||||
|
||||
quantity = serializers.FloatField()
|
||||
allocated = serializers.FloatField(source='allocation_count', read_only=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
@ -110,7 +116,10 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
||||
class Meta:
|
||||
model = StockItem
|
||||
fields = [
|
||||
'allocated',
|
||||
'batch',
|
||||
'build_order',
|
||||
'belongs_to',
|
||||
'in_stock',
|
||||
'link',
|
||||
'location',
|
||||
@ -120,6 +129,7 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
||||
'part_detail',
|
||||
'pk',
|
||||
'quantity',
|
||||
'sales_order',
|
||||
'serial',
|
||||
'supplier_part',
|
||||
'supplier_part_detail',
|
||||
|
@ -1,205 +1,248 @@
|
||||
{% extends "stock/stock_app_base.html" %}
|
||||
{% extends "two_column.html" %}
|
||||
{% load static %}
|
||||
{% load inventree_extras %}
|
||||
{% load status_codes %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<h3>{% trans "Stock Item Details" %}</h3>
|
||||
{% if item.serialized %}
|
||||
<p><i>{{ item.part.full_name}} # {{ item.serial }}</i></p>
|
||||
{% else %}
|
||||
<p><i>{% decimal item.quantity %} × {{ item.part.full_name }}</i></p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<div class='btn-group'>
|
||||
{% include "qr_button.html" %}
|
||||
{% if item.in_stock %}
|
||||
{% if not item.serialized %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='stock-add' title='Add to stock'>
|
||||
<span class='glyphicon glyphicon-plus-sign' style='color: #1a1;'/>
|
||||
</button>
|
||||
<button type='button' class='btn btn-default btn-glyph' id='stock-remove' title='Take from stock'>
|
||||
<span class='glyphicon glyphicon-minus-sign' style='color: #a11;'/>
|
||||
</button>
|
||||
<button type='button' class='btn btn-default btn-glyph' id='stock-count' title='Count stock'>
|
||||
<span class='glyphicon glyphicon-ok-circle'/>
|
||||
</button>
|
||||
{% if item.part.trackable %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='stock-serialize' title='Serialize stock'>
|
||||
<span class='glyphicon glyphicon-th-list'/>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='stock-move' title='Transfer stock'>
|
||||
<span class='glyphicon glyphicon-transfer' style='color: #11a;'/>
|
||||
</button>
|
||||
<button type='button' class='btn btn-default btn-glyph' id='stock-duplicate' title='Duplicate stock item'>
|
||||
<span class='glyphicon glyphicon-duplicate'/>
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='stock-edit' title='Edit stock item'>
|
||||
<span class='glyphicon glyphicon-edit'/>
|
||||
</button>
|
||||
{% if item.can_delete %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='stock-delete' title='Edit stock item'>
|
||||
<span class='glyphicon glyphicon-trash'/>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</p>
|
||||
{% if item.serialized %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "This stock item is serialized - it has a unique serial number and the quantity cannot be adjusted." %}
|
||||
</div>
|
||||
{% elif item.child_count > 0 %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "This stock item cannot be deleted as it has child items" %}
|
||||
</div>
|
||||
{% elif item.delete_on_deplete %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "This stock item will be automatically deleted when all stock is depleted." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if item.parent %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "This stock item was split from " %}<a href="{% url 'stock-item-detail' item.parent.id %}">{{ item.parent }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<table class="table table-striped">
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<td><span class='fas fa-shapes'></span></td>
|
||||
<td>Part</td>
|
||||
<td>
|
||||
{% include "hover_image.html" with image=item.part.image hover=True %}
|
||||
<a href="{% url 'part-stock' item.part.id %}">{{ item.part.full_name }}
|
||||
</td>
|
||||
</tr>
|
||||
{% if item.belongs_to %}
|
||||
<tr>
|
||||
<td><span class='fas fa-box'></span></td>
|
||||
<td>{% trans "Belongs To" %}</td>
|
||||
<td><a href="{% url 'stock-item-detail' item.belongs_to.id %}">{{ item.belongs_to }}</a></td>
|
||||
</tr>
|
||||
{% elif item.location %}
|
||||
<tr>
|
||||
<td><span class='fas fa-map-marker-alt'></span></td>
|
||||
<td>{% trans "Location" %}</td>
|
||||
<td><a href="{% url 'stock-location-detail' item.location.id %}">{{ item.location.name }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.uid %}
|
||||
<tr>
|
||||
<td><span class='fas fa-barcode'></span></td>
|
||||
<td>{% trans "Unique Identifier" %}</td>
|
||||
<td>{{ item.uid }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.serialized %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Serial Number" %}</td>
|
||||
<td>{{ item.serial }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Quantity" %}</td>
|
||||
<td>{% decimal item.quantity %} {% if item.part.units %}{{ item.part.units }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.batch %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Batch" %}</td>
|
||||
<td>{{ item.batch }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.build %}
|
||||
<tr>
|
||||
<td><span class='fas fa-tools'></span></td>
|
||||
<td>{% trans "Build" %}</td>
|
||||
<td><a href="{% url 'build-detail' item.build.id %}">{{ item.build }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.purchase_order %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Purchase Order" %}</td>
|
||||
<td><a href="{% url 'po-detail' item.purchase_order.id %}">{{ item.purchase_order }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.customer %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Customer" %}</td>
|
||||
<td>{{ item.customer.name }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.link %}
|
||||
<tr>
|
||||
<td><span class='fas fa-link'></span>
|
||||
<td>{% trans "External Link" %}</td>
|
||||
<td><a href="{{ item.link }}">{{ item.link }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.supplier_part %}
|
||||
<tr>
|
||||
<td><span class='fas fa-industry'></span></td>
|
||||
<td>{% trans "Supplier" %}</td>
|
||||
<td><a href="{% url 'company-detail' item.supplier_part.supplier.id %}">{{ item.supplier_part.supplier.name }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-shapes'></span></td>
|
||||
<td>{% trans "Supplier Part" %}</td>
|
||||
<td><a href="{% url 'supplier-part-detail' item.supplier_part.id %}">{{ item.supplier_part.SKU }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td><span class='fas fa-calendar-alt'></span></td>
|
||||
<td>{% trans "Last Updated" %}</td>
|
||||
<td>{{ item.updated }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-calendar-alt'></span></td>
|
||||
<td>{% trans "Last Stocktake" %}</td>
|
||||
{% if item.stocktake_date %}
|
||||
<td>{{ item.stocktake_date }} <span class='badge'>{{ item.stocktake_user }}</span></td>
|
||||
{% else %}
|
||||
<td>{% trans "No stocktake performed" %}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-info'></span></td>
|
||||
<td>{% trans "Status" %}</td>
|
||||
<td>{% stock_status item.status %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<hr>
|
||||
<div class='container-fluid'>
|
||||
{% block details %}
|
||||
<!-- Stock item details go here -->
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Stock Item" %} - {{ item }}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidenav %}
|
||||
<div id='stock-tree'></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block pre_content %}
|
||||
{% include 'stock/loc_link.html' with location=item.location %}
|
||||
|
||||
{% for allocation in item.sales_order_allocations.all %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "This stock item is allocated to Sales Order" %} <a href="{% url 'so-detail' allocation.line.order.id %}"><b>#{{ allocation.line.order.reference }}</b></a> ({% trans "Quantity" %}: {% decimal allocation.quantity %})
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for allocation in item.allocations.all %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "This stock item is allocated to Build" %} <a href="{% url 'build-detail' allocation.build.id %}"><b>#{{ allocation.build.id }}</b></a> ({% trans "Quantity" %}: {% decimal allocation.quantity %})
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if item.serialized %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "This stock item is serialized - it has a unique serial number and the quantity cannot be adjusted." %}
|
||||
</div>
|
||||
{% elif item.child_count > 0 %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "This stock item cannot be deleted as it has child items" %}
|
||||
</div>
|
||||
{% elif item.delete_on_deplete and item.can_delete %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "This stock item will be automatically deleted when all stock is depleted." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block thumbnail %}
|
||||
<img class='part-thumb' {% if item.part.image %}src="{{ item.part.image.url }}"{% else %}src="{% static 'img/blank_image.png' %}"{% endif %}/>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_data %}
|
||||
<h3>
|
||||
{% trans "Stock Item" %}
|
||||
{% stock_status_label item.status large=True %}
|
||||
</h3>
|
||||
<hr>
|
||||
<h4>
|
||||
{% if item.serialized %}
|
||||
{{ item.part.full_name}} # {{ item.serial }}
|
||||
{% else %}
|
||||
{% decimal item.quantity %} × {{ item.part.full_name }}
|
||||
{% endif %}
|
||||
</h4>
|
||||
|
||||
<div class='btn-group action-buttons'>
|
||||
{% include "qr_button.html" %}
|
||||
{% if item.in_stock %}
|
||||
{% if not item.serialized %}
|
||||
<button type='button' class='btn btn-default' id='stock-add' title='Add to stock'>
|
||||
<span class='fas fa-plus-circle icon-green'/>
|
||||
</button>
|
||||
<button type='button' class='btn btn-default' id='stock-remove' title='Take from stock'>
|
||||
<span class='fas fa-minus-circle icon-red''/>
|
||||
</button>
|
||||
<button type='button' class='btn btn-default' id='stock-count' title='Count stock'>
|
||||
<span class='fas fa-clipboard-list'/>
|
||||
</button>
|
||||
{% if item.part.trackable %}
|
||||
<button type='button' class='btn btn-default' id='stock-serialize' title='Serialize stock'>
|
||||
<span class='fas fa-hashtag'/>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<button type='button' class='btn btn-default' id='stock-move' title='Transfer stock'>
|
||||
<span class='fas fa-exchange-alt icon-blue'/>
|
||||
</button>
|
||||
<button type='button' class='btn btn-default' id='stock-duplicate' title='Duplicate stock item'>
|
||||
<span class='fas fa-copy'/>
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type='button' class='btn btn-default' id='stock-edit' title='Edit stock item'>
|
||||
<span class='fas fa-edit'/>
|
||||
</button>
|
||||
{% if item.can_delete %}
|
||||
<button type='button' class='btn btn-default' id='stock-delete' title='Edit stock item'>
|
||||
<span class='fas fa-trash-alt icon-red'/>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block page_details %}
|
||||
<h4>{% trans "Stock Item Details" %}</h4>
|
||||
<table class="table table-striped">
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<td><span class='fas fa-shapes'></span></td>
|
||||
<td>Part</td>
|
||||
<td>
|
||||
{% include "hover_image.html" with image=item.part.image hover=True %}
|
||||
<a href="{% url 'part-stock' item.part.id %}">{{ item.part.full_name }}
|
||||
</td>
|
||||
</tr>
|
||||
{% if item.belongs_to %}
|
||||
<tr>
|
||||
<td><span class='fas fa-box'></span></td>
|
||||
<td>{% trans "Belongs To" %}</td>
|
||||
<td><a href="{% url 'stock-item-detail' item.belongs_to.id %}">{{ item.belongs_to }}</a></td>
|
||||
</tr>
|
||||
{% elif item.sales_order %}
|
||||
<tr>
|
||||
<td><span class='fas fa-user-tie'></span></td>
|
||||
<td>{% trans "Sales Order" %}</td>
|
||||
<td><a href="{% url 'so-detail' item.sales_order.id %}">{{ item.sales_order.reference }}</a> - <a href="{% url 'company-detail' item.sales_order.customer.id %}">{{ item.sales_order.customer.name }}</a></td>
|
||||
</tr>
|
||||
{% elif item.build_order %}
|
||||
<tr>
|
||||
<td><span class='fas fa-tools'></span></td>
|
||||
<td>{% trans "Build Order" %}</td>
|
||||
<td><a href="{% url 'build-detail' item.build_order.id %}">{{ item.build_order }}</a></td>
|
||||
</tr>
|
||||
{% elif item.location %}
|
||||
<tr>
|
||||
<td><span class='fas fa-map-marker-alt'></span></td>
|
||||
<td>{% trans "Location" %}</td>
|
||||
<td><a href="{% url 'stock-location-detail' item.location.id %}">{{ item.location.name }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.uid %}
|
||||
<tr>
|
||||
<td><span class='fas fa-barcode'></span></td>
|
||||
<td>{% trans "Unique Identifier" %}</td>
|
||||
<td>{{ item.uid }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.serialized %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Serial Number" %}</td>
|
||||
<td>{{ item.serial }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Quantity" %}</td>
|
||||
<td>{% decimal item.quantity %} {% if item.part.units %}{{ item.part.units }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.batch %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Batch" %}</td>
|
||||
<td>{{ item.batch }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.build %}
|
||||
<tr>
|
||||
<td><span class='fas fa-tools'></span></td>
|
||||
<td>{% trans "Build" %}</td>
|
||||
<td><a href="{% url 'build-detail' item.build.id %}">{{ item.build }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.purchase_order %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Purchase Order" %}</td>
|
||||
<td><a href="{% url 'po-detail' item.purchase_order.id %}">{{ item.purchase_order }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.parent %}
|
||||
<tr>
|
||||
<td><span class='fas fa-sitemap'></span></td>
|
||||
<td>{% trans "Parent Item" %}</td>
|
||||
<td><a href="{% url 'stock-item-detail' item.parent.id %}">{% trans "Stock Item" %} #{{ item.parent.id }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.link %}
|
||||
<tr>
|
||||
<td><span class='fas fa-link'></span>
|
||||
<td>{% trans "External Link" %}</td>
|
||||
<td><a href="{{ item.link }}">{{ item.link }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.supplier_part %}
|
||||
<tr>
|
||||
<td><span class='fas fa-industry'></span></td>
|
||||
<td>{% trans "Supplier" %}</td>
|
||||
<td><a href="{% url 'company-detail' item.supplier_part.supplier.id %}">{{ item.supplier_part.supplier.name }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-shapes'></span></td>
|
||||
<td>{% trans "Supplier Part" %}</td>
|
||||
<td><a href="{% url 'supplier-part-detail' item.supplier_part.id %}">{{ item.supplier_part.SKU }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td><span class='fas fa-calendar-alt'></span></td>
|
||||
<td>{% trans "Last Updated" %}</td>
|
||||
<td>{{ item.updated }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-calendar-alt'></span></td>
|
||||
<td>{% trans "Last Stocktake" %}</td>
|
||||
{% if item.stocktake_date %}
|
||||
<td>{{ item.stocktake_date }} <span class='badge'>{{ item.stocktake_user }}</span></td>
|
||||
{% else %}
|
||||
<td>{% trans "No stocktake performed" %}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-info'></span></td>
|
||||
<td>{% trans "Status" %}</td>
|
||||
<td>{% stock_status_label item.status %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
|
||||
{{ block.super }}
|
||||
|
||||
loadTree("{% url 'api-stock-tree' %}",
|
||||
"#stock-tree",
|
||||
{
|
||||
name: 'stock',
|
||||
}
|
||||
);
|
||||
|
||||
$("#toggle-stock-tree").click(function() {
|
||||
toggleSideNav("#sidenav");
|
||||
return false;
|
||||
})
|
||||
|
||||
initSideNav();
|
||||
|
||||
$("#stock-serialize").click(function() {
|
||||
launchModalForm(
|
||||
"{% url 'stock-item-serialize' item.id %}",
|
||||
|
@ -13,20 +13,20 @@
|
||||
<p>All stock items</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<div class='btn-group'>
|
||||
<button class='btn btn-default btn-glyph' id='location-create' title='Create new stock location'>
|
||||
<span class='glyphicon glyphicon-plus'/>
|
||||
<div class='btn-group action-buttons'>
|
||||
<button class='btn btn-default' id='location-create' title='Create new stock location'>
|
||||
<span class='fas fa-plus-circle icon-green'/>
|
||||
</button>
|
||||
{% if location %}
|
||||
{% include "qr_button.html" %}
|
||||
<button class='btn btn-default btn-glyph' id='location-count' title='Count stock items'>
|
||||
<span class='glyphicon glyphicon-ok-circle'/>
|
||||
<button class='btn btn-default' id='location-count' title='Count stock items'>
|
||||
<span class='fas fa-clipboard-list'/>
|
||||
</button>
|
||||
<button class='btn btn-default btn-glyph' id='location-edit' title='Edit stock location'>
|
||||
<span class='glyphicon glyphicon-edit'/>
|
||||
<span class='fas fa-edit icon-blue'/>
|
||||
</button>
|
||||
<button class='btn btn-default btn-glyph' id='location-delete' title='Delete stock location'>
|
||||
<span class='glyphicon glyphicon-trash'/>
|
||||
<span class='glyphicon glyphicon-trash icon-red'/>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -233,6 +233,7 @@
|
||||
{% endif %}
|
||||
part_detail: true,
|
||||
location_detail: true,
|
||||
in_stock: true,
|
||||
},
|
||||
url: "{% url 'api-stock-list' %}",
|
||||
});
|
||||
|
@ -3,9 +3,7 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}
|
||||
{% if item %}
|
||||
InvenTree | {% trans "Stock Item" %} - {{ item }}
|
||||
{% elif location %}
|
||||
{% if location %}
|
||||
InvenTree | {% trans "Stock Location" %} - {{ location }}
|
||||
{% else %}
|
||||
InvenTree | Stock
|
||||
|
@ -258,9 +258,7 @@ class StockExport(AjaxView):
|
||||
stock_items = stock_items.filter(supplier_part=supplier_part)
|
||||
|
||||
# Filter out stock items that are not 'in stock'
|
||||
# TODO - This might need some more thought in the future...
|
||||
stock_items = stock_items.filter(customer=None)
|
||||
stock_items = stock_items.filter(belongs_to=None)
|
||||
stock_items = stock_items.filter(StockItem.IN_STOCK_FILTER)
|
||||
|
||||
# Pre-fetch related fields to reduce DB queries
|
||||
stock_items = stock_items.prefetch_related('part', 'supplier_part__supplier', 'location', 'purchase_order', 'build')
|
||||
@ -314,7 +312,7 @@ class StockAdjust(AjaxView, FormMixin):
|
||||
"""
|
||||
|
||||
# Start with all 'in stock' items
|
||||
items = StockItem.objects.filter(customer=None, belongs_to=None)
|
||||
items = StockItem.objects.filter(StockItem.IN_STOCK_FILTER)
|
||||
|
||||
# Client provides a list of individual stock items
|
||||
if 'stock[]' in self.request.GET:
|
||||
@ -731,7 +729,7 @@ class StockItemSerialize(AjaxUpdateView):
|
||||
if k in ['quantity', 'destination', 'serial_numbers']:
|
||||
form.errors[k] = messages[k]
|
||||
else:
|
||||
form.non_field_errors = messages[k]
|
||||
form.non_field_errors = [messages[k]]
|
||||
|
||||
valid = False
|
||||
|
||||
@ -840,9 +838,12 @@ class StockItemCreate(AjaxCreateView):
|
||||
if part_id:
|
||||
try:
|
||||
part = Part.objects.get(pk=part_id)
|
||||
initials['part'] = part
|
||||
initials['location'] = part.get_default_location()
|
||||
initials['supplier_part'] = part.default_supplier
|
||||
# Check that the supplied part is 'valid'
|
||||
if not part.is_template and part.active and not part.virtual:
|
||||
initials['part'] = part
|
||||
initials['location'] = part.get_default_location()
|
||||
initials['supplier_part'] = part.default_supplier
|
||||
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
pass
|
||||
|
||||
|
Reference in New Issue
Block a user