mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-03 04:00:57 +00:00
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:
@ -11,7 +11,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
|
||||
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.core.validators import MinValueValidator
|
||||
from django.contrib.auth.models import User
|
||||
@ -30,7 +30,7 @@ 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
|
||||
|
||||
|
||||
@ -133,6 +133,9 @@ class StockItem(MPTTModel):
|
||||
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
|
||||
@ -215,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):
|
||||
@ -228,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:
|
||||
@ -261,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
|
||||
@ -303,48 +318,75 @@ 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',
|
||||
@ -353,6 +395,7 @@ class StockItem(MPTTModel):
|
||||
purchase_order = models.ForeignKey(
|
||||
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')
|
||||
@ -361,12 +404,14 @@ class StockItem(MPTTModel):
|
||||
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
|
||||
)
|
||||
@ -386,7 +431,11 @@ 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)
|
||||
@ -447,7 +496,7 @@ class StockItem(MPTTModel):
|
||||
- Has child StockItems
|
||||
- Has a serial number and is tracked
|
||||
- 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
|
||||
"""
|
||||
|
||||
@ -457,7 +506,7 @@ class StockItem(MPTTModel):
|
||||
if self.part.trackable and self.serial is not None:
|
||||
return False
|
||||
|
||||
if self.customer is not None:
|
||||
if self.sales_order is not None:
|
||||
return False
|
||||
|
||||
if self.build_order is not None:
|
||||
@ -485,7 +534,7 @@ class StockItem(MPTTModel):
|
||||
return False
|
||||
|
||||
# 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
|
||||
|
||||
# Not 'in stock' if it has been allocated to a BuildOrder
|
||||
|
Reference in New Issue
Block a user