Reputation: 103
Using Django 1.8.5 and django-money 0.7.4, I would like to update a MoneyField using an F expression to avoid possible race conditions. Let's assume I have a wallet defined as
from moneyed.classes import Money
from django.db.models import F
from django.db import models
from djmoney.models.fields import MoneyField
class Wallet(models.Model):
balance = MoneyField(max_digits=5, decimal_places=2, default_currency='EUR')
The following code to top up a wallet fails with AttributeError: 'CombinedExpression' object has no attribute 'children'
amount_to_add = Money(amount="3", currency="EUR")
wallet = Wallet(balance=10)
wallet.balance = F('balance') + amount_to_add
I also tried wallet.balance.amount = F("balance__amount") + 3
which doesn't throw an exception straight away but then a subsequent wallet.save()
fails with TypeError: a float is required
. What would be the correct way to do it?
Upvotes: 3
Views: 829
Reputation: 10719
For anyone using a serializer class, a possible source of problem (like for my case) wasn't due to MoneyField
but rather due to the failure in serializing F()
expressions.
File "/home/nponcian/Documents/GitHub/venv/lib/python3.8/site-packages/djmoney/contrib/django_rest_framework/fields.py", line 56, in to_representation
return super().to_representation(obj)
File "/home/nponcian/Documents/GitHub/venv/lib/python3.8/site-packages/rest_framework/fields.py", line 1148, in to_representation
value = decimal.Decimal(str(value).strip())
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]
You can perform the F increment expression through this:
>>> obj = Wallet.objects.filter(balance_currency="EUR").first()
>>> obj.balance = F('balance') + Money(Decimal("12.24"), "EUR")
>>> obj.save()
So when using e.g. an update
in a serializer class:
class WalletModelSerializer(serializers.ModelSerializer):
class Meta:
model = Wallet
fields = "__all__"
def update(self, instance: Wallet, validated_data: dict) -> Wallet:
# Create a copy to not modify the original validation result
validated_data = deepcopy(validated_data)
if "balance" in validated_data:
validated_data["balance"] += models.F("balance")
# When the update is performed, the result is equivalent to performing:
# instance.balance = models.F("balance") + validated_data["balance"]
# Or more specifically:
# instance.balance = models.F("balance") + djmoney.money.Money("12.34", "USD")
return super().update(instance, validated_data)
But, calling serializer.save()
and then accessing the serialized data via serializer.data
which in turn calls serializer.to_representation()
which in turn calls each of the field.to_representation()
would fail for the field balance
because its value is of type CombinedExpression
e.g. <CombinedExpression: F(balance) + Value(12.34)>
which can't be serialized. Note that the expression is what we initially set as the value of the balance = F("balance") + Money("12.34", "USD")
. As documented, F() assignments persist after Model.save()
F() objects assigned to model fields persist after saving the model instance and will be applied on each
save()
... This persistence can be avoided by reloading the model object after saving it, for example, by usingrefresh_from_db()
.
>>> # Access the balance after the obj.save()
>>> type(obj.balance)
<class 'django.db.models.expressions.CombinedExpression'>
>>> obj.balance
<CombinedExpression: F(balance) + Value(1.2)>
To solve this, depending on your requirements, you can just refresh it to fetch its latest value. As documented, the refresh would replace the CombinedExpression
value into its actual value, which then could be successfully serialized.
>>> obj.refresh_from_db()
>>> type(obj.balance)
<class 'djmoney.money.Money'>
>>> obj.balance
Money('10.28', 'EUR')
Which is translated to this on a serializer class:
class WalletModelSerializer(serializers.ModelSerializer):
...
def update(self, instance: Wallet, validated_data: dict) -> Wallet:
...
instance = super().update(instance, validated_data)
if any(isinstance(value, models.Expression) for value in validated_data.values()):
instance.refresh_from_db()
return instance
After this change, accessing serializer.data
would be successful.
Upvotes: 0
Reputation: 516
Since django-money
0.7.7 all basic F
objects manipulations are supported.
It works for Django 1.4+.
Upvotes: 2
Reputation: 4158
Have you tried giving it a float instead of an integer? Try: wallet.balance.amount = F("balance__amount") + float(3)
I am not sure why their code doesn't automatically try to convert integers to float's if it that is what it requires.
Upvotes: 0