More work

- Consolidated "in_stock" filter to single code location
- Improve 'limit_choices_to' for BuildItem and SalesOrderAllocation
- Various template improvements etc
This commit is contained in:
Oliver Walters 2020-04-26 16:38:29 +10:00
parent 4147163418
commit e768ada83b
20 changed files with 362 additions and 79 deletions

View File

@ -48,7 +48,6 @@ function loadStockTable(table, options) {
options.params['part_detail'] = true; options.params['part_detail'] = true;
options.params['location_detail'] = true; options.params['location_detail'] = true;
options.params['in_stock'] = true;
var params = options.params || {}; var params = options.params || {};

View File

@ -53,6 +53,8 @@ class BuildList(generics.ListCreateAPIView):
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
# Filter by build status? # Filter by build status?
status = self.request.query_params.get('status', None) status = self.request.query_params.get('status', None)

View File

@ -0,0 +1,20 @@
# Generated by Django 3.0.5 on 2020-04-26 05:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('stock', '0033_auto_20200426_0539'),
('build', '0015_auto_20200425_1350'),
]
operations = [
migrations.AlterField(
model_name='builditem',
name='stock_item',
field=models.ForeignKey(help_text='Stock Item to allocate to build', limit_choices_to={'belongs_to': None, 'build_order': None, 'customer': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.0.5 on 2020-04-26 06:12
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('stock', '0034_auto_20200426_0602'),
('build', '0016_auto_20200426_0551'),
]
operations = [
migrations.AlterField(
model_name='builditem',
name='stock_item',
field=models.ForeignKey(help_text='Stock Item to allocate to build', limit_choices_to={'belongs_to': None, 'build_order': None, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem'),
),
]

View File

@ -261,8 +261,6 @@ class Build(MPTTModel):
- Delete pending BuildItem objects - Delete pending BuildItem objects
""" """
print("Complete build...")
# Complete the build allocation for each BuildItem # Complete the build allocation for each BuildItem
for build_item in self.allocated_stock.all().prefetch_related('stock_item'): for build_item in self.allocated_stock.all().prefetch_related('stock_item'):
build_item.complete_allocation(user) build_item.complete_allocation(user)
@ -495,6 +493,11 @@ class BuildItem(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='allocations', related_name='allocations',
help_text=_('Stock Item to allocate to build'), help_text=_('Stock Item to allocate to build'),
limit_choices_to={
'build_order': None,
'sales_order': None,
'belongs_to': None,
}
) )
quantity = models.DecimalField( quantity = models.DecimalField(

View File

@ -82,7 +82,7 @@ src="{% static 'img/blank_image.png' %}"
</tr> </tr>
{% if build.parent %} {% if build.parent %}
<tr> <tr>
<td><span class='fas fa-tools'></span></td> <td><span class='fas fa-sitemap'></span></td>
<td>{% trans "Parent Build" %}</td> <td>{% trans "Parent Build" %}</td>
<td><a href="{% url 'build-detail' build.parent.id %}">{{ build.parent }}</a></td> <td><a href="{% url 'build-detail' build.parent.id %}">{{ build.parent }}</a></td>
</tr> </tr>

View File

@ -0,0 +1,20 @@
# Generated by Django 3.0.5 on 2020-04-26 05:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('stock', '0033_auto_20200426_0539'),
('order', '0029_auto_20200423_1042'),
]
operations = [
migrations.AlterField(
model_name='salesorderallocation',
name='item',
field=models.ForeignKey(help_text='Select stock item to allocate', limit_choices_to={'belongs_to': None, 'build_order': None, 'customer': None, 'part__salable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='sales_order_allocations', to='stock.StockItem'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.0.5 on 2020-04-26 06:12
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('stock', '0034_auto_20200426_0602'),
('order', '0030_auto_20200426_0551'),
]
operations = [
migrations.AlterField(
model_name='salesorderallocation',
name='item',
field=models.ForeignKey(help_text='Select stock item to allocate', limit_choices_to={'belongs_to': None, 'build_order': None, 'part__salable': True, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='sales_order_allocations', to='stock.StockItem'),
),
]

View File

@ -19,7 +19,7 @@ import os
from datetime import datetime from datetime import datetime
from decimal import Decimal from decimal import Decimal
from part.models import Part from part import models as PartModels
from stock import models as stock_models from stock import models as stock_models
from company.models import Company, SupplierPart from company.models import Company, SupplierPart
@ -511,7 +511,7 @@ class SalesOrderAllocation(models.Model):
try: try:
if not self.line.part == self.item.part: if not self.line.part == self.item.part:
errors['item'] = _('Cannot allocate stock item to a line with a different part') errors['item'] = _('Cannot allocate stock item to a line with a different part')
except Part.DoesNotExist: except PartModels.Part.DoesNotExist:
errors['line'] = _('Cannot allocate stock to a line without a part') errors['line'] = _('Cannot allocate stock to a line without a part')
if self.quantity > self.item.quantity: if self.quantity > self.item.quantity:
@ -535,7 +535,12 @@ class SalesOrderAllocation(models.Model):
'stock.StockItem', 'stock.StockItem',
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='sales_order_allocations', related_name='sales_order_allocations',
limit_choices_to={'part__salable': True}, limit_choices_to={
'part__salable': True,
'belongs_to': None,
'sales_order': None,
'build_order': None,
},
help_text=_('Select stock item to allocate') help_text=_('Select stock item to allocate')
) )
@ -565,6 +570,8 @@ class SalesOrderAllocation(models.Model):
- Mark the StockItem as belonging to the Customer (this will remove it from stock) - Mark the StockItem as belonging to the Customer (this will remove it from stock)
""" """
order = self.line.order
item = self.item item = self.item
# If the allocated quantity is less than the amount available, # If the allocated quantity is less than the amount available,
@ -579,7 +586,7 @@ class SalesOrderAllocation(models.Model):
self.save() self.save()
# Assign the StockItem to the SalesOrder customer # Assign the StockItem to the SalesOrder customer
item.customer = self.line.order.customer item.sales_order = order
# Clear the location # Clear the location
item.location = None item.location = None

View File

@ -42,6 +42,7 @@ from InvenTree.helpers import decimal2string, normalize
from InvenTree.status_codes import BuildStatus, StockStatus, PurchaseOrderStatus from InvenTree.status_codes import BuildStatus, StockStatus, PurchaseOrderStatus
from company.models import SupplierPart from company.models import SupplierPart
from stock import models as StockModels
class PartCategory(InvenTreeTree): class PartCategory(InvenTreeTree):
@ -639,11 +640,12 @@ class Part(models.Model):
def stock_entries(self): def stock_entries(self):
""" Return all 'in stock' items. To be in stock: """ Return all 'in stock' items. To be in stock:
- customer is None - build_order is None
- sales_order is None
- belongs_to is None - belongs_to is None
""" """
return self.stock_items.filter(customer=None, belongs_to=None) return self.stock_items.filter(StockModels.StockItem.IN_STOCK_FILTER).exclude(status__in=StockStatus.UNAVAILABLE_CODES)
@property @property
def total_stock(self): def total_stock(self):

View File

@ -6,11 +6,6 @@
{% block content %} {% block content %}
{% if part.active == False %}
<div class='alert alert-danger alert-block'>
{% trans "This part is not active" %}
</div>
{% endif %}
{% if part.is_template %} {% if part.is_template %}
<div class='alert alert-info alert-block'> <div class='alert alert-info alert-block'>
{% trans "This part is a template part." %} {% trans "This part is a template part." %}
@ -28,9 +23,14 @@
<div class="col-sm-6"> <div class="col-sm-6">
{% include "part/part_thumb.html" %} {% include "part/part_thumb.html" %}
<div class="media-body"> <div class="media-body">
<h4> <h3>
{{ part.full_name }} {{ part.full_name }}
</h4> {% if not part.active %}
<div class='label label-large label-large-red'>
{% trans 'Inactive' %}
</div>
{% endif %}
</h3>
<p><i>{{ part.description }}</i></p> <p><i>{{ part.description }}</i></p>
<p> <p>
<div class='btn-row'> <div class='btn-row'>

View File

@ -13,7 +13,7 @@ from .models import StockItemTracking
from build.models import Build from build.models import Build
from company.models import Company, SupplierPart from company.models import Company, SupplierPart
from order.models import PurchaseOrder from order.models import PurchaseOrder, SalesOrder
from part.models import Part from part.models import Part
@ -74,10 +74,12 @@ class StockItemResource(ModelResource):
belongs_to = Field(attribute='belongs_to', widget=widgets.ForeignKeyWidget(StockItem)) 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)) 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)) purchase_order = Field(attribute='purchase_order', widget=widgets.ForeignKeyWidget(PurchaseOrder))
# Date management # Date management

View File

@ -369,10 +369,10 @@ class StockList(generics.ListCreateAPIView):
if in_stock: if in_stock:
# Filter out parts which are not actually "in stock" # Filter out parts which are not actually "in stock"
stock_list = stock_list.filter(customer=None, belongs_to=None, build_order=None) stock_list = stock_list.filter(StockItem.IN_STOCK_FILTER)
else: else:
# Only show parts which are not in stock # Only show parts which are not in stock
stock_list = stock_list.exclude(customer=None, belongs_to=None, build_order=None) stock_list = stock_list.exclude(StockItem.IN_STOCK_FILTER)
# Filter by 'allocated' patrs? # Filter by 'allocated' patrs?
allocated = self.request.query_params.get('allocated', None) allocated = self.request.query_params.get('allocated', None)
@ -511,9 +511,9 @@ class StockList(generics.ListCreateAPIView):
filter_fields = [ filter_fields = [
'supplier_part', 'supplier_part',
'customer',
'belongs_to', 'belongs_to',
'build', 'build',
'build_order',
'sales_order', 'sales_order',
'build_order', 'build_order',
] ]

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,7 +11,7 @@ from django.core.exceptions import ValidationError
from django.urls import reverse from django.urls import reverse
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Sum from django.db.models import Sum, Q
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -30,7 +30,7 @@ from InvenTree.status_codes import StockStatus
from InvenTree.models import InvenTreeTree from InvenTree.models import InvenTreeTree
from InvenTree.fields import InvenTreeURLField from InvenTree.fields import InvenTreeURLField
from part.models import Part from part import models as PartModels
from order.models import PurchaseOrder, SalesOrder from order.models import PurchaseOrder, SalesOrder
@ -133,6 +133,9 @@ class StockItem(MPTTModel):
build_order: Link to a BuildOrder object (if the StockItem has been assigned to a BuildOrder) 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): def save(self, *args, **kwargs):
if not self.pk: if not self.pk:
add_note = True add_note = True
@ -215,7 +218,7 @@ class StockItem(MPTTModel):
raise ValidationError({ raise ValidationError({
'serial': _('A stock item with this serial number already exists') 'serial': _('A stock item with this serial number already exists')
}) })
except Part.DoesNotExist: except PartModels.Part.DoesNotExist:
pass pass
def clean(self): def clean(self):
@ -228,6 +231,18 @@ class StockItem(MPTTModel):
- Quantity must be 1 if the StockItem has a serial number - 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! # The 'supplier_part' field must point to the same part!
try: try:
if self.supplier_part is not None: if self.supplier_part is not None:
@ -261,7 +276,7 @@ class StockItem(MPTTModel):
if self.part.is_template: if self.part.is_template:
raise ValidationError({'part': _('Stock item cannot be created for a template Part')}) 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 # This gets thrown if self.supplier_part is null
# TODO - Find a test than can be perfomed... # TODO - Find a test than can be perfomed...
pass pass
@ -303,12 +318,17 @@ class StockItem(MPTTModel):
uid = models.CharField(blank=True, max_length=128, help_text=("Unique identifier field")) uid = models.CharField(blank=True, max_length=128, help_text=("Unique identifier field"))
parent = TreeForeignKey('self', parent = TreeForeignKey(
'self',
verbose_name=_('Parent Stock Item'),
on_delete=models.DO_NOTHING, on_delete=models.DO_NOTHING,
blank=True, null=True, blank=True, null=True,
related_name='children') related_name='children'
)
part = models.ForeignKey('part.Part', on_delete=models.CASCADE, part = models.ForeignKey(
'part.Part', on_delete=models.CASCADE,
verbose_name=_('Base Part'),
related_name='stock_items', help_text=_('Base part'), related_name='stock_items', help_text=_('Base part'),
limit_choices_to={ limit_choices_to={
'is_template': False, 'is_template': False,
@ -316,35 +336,57 @@ class StockItem(MPTTModel):
'virtual': False 'virtual': False
}) })
supplier_part = models.ForeignKey('company.SupplierPart', blank=True, null=True, on_delete=models.SET_NULL, supplier_part = models.ForeignKey(
help_text=_('Select a matching supplier part for this stock item')) '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, location = TreeForeignKey(
related_name='stock_items', blank=True, null=True, StockLocation, on_delete=models.DO_NOTHING,
help_text=_('Where is this stock item located?')) 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, belongs_to = models.ForeignKey(
'self',
verbose_name=_('Installed In'),
on_delete=models.DO_NOTHING,
related_name='owned_parts', blank=True, null=True, related_name='owned_parts', blank=True, null=True,
help_text=_('Is this item installed in another item?')) help_text=_('Is this item installed in another item?')
)
customer = models.ForeignKey('company.Company', on_delete=models.SET_NULL, serial = models.PositiveIntegerField(
related_name='stockitems', blank=True, null=True, verbose_name=_('Serial Number'),
help_text=_('Item assigned to customer?')) blank=True, null=True,
help_text=_('Serial number for this item')
)
serial = models.PositiveIntegerField(blank=True, null=True, link = InvenTreeURLField(
help_text=_('Serial number for this item')) verbose_name=_('External Link'),
max_length=125, blank=True,
help_text=_("Link to external URL")
)
link = InvenTreeURLField(max_length=125, blank=True, help_text=_("Link to external URL")) batch = models.CharField(
verbose_name=_('Batch Code'),
max_length=100, blank=True, null=True,
help_text=_('Batch code for this stock item')
)
batch = models.CharField(max_length=100, blank=True, null=True, quantity = models.DecimalField(
help_text=_('Batch code for this stock item')) verbose_name=_("Stock Quantity"),
max_digits=15, decimal_places=5, validators=[MinValueValidator(0)],
quantity = models.DecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1) default=1
)
updated = models.DateField(auto_now=True, null=True) updated = models.DateField(auto_now=True, null=True)
build = models.ForeignKey( build = models.ForeignKey(
'build.Build', on_delete=models.SET_NULL, 'build.Build', on_delete=models.SET_NULL,
verbose_name=_('Source Build'),
blank=True, null=True, blank=True, null=True,
help_text=_('Build for this stock item'), help_text=_('Build for this stock item'),
related_name='build_outputs', related_name='build_outputs',
@ -353,6 +395,7 @@ class StockItem(MPTTModel):
purchase_order = models.ForeignKey( purchase_order = models.ForeignKey(
PurchaseOrder, PurchaseOrder,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
verbose_name=_('Source Purchase Order'),
related_name='stock_items', related_name='stock_items',
blank=True, null=True, blank=True, null=True,
help_text=_('Purchase order for this stock item') help_text=_('Purchase order for this stock item')
@ -361,12 +404,14 @@ class StockItem(MPTTModel):
sales_order = models.ForeignKey( sales_order = models.ForeignKey(
SalesOrder, SalesOrder,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
verbose_name=_("Destination Sales Order"),
related_name='stock_items', related_name='stock_items',
null=True, blank=True) null=True, blank=True)
build_order = models.ForeignKey( build_order = models.ForeignKey(
'build.Build', 'build.Build',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
verbose_name=_("Destination Build Order"),
related_name='stock_items', related_name='stock_items',
null=True, blank=True null=True, blank=True
) )
@ -386,7 +431,11 @@ class StockItem(MPTTModel):
choices=StockStatus.items(), choices=StockStatus.items(),
validators=[MinValueValidator(0)]) 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 # If stock item is incoming, an (optional) ETA field
# expected_arrival = models.DateField(null=True, blank=True) # expected_arrival = models.DateField(null=True, blank=True)
@ -447,7 +496,7 @@ class StockItem(MPTTModel):
- Has child StockItems - Has child StockItems
- Has a serial number and is tracked - Has a serial number and is tracked
- Is installed inside another StockItem - Is installed inside another StockItem
- It has been delivered to a customer - It has been assigned to a SalesOrder
- It has been assigned to a BuildOrder - It has been assigned to a BuildOrder
""" """
@ -457,7 +506,7 @@ class StockItem(MPTTModel):
if self.part.trackable and self.serial is not None: if self.part.trackable and self.serial is not None:
return False return False
if self.customer is not None: if self.sales_order is not None:
return False return False
if self.build_order is not None: if self.build_order is not None:
@ -485,7 +534,7 @@ class StockItem(MPTTModel):
return False return False
# Not 'in stock' if it has been sent to a customer # Not 'in stock' if it has been sent to a customer
if self.customer is not None: if self.sales_order is not None:
return False return False
# Not 'in stock' if it has been allocated to a BuildOrder # Not 'in stock' if it has been allocated to a BuildOrder

View File

@ -118,6 +118,8 @@ class StockItemSerializer(InvenTreeModelSerializer):
fields = [ fields = [
'allocated', 'allocated',
'batch', 'batch',
'build_order',
'belongs_to',
'in_stock', 'in_stock',
'link', 'link',
'location', 'location',
@ -127,6 +129,7 @@ class StockItemSerializer(InvenTreeModelSerializer):
'part_detail', 'part_detail',
'pk', 'pk',
'quantity', 'quantity',
'sales_order',
'serial', 'serial',
'supplier_part', 'supplier_part',
'supplier_part_detail', 'supplier_part_detail',

View File

@ -15,11 +15,15 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% block pre_content %} {% block pre_content %}
{% include 'stock/loc_link.html' with location=item.location %} {% include 'stock/loc_link.html' with location=item.location %}
{% if item.customer %} {% if item.sales_order %}
<div class='alert alert-block alert-info'> <div class='alert alert-block alert-info'>
{% trans "This stock item has been sent to" %} <b><a href="{% url 'company-detail-sales-orders' item.customer.id %}">{{ item.customer.name }}</a></b> {% trans "This stock item was assigned to" %} <b><a href="{% url 'so-detail' item.sales_order.id %}"> {% trans "Sales Order" %} {{ item.sales_order.id }}</a></b>
</div> </div>
{% endif %} {% elif item.build_order %}
<div class='alert alert-block alert-info'>
{% trans "This stock item was assigned to" %}<b><a href="{% url 'build-detail' item.build_order.id %}"> {% trans "Build Order" %} #{{ item.build_order.id }}</a></b>
</div>
{% else %}
{% for allocation in item.sales_order_allocations.all %} {% for allocation in item.sales_order_allocations.all %}
<div class='alert alert-block alert-info'> <div class='alert alert-block alert-info'>
@ -32,6 +36,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% 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 %}) {% 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> </div>
{% endfor %} {% endfor %}
{% endif %}
{% if item.serialized %} {% if item.serialized %}
<div class='alert alert-block alert-warning'> <div class='alert alert-block alert-warning'>
@ -46,11 +51,6 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% trans "This stock item will be automatically deleted when all stock is depleted." %} {% trans "This stock item will be automatically deleted when all stock is depleted." %}
</div> </div>
{% endif %} {% 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 %}
{% endblock %} {% endblock %}
@ -59,7 +59,19 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% endblock %} {% endblock %}
{% block page_data %} {% block page_data %}
<h3>{% trans "Stock Item" %}{% if item.status in StockStatus.UNAVAILABLE_CODES %}{% stock_status_label item.status large=True %}{% endif %}</h3> <h3>
{% trans "Stock Item" %}
{% if item.sales_order %}
<div class='label label-large label-large-blue'>
{% trans "Sold" $}
</div>
{% elif item.build_order %}
<div class='label label-large label-large-blue'>
{% trans "Used in Build" %}
</div>
{% elif item.status in StockStatus.UNAVAILABLE_CODES %}{% stock_status_label item.status large=True %}
{% endif %}
</h3>
<hr> <hr>
<h4> <h4>
{% if item.serialized %} {% if item.serialized %}
@ -74,10 +86,10 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% if item.in_stock %} {% if item.in_stock %}
{% if not item.serialized %} {% if not item.serialized %}
<button type='button' class='btn btn-default' id='stock-add' title='Add to stock'> <button type='button' class='btn btn-default' id='stock-add' title='Add to stock'>
<span class='fas fa-plus-circle' style='color: #1a1;'/> <span class='fas fa-plus-circle icon-green'/>
</button> </button>
<button type='button' class='btn btn-default' id='stock-remove' title='Take from stock'> <button type='button' class='btn btn-default' id='stock-remove' title='Take from stock'>
<span class='fas fa-minus-circle' style='color: #a11;'/> <span class='fas fa-minus-circle icon-red''/>
</button> </button>
<button type='button' class='btn btn-default' id='stock-count' title='Count stock'> <button type='button' class='btn btn-default' id='stock-count' title='Count stock'>
<span class='fas fa-clipboard-list'/> <span class='fas fa-clipboard-list'/>
@ -125,11 +137,17 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
<td>{% trans "Belongs To" %}</td> <td>{% trans "Belongs To" %}</td>
<td><a href="{% url 'stock-item-detail' item.belongs_to.id %}">{{ item.belongs_to }}</a></td> <td><a href="{% url 'stock-item-detail' item.belongs_to.id %}">{{ item.belongs_to }}</a></td>
</tr> </tr>
{% elif item.customer %} {% elif item.sales_order %}
<tr> <tr>
<td><span class='fas fa-user-tie'></span></td> <td><span class='fas fa-user-tie'></span></td>
<td>{% trans "Customer" %}</td> <td>{% trans "Sales Order" %}</td>
<td><a href="{% url 'company-detail' item.customer.id %}">{{ item.customer.name }}</a></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> </tr>
{% elif item.location %} {% elif item.location %}
<tr> <tr>
@ -183,7 +201,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
<tr> <tr>
<td><span class='fas fa-sitemap'></span></td> <td><span class='fas fa-sitemap'></span></td>
<td>{% trans "Parent Item" %}</td> <td>{% trans "Parent Item" %}</td>
<td><a href="{% url 'stock-item-detail' item.parent.id %}">{{ item.parent }}</a></td> <td><a href="{% url 'stock-item-detail' item.parent.id %}">{% trans "Stock Item" %} #{{ item.parent.id }}</a></td>
</tr> </tr>
{% endif %} {% endif %}
{% if item.link %} {% if item.link %}

View File

@ -258,9 +258,7 @@ class StockExport(AjaxView):
stock_items = stock_items.filter(supplier_part=supplier_part) stock_items = stock_items.filter(supplier_part=supplier_part)
# Filter out stock items that are not 'in stock' # Filter out stock items that are not 'in stock'
# TODO - This might need some more thought in the future... stock_items = stock_items.filter(StockItem.IN_STOCK_FILTER)
stock_items = stock_items.filter(customer=None)
stock_items = stock_items.filter(belongs_to=None)
# Pre-fetch related fields to reduce DB queries # Pre-fetch related fields to reduce DB queries
stock_items = stock_items.prefetch_related('part', 'supplier_part__supplier', 'location', 'purchase_order', 'build') 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 # 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 # Client provides a list of individual stock items
if 'stock[]' in self.request.GET: if 'stock[]' in self.request.GET:

View File

@ -21,6 +21,11 @@ function getAvailableTableFilters(tableKey) {
title: '{% trans "Include sublocations" %}', title: '{% trans "Include sublocations" %}',
description: '{% trans "Include stock in sublocations" %}', description: '{% trans "Include stock in sublocations" %}',
}, },
in_stock: {
type: 'bool',
title: '{% trans "In stock" %}',
description: '{% trans "Item is in stock" %}',
},
active: { active: {
type: 'bool', type: 'bool',
title: '{% trans "Active parts" %}', title: '{% trans "Active parts" %}',