mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-01 13:06:45 +00:00
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:
parent
2cb1b076f6
commit
1373425c29
@ -393,6 +393,15 @@ class BuildItem(models.Model):
|
|||||||
q=self.stock_item.quantity
|
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:
|
except StockItem.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ from import_export.fields import Field
|
|||||||
|
|
||||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||||
from .models import SalesOrder, SalesOrderLineItem
|
from .models import SalesOrder, SalesOrderLineItem
|
||||||
|
from .models import SalesOrderAllocation
|
||||||
|
|
||||||
class PurchaseOrderAdmin(ImportExportModelAdmin):
|
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(PurchaseOrder, PurchaseOrderAdmin)
|
||||||
admin.site.register(PurchaseOrderLineItem, PurchaseOrderLineItemAdmin)
|
admin.site.register(PurchaseOrderLineItem, PurchaseOrderLineItemAdmin)
|
||||||
|
|
||||||
admin.site.register(SalesOrder, SalesOrderAdmin)
|
admin.site.register(SalesOrder, SalesOrderAdmin)
|
||||||
admin.site.register(SalesOrderLineItem, SalesOrderLineItemAdmin)
|
admin.site.register(SalesOrderLineItem, SalesOrderLineItemAdmin)
|
||||||
|
|
||||||
|
admin.site.register(SalesOrderAllocation, SalesOrderAllocationAdmin)
|
||||||
|
18
InvenTree/order/migrations/0025_auto_20200422_0222.py
Normal file
18
InvenTree/order/migrations/0025_auto_20200422_0222.py
Normal 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')},
|
||||||
|
),
|
||||||
|
]
|
20
InvenTree/order/migrations/0026_auto_20200422_0224.py
Normal file
20
InvenTree/order/migrations/0026_auto_20200422_0224.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
20
InvenTree/order/migrations/0027_auto_20200422_0236.py
Normal file
20
InvenTree/order/migrations/0027_auto_20200422_0236.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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')
|
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)
|
quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1)
|
||||||
|
@ -374,9 +374,11 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
allocated = str2bool(allocated)
|
allocated = str2bool(allocated)
|
||||||
|
|
||||||
if 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:
|
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"
|
# Do we wish to filter by "active parts"
|
||||||
active = self.request.query_params.get('active', None)
|
active = self.request.query_params.get('active', None)
|
||||||
@ -477,22 +479,10 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
if manufacturer is not None:
|
if manufacturer is not None:
|
||||||
stock_list = stock_list.filter(supplier_part__manufacturer=manufacturer)
|
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
|
# Also ensure that we pre-fecth all the related items
|
||||||
stock_list = stock_list.prefetch_related(
|
stock_list = stock_list.prefetch_related(
|
||||||
'part',
|
'part',
|
||||||
'part__category',
|
'part__category',
|
||||||
'sales_order_line__order',
|
|
||||||
'location'
|
'location'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -517,7 +507,7 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
'customer',
|
'customer',
|
||||||
'belongs_to',
|
'belongs_to',
|
||||||
'build',
|
'build',
|
||||||
'sales_order_line'
|
'sales_order',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ 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.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
|
||||||
from django.db.models.signals import pre_delete
|
from django.db.models.signals import pre_delete
|
||||||
@ -29,7 +31,7 @@ from InvenTree.models import InvenTreeTree
|
|||||||
from InvenTree.fields import InvenTreeURLField
|
from InvenTree.fields import InvenTreeURLField
|
||||||
|
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from order.models import PurchaseOrder, SalesOrder
|
from order.models import PurchaseOrder, SalesOrder, SalesOrderAllocation
|
||||||
|
|
||||||
|
|
||||||
class StockLocation(InvenTreeTree):
|
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 - 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
|
# 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):
|
def can_delete(self):
|
||||||
""" Can this stock item be deleted? It can NOT be deleted under the following circumstances:
|
""" Can this stock item be deleted? It can NOT be deleted under the following circumstances:
|
||||||
|
@ -67,6 +67,8 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
|||||||
'supplier_part',
|
'supplier_part',
|
||||||
'supplier_part__supplier',
|
'supplier_part__supplier',
|
||||||
'supplier_part__manufacturer',
|
'supplier_part__manufacturer',
|
||||||
|
'allocations',
|
||||||
|
'sales_order_allocations',
|
||||||
'location',
|
'location',
|
||||||
'part',
|
'part',
|
||||||
'tracking_info',
|
'tracking_info',
|
||||||
|
@ -15,12 +15,19 @@ 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.sales_order_line %}
|
{% for allocation in item.sales_order_allocations.all %}
|
||||||
<div class='alert alert-block alert-info'>
|
<div class='alert alert-block alert-info'>
|
||||||
{% trans "This stock item is allocated to Sales Order" %}
|
{% 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>
|
</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 %}
|
{% if item.serialized %}
|
||||||
<div class='alert alert-block alert-info'>
|
<div class='alert alert-block alert-info'>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user