2014-07-28 17:28:00 +00:00
|
|
|
from django.core.exceptions import ValidationError
|
2014-07-23 16:24:56 +00:00
|
|
|
from django.db import models
|
2014-07-28 17:28:00 +00:00
|
|
|
from django.utils.functional import cached_property
|
2023-10-24 16:59:02 +00:00
|
|
|
from django.utils.translation import gettext_lazy as _
|
2014-07-23 16:24:56 +00:00
|
|
|
from jsonfield import JSONField
|
|
|
|
|
2015-06-02 12:59:49 +00:00
|
|
|
from orchestra.models.fields import PrivateFileField
|
2014-09-04 15:55:43 +00:00
|
|
|
from orchestra.models.queryset import group_by
|
2014-07-28 17:28:00 +00:00
|
|
|
|
2014-07-23 16:24:56 +00:00
|
|
|
from . import settings
|
|
|
|
from .methods import PaymentMethod
|
|
|
|
|
|
|
|
|
2014-09-04 15:55:43 +00:00
|
|
|
class PaymentSourcesQueryset(models.QuerySet):
|
2014-09-05 14:27:30 +00:00
|
|
|
def get_default(self):
|
2014-09-04 15:55:43 +00:00
|
|
|
return self.filter(is_active=True).first()
|
|
|
|
|
|
|
|
|
2014-07-23 16:24:56 +00:00
|
|
|
class PaymentSource(models.Model):
|
|
|
|
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
2021-04-22 08:28:00 +00:00
|
|
|
related_name='paymentsources', on_delete=models.CASCADE)
|
2014-07-23 16:24:56 +00:00
|
|
|
method = models.CharField(_("method"), max_length=32,
|
2015-04-05 10:46:24 +00:00
|
|
|
choices=PaymentMethod.get_choices())
|
2015-03-29 16:10:07 +00:00
|
|
|
data = JSONField(_("data"), default={})
|
2014-09-30 10:20:11 +00:00
|
|
|
is_active = models.BooleanField(_("active"), default=True)
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-04 15:55:43 +00:00
|
|
|
objects = PaymentSourcesQueryset.as_manager()
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2015-04-02 16:14:55 +00:00
|
|
|
def __str__(self):
|
2014-09-04 15:55:43 +00:00
|
|
|
return "%s (%s)" % (self.label, self.method_class.verbose_name)
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-04 15:55:43 +00:00
|
|
|
@cached_property
|
|
|
|
def method_class(self):
|
2015-03-31 12:39:08 +00:00
|
|
|
return PaymentMethod.get(self.method)
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2015-03-04 21:06:16 +00:00
|
|
|
@cached_property
|
2015-03-26 16:00:30 +00:00
|
|
|
def method_instance(self):
|
2015-03-04 21:06:16 +00:00
|
|
|
""" Per request lived method_instance """
|
2015-03-11 16:32:33 +00:00
|
|
|
return self.method_class(self)
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-07-28 17:28:00 +00:00
|
|
|
@cached_property
|
|
|
|
def label(self):
|
2015-03-11 16:32:33 +00:00
|
|
|
return self.method_instance.get_label()
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-07-28 17:28:00 +00:00
|
|
|
@cached_property
|
|
|
|
def number(self):
|
2015-03-11 16:32:33 +00:00
|
|
|
return self.method_instance.get_number()
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-05 14:27:30 +00:00
|
|
|
def get_bill_context(self):
|
2015-03-04 21:06:16 +00:00
|
|
|
method = self.method_instance
|
2014-09-05 14:27:30 +00:00
|
|
|
return {
|
2015-03-11 16:32:33 +00:00
|
|
|
'message': method.get_bill_message(),
|
2014-09-05 14:27:30 +00:00
|
|
|
}
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-05 14:27:30 +00:00
|
|
|
def get_due_delta(self):
|
2015-03-04 21:06:16 +00:00
|
|
|
return self.method_instance.due_delta
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-10-30 16:34:02 +00:00
|
|
|
def clean(self):
|
2015-03-11 16:32:33 +00:00
|
|
|
self.data = self.method_instance.clean_data()
|
2014-09-04 15:55:43 +00:00
|
|
|
|
|
|
|
|
|
|
|
class TransactionQuerySet(models.QuerySet):
|
|
|
|
group_by = group_by
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-06 10:56:30 +00:00
|
|
|
def create(self, **kwargs):
|
|
|
|
source = kwargs.get('source')
|
|
|
|
if source is None or not hasattr(source.method_class, 'process'):
|
|
|
|
# Manual payments don't need processing
|
2015-07-09 10:44:22 +00:00
|
|
|
kwargs['state'] = self.model.WAITTING_EXECUTION
|
|
|
|
amount = kwargs.get('amount')
|
|
|
|
if amount == 0:
|
|
|
|
kwargs['state'] = self.model.SECURED
|
2014-09-06 10:56:30 +00:00
|
|
|
return super(TransactionQuerySet, self).create(**kwargs)
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-18 15:07:39 +00:00
|
|
|
def secured(self):
|
|
|
|
return self.filter(state=Transaction.SECURED)
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-18 15:07:39 +00:00
|
|
|
def exclude_rejected(self):
|
|
|
|
return self.exclude(state=Transaction.REJECTED)
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-18 15:07:39 +00:00
|
|
|
def amount(self):
|
2015-06-22 14:14:16 +00:00
|
|
|
return next(iter(self.aggregate(models.Sum('amount')).values())) or 0
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-18 15:07:39 +00:00
|
|
|
def processing(self):
|
|
|
|
return self.filter(state__in=[Transaction.EXECUTED, Transaction.WAITTING_EXECUTION])
|
2014-07-23 16:24:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Transaction(models.Model):
|
2014-09-06 10:56:30 +00:00
|
|
|
WAITTING_PROCESSING = 'WAITTING_PROCESSING' # CREATED
|
2014-09-18 15:07:39 +00:00
|
|
|
WAITTING_EXECUTION = 'WAITTING_EXECUTION' # PROCESSED
|
2014-09-16 17:14:24 +00:00
|
|
|
EXECUTED = 'EXECUTED'
|
2014-09-05 14:27:30 +00:00
|
|
|
SECURED = 'SECURED'
|
2014-09-16 17:14:24 +00:00
|
|
|
REJECTED = 'REJECTED'
|
2014-07-23 16:24:56 +00:00
|
|
|
STATES = (
|
2014-07-24 09:53:34 +00:00
|
|
|
(WAITTING_PROCESSING, _("Waitting processing")),
|
2014-09-18 15:07:39 +00:00
|
|
|
(WAITTING_EXECUTION, _("Waitting execution")),
|
2014-09-16 17:14:24 +00:00
|
|
|
(EXECUTED, _("Executed")),
|
2014-09-05 14:27:30 +00:00
|
|
|
(SECURED, _("Secured")),
|
2014-09-16 17:14:24 +00:00
|
|
|
(REJECTED, _("Rejected")),
|
2014-07-23 16:24:56 +00:00
|
|
|
)
|
2015-09-29 12:35:22 +00:00
|
|
|
STATE_HELP = {
|
|
|
|
WAITTING_PROCESSING: _("The transaction is created and requires processing by the "
|
|
|
|
"specific payment method."),
|
|
|
|
WAITTING_EXECUTION: _("The transaction is processed and its pending execution on "
|
|
|
|
"the related financial institution."),
|
|
|
|
EXECUTED: _("The transaction is executed on the financial institution."),
|
|
|
|
SECURED: _("The transaction ammount is secured."),
|
|
|
|
REJECTED: _("The transaction has failed and the ammount is lost, a new transaction "
|
|
|
|
"should be created for recharging."),
|
|
|
|
}
|
2021-04-22 08:28:00 +00:00
|
|
|
|
|
|
|
bill = models.ForeignKey('bills.bill', on_delete=models.CASCADE, verbose_name=_("bill"),
|
2015-04-05 10:46:24 +00:00
|
|
|
related_name='transactions')
|
2016-04-27 08:35:13 +00:00
|
|
|
source = models.ForeignKey(PaymentSource, null=True, blank=True, on_delete=models.SET_NULL,
|
2015-04-05 10:46:24 +00:00
|
|
|
verbose_name=_("source"), related_name='transactions')
|
2015-07-09 10:44:22 +00:00
|
|
|
process = models.ForeignKey('payments.TransactionProcess', null=True, blank=True,
|
|
|
|
on_delete=models.SET_NULL, verbose_name=_("process"), related_name='transactions')
|
2014-07-23 16:24:56 +00:00
|
|
|
state = models.CharField(_("state"), max_length=32, choices=STATES,
|
2015-04-05 10:46:24 +00:00
|
|
|
default=WAITTING_PROCESSING)
|
2014-07-23 16:24:56 +00:00
|
|
|
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
|
|
|
|
currency = models.CharField(max_length=10, default=settings.PAYMENT_CURRENCY)
|
2014-09-26 15:05:20 +00:00
|
|
|
created_at = models.DateTimeField(_("created"), auto_now_add=True)
|
|
|
|
modified_at = models.DateTimeField(_("modified"), auto_now=True)
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-18 15:07:39 +00:00
|
|
|
objects = TransactionQuerySet.as_manager()
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2015-04-02 16:14:55 +00:00
|
|
|
def __str__(self):
|
2015-05-30 14:44:05 +00:00
|
|
|
return "#%i" % self.id
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-04 15:55:43 +00:00
|
|
|
@property
|
|
|
|
def account(self):
|
|
|
|
return self.bill.account
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-18 15:07:39 +00:00
|
|
|
def clean(self):
|
|
|
|
if not self.pk:
|
|
|
|
amount = self.bill.transactions.exclude(state=self.REJECTED).amount()
|
|
|
|
if amount >= self.bill.total:
|
2016-05-09 12:02:10 +00:00
|
|
|
raise ValidationError(
|
|
|
|
_("Bill %(number)s already has valid transactions that cover bill total amount (%(amount)s).") % {
|
|
|
|
'number': self.bill.number,
|
|
|
|
'amount': amount,
|
|
|
|
}
|
|
|
|
)
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2015-09-29 12:35:22 +00:00
|
|
|
def get_state_help(self):
|
|
|
|
if self.source:
|
|
|
|
return self.source.method_instance.state_help.get(self.state) or self.STATE_HELP.get(self.state)
|
|
|
|
return self.STATE_HELP.get(self.state)
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-18 15:07:39 +00:00
|
|
|
def mark_as_processed(self):
|
|
|
|
self.state = self.WAITTING_EXECUTION
|
2015-09-22 10:24:04 +00:00
|
|
|
self.save(update_fields=('state', 'modified_at'))
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-16 17:14:24 +00:00
|
|
|
def mark_as_executed(self):
|
|
|
|
self.state = self.EXECUTED
|
2015-09-22 10:24:04 +00:00
|
|
|
self.save(update_fields=('state', 'modified_at'))
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-16 17:14:24 +00:00
|
|
|
def mark_as_secured(self):
|
|
|
|
self.state = self.SECURED
|
2015-09-22 10:24:04 +00:00
|
|
|
self.save(update_fields=('state', 'modified_at'))
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-16 17:14:24 +00:00
|
|
|
def mark_as_rejected(self):
|
|
|
|
self.state = self.REJECTED
|
2015-09-22 10:24:04 +00:00
|
|
|
self.save(update_fields=('state', 'modified_at'))
|
2014-07-28 17:28:00 +00:00
|
|
|
|
|
|
|
|
2014-09-05 14:27:30 +00:00
|
|
|
class TransactionProcess(models.Model):
|
2014-07-29 20:10:37 +00:00
|
|
|
"""
|
|
|
|
Stores arbitrary data generated by payment methods while processing transactions
|
|
|
|
"""
|
2014-09-18 15:07:39 +00:00
|
|
|
CREATED = 'CREATED'
|
|
|
|
EXECUTED = 'EXECUTED'
|
|
|
|
ABORTED = 'ABORTED'
|
|
|
|
COMMITED = 'COMMITED'
|
|
|
|
STATES = (
|
|
|
|
(CREATED, _("Created")),
|
|
|
|
(EXECUTED, _("Executed")),
|
|
|
|
(ABORTED, _("Aborted")),
|
|
|
|
(COMMITED, _("Commited")),
|
|
|
|
)
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-07-29 20:10:37 +00:00
|
|
|
data = JSONField(_("data"), blank=True)
|
2015-06-02 12:59:49 +00:00
|
|
|
file = PrivateFileField(_("file"), blank=True)
|
2014-09-18 15:07:39 +00:00
|
|
|
state = models.CharField(_("state"), max_length=16, choices=STATES, default=CREATED)
|
2015-07-09 10:19:30 +00:00
|
|
|
created_at = models.DateTimeField(_("created"), auto_now_add=True, db_index=True)
|
2014-09-18 15:07:39 +00:00
|
|
|
updated_at = models.DateTimeField(_("updated"), auto_now=True)
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-05 14:27:30 +00:00
|
|
|
class Meta:
|
|
|
|
verbose_name_plural = _("Transaction processes")
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2015-04-02 16:14:55 +00:00
|
|
|
def __str__(self):
|
2014-10-11 16:21:51 +00:00
|
|
|
return '#%i' % self.id
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-18 15:07:39 +00:00
|
|
|
def mark_as_executed(self):
|
|
|
|
self.state = self.EXECUTED
|
|
|
|
for transaction in self.transactions.all():
|
|
|
|
transaction.mark_as_executed()
|
2016-04-07 11:14:44 +00:00
|
|
|
self.save(update_fields=('state', 'updated_at'))
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-18 15:07:39 +00:00
|
|
|
def abort(self):
|
|
|
|
self.state = self.ABORTED
|
2016-06-30 09:46:43 +00:00
|
|
|
for transaction in self.transactions.all():
|
|
|
|
transaction.mark_as_rejected()
|
2016-04-07 11:14:44 +00:00
|
|
|
self.save(update_fields=('state', 'updated_at'))
|
2021-04-22 08:28:00 +00:00
|
|
|
|
2014-09-18 15:07:39 +00:00
|
|
|
def commit(self):
|
|
|
|
self.state = self.COMMITED
|
|
|
|
for transaction in self.transactions.processing():
|
|
|
|
transaction.mark_as_secured()
|
2016-04-07 11:14:44 +00:00
|
|
|
self.save(update_fields=('state', 'updated_at'))
|