mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 19:46:46 +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:
parent
6cde460567
commit
324d5929b5
@ -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}">`;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user