Update definition for StockItemAllocation model

- Limit foreignkey choices
- Error checking
- Check if a StockItem is over-allocated
- Fix API serialization and filtering
This commit is contained in:
Oliver Walters 2020-04-22 13:11:19 +10:00
parent 2cb1b076f6
commit 1373425c29
10 changed files with 181 additions and 23 deletions

View File

@ -393,6 +393,15 @@ class BuildItem(models.Model):
q=self.stock_item.quantity
))]
if self.stock_item.quantity - self.stock_item.allocation_count() < self.quantity:
errors['quantity'] = _('StockItem is over-allocated')
if self.quantity <= 0:
errors['quantity'] = _('Allocation quantity must be greater than zero')
if self.stock_item.serial and not self.quantity == 1:
errors['quantity'] = _('Quantity must be 1 for serialized stock')
except StockItem.DoesNotExist:
pass

View File

@ -10,7 +10,7 @@ from import_export.fields import Field
from .models import PurchaseOrder, PurchaseOrderLineItem
from .models import SalesOrder, SalesOrderLineItem
from .models import SalesOrderAllocation
class PurchaseOrderAdmin(ImportExportModelAdmin):
@ -86,8 +86,19 @@ class SalesOrderLineItemAdmin(ImportExportModelAdmin):
)
class SalesOrderAllocationAdmin(ImportExportModelAdmin):
list_display = (
'line',
'item',
'quantity'
)
admin.site.register(PurchaseOrder, PurchaseOrderAdmin)
admin.site.register(PurchaseOrderLineItem, PurchaseOrderLineItemAdmin)
admin.site.register(SalesOrder, SalesOrderAdmin)
admin.site.register(SalesOrderLineItem, SalesOrderLineItemAdmin)
admin.site.register(SalesOrderAllocation, SalesOrderAllocationAdmin)

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.5 on 2020-04-22 02:22
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('stock', '0031_auto_20200422_0209'),
('order', '0024_salesorderallocation'),
]
operations = [
migrations.AlterUniqueTogether(
name='salesorderallocation',
unique_together={('line', 'item')},
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.0.5 on 2020-04-22 02:24
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('stock', '0031_auto_20200422_0209'),
('order', '0025_auto_20200422_0222'),
]
operations = [
migrations.AlterField(
model_name='salesorderallocation',
name='item',
field=models.OneToOneField(limit_choices_to={'part__salable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='sales_order_allocation', to='stock.StockItem'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.0.5 on 2020-04-22 02:36
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('stock', '0031_auto_20200422_0209'),
('order', '0026_auto_20200422_0224'),
]
operations = [
migrations.AlterField(
model_name='salesorderallocation',
name='item',
field=models.ForeignKey(limit_choices_to={'part__salable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='sales_order_allocations', to='stock.StockItem'),
),
]

View File

@ -398,8 +398,55 @@ class SalesOrderAllocation(models.Model):
"""
class Meta:
unique_together = [
# Cannot allocate any given StockItem to the same line more than once
('line', 'item'),
]
def clean(self):
"""
Validate the SalesOrderAllocation object:
- Cannot allocate stock to a line item without a part reference
- The referenced part must match the part associated with the line item
- Allocated quantity cannot exceed the quantity of the stock item
- Allocation quantity must be "1" if the StockItem is serialized
- Allocation quantity cannot be zero
"""
super().clean()
errors = {}
try:
if not self.line.part == self.item.part:
errors['item'] = _('Cannot allocate stock item to a line with a different part')
except Part.DoesNotExist:
errors['line'] = _('Cannot allocate stock to a line without a part')
if self.quantity > self.item.quantity:
errors['quantity'] = _('Allocation quantity cannot exceed stock quantity')
if self.item.quantity - self.item.allocation_count() < self.quantity:
errors['quantity'] = _('StockItem is over-allocated')
if self.quantity <= 0:
errors['quantity'] = _('Allocation quantity must be greater than zero')
if self.item.serial and not self.quantity == 1:
errors['quantity'] = _('Quantity must be 1 for serialized stock item')
if len(errors) > 0:
raise ValidationError(errors)
line = models.ForeignKey(SalesOrderLineItem, on_delete=models.CASCADE, related_name='allocations')
item = models.OneToOneField('stock.StockItem', on_delete=models.CASCADE, related_name='sales_order_allocation')
item = models.ForeignKey(
'stock.StockItem',
on_delete=models.CASCADE,
related_name='sales_order_allocations',
limit_choices_to={'part__salable': True},
)
quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1)

View File

@ -374,10 +374,12 @@ class StockList(generics.ListCreateAPIView):
allocated = str2bool(allocated)
if allocated:
stock_list = stock_list.exclude(Q(sales_order_line=None))
# 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:
stock_list = stock_list.filter(Q(sales_order_line=None))
# 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)
@ -477,22 +479,10 @@ class StockList(generics.ListCreateAPIView):
if manufacturer is not None:
stock_list = stock_list.filter(supplier_part__manufacturer=manufacturer)
# Filter by sales order
sales_order = self.request.query_params.get('sales_order', None)
if sales_order is not None:
try:
sales_order = SalesOrder.objects.get(pk=sales_order)
lines = [line.pk for line in sales_order.lines.all()]
stock_list = stock_list.filter(sales_order_line__in=lines)
except (SalesOrder.DoesNotExist, ValueError):
raise ValidationError({'sales_order': 'Invalid SalesOrder object specified'})
# Also ensure that we pre-fecth all the related items
stock_list = stock_list.prefetch_related(
'part',
'part__category',
'sales_order_line__order',
'location'
)
@ -517,7 +507,7 @@ class StockList(generics.ListCreateAPIView):
'customer',
'belongs_to',
'build',
'sales_order_line'
'sales_order',
]

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
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
@ -29,7 +31,7 @@ from InvenTree.models import InvenTreeTree
from InvenTree.fields import InvenTreeURLField
from part.models import Part
from order.models import PurchaseOrder, SalesOrder
from order.models import PurchaseOrder, SalesOrder, SalesOrderAllocation
class StockLocation(InvenTreeTree):
@ -391,7 +393,39 @@ class StockItem(MPTTModel):
# 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
return self.sales_order_line is not None
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 can_delete(self):
""" Can this stock item be deleted? It can NOT be deleted under the following circumstances:

View File

@ -67,6 +67,8 @@ class StockItemSerializer(InvenTreeModelSerializer):
'supplier_part',
'supplier_part__supplier',
'supplier_part__manufacturer',
'allocations',
'sales_order_allocations',
'location',
'part',
'tracking_info',

View File

@ -15,12 +15,19 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% block pre_content %}
{% include 'stock/loc_link.html' with location=item.location %}
{% if item.sales_order_line %}
{% 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' item.sales_order_line.order.id %}"><b>{{ item.sales_order_line.order }}</b></a>
<a href="{% url 'so-detail' allocation.line.order.id %}"><b>{{ allcation.line.order }}</b></a>
</div>
{% endif %}
{% 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 }}</b></a>
</div>
{% endfor %}
{% if item.serialized %}
<div class='alert alert-block alert-info'>