예제 #1
0
파일: models.py 프로젝트: jayvdb/bluebottle
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'
예제 #2
0
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)
예제 #3
0
파일: models.py 프로젝트: raux/bluebottle
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)
예제 #4
0
파일: models.py 프로젝트: raux/bluebottle
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')
예제 #5
0
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')
예제 #6
0
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'))
예제 #7
0
파일: models.py 프로젝트: jayvdb/bluebottle
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)
예제 #8
0
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)
예제 #9
0
파일: models.py 프로젝트: raux/bluebottle
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
예제 #10
0
파일: models.py 프로젝트: jayvdb/bluebottle
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)
예제 #11
0
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')
예제 #12
0
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)