Create "SalesOrderAllocation" object

- Links multiple StockItem objects to a single SalesOrderLineItem
This commit is contained in:
Oliver Walters 2020-04-22 12:12:48 +10:00
parent 3a71a4f63a
commit 2cb1b076f6
7 changed files with 79 additions and 23 deletions

View File

@ -335,7 +335,7 @@ class SOLineItemList(generics.ListCreateAPIView):
return queryset.prefetch_related( return queryset.prefetch_related(
'part', 'part',
'part__stock_items', 'part__stock_items',
'stock_items', 'allocations',
'order', 'order',
) )

View File

@ -0,0 +1,26 @@
# Generated by Django 3.0.5 on 2020-04-22 02:09
import InvenTree.fields
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('stock', '0030_auto_20200422_0015'),
('order', '0023_auto_20200420_2309'),
]
operations = [
migrations.CreateModel(
name='SalesOrderAllocation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, max_digits=15, validators=[django.core.validators.MinValueValidator(0)])),
('item', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='sales_order_allocation', to='stock.StockItem')),
('line', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='order.SalesOrderLineItem')),
],
),
]

View File

@ -380,6 +380,26 @@ class SalesOrderLineItem(OrderLineItem):
This is a summation of the quantity of each attached StockItem This is a summation of the quantity of each attached StockItem
""" """
query = self.stock_items.aggregate(allocated=Coalesce(Sum('quantity'), Decimal(0))) query = self.allocations.aggregate(allocated=Coalesce(Sum('quantity'), Decimal(0)))
return query['allocated'] return query['allocated']
class SalesOrderAllocation(models.Model):
"""
This model is used to 'allocate' stock items to a SalesOrder.
Items that are "allocated" to a SalesOrder are not yet "attached" to the order,
but they will be once the order is fulfilled.
Attributes:
line: SalesOrderLineItem reference
item: StockItem reference
quantity: Quantity to take from the StockItem
"""
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')
quantity = RoundingDecimalField(max_digits=15, decimal_places=5, validators=[MinValueValidator(0)], default=1)

View File

@ -31,11 +31,11 @@ class OrderTest(TestCase):
self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/') self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/')
self.assertEqual(str(order), 'PO 1') self.assertEqual(str(order), 'PO 1 - ACME')
line = PurchaseOrderLineItem.objects.get(pk=1) line = PurchaseOrderLineItem.objects.get(pk=1)
self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO 1)") self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO 1 - ACME)")
def test_on_order(self): def test_on_order(self):
""" There should be 3 separate items on order for the M2x4 LPHS part """ """ There should be 3 separate items on order for the M2x4 LPHS part """

View File

@ -6,4 +6,4 @@ It includes models for:
- StockLocation - StockLocation
- StockItem - StockItem
- StockItemTracking - StockItemTracking
""" """

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

@ -29,7 +29,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, SalesOrderLineItem from order.models import PurchaseOrder, SalesOrder
class StockLocation(InvenTreeTree): class StockLocation(InvenTreeTree):
@ -127,7 +127,7 @@ class StockItem(MPTTModel):
build: Link to a Build (if this stock item was created from a build) 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) purchase_order: Link to a PurchaseOrder (if this stock item was created from a PurchaseOrder)
infinite: If True this StockItem can never be exhausted infinite: If True this StockItem can never be exhausted
sales_order: Link to a SalesOrderLineItem (if this stockitem has been allocated to a sales order) sales_order: Link to a SalesOrder object (if the StockItem has been assigned to a SalesOrder)
""" """
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -263,20 +263,6 @@ class StockItem(MPTTModel):
# TODO - Find a test than can be perfomed... # TODO - Find a test than can be perfomed...
pass pass
try:
# If this StockItem is assigned to a SalesOrderLineItem,
# the "Part" that the line item references is the same as the part that THIS references
if self.sales_order_line is not None:
if self.sales_order_line.part is None:
raise ValidationError({'sales_order_line': _('Stock item cannot be assigned to a LineItem which does not reference a part')})
if not self.sales_order_line.part == self.part:
raise ValidationError({'sales_order_line': _('Stock item does not reference the same part object as the LineItem')})
except SalesOrderLineItem.DoesNotExist:
pass
if self.belongs_to and self.belongs_to.pk == self.pk: if self.belongs_to and self.belongs_to.pk == self.pk:
raise ValidationError({ raise ValidationError({
'belongs_to': _('Item cannot belong to itself') 'belongs_to': _('Item cannot belong to itself')
@ -369,8 +355,8 @@ class StockItem(MPTTModel):
help_text=_('Purchase order for this stock item') help_text=_('Purchase order for this stock item')
) )
sales_order_line = models.ForeignKey( sales_order = models.ForeignKey(
SalesOrderLineItem, SalesOrder,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name='stock_items', related_name='stock_items',
null=True, blank=True) null=True, blank=True)