class Donation(Contribution): amount = MoneyField() payout_amount = MoneyField() client_secret = models.CharField(max_length=32, blank=True, null=True) reward = models.ForeignKey(Reward, null=True, blank=True, related_name="donations") fundraiser = models.ForeignKey(Fundraiser, null=True, blank=True, related_name="donations") name = models.CharField( max_length=200, null=True, blank=True, verbose_name=_('Fake name'), help_text=_('Override donor name / Name for guest donation')) anonymous = models.BooleanField(_('anonymous'), default=False) payout = models.ForeignKey('funding.Payout', null=True, blank=True, on_delete=SET_NULL, related_name='donations') def save(self, *args, **kwargs): if not self.user and not self.client_secret: self.client_secret = ''.join( random.choice(string.ascii_lowercase) for i in range(32)) if not self.payout_amount: self.payout_amount = self.amount super(Donation, self).save(*args, **kwargs) @property def date(self): return self.created @property def payment_method(self): if not self.payment: return None return self.payment.type class Meta(object): verbose_name = _('Donation') verbose_name_plural = _('Donations') def __str__(self): return u'{}'.format(self.amount) class JSONAPIMeta(object): resource_name = 'contributions/donations'
class ProjectBudgetLine(models.Model): """ BudgetLine: Entries to the Project Budget sheet. This is the budget for the amount asked from this website. """ project = models.ForeignKey('projects.Project') description = models.CharField(_('description'), max_length=255, default='') amount = MoneyField() created = CreationDateTimeField() updated = ModificationDateTimeField() @property def owner(self): return self.project.owner @property def parent(self): return self.project class Meta: verbose_name = _('budget line') verbose_name_plural = _('budget lines') def __unicode__(self): return u'{0} - {1}'.format(self.description, self.amount)
class Reward(models.Model): """ Rewards for donations """ amount = MoneyField(_('Amount')) title = models.CharField(_('Title'), max_length=200) description = models.CharField(_('Description'), max_length=500) project = models.ForeignKey('projects.Project', verbose_name=_('Project')) limit = models.IntegerField(_('Limit'), null=True, blank=True, help_text=_('How many of this rewards are available')) created = CreationDateTimeField(_('creation date')) updated = ModificationDateTimeField(_('last modification')) @property def owner(self): return self.project.owner @property def parent(self): return self.project @property def count(self): return self.donations.exclude( order__status=StatusDefinition.FAILED ).count() @property def success_count(self): return self.donations.filter( order__status__in=[StatusDefinition.PENDING, StatusDefinition.SUCCESS] ).count() def __unicode__(self): return self.title class Meta: ordering = ['-project__created', 'amount'] verbose_name = _("Gift") verbose_name_plural = _("Gifts") permissions = ( ('api_read_reward', 'Can view reward through the API'), ('api_add_reward', 'Can add reward through the API'), ('api_change_reward', 'Can change reward through the API'), ('api_delete_reward', 'Can delete reward through the API'), ('api_read_own_reward', 'Can view own reward through the API'), ('api_add_own_reward', 'Can add own reward through the API'), ('api_change_own_reward', 'Can change own reward through the API'), ('api_delete_own_reward', 'Can delete own reward through the API'), ) def delete(self, using=None, keep_parents=False): if self.success_count: raise ValueError(_('Not allowed to delete a reward with successful donations.')) return super(Reward, self).delete(using=using, keep_parents=False)
class BaseFundraiser(models.Model): owner = models.ForeignKey('members.Member', verbose_name=_("initiator"), help_text=_("Project owner")) project = models.ForeignKey('projects.Project', verbose_name=_("project")) title = models.CharField(_("title"), max_length=255) description = models.TextField(_("description"), blank=True) image = ImageField(_("picture"), max_length=255, blank=True, null=True, upload_to='fundraiser_images/', help_text=_("Minimal of 800px wide")) video_url = models.URLField(max_length=100, blank=True, default='') amount = MoneyField(_("amount")) deadline = models.DateTimeField(null=True) created = CreationDateTimeField( _("created"), help_text=_("When this fundraiser was created.")) updated = ModificationDateTimeField(_('updated')) deleted = models.DateTimeField(_('deleted'), blank=True, null=True) location = models.ForeignKey('geo.Location', null=True, blank=True) wallposts = GenericRelation(Wallpost, related_query_name='fundraiser_wallposts') def __unicode__(self): return self.title @property def amount_donated(self): donations = self.donation_set.filter(order__status__in=[ StatusDefinition.SUCCESS, StatusDefinition.PENDING, StatusDefinition.PLEDGED ]) totals = [ Money(data['amount__sum'], data['amount_currency']) for data in donations.values('amount_currency').annotate( Sum('amount')).order_by() ] totals = [convert(amount, self.amount.currency) for amount in totals] return sum(totals) or Money(0, self.amount.currency) class Meta(): abstract = True verbose_name = _('fundraiser') verbose_name_plural = _('fundraisers')
class Fundraiser(AnonymizationMixin, models.Model): owner = models.ForeignKey('members.Member', related_name="funding_fundraisers") activity = models.ForeignKey( 'funding.Funding', verbose_name=_("activity"), related_name="fundraisers" ) title = models.CharField(_("title"), max_length=255) description = models.TextField(_("description"), blank=True) image = ImageField(blank=True, null=True) amount = MoneyField(_("amount")) deadline = models.DateTimeField(_('deadline'), null=True, blank=True) created = models.DateTimeField(default=timezone.now) updated = models.DateTimeField(auto_now=True) def __str__(self): return self.title @cached_property def amount_donated(self): from .states import DonationStateMachine donations = self.donations.filter( status__in=[ DonationStateMachine.succeeded.value, DonationStateMachine.activity_refunded.value, ] ) totals = [ Money(data['amount__sum'], data['amount_currency']) for data in donations.values('amount_currency').annotate(Sum('amount')).order_by() ] totals = [convert(amount, self.amount.currency) for amount in totals] return sum(totals) or Money(0, self.amount.currency) class Meta(object): verbose_name = _('fundraiser') verbose_name_plural = _('fundraisers')
class ProjectCreateTemplate(models.Model): project_settings = models.ForeignKey('projects.ProjectPlatformSettings', null=True, related_name='templates') name = models.CharField(max_length=300) sub_name = models.CharField(max_length=300) description = models.TextField(null=True, blank=True) image = models.ImageField(null=True, blank=True) default_amount_asked = MoneyField(null=True, blank=True) default_title = models.CharField(max_length=300, null=True, blank=True, help_text=_('Default project title')) default_pitch = models.TextField(null=True, blank=True, help_text=_('Default project pitch')) default_description = models.TextField(null=True, blank=True, help_text=_('Default project description')) default_image = models.ImageField(null=True, blank=True, help_text=_('Default project image'))
class Reward(models.Model): """ Rewards for donations """ amount = MoneyField(_('Amount')) title = models.CharField(_('Title'), max_length=200) description = models.CharField(_('Description'), max_length=500) activity = models.ForeignKey('funding.Funding', verbose_name=_('Activity'), related_name='rewards') limit = models.IntegerField( _('Limit'), null=True, blank=True, help_text=_('How many of this rewards are available')) created = models.DateTimeField(default=timezone.now) updated = models.DateTimeField(auto_now=True) @property def count(self): from .states import DonationStateMachine return self.donations.filter( status=DonationStateMachine.succeeded.value).count() def __str__(self): return self.title class Meta(object): ordering = ['-activity__created', 'amount'] verbose_name = _("Gift") verbose_name_plural = _("Gifts") class JSONAPIMeta(object): resource_name = 'activities/rewards' def delete(self, *args, **kwargs): if self.count: raise ValueError( _('Not allowed to delete a reward with successful donations.')) return super(Reward, self).delete(*args, **kwargs)
class BudgetLine(models.Model): """ BudgetLine: Entries to the Activity Budget sheet. """ activity = models.ForeignKey('funding.Funding', related_name='budget_lines') description = models.CharField(_('description'), max_length=255, default='') amount = MoneyField() created = models.DateTimeField(default=timezone.now) updated = models.DateTimeField(auto_now=True) class JSONAPIMeta(object): resource_name = 'activities/budget-lines' class Meta(object): verbose_name = _('budget line') verbose_name_plural = _('budget lines') def __str__(self): return u'{0} - {1}'.format(self.description, self.amount)
class BaseProject(models.Model): class Type(DjangoChoices): sourcing = ChoiceItem('sourcing', label=_('Crowd-sourcing')) funding = ChoiceItem('funding', label=_('Crowd-funding')) both = ChoiceItem('both', label=_('Crowd-funding & Crowd-sourcing')) """ The base Project model. """ owner = models.ForeignKey('members.Member', verbose_name=_('initiator'), help_text=_('Project owner'), related_name='owner') reviewer = models.ForeignKey('members.Member', verbose_name=_('reviewer'), help_text=_('Project Reviewer'), related_name='reviewer', null=True, blank=True) task_manager = models.ForeignKey('members.Member', verbose_name=_('task manager'), help_text=_('Project Task Manager'), related_name='task_manager', null=True, blank=True) promoter = models.ForeignKey('members.Member', verbose_name=_('promoter'), help_text=_('Project Promoter'), related_name='promoter', null=True, blank=True) organization = models.ForeignKey('organizations.Organization', verbose_name=_('organization'), help_text=_('Project organization'), related_name='projects', null=True, blank=True) project_type = models.CharField(_('Project type'), max_length=50, choices=Type.choices, null=True, blank=True) # Basics created = models.DateTimeField( _('created'), help_text=_('When this project was created.'), auto_now_add=True) updated = models.DateTimeField(_('updated'), auto_now=True) title = models.CharField(_('title'), max_length=255, unique=True, db_index=True) slug = models.SlugField(_('slug'), max_length=100, unique=True) pitch = models.TextField( _('pitch'), help_text=_('Pitch your smart idea in one sentence'), blank=True) status = models.ForeignKey('bb_projects.ProjectPhase') theme = models.ForeignKey('bb_projects.ProjectTheme', null=True, blank=True, on_delete=SET_NULL) favorite = models.BooleanField(default=True) deadline = models.DateTimeField(_('deadline'), null=True, blank=True) location = models.ForeignKey('geo.Location', null=True, blank=True, on_delete=models.SET_NULL) place = models.CharField(help_text=_('Geographical location'), max_length=200, null=True, blank=True) # Extended Description description = models.TextField( _('why, what and how'), help_text=_('Blow us away with the details!'), blank=True) # Media image = ImageField(_('image'), max_length=255, blank=True, upload_to='project_images/', help_text=_('Main project picture')) country = models.ForeignKey('geo.Country', blank=True, null=True) language = models.ForeignKey('utils.Language', blank=True, null=True) # For convenience and performance we also store money donated and needed # here. amount_asked = MoneyField() amount_donated = MoneyField() amount_needed = MoneyField() amount_extra = MoneyField( help_text=_("Amount pledged by organisation (matching fund).")) # Bank detail data # Account holder Info account_holder_name = models.CharField(_("account holder name"), max_length=255, null=True, blank=True) account_holder_address = models.CharField(_("account holder address"), max_length=255, null=True, blank=True) account_holder_postal_code = models.CharField( _("account holder postal code"), max_length=20, null=True, blank=True) account_holder_city = models.CharField(_("account holder city"), max_length=255, null=True, blank=True) account_holder_country = models.ForeignKey( 'geo.Country', blank=True, null=True, related_name="project_account_holder_country") # Bank details account_number = models.CharField(_("Account number"), max_length=255, null=True, blank=True) account_details = models.CharField(_("account details"), max_length=500, null=True, blank=True) account_bank_country = models.ForeignKey( 'geo.Country', blank=True, null=True, related_name="project_account_bank_country") @property def is_realised(self): return self.status == ProjectPhase.objects.get(slug='done-complete') @property def is_closed(self): return self.status == ProjectPhase.objects.get(slug='closed') @property def amount_pending(self): return self.get_amount_total([StatusDefinition.PENDING]) @property def amount_safe(self): return self.get_amount_total([StatusDefinition.SUCCESS]) @property def people_registered(self): # Number of people that where accepted for tasks of this project. counts = self.task_set.filter( status__in=['open', 'in_progress', 'realized'], members__status__in=['accepted', 'realized']).aggregate( total=Count('members'), externals=Sum('members__externals')) # If there are no members, externals is None return 0 return counts['total'] + (counts['externals'] or 0) @property def people_needed(self): # People still needed for tasks of this project. # This can only be tasks that are open en in the future. requested = self.task_set.filter( status='open', deadline__gt=now(), ).aggregate(total=Sum('people_needed'))['total'] or 0 counts = self.task_set.filter(status='open', members__status__in=[ 'accepted', 'realized' ]).aggregate( total=Count('members'), externals=Sum('members__externals')) return requested - counts['total'] + (counts['externals'] or 0) @property def account_bic(self): return self.account_details @account_bic.setter def account_bic(self, value): self.account_details = value _initial_status = None class Meta: abstract = True ordering = ['title'] verbose_name = _('project') verbose_name_plural = _('projects') def __unicode__(self): return self.slug if not self.title else self.title def get_amount_total(self, status_in=None): """ Calculate the total (real time) amount of money for donations, filtered by status. """ if self.amount_asked.amount == 0: # No money asked, return 0 return 0 donations = self.donation_set.all() if status_in: donations = donations.filter(order__status__in=status_in) total = donations.aggregate(sum=Sum('amount')) if not total['sum']: # No donations, manually set amount to 0 return 0 return total['sum'] @property def editable(self): return self.status.editable @property def viewable(self): return self.status.viewable def set_status(self, phase_slug, save=True): self.status = ProjectPhase.objects.get(slug=phase_slug) if save: self.save() @property def region(self): try: return self.country.subregion.region except AttributeError: return None @cached_property def funding(self): """ Return the amount of people funding this project """ return self.donation_set.filter(order__status__in=[ StatusDefinition.PLEDGED, StatusDefinition.PENDING, StatusDefinition.SUCCESS ]).distinct('order__user').count() @cached_property def sourcing(self): taskmembers = TaskMember.objects.filter( task__project=self, status__in=['applied', 'accepted', 'realized']).distinct('member') return taskmembers.count() @property def supporters(self): return self.funding + self.sourcing
class Funding(Activity): deadline = models.DateTimeField( _('deadline'), null=True, blank=True, help_text= _('If you enter a deadline, leave the duration field empty. This will override the duration.' )) duration = models.PositiveIntegerField( _('duration'), null=True, blank=True, help_text= _('If you enter a duration, leave the deadline field empty for it to be automatically calculated.' )) target = MoneyField(default=Money(0, 'EUR'), null=True, blank=True) amount_matching = MoneyField(default=Money(0, 'EUR'), null=True, blank=True) country = models.ForeignKey('geo.Country', null=True, blank=True) bank_account = models.ForeignKey('funding.BankAccount', null=True, blank=True, on_delete=SET_NULL) started = models.DateTimeField( _('started'), null=True, blank=True, ) needs_review = True validators = [ KYCReadyValidator, DeadlineValidator, BudgetLineValidator, TargetValidator ] @property def required_fields(self): fields = ['title', 'description', 'target', 'bank_account'] if not self.duration: fields.append('deadline') return fields class JSONAPIMeta(object): resource_name = 'activities/fundings' class Meta(object): verbose_name = _("Funding") verbose_name_plural = _("Funding Activities") permissions = ( ('api_read_funding', 'Can view funding through the API'), ('api_add_funding', 'Can add funding through the API'), ('api_change_funding', 'Can change funding through the API'), ('api_delete_funding', 'Can delete funding through the API'), ('api_read_own_funding', 'Can view own funding through the API'), ('api_add_own_funding', 'Can add own funding through the API'), ('api_change_own_funding', 'Can change own funding through the API'), ('api_delete_own_funding', 'Can delete own funding through the API'), ) def update_amounts(self): cache_key = '{}.{}.amount_donated'.format( connection.tenant.schema_name, self.id) cache.delete(cache_key) cache_key = '{}.{}.genuine_amount_donated'.format( connection.tenant.schema_name, self.id) cache.delete(cache_key) @property def contribution_date(self): return self.deadline @property def donations(self): return self.contributions.instance_of(Donation) @property def amount_donated(self): """ The sum of all contributions (donations) converted to the targets currency """ from .states import DonationStateMachine from bluebottle.funding.utils import calculate_total cache_key = '{}.{}.amount_donated'.format( connection.tenant.schema_name, self.id) total = cache.get(cache_key) if not total: donations = self.donations.filter(status__in=( DonationStateMachine.succeeded.value, DonationStateMachine.activity_refunded.value, )) if self.target and self.target.currency: total = calculate_total(donations, self.target.currency) else: total = calculate_total(donations, properties.DEFAULT_CURRENCY) cache.set(cache_key, total) return total @property def genuine_amount_donated(self): """ The sum of all contributions (donations) without pledges converted to the targets currency """ from .states import DonationStateMachine from bluebottle.funding.utils import calculate_total cache_key = '{}.{}.genuine_amount_donated'.format( connection.tenant.schema_name, self.id) total = cache.get(cache_key) if not total: donations = self.donations.filter( status__in=( DonationStateMachine.succeeded.value, DonationStateMachine.activity_refunded.value, ), donation__payment__pledgepayment__isnull=True) if self.target and self.target.currency: total = calculate_total(donations, self.target.currency) else: total = calculate_total(donations, properties.DEFAULT_CURRENCY) cache.set(cache_key, total) return total @cached_property def amount_pledged(self): """ The sum of all contributions (donations) converted to the targets currency """ from .states import DonationStateMachine from bluebottle.funding.utils import calculate_total donations = self.donations.filter( status__in=( DonationStateMachine.succeeded.value, DonationStateMachine.activity_refunded.value, ), donation__payment__pledgepayment__isnull=False) if self.target and self.target.currency: total = calculate_total(donations, self.target.currency) else: total = calculate_total(donations, properties.DEFAULT_CURRENCY) return total @property def amount_raised(self): """ The sum of amount donated + amount matching """ if self.target: currency = self.target.currency else: currency = 'EUR' total = self.amount_donated if self.amount_matching: total += convert(self.amount_matching, currency) return total @property def stats(self): from .states import DonationStateMachine stats = self.donations.filter( status=DonationStateMachine.succeeded.value).aggregate( count=Count('user__id')) stats['amount'] = { 'amount': self.amount_raised.amount, 'currency': str(self.amount_raised.currency) } return stats def save(self, *args, **kwargs): for reward in self.rewards.all(): if not reward.amount.currency == self.target.currency: reward.amount = Money(reward.amount.amount, self.target.currency) reward.save() for line in self.budget_lines.all(): if self.target and not line.amount.currency == self.target.currency: line.amount = Money(line.amount.amount, self.target.currency) line.save() super(Funding, self).save(*args, **kwargs)
class BaseOrder(models.Model, FSMTransition): """ An Order is a collection of Donations with one or more OrderPayments referring to it. """ # Mapping the Order Payment Status to the Order Status STATUS_MAPPING = { StatusDefinition.CREATED: StatusDefinition.LOCKED, StatusDefinition.STARTED: StatusDefinition.LOCKED, StatusDefinition.PLEDGED: StatusDefinition.PLEDGED, StatusDefinition.AUTHORIZED: StatusDefinition.PENDING, StatusDefinition.SETTLED: StatusDefinition.SUCCESS, StatusDefinition.CHARGED_BACK: StatusDefinition.FAILED, StatusDefinition.REFUND_REQUESTED: StatusDefinition.REFUND_REQUESTED, StatusDefinition.REFUNDED: StatusDefinition.REFUNDED, StatusDefinition.FAILED: StatusDefinition.FAILED, StatusDefinition.UNKNOWN: StatusDefinition.FAILED } STATUS_CHOICES = ( (StatusDefinition.CREATED, _('Created')), (StatusDefinition.LOCKED, _('Locked')), (StatusDefinition.PLEDGED, _('Pledged')), (StatusDefinition.PENDING, _('Pending')), (StatusDefinition.SUCCESS, _('Success')), (StatusDefinition.REFUND_REQUESTED, _('Refund requested')), (StatusDefinition.REFUNDED, _('Refunded')), (StatusDefinition.FAILED, _('Failed')), (StatusDefinition.CANCELLED, _('Cancelled')), ) user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_("user"), blank=True, null=True) status = FSMField(default=StatusDefinition.CREATED, choices=STATUS_CHOICES, protected=True) order_type = models.CharField(max_length=100, default='one-off') created = CreationDateTimeField(_("Created")) updated = ModificationDateTimeField(_("Updated")) confirmed = models.DateTimeField(_("Confirmed"), blank=True, editable=False, null=True) completed = models.DateTimeField(_("Completed"), blank=True, editable=False, null=True) total = MoneyField(_("Amount"), ) @property def owner(self): return self.user @transition(field=status, source=[ StatusDefinition.PLEDGED, StatusDefinition.CREATED, StatusDefinition.FAILED ], target=StatusDefinition.LOCKED) def locked(self): pass @transition(field=status, source=[StatusDefinition.LOCKED, StatusDefinition.CREATED], target=StatusDefinition.PLEDGED) def pledged(self): pass @transition(field=status, source=[StatusDefinition.LOCKED, StatusDefinition.FAILED], target=StatusDefinition.PENDING) def pending(self): self.confirmed = now() @transition(field=status, source=[ StatusDefinition.PENDING, StatusDefinition.LOCKED, StatusDefinition.FAILED, StatusDefinition.REFUND_REQUESTED, StatusDefinition.REFUNDED ], target=StatusDefinition.SUCCESS) def success(self): if not self.confirmed: self.confirmed = now() self.completed = now() @transition(field=status, source=[ StatusDefinition.CREATED, StatusDefinition.LOCKED, StatusDefinition.PENDING, StatusDefinition.SUCCESS ], target=StatusDefinition.FAILED) def failed(self): self.completed = None self.confirmed = None @transition(field=status, source=[ StatusDefinition.PENDING, StatusDefinition.SUCCESS, StatusDefinition.PLEDGED, StatusDefinition.FAILED, StatusDefinition.REFUND_REQUESTED ], target=StatusDefinition.REFUNDED) def refunded(self): pass @transition(field=status, source=[ StatusDefinition.PENDING, StatusDefinition.SUCCESS, StatusDefinition.PLEDGED, StatusDefinition.FAILED, ], target=StatusDefinition.REFUND_REQUESTED) def refund_requested(self): pass @transition(field=status, source=[ StatusDefinition.REFUNDED, ], target=StatusDefinition.CANCELLED) def cancelled(self): pass def update_total(self, save=True): donations = Donation.objects.filter(order=self, amount__gt=0).\ values('amount_currency').annotate(Sum('amount')).order_by() total = [ Money(data['amount__sum'], data['amount_currency']) for data in donations ] if len(total) > 1: raise ValueError('Multiple currencies in one order') if len(total) == 1: self.total = total[0] if save: self.save() def get_status_mapping(self, order_payment_status): return self.STATUS_MAPPING.get(order_payment_status, StatusDefinition.FAILED) def set_status(self, status, save=True): self.status = status if save: self.save() def process_order_payment_status_change(self, order_payment, **kwargs): # Get the mapped status OrderPayment to Order new_order_status = self.get_status_mapping(kwargs['target']) successful_payments = self.order_payments.\ filter(status__in=['settled', 'authorized']).\ exclude(id=order_payment.id).count() # If this order has other order_payments that were successful it should no change status if successful_payments: pass else: self.transition_to(new_order_status) def __unicode__(self): return "{0} : {1}".format(self.id, self.created) def get_latest_order_payment(self): if self.order_payments.count(): return self.order_payments.order_by('-created').all()[0] return None @property def order_payment(self): return self.get_latest_order_payment() class Meta: abstract = True permissions = ( ('api_read_order', 'Can view order through the API'), ('api_add_order', 'Can add order through the API'), ('api_change_order', 'Can change order through the API'), ('api_delete_order', 'Can delete order through the API'), ('api_read_own_order', 'Can view own order through the API'), ('api_add_own_order', 'Can add own order through the API'), ('api_change_own_order', 'Can change own order through the API'), ('api_delete_own_order', 'Can delete own order through the API'), ) verbose_name = _('order') verbose_name_plural = _('orders')
class OrderPayment(models.Model, FSMTransition): """ An order is a collection of OrderItems and vouchers with a connected payment. """ STATUS_CHOICES = Payment.STATUS_CHOICES user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_("user"), blank=True, null=True) order = models.ForeignKey('orders.Order', related_name='order_payments') status = FSMField(default=StatusDefinition.CREATED, choices=STATUS_CHOICES, protected=True) created = CreationDateTimeField(_("Created")) updated = ModificationDateTimeField(_("Updated")) closed = models.DateTimeField(_("Closed"), blank=True, editable=False, null=True) amount = MoneyField(_("Amount")) transaction_fee = models.DecimalField( _("Transaction Fee"), max_digits=16, decimal_places=2, null=True, help_text=_("Bank & transaction fee, withheld by payment provider.")) # Payment method used payment_method = models.CharField(max_length=60, default='', blank=True) integration_data = JSONField(_("Integration data"), max_length=5000, blank=True) authorization_action = models.OneToOneField( OrderPaymentAction, verbose_name=_("Authorization action"), null=True) previous_status = None card_data = None class Meta: permissions = (('refund_orderpayment', 'Can refund order payments'), ) verbose_name = _('order payment') verbose_name_plural = _('order payments') @classmethod def get_latest_by_order(cls, order): order_payments = cls.objects.order_by('-created').filter( order=order).all() if len(order_payments) > 0: return order_payments[0] return None @transition(field=status, source=StatusDefinition.CREATED, target=StatusDefinition.STARTED) def started(self): pass @transition(field=status, source=StatusDefinition.CREATED, target=StatusDefinition.PLEDGED) def pledged(self): pass @transition(field=status, source=[ StatusDefinition.STARTED, StatusDefinition.CANCELLED, StatusDefinition.FAILED ], target=StatusDefinition.AUTHORIZED) def authorized(self): pass @transition(field=status, source=[ StatusDefinition.AUTHORIZED, StatusDefinition.STARTED, StatusDefinition.CANCELLED, StatusDefinition.REFUNDED, StatusDefinition.REFUND_REQUESTED, StatusDefinition.FAILED, StatusDefinition.UNKNOWN ], target=StatusDefinition.SETTLED) def settled(self): self.closed = now() @transition(field=status, source=[ StatusDefinition.STARTED, StatusDefinition.AUTHORIZED, StatusDefinition.REFUND_REQUESTED, StatusDefinition.REFUNDED, StatusDefinition.CANCELLED, StatusDefinition.SETTLED ], target=StatusDefinition.FAILED) def failed(self): self.closed = None @transition(field=status, source=[StatusDefinition.STARTED, StatusDefinition.FAILED], target=StatusDefinition.CANCELLED) def cancelled(self): pass @transition(field=status, source=[StatusDefinition.AUTHORIZED, StatusDefinition.SETTLED], target=StatusDefinition.CHARGED_BACK) def charged_back(self): self.closed = None @transition(field=status, source=[ StatusDefinition.AUTHORIZED, StatusDefinition.SETTLED, StatusDefinition.REFUND_REQUESTED, StatusDefinition.PLEDGED ], target=StatusDefinition.REFUNDED) def refunded(self): self.closed = None @transition(field=status, source=[ StatusDefinition.STARTED, StatusDefinition.AUTHORIZED, StatusDefinition.SETTLED ], target=StatusDefinition.UNKNOWN) def unknown(self): pass @transition(field=status, source=[StatusDefinition.AUTHORIZED, StatusDefinition.SETTLED], target=StatusDefinition.REFUND_REQUESTED) def refund_requested(self): pass def get_status_mapping(self, payment_status): # Currently the status in Payment and OrderPayment is one to one. return payment_status def set_authorization_action(self, action, save=True): self.authorization_action = OrderPaymentAction(**action) self.authorization_action.save() if save: self.save() @property def status_code(self): try: return self.payment.status_code except Payment.DoesNotExist: return "" @property def status_description(self): try: return self.payment.status_description except Payment.DoesNotExist: return "" @property def can_refund(self): return self.status in ( 'settled', 'success', ) @property def info_text(self): """ The description on the payment receipt. """ tenant_url = clients.utils.tenant_site().domain docdata_max_length = 50 if tenant_url == 'onepercentclub.com': info_text = _('%(tenant_url)s donation %(payment_id)s') # 10 chars for ' donation ' and 6 chars for the payment id max_tenant_chars = docdata_max_length - 10 - len(str(self.id)) else: info_text = _('%(tenant_url)s via goodup %(payment_id)s') # 20 chars for ' via onepercentclub ' and 6 chars # for the payment id max_tenant_chars = docdata_max_length - 20 - len(str(self.id)) length = len(tenant_url) if length > max_tenant_chars: # Note that trimming the url will change the translation string # This change will occur when the payment id adds a number, so # every now and then we'll need to update the transstring. tenant_url = trim_tenant_url(max_tenant_chars, tenant_url) return info_text % {'tenant_url': tenant_url, 'payment_id': self.id} def save(self, force_insert=False, force_update=False, using=None, update_fields=None): self.amount = self.order.total self.card_data = self.integration_data self.integration_data = {} if self.id: # If the payment method has changed we should recalculate the fee. try: self.transaction_fee = self.payment.get_fee() except ObjectDoesNotExist: pass super(OrderPayment, self).save(force_insert, force_update, using, update_fields)