diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 6599d1a209..127ae7399a 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -335,7 +335,7 @@ class SOLineItemList(generics.ListCreateAPIView): return queryset.prefetch_related( 'part', 'part__stock_items', - 'stock_items', + 'allocations', 'order', ) diff --git a/InvenTree/order/migrations/0024_salesorderallocation.py b/InvenTree/order/migrations/0024_salesorderallocation.py new file mode 100644 index 0000000000..ca8ed182d9 --- /dev/null +++ b/InvenTree/order/migrations/0024_salesorderallocation.py @@ -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')), + ], + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 624519dd8b..bfaff7e84e 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -380,6 +380,26 @@ class SalesOrderLineItem(OrderLineItem): 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'] + + +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) diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index 35cf8909be..8ad7422259 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -31,11 +31,11 @@ class OrderTest(TestCase): 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) - 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): """ There should be 3 separate items on order for the M2x4 LPHS part """ diff --git a/InvenTree/stock/__init__.py b/InvenTree/stock/__init__.py index 6970329be1..7b58c08fc9 100644 --- a/InvenTree/stock/__init__.py +++ b/InvenTree/stock/__init__.py @@ -6,4 +6,4 @@ It includes models for: - StockLocation - StockItem - StockItemTracking -""" +""" \ No newline at end of file diff --git a/InvenTree/stock/migrations/0031_auto_20200422_0209.py b/InvenTree/stock/migrations/0031_auto_20200422_0209.py new file mode 100644 index 0000000000..1da143aac5 --- /dev/null +++ b/InvenTree/stock/migrations/0031_auto_20200422_0209.py @@ -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'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 61683a361f..17dea77305 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -29,7 +29,7 @@ from InvenTree.models import InvenTreeTree from InvenTree.fields import InvenTreeURLField from part.models import Part -from order.models import PurchaseOrder, SalesOrderLineItem +from order.models import PurchaseOrder, SalesOrder class StockLocation(InvenTreeTree): @@ -127,7 +127,7 @@ class StockItem(MPTTModel): 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) 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): @@ -263,20 +263,6 @@ class StockItem(MPTTModel): # TODO - Find a test than can be perfomed... 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: raise ValidationError({ 'belongs_to': _('Item cannot belong to itself') @@ -369,8 +355,8 @@ class StockItem(MPTTModel): help_text=_('Purchase order for this stock item') ) - sales_order_line = models.ForeignKey( - SalesOrderLineItem, + sales_order = models.ForeignKey( + SalesOrder, on_delete=models.SET_NULL, related_name='stock_items', null=True, blank=True)