2
0
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:
Oliver Walters
2020-04-27 22:18:45 +10:00
135 changed files with 7725 additions and 2360 deletions

View File

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

View File

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

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

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

View 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)]),
),
]

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

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

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

View 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)]),
),
]

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

View File

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

View File

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

View File

@ -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 %} &times {{ 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 %} &times {{ 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 %}",

View File

@ -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' %}",
});

View File

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

View File

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