From aa30e62ad80b120d3b36bf2024cd844a060263a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:29:36 +0200 Subject: [PATCH 01/27] [BUG] Deleting a Customer Breaks Associated Sales Orders Add special protected deleted company Fixes #2788 --- .../migrations/0043_company_is_deleted.py | 18 +++++++++++++ InvenTree/company/models.py | 16 +++++++++++- .../migrations/0064_auto_20220329_2246.py | 25 +++++++++++++++++++ InvenTree/order/models.py | 7 ++++-- 4 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 InvenTree/company/migrations/0043_company_is_deleted.py create mode 100644 InvenTree/order/migrations/0064_auto_20220329_2246.py diff --git a/InvenTree/company/migrations/0043_company_is_deleted.py b/InvenTree/company/migrations/0043_company_is_deleted.py new file mode 100644 index 0000000000..cf42f7cc6a --- /dev/null +++ b/InvenTree/company/migrations/0043_company_is_deleted.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2022-03-29 22:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0042_supplierpricebreak_updated'), + ] + + operations = [ + migrations.AddField( + model_name='company', + name='is_deleted', + field=models.BooleanField(default=False, help_text='Is this company a deleted placeholder?', verbose_name='is deleted'), + ), + ] diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index f72668f9f0..8058b7e358 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -9,7 +9,7 @@ import os from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator -from django.core.exceptions import ValidationError +from django.core.exceptions import ValidationError, PermissionDenied from django.db import models from django.db.models import Sum, Q, UniqueConstraint @@ -147,6 +147,8 @@ class Company(models.Model): is_manufacturer = models.BooleanField(default=False, verbose_name=_('is manufacturer'), help_text=_('Does this company manufacture parts?')) + is_deleted = models.BooleanField(default=False, verbose_name=_('is deleted'), help_text=_('Is this company a deleted placeholder?')) + currency = models.CharField( max_length=3, verbose_name=_('Currency'), @@ -266,6 +268,18 @@ class Company(models.Model): return self.purchase_orders.filter(status__in=PurchaseOrderStatus.FAILED) + def save(self, *args, **kwargs): + """Save the instance, unless it is the magic already deleted object""" + if self.pk and self.is_deleted: + raise PermissionDenied(_('This company is a placeholder and can not be updated')) + return super().save(*args, **kwargs) + + def delete(self, *args, **kwargs): + """Delete the instance, unless it is the magic already deleted object""" + if self.is_deleted: + raise PermissionDenied(_('This company is a placeholder and can not be deleted')) + return super().delete(*args, **kwargs) + class Contact(models.Model): """ A Contact represents a person who works at a particular company. diff --git a/InvenTree/order/migrations/0064_auto_20220329_2246.py b/InvenTree/order/migrations/0064_auto_20220329_2246.py new file mode 100644 index 0000000000..ad0a804d11 --- /dev/null +++ b/InvenTree/order/migrations/0064_auto_20220329_2246.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.12 on 2022-03-29 22:46 + +from django.db import migrations, models +import order.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0043_company_is_deleted'), + ('order', '0063_alter_purchaseorderlineitem_unique_together'), + ] + + 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}, on_delete=models.SET(order.models.get_deleted_company), related_name='purchase_orders', to='company.company', verbose_name='Supplier'), + ), + migrations.AlterField( + model_name='salesorder', + name='customer', + field=models.ForeignKey(help_text='Company to which the items are being sold', limit_choices_to={'is_customer': True}, null=True, on_delete=models.SET(order.models.get_deleted_company), related_name='sales_orders', to='company.company', verbose_name='Customer'), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index f08880a882..9225f3faf8 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -92,6 +92,9 @@ def get_next_so_number(): return reference +def get_deleted_company(): + return Company.objects.get_or_create(name='deleted', email='deleted',is_deleted=True)[0] + class Order(ReferenceIndexingMixin): """ Abstract model for an order. @@ -219,7 +222,7 @@ class PurchaseOrder(Order): help_text=_('Purchase order status')) supplier = models.ForeignKey( - Company, on_delete=models.CASCADE, + Company, on_delete=models.SET(get_deleted_company), limit_choices_to={ 'is_supplier': True, }, @@ -567,7 +570,7 @@ class SalesOrder(Order): customer = models.ForeignKey( Company, - on_delete=models.SET_NULL, + on_delete=models.SET(get_deleted_company), null=True, limit_choices_to={'is_customer': True}, related_name='sales_orders', From 6c6c47c60e29f7ff113427e01f5b7d3e57203280 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:30:02 +0200 Subject: [PATCH 02/27] do not import / export --- InvenTree/company/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index 97327a559a..5a19d2c391 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -21,6 +21,7 @@ class CompanyResource(ModelResource): class Meta: model = Company + exclude = ('is_deleted', ) skip_unchanged = True report_skipped = False clean_model_instances = True From d068b0a064cd522506646975e3b441c33b56fbb4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:30:21 +0200 Subject: [PATCH 03/27] do not show in admin --- InvenTree/company/admin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index 5a19d2c391..af78e76a7f 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -38,6 +38,8 @@ class CompanyAdmin(ImportExportModelAdmin): 'description', ] + exclude = ('is_deleted',) + class SupplierPartResource(ModelResource): """ From 44239cba081fd3afbce3401fed6721898e25c15d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:30:32 +0200 Subject: [PATCH 04/27] hide delete button --- InvenTree/company/admin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index af78e76a7f..6412aff934 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -40,6 +40,11 @@ class CompanyAdmin(ImportExportModelAdmin): exclude = ('is_deleted',) + def has_delete_permission(self, request, obj=None): + if obj and obj.is_deleted: + return False + return True + class SupplierPartResource(ModelResource): """ From 0d2fbe16f5d2ae583478767fb236231059a3a4c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:31:02 +0200 Subject: [PATCH 05/27] hide the action buttons if special object --- InvenTree/company/templates/company/company_base.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index 489493fd06..4caf37272e 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -27,12 +27,12 @@ +{% endif %} {% endblock actions %} {% block thumbnail %} From 924e46a0e357abb5444732abf352e580447676ae Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:32:09 +0200 Subject: [PATCH 07/27] hide purchase buttopn if special button --- InvenTree/company/templates/company/company_base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index 6bf4c929b3..4adf4adb3b 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -18,7 +18,7 @@ {% url 'admin:company_company_change' company.pk as url %} {% include "admin_button.html" with url=url %} {% endif %} -{% if company.is_supplier and roles.purchase_order.add %} +{% if company.is_supplier and roles.purchase_order.add and not company.is_deleted %} From 48441ea48e51fe2a61625ce9146b61f6b00cdc36 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:32:29 +0200 Subject: [PATCH 08/27] add docstring --- InvenTree/order/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 9225f3faf8..a9db03ca54 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -93,6 +93,9 @@ def get_next_so_number(): def get_deleted_company(): + """ + Returns the deleted company object + """ return Company.objects.get_or_create(name='deleted', email='deleted',is_deleted=True)[0] class Order(ReferenceIndexingMixin): From e7f940810ad926b8f1fd82c078f1255d4827d259 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:32:41 +0200 Subject: [PATCH 09/27] PEP style fix --- InvenTree/order/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index a9db03ca54..270e1ca223 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -98,6 +98,7 @@ def get_deleted_company(): """ return Company.objects.get_or_create(name='deleted', email='deleted',is_deleted=True)[0] + class Order(ReferenceIndexingMixin): """ Abstract model for an order. From 1d24f3586d3b95bcdc7d9947acd07d6d019c6ddb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:35:21 +0200 Subject: [PATCH 10/27] PEP fix --- InvenTree/order/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 270e1ca223..ee18fb9a21 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -96,7 +96,7 @@ def get_deleted_company(): """ Returns the deleted company object """ - return Company.objects.get_or_create(name='deleted', email='deleted',is_deleted=True)[0] + return Company.objects.get_or_create(name='deleted', email='deleted', is_deleted=True)[0] class Order(ReferenceIndexingMixin): From c278a5397f1fa65fb37842ec9d66a57a5138bda7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:42:00 +0200 Subject: [PATCH 11/27] add doc --- InvenTree/company/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index 6412aff934..af767f3fdc 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -41,6 +41,7 @@ class CompanyAdmin(ImportExportModelAdmin): exclude = ('is_deleted',) def has_delete_permission(self, request, obj=None): + """Magic objects are not allowd to be deleted""" if obj and obj.is_deleted: return False return True From 57f9ef75e9bb2cf36ccaa9ab291691527ea3acef Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Apr 2022 23:55:08 +0200 Subject: [PATCH 12/27] enable all functions for deleted company --- InvenTree/order/models.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index ee18fb9a21..9dcb360bcd 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -96,7 +96,14 @@ def get_deleted_company(): """ Returns the deleted company object """ - return Company.objects.get_or_create(name='deleted', email='deleted', is_deleted=True)[0] + return Company.objects.get_or_create( + name='deleted', + email='deleted', + is_deleted=True, + is_customer = True, + is_supplier = True, + is_manufacturer = True + )[0] class Order(ReferenceIndexingMixin): From 80fa8f6d1849e175cc9aece62f85bc66af3fc708 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Apr 2022 23:57:49 +0200 Subject: [PATCH 13/27] remove double definition --- InvenTree/company/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 8058b7e358..7bb3a0701d 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -304,9 +304,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 From 66e14b6ad02051f7e5dda868d97695f3dcc56aa1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Apr 2022 00:01:38 +0200 Subject: [PATCH 14/27] move helper function to models.py --- InvenTree/company/models.py | 14 ++++++++++++++ InvenTree/order/models.py | 16 +--------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 7bb3a0701d..8d010f0457 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -60,6 +60,20 @@ def rename_company_image(instance, filename): return os.path.join(base, fn) +def get_deleted_company(): + """ + Returns the deleted company object + """ + return Company.objects.get_or_create( + name='deleted', + email='deleted', + is_deleted=True, + is_customer = True, + is_supplier = True, + is_manufacturer = True + )[0] + + class Company(models.Model): """ A Company object represents an external company. It may be a supplier or a customer or a manufacturer (or a combination) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 9dcb360bcd..b9bec62890 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -24,7 +24,7 @@ from mptt.models import TreeForeignKey from users import models as UserModels from part import models as PartModels from stock import models as stock_models -from company.models import Company, SupplierPart +from company.models import Company, SupplierPart, get_deleted_company from plugin.events import trigger_event import InvenTree.helpers @@ -92,20 +92,6 @@ def get_next_so_number(): return reference -def get_deleted_company(): - """ - Returns the deleted company object - """ - return Company.objects.get_or_create( - name='deleted', - email='deleted', - is_deleted=True, - is_customer = True, - is_supplier = True, - is_manufacturer = True - )[0] - - class Order(ReferenceIndexingMixin): """ Abstract model for an order. From 07aaa457de573c0d85b9735416dce44c58ec8401 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Apr 2022 00:04:25 +0200 Subject: [PATCH 15/27] PEP fix --- InvenTree/company/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 8d010f0457..7866fbc8a7 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -68,9 +68,9 @@ def get_deleted_company(): name='deleted', email='deleted', is_deleted=True, - is_customer = True, - is_supplier = True, - is_manufacturer = True + is_customer=True, + is_supplier=True, + is_manufacturer=True )[0] From 132f4aa82e427c2befdf7b6ebcaf2959bb03ef45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 1 May 2022 00:07:13 +0200 Subject: [PATCH 16/27] Use set_null instead --- InvenTree/company/admin.py | 9 --------- InvenTree/company/models.py | 28 ---------------------------- InvenTree/order/models.py | 6 +++--- 3 files changed, 3 insertions(+), 40 deletions(-) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index af767f3fdc..97327a559a 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -21,7 +21,6 @@ class CompanyResource(ModelResource): class Meta: model = Company - exclude = ('is_deleted', ) skip_unchanged = True report_skipped = False clean_model_instances = True @@ -38,14 +37,6 @@ class CompanyAdmin(ImportExportModelAdmin): 'description', ] - exclude = ('is_deleted',) - - def has_delete_permission(self, request, obj=None): - """Magic objects are not allowd to be deleted""" - if obj and obj.is_deleted: - return False - return True - class SupplierPartResource(ModelResource): """ diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 7866fbc8a7..e4b3a1b640 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -60,20 +60,6 @@ def rename_company_image(instance, filename): return os.path.join(base, fn) -def get_deleted_company(): - """ - Returns the deleted company object - """ - return Company.objects.get_or_create( - name='deleted', - email='deleted', - is_deleted=True, - is_customer=True, - is_supplier=True, - is_manufacturer=True - )[0] - - class Company(models.Model): """ A Company object represents an external company. It may be a supplier or a customer or a manufacturer (or a combination) @@ -161,8 +147,6 @@ class Company(models.Model): is_manufacturer = models.BooleanField(default=False, verbose_name=_('is manufacturer'), help_text=_('Does this company manufacture parts?')) - is_deleted = models.BooleanField(default=False, verbose_name=_('is deleted'), help_text=_('Is this company a deleted placeholder?')) - currency = models.CharField( max_length=3, verbose_name=_('Currency'), @@ -282,18 +266,6 @@ class Company(models.Model): return self.purchase_orders.filter(status__in=PurchaseOrderStatus.FAILED) - def save(self, *args, **kwargs): - """Save the instance, unless it is the magic already deleted object""" - if self.pk and self.is_deleted: - raise PermissionDenied(_('This company is a placeholder and can not be updated')) - return super().save(*args, **kwargs) - - def delete(self, *args, **kwargs): - """Delete the instance, unless it is the magic already deleted object""" - if self.is_deleted: - raise PermissionDenied(_('This company is a placeholder and can not be deleted')) - return super().delete(*args, **kwargs) - class Contact(models.Model): """ A Contact represents a person who works at a particular company. diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index b9bec62890..2eb5b0d69c 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -24,7 +24,7 @@ from mptt.models import TreeForeignKey from users import models as UserModels from part import models as PartModels from stock import models as stock_models -from company.models import Company, SupplierPart, get_deleted_company +from company.models import Company, SupplierPart from plugin.events import trigger_event import InvenTree.helpers @@ -219,7 +219,7 @@ class PurchaseOrder(Order): help_text=_('Purchase order status')) supplier = models.ForeignKey( - Company, on_delete=models.SET(get_deleted_company), + Company, on_delete=models.SET_NULL, limit_choices_to={ 'is_supplier': True, }, @@ -567,7 +567,7 @@ class SalesOrder(Order): customer = models.ForeignKey( Company, - on_delete=models.SET(get_deleted_company), + on_delete=models.SET_NULL, null=True, limit_choices_to={'is_customer': True}, related_name='sales_orders', From 9947cc2b08721e2727bf7a95ce1b65ee0b7983e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 1 May 2022 00:07:37 +0200 Subject: [PATCH 17/27] remove old permissionset --- InvenTree/company/templates/company/company_base.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index 4adf4adb3b..c58ea63791 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -18,23 +18,23 @@ {% url 'admin:company_company_change' company.pk as url %} {% include "admin_button.html" with url=url %} {% endif %} -{% if company.is_supplier and roles.purchase_order.add and not company.is_deleted %} +{% if company.is_supplier and roles.purchase_order.add %} {% endif %} {% define perms.company.change_company or perms.company.delete_company as has_permission %} -{% if not company.is_deleted and has_permission %} +{% if has_permission %}