Repository: inventree/InvenTree

Issue:
[BUG] Deleting a Customer Breaks Associated Sales Orders
<!---
Everything inside these brackets is hidden - please remove them where you fill out information.
--->


**Describe the bug**
<!---
A clear and concise description of what the bug is.
--->
After deleting a Customer, any existing Sales Orders associated with the Customer receive the name "Invalid Customer", and cannot be opened using the left-side link in the Sales Order table.

**Steps to Reproduce**

Steps to reproduce the behavior:
<!---
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
--->
1. Go to page for specific Customer.
2. Access admin panel using blue icon with tooltip "View in Administration Panel".
3. Scroll to bottom of admin panel page and delete customer.
4. Return to Sales Order list page.
5. Click on Sales Order which now says "Invalid Customer".

**Expected behavior**
<!---
A clear and concise description of what you expected to happen.
--->
Open Sales Order window where line items, shipments and allocations can be viewed.
If this expected behaviour is incorrect, please let me know an alternative for handling access to the broken SO.

**Screenshots**
<!---
If applicable, add screenshots to help explain your problem.
--->
![image](https://user-images.githubusercontent.com/18454392/160542110-3060d0fb-5e67-47b1-8980-a37c62ffe775.png)

I believe the following line in `InvenTree/order/models.py` is the cause of the issue as it references `None` since the customer no longer exists.

![image](https://user-images.githubusercontent.com/18454392/160542692-a4dffc0c-f663-4b60-a530-d9b6dd922d4a.png)


**Deployment Method**
- [x] Docker
- [ ] Bare Metal

**Version Information**
<!---
You can get this by going to the "About InvenTree" section in the upper right corner and clicking on to the "copy version information"
--->
InvenTree-Version: 0.6.2
Django Version: 3.2.12
Commit Hash: 950d185
Commit Date: 2022-03-10
Database: mysql
Debug-Mode: True
Deployed using Docker: True


Discussion:
Welcome to InvenTree! Please check the [contributing docs](https://inventree.readthedocs.io/en/latest/contribute/) on how to help.\nIf you experience setup / install issues please read all [install docs]( https://inventree.readthedocs.io/en/latest/start/intro/).
Hi there @maksimstojkovic !
Is there a specific reason you use the admin panel to delete the company? We generally do not recommend to use the admin panel if not advised to as some business logic is not applied there.
I will try to replicate later this day - I think we should find a way to fix this.
Hi @matmair!

There was no particular reason for using the admin panel. I just tested deleting a customer without accessing the admin panel and the same behaviour was observed.

It appears the issue is in the string shown in the page breadcrumb.
I believe that the behaviour of Sales Orders remaining after customer deletion is correct as allocations should not be destroyed during customer deletion (e.g. deleting customer after an SO has been shipped).
@maksimstojkovic just trying to root out causes while I am away from my dev environment.
I think it should be an easy fix. You are right about orders being persistent. In general i do not recommend deleting anything.
@maksimstojkovic thanks for raising this issue. Deciding on the "correct" behaviour here is tricky. If the customer (`Company`) object is deleted, but you wish to retain orders associated with the customer, then those orders lose the reference to the original customer.

As @matmair suggests, if you want to retain this data, then not deleting the customer is the way to go.

However, clearly you've found a non-viable situation - deleting the existing customer breaks the sales order page.

I think that this problem might also affect "suppliers" and "purchase orders".

So, the options, as I see them, are:

1. Delete orders associated with a company when that company is deleted
2. Retain order information when a company is deleted, and allow these orders to be displayed without error
3. Do not let a company be deleted if there are orders associated with the company

There may be some other options here to, which I'd be open to.

But, clearly we need to make a decision!
@SchrodingersGat I choose to implement 2 as it seems to be the solution that keeps the most options for users whilst providing good data retention. I will use a custom manager and models.SET() for that.

Resolving patch:
diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py
--- a/InvenTree/company/models.py
+++ b/InvenTree/company/models.py
@@ -290,9 +290,6 @@ class Contact(models.Model):
 
     role = models.CharField(max_length=100, blank=True)
 
-    company = models.ForeignKey(Company, related_name='contacts',
-                                on_delete=models.CASCADE)
-
 
 class ManufacturerPart(models.Model):
     """ Represents a unique part as provided by a Manufacturer
diff --git a/InvenTree/order/migrations/0066_alter_purchaseorder_supplier.py b/InvenTree/order/migrations/0066_alter_purchaseorder_supplier.py
new file mode 100644
--- /dev/null
+++ b/InvenTree/order/migrations/0066_alter_purchaseorder_supplier.py
@@ -0,0 +1,20 @@
+# Generated by Django 3.2.13 on 2022-04-30 22:11
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('company', '0042_supplierpricebreak_updated'),
+        ('order', '0065_alter_purchaseorderlineitem_part'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='purchaseorder',
+            name='supplier',
+            field=models.ForeignKey(help_text='Company from which the items are being ordered', limit_choices_to={'is_supplier': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_orders', to='company.company', verbose_name='Supplier'),
+        ),
+    ]
diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py
--- a/InvenTree/order/models.py
+++ b/InvenTree/order/models.py
@@ -228,7 +228,7 @@ def __str__(self):
 
         prefix = getSetting('PURCHASEORDER_REFERENCE_PREFIX')
 
-        return f"{prefix}{self.reference} - {self.supplier.name}"
+        return f"{prefix}{self.reference} - {self.supplier.name if self.supplier else _('deleted')}"
 
     reference = models.CharField(
         unique=True,
@@ -243,7 +243,8 @@ def __str__(self):
                                          help_text=_('Purchase order status'))
 
     supplier = models.ForeignKey(
-        Company, on_delete=models.CASCADE,
+        Company, on_delete=models.SET_NULL,
+        null=True,
         limit_choices_to={
             'is_supplier': True,
         },
@@ -575,7 +576,7 @@ def __str__(self):
 
         prefix = getSetting('SALESORDER_REFERENCE_PREFIX')
 
-        return f"{prefix}{self.reference} - {self.customer.name}"
+        return f"{prefix}{self.reference} - {self.customer.name if self.customer else _('deleted')}"
 
     def get_absolute_url(self):
         return reverse('so-detail', kwargs={'pk': self.id})
@@ -938,7 +939,7 @@ def __str__(self):
         return "{n} x {part} from {supplier} (for {po})".format(
             n=decimal2string(self.quantity),
             part=self.part.SKU if self.part else 'unknown part',
-            supplier=self.order.supplier.name,
+            supplier=self.order.supplier.name if self.order.supplier else _('deleted'),
             po=self.order)
 
     order = models.ForeignKey(


Test patch: