From ce5b47460a79c132235b4d9a722dac24095f4d47 Mon Sep 17 00:00:00 2001
From: Oliver <oliver.henry.walters@gmail.com>
Date: Mon, 25 Oct 2021 22:35:27 +1100
Subject: [PATCH] Added data migration for existing SalesOrder instances

- If a SalesOrder is "PENDING" or there are allocations available, a shipment is created
---
 .../migrations/0053_salesordershipment.py     |  1 -
 .../migrations/0055_auto_20211025_0645.py     | 89 +++++++++++++++++++
 InvenTree/order/models.py                     |  7 --
 InvenTree/order/test_migrations.py            | 47 ++++++++++
 InvenTree/templates/js/translated/order.js    |  1 +
 5 files changed, 137 insertions(+), 8 deletions(-)
 create mode 100644 InvenTree/order/migrations/0055_auto_20211025_0645.py

diff --git a/InvenTree/order/migrations/0053_salesordershipment.py b/InvenTree/order/migrations/0053_salesordershipment.py
index c981e9c4c3..e20a95e3f8 100644
--- a/InvenTree/order/migrations/0053_salesordershipment.py
+++ b/InvenTree/order/migrations/0053_salesordershipment.py
@@ -18,7 +18,6 @@ class Migration(migrations.Migration):
             name='SalesOrderShipment',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('status', models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Shipped'), (40, 'Cancelled'), (50, 'Lost'), (60, 'Returned')], default=10, help_text='Shipment status', verbose_name='Status')),
                 ('shipment_date', models.DateField(blank=True, help_text='Date of shipment', null=True, verbose_name='Shipment Date')),
                 ('reference', models.CharField(blank=True, help_text='Shipment reference', max_length=100, verbose_name='Reference')),
                 ('notes', markdownx.models.MarkdownxField(blank=True, help_text='Shipment notes', verbose_name='Notes')),
diff --git a/InvenTree/order/migrations/0055_auto_20211025_0645.py b/InvenTree/order/migrations/0055_auto_20211025_0645.py
new file mode 100644
index 0000000000..13ea184cfe
--- /dev/null
+++ b/InvenTree/order/migrations/0055_auto_20211025_0645.py
@@ -0,0 +1,89 @@
+# Generated by Django 3.2.5 on 2021-10-25 06:45
+
+from django.db import migrations
+
+
+from InvenTree.status_codes import SalesOrderStatus
+
+
+def add_shipment(apps, schema_editor):
+    """
+    Create a SalesOrderShipment for each existing SalesOrder instance.
+
+    Any "allocations" are marked against that shipment.
+
+    For each existing SalesOrder instance, we create a default SalesOrderShipment,
+    and associate each SalesOrderAllocation with this shipment
+    """
+
+    Allocation = apps.get_model('order', 'salesorderallocation')
+    SalesOrder = apps.get_model('order', 'salesorder')
+    Shipment = apps.get_model('order', 'salesordershipment')
+    
+    n = 0
+
+    for order in SalesOrder.objects.all():
+
+        """
+        We only create an automatic shipment for "PENDING" orders,
+        as SalesOrderAllocations were historically deleted for "SHIPPED" or "CANCELLED" orders
+        """
+
+        allocations = Allocation.objects.filter(
+            line__order=order
+        )
+
+        if allocations.count() == 0 and order.status != SalesOrderStatus.PENDING:
+            continue
+
+        # Create a new Shipment instance against this order
+        shipment = Shipment.objects.create(
+            order=order,
+        )
+
+        shipment.save()
+
+        # Iterate through each allocation associated with this order
+        for allocation in allocations:
+            allocation.shipment = shipment
+            allocation.save()
+
+        n += 1
+
+    if n > 0:
+        print(f"\nCreated SalesOrderShipment for {n} SalesOrder instances")
+
+
+def reverse_add_shipment(apps, schema_editor):
+    """
+    Reverse the migration, delete and SalesOrderShipment instances
+    """
+
+    Allocation = apps.get_model('order', 'salesorderallocation')
+
+    # First, ensure that all SalesOrderAllocation objects point to a null shipment
+    for allocation in Allocation.objects.exclude(shipment=None):
+        allocation.shipment = None
+        allocation.save()
+
+    SOS = apps.get_model('order', 'salesordershipment')
+
+    n = SOS.objects.count()
+
+    print(f"Deleting {n} SalesOrderShipment instances")
+
+    SOS.objects.all().delete()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('order', '0054_salesorderallocation_shipment'),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            add_shipment,
+            reverse_code=reverse_add_shipment,
+        )
+    ]
diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py
index 0566ec9312..dcb63430a3 100644
--- a/InvenTree/order/models.py
+++ b/InvenTree/order/models.py
@@ -929,13 +929,6 @@ class SalesOrderShipment(models.Model):
         help_text=_('Sales Order'),
     )
 
-    status = models.PositiveIntegerField(
-        default=SalesOrderStatus.PENDING,
-        choices=SalesOrderStatus.items(),
-        verbose_name=_('Status'),
-        help_text=_('Shipment status'),
-    )
-
     shipment_date = models.DateField(
         null=True, blank=True,
         verbose_name=_('Shipment Date'),
diff --git a/InvenTree/order/test_migrations.py b/InvenTree/order/test_migrations.py
index b7db1f1b70..6e4d6668d3 100644
--- a/InvenTree/order/test_migrations.py
+++ b/InvenTree/order/test_migrations.py
@@ -5,6 +5,7 @@ Unit tests for the 'order' model data migrations
 from django_test_migrations.contrib.unittest_case import MigratorTestCase
 
 from InvenTree import helpers
+from InvenTree.status_codes import SalesOrderStatus
 
 
 class TestForwardMigrations(MigratorTestCase):
@@ -57,3 +58,49 @@ class TestForwardMigrations(MigratorTestCase):
 
             # The integer reference field must have been correctly updated
         self.assertEqual(order.reference_int, ii)
+
+
+class TestShipmentMigration(MigratorTestCase):
+    """
+    Test data migration for the "SalesOrderShipment" model
+    """
+
+    migrate_from = ('order', '0051_auto_20211014_0623')
+    migrate_to = ('order', '0055_auto_20211025_0645')
+
+    def prepare(self):
+        """
+        Create an initial SalesOrder
+        """
+
+        Company = self.old_state.apps.get_model('company', 'company')
+
+        customer = Company.objects.create(
+            name='My customer',
+            description='A customer we sell stuff too',
+            is_customer=True
+        )
+
+        SalesOrder = self.old_state.apps.get_model('order', 'salesorder')
+
+        for ii in range(5):
+            order = SalesOrder.objects.create(
+                reference=f'SO{ii}',
+                customer=customer,
+                description='A sales order for stuffs',
+                status=SalesOrderStatus.PENDING,
+            )
+
+        order.save()
+
+    def test_shipment_creation(self):
+        """
+        Check that a SalesOrderShipment has been created
+        """
+
+        SalesOrder = self.new_state.apps.get_model('order', 'salesorder')
+        Shipment = self.new_state.apps.get_model('order', 'salesordershipment')
+
+        # Check that the correct number of Shipments have been created
+        self.assertEqual(SalesOrder.objects.count(), 5)
+        self.assertEqual(Shipment.objects.count(), 5)
diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js
index 75f71e3c4c..e55371703c 100644
--- a/InvenTree/templates/js/translated/order.js
+++ b/InvenTree/templates/js/translated/order.js
@@ -1681,6 +1681,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
                         location_detail: true,
                         in_stock: true,
                         part: line_item.part,
+                        include_variants: false,
                         exclude_so_allocation: options.order,
                     },
                     auto_fill: true,