Repository: saleor/saleor

Issue:
CustomJsonEncoder drops currency information from Money instance
Usually, we are using `settings.DEFAULT_CURRENCY` all over the place, however, there can be a bug, where `Money` is saved into `JSONField`, and `settings.DEFAULT_CURRENCY` changed meanwhile.




Discussion:

Resolving patch:
diff --git a/saleor/core/utils/json_serializer.py b/saleor/core/utils/json_serializer.py
--- a/saleor/core/utils/json_serializer.py
+++ b/saleor/core/utils/json_serializer.py
@@ -1,8 +1,12 @@
+import json
+
+from django.core.serializers.base import DeserializationError
 from django.core.serializers.json import (
-    Deserializer as JsonDeserializer, DjangoJSONEncoder,
-    Serializer as JsonSerializer)
+    DjangoJSONEncoder, PythonDeserializer, Serializer as JsonSerializer)
 from prices import Money
 
+MONEY_TYPE = 'Money'
+
 
 class Serializer(JsonSerializer):
     def _init_options(self):
@@ -13,8 +17,32 @@ def _init_options(self):
 class CustomJsonEncoder(DjangoJSONEncoder):
     def default(self, obj):
         if isinstance(obj, Money):
-            return obj.amount
+            return {
+                '_type': MONEY_TYPE, 'amount': obj.amount,
+                'currency': obj.currency}
         return super().default(obj)
 
 
-Deserializer = JsonDeserializer
+def object_hook(obj):
+    if '_type' in obj and obj['_type'] == MONEY_TYPE:
+        return Money(obj['amount'], obj['currency'])
+    return obj
+
+
+def Deserializer(stream_or_string, **options):
+    """Deserialize a stream or string of JSON data. This is a slightly modified
+    copy of Django implementation with additional argument <object_hook> in
+    json.loads"""
+    if not isinstance(stream_or_string, (bytes, str)):
+        stream_or_string = stream_or_string.read()
+    if isinstance(stream_or_string, bytes):
+        stream_or_string = stream_or_string.decode()
+    try:
+        objects = json.loads(stream_or_string, object_hook=object_hook)
+        yield from PythonDeserializer(objects, **options)
+    except Exception as exc:
+        # ugly construction to overcome pylint's warning
+        # "The except handler raises immediately"
+        if isinstance(exc, (GeneratorExit, DeserializationError)):
+            raise
+        raise DeserializationError() from exc


Test patch:
diff --git a/tests/test_product.py b/tests/test_product.py
--- a/tests/test_product.py
+++ b/tests/test_product.py
@@ -1,4 +1,5 @@
 import datetime
+import io
 import json
 from decimal import Decimal
 from unittest.mock import patch
@@ -7,6 +8,7 @@
 
 from django.conf import settings
 from django.core import serializers
+from django.core.serializers.base import DeserializationError
 from django.urls import reverse
 from prices import Money, TaxedMoney, TaxedMoneyRange
 from saleor.checkout import utils
@@ -597,7 +599,8 @@ def test_product_json_serialization(product):
     product.save()
     data = json.loads(serializers.serialize(
         "json", models.Product.objects.all()))
-    assert data[0]['fields']['price'] == '10.00'
+    assert data[0]['fields']['price'] == {
+        '_type': 'Money', 'amount': '10.00', 'currency': 'USD'}
 
 
 def test_product_json_deserialization(category, product_type):
@@ -612,7 +615,7 @@ def test_product_json_deserialization(category, product_type):
             "name": "Kelly-Clark",
             "description": "Future almost cup national",
             "category": {category_pk},
-            "price": "35.98",
+            "price": {{"_type": "Money", "amount": "35.98", "currency": "USD"}},
             "available_on": null,
             "is_published": true,
             "attributes": "{{\\"9\\": \\"24\\", \\"10\\": \\"26\\"}}",
@@ -630,6 +633,51 @@ def test_product_json_deserialization(category, product_type):
     product = models.Product.objects.first()
     assert product.price == Money(Decimal('35.98'), 'USD')
 
+    # same test for bytes
+    product_json_bytes = bytes(product_json, 'utf-8')
+    product_deserialized = list(serializers.deserialize(
+        'json', product_json_bytes, ignorenonexistent=True))[0]
+    product_deserialized.save()
+    product = models.Product.objects.first()
+    assert product.price == Money(Decimal('35.98'), 'USD')
+
+    # same test for stream
+    product_json_stream = io.StringIO(product_json)
+    product_deserialized = list(serializers.deserialize(
+        'json', product_json_stream, ignorenonexistent=True))[0]
+    product_deserialized.save()
+    product = models.Product.objects.first()
+    assert product.price == Money(Decimal('35.98'), 'USD')
+
+
+def test_json_no_currency_deserialization(category, product_type):
+    product_json = """
+    [{{
+        "model": "product.product",
+        "pk": 60,
+        "fields": {{
+            "seo_title": null,
+            "seo_description": "Future almost cup national.",
+            "product_type": {product_type_pk},
+            "name": "Kelly-Clark",
+            "description": "Future almost cup national",
+            "category": {category_pk},
+            "price": {{"_type": "Money", "amount": "35.98"}},
+            "available_on": null,
+            "is_published": true,
+            "attributes": "{{\\"9\\": \\"24\\", \\"10\\": \\"26\\"}}",
+            "updated_at": "2018-07-19T13:30:24.195Z",
+            "is_featured": false,
+            "charge_taxes": true,
+            "tax_rate": "standard"
+        }}
+    }}]
+    """.format(
+        category_pk=category.pk, product_type_pk=product_type.pk)
+    with pytest.raises(DeserializationError):
+        list(serializers.deserialize(
+            'json', product_json, ignorenonexistent=True))
+
 
 def test_variant_picker_data_with_translations(
         product, translated_variant_fr, settings):