def create_payout_finished_project(sender, instance, created, **kwargs): """ Create or update Payout for finished projects. Project finish when deadline is hit or when it's changed manually in admin. """ from localflavor.generic.validators import IBANValidator PROJECT_PAYOUT_MODEL = get_project_payout_model() project = instance if project.status.slug in ['done-complete', 'done-incomplete'] \ and project.amount_asked: PROJECT_PAYOUT_MODEL = get_project_payout_model() next_date = PROJECT_PAYOUT_MODEL.get_next_planned_date() try: # Update existing Payout payout = PROJECT_PAYOUT_MODEL.objects.get(project=project, protected=False) if payout.status == StatusDefinition.NEW: # Update planned payout date for new Payouts payout.calculate_amounts() payout.planned = next_date payout.save() except PROJECT_PAYOUT_MODEL.DoesNotExist: if project.campaign_started: # Create new Payout payout = PROJECT_PAYOUT_MODEL( planned=next_date, project=project ) # Calculate amounts payout.calculate_amounts() if project.is_closed: payout.status = StatusDefinition.SETTLED # Set payment details try: IBANValidator()(project.account_number) payout.receiver_account_iban = project.account_number except ValidationError as e: logger.info( "IBAN error payout {0}, project: {1}: {2}".format( payout.id, project.id, e.message)) payout.receiver_account_bic = project.account_bic payout.receiver_account_number = project.account_number payout.receiver_account_name = project.account_holder_name payout.receiver_account_city = project.account_holder_city payout.receiver_account_country = project.account_bank_country payout.save()
def create_payout_finished_project(sender, instance, created, **kwargs): """ Create or update Payout for finished projects. Project finish when deadline is hit or when it's changed manually in admin. """ from localflavor.generic.validators import IBANValidator project = instance now = timezone.now() if (project.is_realised or project.is_closed) and project.amount_asked: with LocalTenant(): if now.day <= 15: next_date = timezone.datetime(now.year, now.month, 15) else: next_date = timezone.datetime(now.year, now.month, 1) + timedelta(days=20) PROJECT_PAYOUT_MODEL = get_project_payout_model() try: # Update existing Payout payout = PROJECT_PAYOUT_MODEL.objects.get(project=project) if payout.status == StatusDefinition.NEW: # Update planned payout date for new Payouts payout.calculate_amounts() payout.planned = next_date payout.save() except PROJECT_PAYOUT_MODEL.DoesNotExist: if project.campaign_started: # Create new Payout payout = PROJECT_PAYOUT_MODEL( planned=next_date, project=project ) # Calculate amounts payout.calculate_amounts() if project.is_closed: payout.status = StatusDefinition.SETTLED # Set payment details try: IBANValidator()(project.account_number) payout.receiver_account_iban = project.account_number except ValidationError as e: logger.info("IBAN error for payout id {0} and project id: {1}: {2}".format(payout.id, project.id, e.message)) payout.receiver_account_bic = project.account_bic payout.receiver_account_number = project.account_number payout.receiver_account_name = project.account_holder_name payout.receiver_account_city = project.account_holder_city payout.receiver_account_country = project.account_bank_country payout.save()
def create_payout_finished_project(sender, instance, created, **kwargs): """ Create or update Payout for finished projects. Project finish when deadline is hit or when it's changed manually in admin. """ project = instance now = timezone.now() if project.is_realised and project.amount_asked: if now.day <= 15: next_date = timezone.datetime(now.year, now.month, 15) else: next_date = timezone.datetime(now.year, now.month, 1) + timedelta(days=20) PROJECT_PAYOUT_MODEL = get_project_payout_model() try: # Update existing Payout payout = PROJECT_PAYOUT_MODEL.objects.get(project=project) if payout.status == StatusDefinition.NEW: # Update planned payout date for new Payouts payout.calculate_amounts() payout.planned = next_date payout.save() except PROJECT_PAYOUT_MODEL.DoesNotExist: # Create new Payout payout = PROJECT_PAYOUT_MODEL( planned=next_date, project=project ) # Calculate amounts payout.calculate_amounts() # Set payment details organization = project.organization payout.receiver_account_bic = organization.account_bic payout.receiver_account_iban = organization.account_iban payout.receiver_account_number = organization.account_number payout.receiver_account_name = organization.account_holder_name payout.receiver_account_city = organization.account_holder_city payout.receiver_account_country = organization.account_bank_country # Generate invoice reference, saves twice payout.update_invoice_reference(auto_save=True)
def get_affected_records(self): """ Retrieve the donations and payouts that will be affected by taking a cut. """ ProjectPayout = get_project_payout_model() affected = {} donations = self.object.local_payment.order_payment.order.donations.select_related( "project", "project__projectpayout" ) for donation in donations: payouts = donation.project.projectpayout_set.all() in_progress_payout, updateable_payout, new_payout, new_status = None, None, None, False # defaults if not payouts.exists(): new_status = donation.order.get_status_mapping(self.object.payment_type) else: updateable_payout = payouts.filter(status=StatusDefinition.NEW, protected=False).first() if updateable_payout is not None: new_status = donation.order.get_status_mapping(self.object.payment_type) else: # figure out if we can take cut from an unprocessed payout in_progress_payout = payouts.filter(status=StatusDefinition.IN_PROGRESS).first() if in_progress_payout is None: new_payout = ProjectPayout( project=donation.project, protected=True, amount_raised=0, amount_payable=0, organization_fee=-donation.amount, planned=ProjectPayout.get_next_planned_date(), description_line1="Taking cut from organization fees", description_line2="from failed payment %d" % self.object.local_payment.pk, ) affected[donation] = { "new_status": new_status, "updateable_payout": updateable_payout, "in_progress_payout": in_progress_payout, "new_payout": new_payout, } return affected
def _get_organization_fee(self): """ Calculate and return the organization fee for Payouts within this OrganizationPayout's period, including VAT. Note: this should *only* be called internally. """ PROJECT_PAYOUT_MODEL = get_project_payout_model() # Get Payouts payouts = PROJECT_PAYOUT_MODEL.objects.filter( completed__gte=self.start_date, completed__lte=self.end_date ) # Aggregate value aggregate = payouts.aggregate(models.Sum('organization_fee')) # Return aggregated value or 0.00 fee = aggregate.get( 'organization_fee__sum', decimal.Decimal('0.00') ) or decimal.Decimal('0.00') return fee
def get_queryset(self): """ Ensure that only the complex 'take cut' RDP's are considered. Exclude RDP's that only have payouts that are new and unprotected, as these will automatically be updated by re-calculating the payout. """ # find RDP's that have project payouts and a 'bad' status queryset = RemoteDocdataPayment.objects.filter( status=RemoteDocdataPayment.IntegrityStatus.InconsistentChargeback, local_payment__order_payment__order__donations__project__projectpayout__isnull=False, ) payout_ids = list( queryset.values_list( "local_payment__order_payment__order__donations__project__projectpayout", flat=True ).distinct() ) if not payout_ids: messages.warn(_("There were no payouts for this payment - aborting.")) ProjectPayout = get_project_payout_model() payouts_to_ignore = list( ProjectPayout.objects.filter(id__in=payout_ids) .exclude(status=StatusDefinition.NEW, protected=False) .values_list("id", flat=True) ) for pk in payouts_to_ignore: payout_ids.remove(pk) return queryset.filter( local_payment__order_payment__order__donations__project__projectpayout__id__in=payout_ids ).distinct()
import decimal from django.contrib import admin from django.core.exceptions import PermissionDenied from django.http import HttpResponse from django.utils import timezone from django.utils.translation import ugettext as _ from django.utils.text import Truncator from .models import ProjectPayoutLog, OrganizationPayoutLog from .admin_filters import HasIBANPayoutFilter from .admin_utils import link_to from django import forms PROJECT_PAYOUT_MODEL = get_project_payout_model() ORGANIZATION_PAYOUT_MODEL = get_organization_payout_model() MODEL_MAP = get_model_mapping() class PayoutLogBase(admin.TabularInline): extra = 0 max_num = 0 can_delete = False fields = ['created', 'old_status', 'new_status'] readonly_fields = fields class PayoutLogInline(PayoutLogBase): model = ProjectPayoutLog
def form_valid(self, form): with db_transaction.atomic(): order = get_order_model().objects.create( user=self.request.user, order_type="manual", total=form.cleaned_data["amount"] ) form.instance.order = order form.instance.anonymous = True self.object = donation = form.save() order_payment = OrderPayment.objects.create( user=self.request.user, order=order, amount=donation.amount, payment_method="manual" ) payment = ManualPayment.objects.create( amount=donation.amount, transaction=self.transaction, user=self.request.user, order_payment=order_payment, ) # pull us through the statuses to consider it done payment.status = StatusDefinition.AUTHORIZED payment.save() payment.status = StatusDefinition.SETTLED payment.save() # update/create the required payout ProjectPayout = get_project_payout_model() project = donation.project project.update_amounts() payouts = ProjectPayout.objects.filter(project=project) # check the payouts and only update 'new' payouts, else create a new payout if payouts.exists(): updateable = payouts.filter(status=StatusDefinition.NEW).first() # only new payouts can be updated if updateable is None: rules = payouts.values_list("payout_rule", flat=True).distinct() if len(rules) == 1: rule = rules[0] _message = messages.success msg = _("Created a new project payout with payment rule {rule}") else: _message = messages.warning msg = _("There were {n} payout rules, the choosen rule was: '{rule}'") # create a new payout, since the other payouts are on their way for processing and can't be altered payout = ProjectPayout( planned=ProjectPayout.get_next_planned_date(), project=project, payout_rule=rule, protected=True ) # we need to manually calculate the amounts, else all project donations will be taken into account # FIXME: this needs to be refactored on the BB_PAYOUT level! calculator = payout.get_calculator() payout.calculate_payable_and_fee(calculator, donation.amount) payout.save() rule = dict(ProjectPayout.PayoutRules.choices)[rule] _message(self.request, msg.format(n=len(rules), rule=rule)) else: # there is a payout that is still 'new', update that one if updateable.protected is False: updateable.calculate_amounts() else: # this is already an 'irregular' payout, so add the diff # manually. There should be a journal entry for the modification updateable.amount_raised += donation.amount calculator = updateable.get_calculator() updateable.calculate_payable_and_fee(calculator, updateable.get_amount_raised()) updateable.save() messages.success( self.request, _("Created a manual donation and updated project payout %r") % updateable ) # NOTE theoretically there is a situation where the only updateable # payout is a payout for an open project and the payout is protected # It would then theoretically be possible to make a new donation via # the web interface and the project payout will not be re-calculateable self.transaction.status = BankTransaction.IntegrityStatus.Valid self.transaction.save() # TODO FIXME: organization payouts?! recalculate? return redirect(self.get_success_url())