From cf13b1859c962a735645060e8e9cc355c13f3201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Caux?= <git@jscaux.org> Date: Mon, 18 Mar 2024 07:07:21 +0100 Subject: [PATCH] Improve Subsidy allocation logic --- scipost_django/finances/admin.py | 20 ++++++++++++ scipost_django/finances/allocate.py | 31 ++++++++++--------- scipost_django/finances/managers.py | 8 +++++ scipost_django/finances/models/pubfrac.py | 4 +++ scipost_django/finances/models/subsidy.py | 21 +++++++++++-- .../templates/finances/_subsidy_details.html | 6 ++-- 6 files changed, 71 insertions(+), 19 deletions(-) diff --git a/scipost_django/finances/admin.py b/scipost_django/finances/admin.py index 1d09bb7a5..01e921370 100644 --- a/scipost_django/finances/admin.py +++ b/scipost_django/finances/admin.py @@ -29,6 +29,18 @@ class SubsidyAttachmentInline(admin.TabularInline): @admin.register(Subsidy) class SubsidyAdmin(admin.ModelAdmin): + list_display = [ + "organization_name_short", + "orgtype_display", + "amount", + "status", + "date_from", + "date_until", + "total_compensations", + ] + list_filter = [ + "organization__orgtype", + ] inlines = [ SubsidyPaymentInline, SubsidyAttachmentInline, @@ -43,6 +55,14 @@ class SubsidyAdmin(admin.ModelAdmin): "organization__acronym", ] + @admin.display(description="org name short") + def organization_name_short(self, obj): + return obj.organization.name[:40] + + @admin.display(description='org type') + def orgtype_display(self, obj): + return obj.organization.get_orgtype_display() + @admin.register(SubsidyAttachment) class SubsidyAttachmentAdmin(admin.ModelAdmin): diff --git a/scipost_django/finances/allocate.py b/scipost_django/finances/allocate.py index 6284cfeb3..3ecdd65ca 100644 --- a/scipost_django/finances/allocate.py +++ b/scipost_django/finances/allocate.py @@ -3,6 +3,7 @@ __license__ = "AGPL v3" from .models import Subsidy, PubFrac +from .managers import PubFracQuerySet """ @@ -34,21 +35,26 @@ The algorithms are implemented in the following order, """ +def compensate(subsidy: Subsidy, pubfracs: PubFracQuerySet): + """ + Allocate subsidy to unallocated pubfracs in queryset, up to depletion. + """ + for pubfrac in pubfracs.uncompensated(): + if pubfrac.cf_value <= subsidy.remainder: + pubfrac.compensated_by = subsidy + pubfrac.save() + + def allocate_to_any_aff(subsidy: Subsidy): """ Allocate to PubFracs with affiliation to Subsidy-giver. """ max_year = subsidy.date_until.year if subsidy.date_until else subsidy.date_from.year - uncompensated_pubfracs = PubFrac.objects.filter( - organization=subsidy.organization, + pubfracs = subsidy.organization.pubfracs.filter( publication__publication_date__year__gte=subsidy.date_from.year, publication__publication_date__year__lte=max_year, - compensated_by__isnull=True, ) - for pubfrac in uncompensated_pubfracs.all(): - if pubfrac.cf_value <= subsidy.remainder: - pubfrac.compensated_by = subsidy - pubfrac.save() + compensate(subsidy, pubfracs) def allocate_to_all_aff(subsidy: Subsidy): @@ -56,18 +62,13 @@ def allocate_to_all_aff(subsidy: Subsidy): Allocate to all PubFracs of Publications with at least one aff to Subsidy-giver. """ max_year = subsidy.date_until.year if subsidy.date_until else subsidy.date_from.year - uncompensated_pubfracs = PubFrac.objects.filter( - organization=subsidy.organization, + pubfracs = subsidy.organization.pubfracs.filter( publication__publication_date__year__gte=subsidy.date_from.year, publication__publication_date__year__lte=max_year, ) - for pubfrac in uncompensated_pubfracs.all(): + for pubfrac in pubfracs.all(): # retrieve all uncompensated PubFracs for the relevant Publication pubfracs_for_pub = PubFrac.objects.filter( publication__doi_label=pubfrac.publication.doi_label, - compensated_by__isnull=True, ) - for pf in pubfracs_for_pub.all(): - if pf.cf_value <= subsidy.remainder: - pf.compensated_by = subsidy - pf.save() + compensate(subsidy, pubfracs_for_pub) diff --git a/scipost_django/finances/managers.py b/scipost_django/finances/managers.py index cf400aa15..d2adcc17a 100644 --- a/scipost_django/finances/managers.py +++ b/scipost_django/finances/managers.py @@ -32,3 +32,11 @@ class SubsidyAttachmentQuerySet(models.QuerySet): def orphaned(self): return self.filter(subsidy__isnull=True) + + +class PubFracQuerySet(models.QuerySet): + def uncompensated(self): + return self.filter(compensated_by__isnull=True) + + def compensated(self): + return self.exclude(compensated_by__isnull=True) diff --git a/scipost_django/finances/models/pubfrac.py b/scipost_django/finances/models/pubfrac.py index d347408e7..cdfa9ff45 100644 --- a/scipost_django/finances/models/pubfrac.py +++ b/scipost_django/finances/models/pubfrac.py @@ -6,6 +6,8 @@ from decimal import Decimal from django.db import models +from ..managers import PubFracQuerySet + class PubFrac(models.Model): """ @@ -43,6 +45,8 @@ class PubFrac(models.Model): max_digits=16, decimal_places=3, blank=True, null=True ) + objects = PubFracQuerySet.as_manager() + class Meta: unique_together = (("organization", "publication"),) verbose_name = "PubFrac" diff --git a/scipost_django/finances/models/subsidy.py b/scipost_django/finances/models/subsidy.py index fb42e1354..17d9e6733 100644 --- a/scipost_django/finances/models/subsidy.py +++ b/scipost_django/finances/models/subsidy.py @@ -10,8 +10,13 @@ from django.db.models import Sum from django.urls import reverse from django.utils.html import format_html -from ..constants import SUBSIDY_TYPES, SUBSIDY_TYPE_SPONSORSHIPAGREEMENT, SUBSIDY_STATUS -from ..managers import SubsidyQuerySet +from ..constants import ( + SUBSIDY_TYPES, + SUBSIDY_TYPE_SPONSORSHIPAGREEMENT, + SUBSIDY_STATUS, + SUBSIDY_WITHDRAWN, +) +from ..managers import SubsidyQuerySet, PubFracQuerySet class Subsidy(models.Model): @@ -180,6 +185,18 @@ class Subsidy(models.Model): """ return self.amount == self.payments.aggregate(Sum("amount"))["amount__sum"] + @property + def allocatable(self): + """ + Determine whether this Subsidy can be allocated. + """ + implemented_algorithms = [self.ALGORITHM_ANY_AFF, self.ALGORITHM_ALL_AFF] + return ( + self.status != SUBSIDY_WITHDRAWN + and self.algorithm != self.ALGORITHM_RESERVES + and self.algorithm in implemented_algorithms + ) + def allocate(self): """ Allocate the funds according to the algorithm specific by the instance. diff --git a/scipost_django/finances/templates/finances/_subsidy_details.html b/scipost_django/finances/templates/finances/_subsidy_details.html index 1c3df29fb..6722e3012 100644 --- a/scipost_django/finances/templates/finances/_subsidy_details.html +++ b/scipost_django/finances/templates/finances/_subsidy_details.html @@ -116,8 +116,10 @@ <h3 class="highlight">Expenditures compensated by this Subsidy</h3> - {% if 'edadmin' in user_roles %} - <a class="btn btn-primary" href="{% url 'finances:allocate_subsidy' subsidy_id=subsidy.id %}">Allocate this Subsidy</a> + {% if 'finadmin' in user_roles %} + {% if subsidy.allocatable %} + <a class="btn btn-primary" href="{% url 'finances:allocate_subsidy' subsidy_id=subsidy.id %}">Allocate this Subsidy</a> + {% endif %} {% endif %} <div class="row"> -- GitLab