|
|
@@ -1,19 +1,30 @@
|
|
1
|
1
|
# Generated by Django 3.2.9 on 2022-01-25 19:01
|
|
|
2
|
+import logging
|
|
2
|
3
|
|
|
3
|
4
|
from django.db import migrations
|
|
4
|
5
|
from django.db.models import CharField, Count, Value
|
|
5
|
6
|
from django.db.models.functions import Concat
|
|
6
|
7
|
|
|
|
8
|
+from oscar.core.loading import get_model
|
|
|
9
|
+
|
|
|
10
|
+# Needed for calling _get_value, the historical model can't be used for that.
|
|
|
11
|
+NonHistoricalProductAttributeValue = get_model('catalogue', 'ProductAttributeValue')
|
|
|
12
|
+
|
|
|
13
|
+
|
|
|
14
|
+logger = logging.getLogger(__name__)
|
|
|
15
|
+
|
|
|
16
|
+
|
|
7
|
17
|
def remove_duplicate_attributes(apps, schema_editor):
|
|
8
|
18
|
"""
|
|
9
|
19
|
Removes duplicate attributes that have the same code and product class.
|
|
10
|
20
|
"""
|
|
11
|
21
|
ProductAttribute = apps.get_model('catalogue', 'ProductAttribute')
|
|
|
22
|
+ ProductAttributeValue = apps.get_model('catalogue', 'ProductAttributeValue')
|
|
12
|
23
|
ProductClass = apps.get_model("catalogue", "ProductClass")
|
|
13
|
24
|
|
|
14
|
25
|
# Instead of iterating over all attributes, we concat the code and product class pk
|
|
15
|
26
|
# with a "|" so we can find duplicate attributes in one query.
|
|
16
|
|
- duplicate_attributes = ProductAttribute.objects.annotate(
|
|
|
27
|
+ duplicate_attributes = ProductAttribute.objects.filter(product_class__isnull=False).annotate(
|
|
17
|
28
|
code_and_product_class=Concat('code', Value('|'), 'product_class__pk', output_field=CharField())
|
|
18
|
29
|
).values('code_and_product_class').annotate(
|
|
19
|
30
|
same_code_count=Count('code_and_product_class')
|
|
|
@@ -52,8 +63,32 @@ def remove_duplicate_attributes(apps, schema_editor):
|
|
52
|
63
|
# Choose one attribute that will be used to move to and others to be deleted.
|
|
53
|
64
|
to_be_used_attribute = used_attributes.first()
|
|
54
|
65
|
to_be_deleted_attributes = used_attributes.exclude(pk=to_be_used_attribute.pk)
|
|
|
66
|
+
|
|
55
|
67
|
for attribute in to_be_deleted_attributes:
|
|
56
|
|
- attribute.productattributevalue_set.all().update(attribute=to_be_used_attribute)
|
|
|
68
|
+ for attribute_value in attribute.productattributevalue_set.all():
|
|
|
69
|
+ product = attribute_value.product
|
|
|
70
|
+
|
|
|
71
|
+ # ProductAttributeValue has a unique together constraint on 'product' and 'attribute'.
|
|
|
72
|
+ # This means, if the product of the current 'attribute_value' already has a ProductAttributeValue
|
|
|
73
|
+ # linked to the 'to_be_used_attribute' attribute, we can't update the attribute on the
|
|
|
74
|
+ # 'attribute_value' as this would raise an IntegrityError.
|
|
|
75
|
+ to_be_used_attribute_value = to_be_used_attribute.productattributevalue_set.filter(product=product).first()
|
|
|
76
|
+ if not to_be_used_attribute_value:
|
|
|
77
|
+ attribute_value.attribute = to_be_used_attribute
|
|
|
78
|
+ attribute_value.save()
|
|
|
79
|
+ else:
|
|
|
80
|
+ msg = """Product with ID '%s' had more than one attribute value linked to an attribute
|
|
|
81
|
+ with code '%s'. We've kept the value '%s' and removed the value '%s' as this is the one you
|
|
|
82
|
+ would see in the dashboard when editing the product.
|
|
|
83
|
+ """ % (
|
|
|
84
|
+ product.id,
|
|
|
85
|
+ attribute.code,
|
|
|
86
|
+ NonHistoricalProductAttributeValue._get_value(to_be_used_attribute_value),
|
|
|
87
|
+ NonHistoricalProductAttributeValue._get_value(attribute_value)
|
|
|
88
|
+ )
|
|
|
89
|
+ logger.warning(msg)
|
|
|
90
|
+
|
|
|
91
|
+ # Once the attribute values have been updated, we can safely remove the attribute instance.
|
|
57
|
92
|
attribute.delete()
|
|
58
|
93
|
|
|
59
|
94
|
|
|
|
@@ -66,4 +101,4 @@ class Migration(migrations.Migration):
|
|
66
|
101
|
|
|
67
|
102
|
operations = [
|
|
68
|
103
|
migrations.RunPython(remove_duplicate_attributes, migrations.RunPython.noop)
|
|
69
|
|
- ]
|
|
|
104
|
+ ]
|