mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Primary address fix (#5592)
* Improve management of primary address for a company - Simplify approach (remove "confirm_primary" field) - Remove @receiver hook - Move all logic into Address.save() method * Make address primary if it is the only one defined for a company * Update frontend table * Fix saving logic * Actually fix it this time * Fix for unit test * Another test fix
This commit is contained in:
		| @@ -9,7 +9,7 @@ from django.core.exceptions import ValidationError | |||||||
| from django.core.validators import MinValueValidator | from django.core.validators import MinValueValidator | ||||||
| from django.db import models | from django.db import models | ||||||
| from django.db.models import Q, Sum, UniqueConstraint | from django.db.models import Q, Sum, UniqueConstraint | ||||||
| from django.db.models.signals import post_delete, post_save, pre_save | from django.db.models.signals import post_delete, post_save | ||||||
| from django.dispatch import receiver | from django.dispatch import receiver | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| @@ -281,10 +281,13 @@ class Address(models.Model): | |||||||
|         link: External link to additional address information |         link: External link to additional address information | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |         """Metaclass defines extra model options""" | ||||||
|  |         verbose_name_plural = "Addresses" | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|         """Custom init function""" |         """Custom init function""" | ||||||
|         if 'confirm_primary' in kwargs: |  | ||||||
|             self.confirm_primary = kwargs.pop('confirm_primary', None) |  | ||||||
|         super().__init__(*args, **kwargs) |         super().__init__(*args, **kwargs) | ||||||
|  |  | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
| @@ -304,25 +307,32 @@ class Address(models.Model): | |||||||
|  |  | ||||||
|         return ", ".join(populated_lines) |         return ", ".join(populated_lines) | ||||||
|  |  | ||||||
|     class Meta: |     def save(self, *args, **kwargs): | ||||||
|         """Metaclass defines extra model options""" |         """Run checks when saving an address: | ||||||
|         verbose_name_plural = "Addresses" |  | ||||||
|  |         - If this address is marked as "primary", ensure that all other addresses for this company are marked as non-primary | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         others = list(Address.objects.filter(company=self.company).exclude(pk=self.pk).all()) | ||||||
|  |  | ||||||
|  |         # If this is the *only* address for this company, make it the primary one | ||||||
|  |         if len(others) == 0: | ||||||
|  |             self.primary = True | ||||||
|  |  | ||||||
|  |         super().save(*args, **kwargs) | ||||||
|  |  | ||||||
|  |         # Once this address is saved, check others | ||||||
|  |         if self.primary: | ||||||
|  |             for addr in others: | ||||||
|  |                 if addr.primary: | ||||||
|  |                     addr.primary = False | ||||||
|  |                     addr.save() | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def get_api_url(): |     def get_api_url(): | ||||||
|         """Return the API URL associated with the Contcat model""" |         """Return the API URL associated with the Contcat model""" | ||||||
|         return reverse('api-address-list') |         return reverse('api-address-list') | ||||||
|  |  | ||||||
|     def validate_unique(self, exclude=None): |  | ||||||
|         """Ensure that only one primary address exists per company""" |  | ||||||
|  |  | ||||||
|         super().validate_unique(exclude=exclude) |  | ||||||
|  |  | ||||||
|         if self.primary: |  | ||||||
|             # Check that no other primary address exists for this company |  | ||||||
|             if Address.objects.filter(company=self.company, primary=True).exclude(pk=self.pk).exists(): |  | ||||||
|                 raise ValidationError({'primary': _('Company already has a primary address')}) |  | ||||||
|  |  | ||||||
|     company = models.ForeignKey(Company, related_name='addresses', |     company = models.ForeignKey(Company, related_name='addresses', | ||||||
|                                 on_delete=models.CASCADE, |                                 on_delete=models.CASCADE, | ||||||
|                                 verbose_name=_('Company'), |                                 verbose_name=_('Company'), | ||||||
| @@ -382,26 +392,6 @@ class Address(models.Model): | |||||||
|                              help_text=_('Link to address information (external)')) |                              help_text=_('Link to address information (external)')) | ||||||
|  |  | ||||||
|  |  | ||||||
| @receiver(pre_save, sender=Address) |  | ||||||
| def check_primary(sender, instance, **kwargs): |  | ||||||
|     """Removes primary flag from current primary address if the to-be-saved address is marked as primary""" |  | ||||||
|  |  | ||||||
|     if instance.company.primary_address is None: |  | ||||||
|         instance.primary = True |  | ||||||
|  |  | ||||||
|     # If confirm_primary is not present, this function does not need to do anything |  | ||||||
|     if not hasattr(instance, 'confirm_primary') or \ |  | ||||||
|        instance.primary is False or \ |  | ||||||
|        instance.company.primary_address is None or \ |  | ||||||
|        instance.id == instance.company.primary_address.id: |  | ||||||
|         return |  | ||||||
|  |  | ||||||
|     if instance.confirm_primary is True: |  | ||||||
|         adr = Address.objects.get(id=instance.company.primary_address.id) |  | ||||||
|         adr.primary = False |  | ||||||
|         adr.save() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ManufacturerPart(MetadataMixin, models.Model): | class ManufacturerPart(MetadataMixin, models.Model): | ||||||
|     """Represents a unique part as provided by a Manufacturer Each ManufacturerPart is identified by a MPN (Manufacturer Part Number) Each ManufacturerPart is also linked to a Part object. A Part may be available from multiple manufacturers. |     """Represents a unique part as provided by a Manufacturer Each ManufacturerPart is identified by a MPN (Manufacturer Part Number) Each ManufacturerPart is also linked to a Part object. A Part may be available from multiple manufacturers. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -67,11 +67,8 @@ class AddressSerializer(InvenTreeModelSerializer): | |||||||
|             'shipping_notes', |             'shipping_notes', | ||||||
|             'internal_shipping_notes', |             'internal_shipping_notes', | ||||||
|             'link', |             'link', | ||||||
|             'confirm_primary' |  | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|     confirm_primary = serializers.BooleanField(default=False) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class AddressBriefSerializer(InvenTreeModelSerializer): | class AddressBriefSerializer(InvenTreeModelSerializer): | ||||||
|     """Serializer for Address Model (limited)""" |     """Serializer for Address Model (limited)""" | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ import os | |||||||
| from decimal import Decimal | from decimal import Decimal | ||||||
|  |  | ||||||
| from django.core.exceptions import ValidationError | from django.core.exceptions import ValidationError | ||||||
| from django.db import transaction |  | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| from part.models import Part | from part.models import Part | ||||||
| @@ -195,40 +194,27 @@ class AddressTest(TestCase): | |||||||
|  |  | ||||||
|     def test_primary_constraint(self): |     def test_primary_constraint(self): | ||||||
|         """Test that there can only be one company-'primary=true' pair""" |         """Test that there can only be one company-'primary=true' pair""" | ||||||
|         c2 = Company.objects.create(name='Test Corp2.', description='We make stuff good') |  | ||||||
|         Address.objects.create(company=self.c, primary=True) |         Address.objects.create(company=self.c, primary=True) | ||||||
|         Address.objects.create(company=self.c, primary=False) |         Address.objects.create(company=self.c, primary=False) | ||||||
|  |  | ||||||
|         self.assertEqual(Address.objects.count(), 2) |         self.assertEqual(Address.objects.count(), 2) | ||||||
|  |  | ||||||
|         # Testing the constraint itself |         self.assertTrue(Address.objects.first().primary) | ||||||
|         # Intentionally throwing exceptions breaks unit tests unless performed in an atomic block |  | ||||||
|         with transaction.atomic(): |  | ||||||
|             with self.assertRaises(ValidationError): |  | ||||||
|                 addr = Address(company=self.c, primary=True, confirm_primary=False) |  | ||||||
|                 addr.validate_unique() |  | ||||||
|  |  | ||||||
|         Address.objects.create(company=c2, primary=True, line1="Hellothere", line2="generalkenobi") |         # Create another address, specify *this* as primary | ||||||
|  |         Address.objects.create(company=self.c, primary=True) | ||||||
|         with transaction.atomic(): |  | ||||||
|             with self.assertRaises(ValidationError): |  | ||||||
|                 addr = Address(company=c2, primary=True, confirm_primary=False) |  | ||||||
|                 addr.validate_unique() |  | ||||||
|  |  | ||||||
|         self.assertEqual(Address.objects.count(), 3) |         self.assertEqual(Address.objects.count(), 3) | ||||||
|  |         self.assertFalse(Address.objects.first().primary) | ||||||
|  |         self.assertTrue(Address.objects.last().primary) | ||||||
|  |  | ||||||
|     def test_first_address_is_primary(self): |     def test_first_address_is_primary(self): | ||||||
|         """Test that first address related to company is always set to primary""" |         """Test that first address related to company is always set to primary""" | ||||||
|  |  | ||||||
|         addr = Address.objects.create(company=self.c) |         addr = Address.objects.create(company=self.c) | ||||||
|  |  | ||||||
|         self.assertTrue(addr.primary) |         self.assertTrue(addr.primary) | ||||||
|  |  | ||||||
|         # Create another address, which should error out if primary is not set to False |  | ||||||
|         with self.assertRaises(ValidationError): |  | ||||||
|             addr = Address(company=self.c, primary=True) |  | ||||||
|             addr.validate_unique() |  | ||||||
|  |  | ||||||
|     def test_model_str(self): |     def test_model_str(self): | ||||||
|         """Test value of __str__""" |         """Test value of __str__""" | ||||||
|         t = "Test address" |         t = "Test address" | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ | |||||||
|     showFormInput, |     showFormInput, | ||||||
|     thumbnailImage, |     thumbnailImage, | ||||||
|     wrapButtons, |     wrapButtons, | ||||||
|  |     yesNoLabel, | ||||||
| */ | */ | ||||||
|  |  | ||||||
| /* exported | /* exported | ||||||
| @@ -798,45 +799,7 @@ function addressFields(options={}) { | |||||||
|         company: { |         company: { | ||||||
|             icon: 'fa-building', |             icon: 'fa-building', | ||||||
|         }, |         }, | ||||||
|         primary: { |         primary: {}, | ||||||
|             onEdit: function(val, name, field, opts) { |  | ||||||
|  |  | ||||||
|                 if (val === false) { |  | ||||||
|  |  | ||||||
|                     hideFormInput("confirm_primary", opts); |  | ||||||
|                     $('#id_confirm_primary').prop("checked", false); |  | ||||||
|                     clearFormErrors(opts); |  | ||||||
|                     enableSubmitButton(opts, true); |  | ||||||
|  |  | ||||||
|                 } else if (val === true) { |  | ||||||
|  |  | ||||||
|                     showFormInput("confirm_primary", opts); |  | ||||||
|                     if($('#id_confirm_primary').prop("checked") === false) { |  | ||||||
|                         handleFormErrors({'confirm_primary': 'WARNING: Setting this address as primary will remove primary flag from other addresses'}, field, {}); |  | ||||||
|                         enableSubmitButton(opts, false); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         confirm_primary: { |  | ||||||
|             help_text: "Confirm", |  | ||||||
|             onEdit: function(val, name, field, opts) { |  | ||||||
|  |  | ||||||
|                 if (val === true) { |  | ||||||
|  |  | ||||||
|                     clearFormErrors(opts); |  | ||||||
|                     enableSubmitButton(opts, true); |  | ||||||
|  |  | ||||||
|                 } else if (val === false) { |  | ||||||
|  |  | ||||||
|                     handleFormErrors({'confirm_primary': 'WARNING: Setting this address as primary will remove primary flag from other addresses'}, field, {}); |  | ||||||
|                     enableSubmitButton(opts, false); |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             css: { |  | ||||||
|                 display: 'none' |  | ||||||
|             } |  | ||||||
|         }, |  | ||||||
|         title: {}, |         title: {}, | ||||||
|         line1: { |         line1: { | ||||||
|             icon: 'fa-map' |             icon: 'fa-map' | ||||||
| @@ -984,11 +947,7 @@ function loadAddressTable(table, options={}) { | |||||||
|                 title: '{% trans "Primary" %}', |                 title: '{% trans "Primary" %}', | ||||||
|                 switchable: false, |                 switchable: false, | ||||||
|                 formatter: function(value) { |                 formatter: function(value) { | ||||||
|                     let checked = ''; |                     return yesNoLabel(value); | ||||||
|                     if (value == true) { |  | ||||||
|                         checked = 'checked="checked"'; |  | ||||||
|                     } |  | ||||||
|                     return `<input type="checkbox" ${checked} disabled="disabled" value="${value? 1 : 0}">`; |  | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             { |             { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user