diff --git a/scipost_django/finances/admin.py b/scipost_django/finances/admin.py index a56da3b0247c69fff534afb67b55a2caab38ac27..4311d246c0c3a47a58ca50ba5dd87b4f4fd6ed74 100644 --- a/scipost_django/finances/admin.py +++ b/scipost_django/finances/admin.py @@ -9,8 +9,6 @@ from .models import ( SubsidyPayment, SubsidyAttachment, PubFrac, - PubFracCompensation, - PublicationExpenditureCoverage, WorkLog, PeriodicReportType, PeriodicReport, @@ -79,6 +77,7 @@ class PubFracAdmin(admin.ModelAdmin): autocomplete_fields = [ "organization", "publication", + "compensated_by", ] search_fields = [ "publication__doi_label", @@ -92,54 +91,6 @@ class PubFracAdmin(admin.ModelAdmin): return (obj.publication.doi_label) -@admin.register(PubFracCompensation) -class PubFracCompensationAdmin(admin.ModelAdmin): - list_display = [ - "subsidy", - "doi_label_display", - "amount", - ] - autocomplete_fields = [ - "subsidy", - "pubfrac", - ] - search_fields = [ - "subsidy", - "pubfrac__organization__name", - "pubfrac__organization__name_original", - "pubfrac__organization__acronym", - "pubfrac__publication__doi_label", - ] - - @admin.display(description='doi label') - def doi_label_display(self, obj): - return (obj.pubfrac.publication.doi_label) - - -@admin.register(PublicationExpenditureCoverage) -class PublicationExpenditureCoverageAdmin(admin.ModelAdmin): - list_display = [ - "subsidy", - "doi_label_display", - "amount", - ] - autocomplete_fields = [ - "subsidy", - "publication", - ] - search_fields = [ - "subsidy", - "subsidy__organization__name", - "subsidy__organization__name_original", - "subsidy__organization__acronym", - "publication__doi_label", - ] - - @admin.display(description='doi label') - def doi_label_display(self, obj): - return (obj.publication.doi_label) - - @admin.register(WorkLog) class WorkLogAdmin(admin.ModelAdmin): autocomplete_fields = ["user"] diff --git a/scipost_django/finances/migrations/0041_remove_publicationexpenditurecoverage_publication_and_more.py b/scipost_django/finances/migrations/0041_remove_publicationexpenditurecoverage_publication_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..dfad70c26f0d21e58fc49e031275b90621f37ebf --- /dev/null +++ b/scipost_django/finances/migrations/0041_remove_publicationexpenditurecoverage_publication_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.10 on 2024-03-16 09:09 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("finances", "0040_alter_pubfraccompensation_options_and_more"), + ] + + operations = [ + migrations.RemoveField( + model_name="publicationexpenditurecoverage", + name="publication", + ), + migrations.RemoveField( + model_name="publicationexpenditurecoverage", + name="subsidy", + ), + migrations.AddField( + model_name="pubfrac", + name="compensated_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="compensated_pubfracs", + to="finances.subsidy", + ), + ), + migrations.DeleteModel( + name="PubFracCompensation", + ), + migrations.DeleteModel( + name="PublicationExpenditureCoverage", + ), + ] diff --git a/scipost_django/finances/models/__init__.py b/scipost_django/finances/models/__init__.py index c1669c99e7df453c2f0a73ee77c213dce8a80170..a3406bafcfe908dd22e605c39a4ea3d3658c457e 100644 --- a/scipost_django/finances/models/__init__.py +++ b/scipost_django/finances/models/__init__.py @@ -8,12 +8,8 @@ from .periodic_report import ( PeriodicReport, ) -from .pex_coverage import PublicationExpenditureCoverage - from .pubfrac import PubFrac -from .pubfrac_compensation import PubFracCompensation - from .subsidy import Subsidy from .subsidy_payment import SubsidyPayment diff --git a/scipost_django/finances/models/pex_coverage.py b/scipost_django/finances/models/pex_coverage.py deleted file mode 100644 index cdc846b7bd09e3b91e3254929e20109b1555dec0..0000000000000000000000000000000000000000 --- a/scipost_django/finances/models/pex_coverage.py +++ /dev/null @@ -1,44 +0,0 @@ -__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" -__license__ = "AGPL v3" - - -from django.db import models - - -class PublicationExpenditureCoverage(models.Model): - """ - An amount from a Subsidy which is ascribed to a Publication as expenditure coverage. - - A Coverage is applied to a Publication as a whole, not to individual PubFracs. - This class thus complements PubFracCompensation, which compensates costs - at the PubFrac level. - """ - - subsidy = models.ForeignKey( - "finances.Subsidy", - related_name="pex_coverages", - on_delete=models.CASCADE, - ) - - publication = models.ForeignKey( - "journals.Publication", - related_name="pex_coverages", - on_delete=models.CASCADE, - ) - - amount = models.PositiveIntegerField() - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=["subsidy", "publication"], name="unique_subsidy_publication" - ), - ] - verbose_name = "PEX coverage" - verbose_name_plural = "PEX coverages" - - def __str__(self): - return ( - f"€{self.amount} for {self.publication.doi_label} " - f"from {self.subsidy.organization}" - ) diff --git a/scipost_django/finances/models/pubfrac.py b/scipost_django/finances/models/pubfrac.py index b4c9015c048f8008af2038bfca8cbbf390f23708..3cd0f66dbf5812f8195aa8aaeab3f8d734862667 100644 --- a/scipost_django/finances/models/pubfrac.py +++ b/scipost_django/finances/models/pubfrac.py @@ -32,6 +32,13 @@ class PubFrac(models.Model): fraction = models.DecimalField( max_digits=4, decimal_places=3, default=Decimal("0.000") ) + compensated_by = models.ForeignKey( + "finances.Subsidy", + related_name="compensated_pubfracs", + blank=True, + null=True, + on_delete=models.CASCADE, + ) # Calculated field cf_value = models.PositiveIntegerField(blank=True, null=True) @@ -49,17 +56,7 @@ class PubFrac(models.Model): @property def compensated(self): - """Compensated part of this PubFrac.""" - return ( - self.pubfrac_compensations.aggregate(models.Sum("amount"))["amount__sum"] - if self.pubfrac_compensations.exists() - else 0 - ) - - @property - def arrears(self): - """Uncovered and uncompensated part of this PubFrac.""" - return self.cf_value - self.compensated + return self.compensated_by is not None @receiver(pre_save, sender=PubFrac) diff --git a/scipost_django/finances/models/pubfrac_compensation.py b/scipost_django/finances/models/pubfrac_compensation.py deleted file mode 100644 index 0aa5c48ab22f3c2de6c79d9d5addedc7336f2645..0000000000000000000000000000000000000000 --- a/scipost_django/finances/models/pubfrac_compensation.py +++ /dev/null @@ -1,34 +0,0 @@ -__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" -__license__ = "AGPL v3" - - -from django.db import models - - -class PubFracCompensation(models.Model): - """ - An amount from a Subsidy which ascribed to a PubFrac as compensation. - """ - - subsidy = models.ForeignKey( - "finances.Subsidy", - related_name="pubfrac_compensations", - on_delete=models.CASCADE, - ) - - pubfrac = models.ForeignKey( - "finances.PubFrac", - related_name="pubfrac_compensations", - on_delete=models.CASCADE, - ) - - amount = models.PositiveIntegerField() - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=["subsidy", "pubfrac"], name="unique_subsidy_pubfrac" - ), - ] - verbose_name = "PubFrac compensation" - verbose_name_plural = "PubFrac compensations" diff --git a/scipost_django/finances/models/subsidy.py b/scipost_django/finances/models/subsidy.py index 96912866704f906f7ec5054506e9f3eeec543c0a..e62a00cca4aabd819adf8e307204da6e3b2d03e5 100644 --- a/scipost_django/finances/models/subsidy.py +++ b/scipost_django/finances/models/subsidy.py @@ -124,22 +124,11 @@ class Subsidy(models.Model): @property def total_compensations(self): """ - Sum of the amounts of all PubFracCompensations related to this Subsidy. + Sum of the amounts of all compensations related to this Subsidy. """ return ( - self.pubfrac_compensations.aggregate(Sum("amount"))["amount__sum"] - if self.pubfrac_compensations.exists() - else 0 - ) - - @property - def total_coverages(self): - """ - Sum of the PublicationExpenditureCoverages related to this Subsidy. - """ - return ( - self.pex_coverages.aggregate(Sum("amount"))["amount__sum"] - if self.pex_coverages.exists() + self.compensated_pubfracs.aggregate(Sum("cf_value"))["cf_value__sum"] + if self.compensated_pubfracs.exists() else 0 ) @@ -148,4 +137,4 @@ class Subsidy(models.Model): """ Part of the Subsidy amount which hasn't been allocated. """ - return self.amount - self.total_compensations - self.total_coverages + return self.amount - self.total_compensations diff --git a/scipost_django/finances/templates/finances/_subsidy_details.html b/scipost_django/finances/templates/finances/_subsidy_details.html index 57de260f9543cba9894bf0bdc108220ffede9fb0..92d363fc9c1901329c603819eb8c3db623d20b26 100644 --- a/scipost_django/finances/templates/finances/_subsidy_details.html +++ b/scipost_django/finances/templates/finances/_subsidy_details.html @@ -110,27 +110,24 @@ {% if 'edadmin' in user_roles %} {% if subsidy.amount_publicly_shown %} + <h3 class="highlight">Expenditures compensated by this Subsidy</h3> <div class="row"> - <div class="col-12"> - <h3 class="highlight">Expenditures compensated or covered by this Subsidy</h3> - + <div class="col-lg-6"> <table class="table mt-2 caption-top"> <caption>PubFrac Compensations</caption> <thead class="table-light"> <tr> <th>Publication</th> <th>PubFrac value</th> - <th>PubFrac compensation</th> </tr> </thead> <tbody> - {% for compensation in subsidy.pubfrac_compensations.all %} + {% for compensated_pubfrac in subsidy.compensated_pubfracs.all %} <tr> <td> - <a href="{% url 'scipost:publication_detail' doi_label=compensation.pubfrac.publication.doi_label %}">{{ compensation.pubfrac.publication.doi_label }}</a> + <a href="{% url 'scipost:publication_detail' doi_label=compensated_pubfrac.publication.doi_label %}">{{ compensated_pubfrac.publication.doi_label }}</a> </td> - <td>€{{ compensation.pubfrac.cf_value }}</td> - <td>€{{ compensation.amount }}</td> + <td>€{{ compensated_pubfrac.cf_value }}</td> </tr> {% empty %} <tr> @@ -139,65 +136,29 @@ {% endfor %} <tr class="bg-secondary bg-opacity-10"> <th>Total compensations from this Subsidy</th> - <td></td> <td>€{{ subsidy.total_compensations }}</td> </tr> </tbody> </table> - - <table class="table caption-top"> - <caption>Expenditure Coverages</caption> - <thead class="table-light"> - <tr> - <th>Publication</th> - <th>Expenditure</th> - <th>Coverage</th> - </tr> - </thead> - <tbody> - {% for coverage in subsidy.pex_coverages.all %} - <tr> - <td> - <a href="{% url 'scipost:publication_detail' doi_label=coverage.publication.doi_label %}">{{ coverage.publication.doi_label }}</a> - </td> - <td>€{{ coverage.publication.expenditures }}</td> - <td>€{{ coverage.amount }}</td> - </tr> - {% empty %} - <tr> - <td>No Coverage defined</td> - </tr> - {% endfor %} - <tr class="bg-secondary bg-opacity-10"> - <th>Total coverages from this Subsidy</th> - <td></td> - <td>€{{ subsidy.total_coverages }}</td> - </tr> - </tbody> - </table> - + </div> + <div class="col-lg-6"> <table class="table mt-2 caption-top"> <caption>Balance</caption> <tbody> <tr> <th>Subsidy amount</th> - <td>{{ subsidy.amount }}</td> + <td>€{{ subsidy.amount }}</td> </tr> <tr> <th>Compensations</th> - <td>{{ subsidy.total_compensations }}</td> - </tr> - <tr> - <th>Coverages</th> - <td>{{ subsidy.total_coverages }}</td> + <td>€{{ subsidy.total_compensations }}</td> </tr> <tr class="bg-secondary bg-opacity-10"> <th>Remainder (allocated to reserve fund)</th> - <td>{{ subsidy.remainder }}</td> + <td>€{{ subsidy.remainder }}</td> </tr> </tbody> </table> - </div> </div> {% endif %} diff --git a/scipost_django/finances/utils.py b/scipost_django/finances/utils.py index 676246545a369471a3887ce762cef973c8d46cf0..ca818cf3c2ad34bf802dffcefb633637f89a38e7 100644 --- a/scipost_django/finances/utils.py +++ b/scipost_django/finances/utils.py @@ -2,7 +2,7 @@ __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" __license__ = "AGPL v3" -from .models import Subsidy, PubFrac, PubFracCompensation +from .models import Subsidy, PubFrac def id_to_slug(id): @@ -13,9 +13,9 @@ def slug_to_id(slug): return max(0, int(slug) - 821) -def distribute_subsidy(subsidy: Subsidy, algorithm: str): +def allocate_subsidy(subsidy: Subsidy, algorithm: str): """ - Allocate subsidy amount to compensations of PubFrac + Allocate subsidy amount to compensations of PubFrac or coverage of expenditures. Algorithm choices: * any PubFrac ascribed to org from affiliations @@ -27,12 +27,13 @@ def distribute_subsidy(subsidy: Subsidy, algorithm: str): """ algorithms = [ - "PubFrac_ascribed_to_Org", - "full_PEX_if_author_affiliated_to_Org", - "PubFrac_author_affiliation_same_country_as_Org", - "full_PEX_author_affiliation_same_country_as_Org", - "full_PEX_if_pub_funding_ack_includes_Org", - "full_PEX_if_pub_matches_specialties", + "compensate_PubFrac_related_to_Org", + "cover_full_PEX_if_PubFrac_related_to_Org", + "compensate_PubFrac_if_author_affiliation_same_country_as_Org", + "cover_full_PEX_if_author_affiliation_same_country_as_Org", + "cover_full_PEX_if_pub_funding_ack_includes_Org", + "cover_full_PEX_if_pub_matches_specialties", + "allocate_to_reserve_fund", ] if algorithm is "PubFrac_ascribed_to_Org": @@ -48,12 +49,8 @@ def distribute_subsidy(subsidy: Subsidy, algorithm: str): for pubfrac in pubfracs.all(): print(f"{distributed = };\tadding {pubfrac = }") if pubfrac.cf_value <= subsidy.remainder: - pfc, created = PubFracCompensation.objects.get_or_create( - subsidy=subsidy, - pubfrac=pubfrac, - amount=pubfrac.cf_value, - ) - if created: - distributed += pubfrac.cf_value + pubfrac.compensated_by = subsidy + pubfrac.save() + distributed += pubfrac.cf_value else: break diff --git a/scipost_django/journals/models/publication.py b/scipost_django/journals/models/publication.py index 2f92b0959b4c8074d7ac6d4a56946a10ae963071..3e3e73dc4555b6d5892f7463224491d813d5bfde 100644 --- a/scipost_django/journals/models/publication.py +++ b/scipost_django/journals/models/publication.py @@ -25,7 +25,6 @@ from ..managers import PublicationQuerySet from ..validators import doi_publication_validator from common.utils import get_current_domain -from finances.models import PubFracCompensation from scipost.constants import SCIPOST_APPROACHES from scipost.fields import ChoiceArrayField @@ -430,32 +429,17 @@ class Publication(models.Model): """Checks that the support fractions sum up to one.""" return self.pubfracs.aggregate(Sum("fraction"))["fraction__sum"] == 1 - @property - def covered_expenditures(self): - """Covered part of expenditures for this Publication.""" - return ( - self.pex_coverages.aggregate(Sum("amount"))["amount__sum"] - if self.pex_coverages.exists() - else 0 - ) - @property def compensated_expenditures(self): """Compensated part of expenditures for this Publication.""" - qs = PubFracCompensation.objects.filter(pubfrac__publication=self) - return qs.aggregate(Sum("amount"))["amount__sum"] if qs.exists() else 0 + qs = self.pubfracs.filter(compensated_by__isnull=False) + return qs.aggregate(Sum("cf_value"))["cf_value__sum"] if qs.exists() else 0 @property def uncompensated_expenditures(self): """Unompensated part of expenditures for this Publication.""" return self.expenditures - self.compensated_expenditures - @property - def outstanding_expenditures(self): - """Expenditures which hasn't been compensated or covered.""" - return (self.expenditures - self.covered_expenditures - - self.compensated_expenditures) - @property def citation(self): if self.cf_citation: diff --git a/scipost_django/journals/templates/journals/publication_detail.html b/scipost_django/journals/templates/journals/publication_detail.html index 200b5c0e4bd29a42f5803036755c8c19990e06d9..0576c8a5c4cd744abf67fb5774773baeafa2a1b2 100644 --- a/scipost_django/journals/templates/journals/publication_detail.html +++ b/scipost_django/journals/templates/journals/publication_detail.html @@ -172,17 +172,16 @@ {% if 'edadmin' in user_roles %} <h3 class="mt-4"> - Expenditures and Balance + Expenditures and Compensations </h3> - <table class="table mt-2 caption-top"> - <caption>PubFracs, Compensations and Arrears</caption> + <table class="table mt-2"> <thead class="table-light"> <tr> <th>Organization</th> <th>PubFrac</th> <th>Value</th> <th>Compensation</th> - <th>Arrears</th> + <th>Uncompensated</th> </tr> </thead> <tbody> @@ -191,88 +190,32 @@ <td><a href="{% url 'organizations:organization_detail' pk=pubfrac.organization.id %}">{{ pubfrac.organization }}</a></td> <td>{{ pubfrac.fraction }}</td> <td>€{{ pubfrac.cf_value }}</td> - <td> - <ul> - {% for pc in pubfrac.pubfrac_compensations.all %} - <li> - €{{ pc.amount }}  - {% if pc.subsidy.organization != pubfrac.organization %} - from ally - <a href="{% url 'organizations:organization_detail' pk=pc.subsidy.organization.id %}">{{ pc.subsidy.organization }}</a> - {% endif %} - (<a href="{% url 'finances:subsidy_details' pk=pc.subsidy.id %}" target="_blank"> - see Subsidy details - </a>) - </li> - {% empty %} - <li>No compensation</li> - {% endfor %} - </ul> - </td> - {% with pubfrac.arrears as arrears %} - <td class="{% if arrears == 0 %}bg-success{{% else %}bg-danger{% endif %} bg-opacity-25"> - €{{ pubfrac.arrears }} + {% if pubfrac.compensated %} + <td class="bg-success bg-opacity-25"> + by <a href="{% url 'organizations:organization_detail' pk=pubfrac.compensated_by.organization.id %}">{{ pubfrac.compensated_by.organization }}</a> + (<a href="{% url 'finances:subsidy_details' pk=pubfrac.compensated_by.id %}"> + see Subsidy details + </a>) </td> - {% endwith %} + <td class="bg-success bg-opacity-25">€0</td> + {% else %} + <td><span class="text-danger">{% include 'bi/x-circle-fill.html' %}</span></td> + <td class="bg-danger bg-opacity-25">€{{ pubfrac.cf_value }}</td> + {% endif %} </tr> {% endfor %} - <tr> - <th>Totals</th> - <td>1</td> - <td>{{ publication.expenditures }}</td> - {% if publication.uncompensated_expenditures == 0 %} - <td class="bg-success bg-opacity-25">{{ publication.compensated_expenditures }}</td> - <td class="bg-success bg-opacity-25">{{ publication.uncompensated_expenditures }}</td> - {% else %} - <td class="bg-danger bg-opacity-25">{{ publication.compensated_expenditures }}</td> - <td class="bg-danger bg-opacity-25">{{ publication.uncompensated_expenditures }}</td> - {% endif %} - </tr> - </tbody> - </table> - - <table class="table mt-2 caption-top"> - <caption>Balance</caption> - <tbody> - <tr> - <th>Expenditures</th> - <td></td> - <td>{{ publication.expenditures }}</td> - </tr> - <tr> - <th>PubFrac Compensations</th> - <td> - </td> - <td>{{ publication.compensated_expenditures }}</td> - </tr> - <tr> - <th>Coverages</th> - <td> - <ul> - {% for coverage in publication.pex_coverages.all %} - <li> - €{{ coverage.amount }} - from <a href="{% url 'organizations:organization_detail' pk=coverage.subsidy.organization.id %}">{{ coverage.subsidy.organization }}</a> - (<a href="{% url 'finances:subsidy_details' pk=coverage.subsidy.id %}" target="_blank"> - see Subsidy details - </a> - </li> - {% empty %} - <li>No coverage received</li> - {% endfor %} - </ul> - </td> - <td>{{ publication.covered_expenditures }}</td> - </tr> - {% with publication.outstanding_expenditures as outstanding %} - <tr class="{% if outstanding > 0 %}bg-danger{% else %}bg-success{% endif %} bg-opacity-25"> - <th>Outstanding Expenditures</th> - <td></td> - <td>{{ outstanding }}</td> + {% with publication.uncompensated_expenditures as uncompensated %} + <tr> + <th>Totals</th> + <td>1</td> + <td>{{ publication.expenditures }}</td> + <td class="{% if uncompensated == 0 %}bg-success{% else %}bg-danger{% endif %} bg-opacity-25">{{ publication.compensated_expenditures }}</td> + <td class="{% if uncompensated == 0 %}bg-success{% else %}bg-danger{% endif %} bg-opacity-25">{{ publication.uncompensated_expenditures }}</td> </tr> {% endwith %} </tbody> </table> + {% endif %} {% if publication.status == 'draft' and perms.scipost.can_draft_publication %}