## # This file is part of TALER # (C) 2014, 2015, 2016 INRIA # # TALER is free software; you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation; either version 3, or # (at your option) any later version. TALER is distributed in the # hope that it will be useful, but WITHOUT ANY WARRANTY; without # even the implied warranty of MERCHANTABILITY or FITNESS FOR A # PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License # along with TALER; see the file COPYING. If not, see # # # @author Marcello Stanisci # @author Florian Dold from __future__ import unicode_literals from typing import Any, Tuple from django.contrib.auth.models import User from django.db import models from django.conf import settings from django.core.exceptions import \ ValidationError, \ ObjectDoesNotExist from .amount import Amount, BadFormatAmount, NumberTooBig class InvalidAmount(Amount): def __init__(self, currency): super(InvalidAmount, self).__init__(currency, value=float('nan'), fraction=float('nan')) def stringify(self, ndigits, pretty): return "Invalid Amount, please report" def dump(self): return "Invalid Amount, please report" ## # Helper function that instantiates a zero-valued @a Amount # object. def get_zero_amount() -> Amount: return Amount(settings.TALER_CURRENCY) ## # Custom implementation of the @a Amount class as a database type. class AmountField(models.Field): description = 'Amount object in Taler style' ## # Return the database type of the serialized amount. # # @param self the object itself. # @param connection the database connection. # @return type of the serialized amount: varchar. def db_type(self, connection: Any) -> str: return "varchar" ## # Stringifies the Amount object to feed the DB connector. # # @param self the object itself. # @para value the @a Amount object to be serialized. def get_prep_value(self, value: Amount) -> str: if not value: return "%s:0.0" % settings.TALER_CURRENCY if settings.TALER_CURRENCY != value.currency: raise CurrencyMismatch(settings.TALER_CURRENCY, value.currency) return value.stringify(settings.TALER_DIGITS) ## # Parse the stringified Amount back to Python. # # @param value serialized amount coming from the database. # (It is just a string in the usual CURRENCY:X.Y form) # @param args currently unused. # @return the @a Amount object. @staticmethod def from_db_value(value: str, *args) -> Amount: del args # pacify PEP checkers if value is None: return Amount.parse(settings.TALER_CURRENCY) try: return Amount.parse(value) except NumberTooBig: # Keep the currency right to avoid causing # exceptions if some operation is attempted # against this invalid amount. NOTE that the # value is defined as NaN, so no actual/useful # amount will ever be generated using this one. # And more, the NaN value will make it easier # to scan the database to find these faulty # amounts. # We also decide to not raise exception here # because they would propagate in too many places # in the code, and it would be too verbose to # just try-cactch any possible exception situation. return InvalidAmount(settings.TALER_CURRENCY) ## # Parse the stringified Amount back to Python. FIXME: # why this serializer consider _more_ cases respect to the # one above ('from_db_value')? # # @param value serialized amount coming from the database. # (It is just a string in the usual CURRENCY:X.Y form) # @param args currently unused. # @return the @a Amount object. def to_python(self, value: Any) -> Amount: if isinstance(value, Amount): return value try: if value is None: return Amount.parse(settings.TALER_CURRENCY) return Amount.parse(value) except BadFormatAmount: raise ValidationError( "Invalid input for an amount string: %s" % value) ## # The class representing a bank account. class BankAccount(models.Model): is_public = models.BooleanField(default=False) debit = models.BooleanField(default=False) account_no = models.AutoField(primary_key=True) user = models.OneToOneField(User, on_delete=models.CASCADE) amount = AmountField(default=get_zero_amount) ## # The class representing a bank transaction. class BankTransaction(models.Model): amount = AmountField(default=False) debit_account = models.ForeignKey( BankAccount, on_delete=models.CASCADE, db_index=True, related_name="debit_account") credit_account = models.ForeignKey( BankAccount, on_delete=models.CASCADE, db_index=True, related_name="credit_account") subject = models.CharField( default="(no subject given)", max_length=200) date = models.DateTimeField( auto_now=True, db_index=True) cancelled = models.BooleanField(default=False)