diff --git a/scipost_django/finances/admin.py b/scipost_django/finances/admin.py index d374c58441ded84b8cd08a9c3a7891f1fd37bbf5..8da21fc79d49a528cff3c204500284f5368ef5ed 100644 --- a/scipost_django/finances/admin.py +++ b/scipost_django/finances/admin.py @@ -10,6 +10,7 @@ from .models import ( SubsidyAttachment, PubFrac, PubFracCompensation, + PublicationExpenditureCoverage, WorkLog, PeriodicReportType, PeriodicReport, @@ -111,6 +112,30 @@ class PubFracCompensationAdmin(admin.ModelAdmin): 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/0040_alter_pubfraccompensation_options_and_more.py b/scipost_django/finances/migrations/0040_alter_pubfraccompensation_options_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..27c0dde3daf5264bac26ce4f9cbebbcda12eec35 --- /dev/null +++ b/scipost_django/finances/migrations/0040_alter_pubfraccompensation_options_and_more.py @@ -0,0 +1,63 @@ +# Generated by Django 4.2.10 on 2024-03-16 03:48 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("journals", "0130_remove_publication_pubfractions_confirmed_by_authors"), + ("finances", "0039_alter_pubfraccompensation_pubfrac"), + ] + + operations = [ + migrations.AlterModelOptions( + name="pubfraccompensation", + options={ + "verbose_name": "PubFrac compensation", + "verbose_name_plural": "PubFrac compensations", + }, + ), + migrations.CreateModel( + name="PublicationExpenditureCoverage", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("amount", models.PositiveIntegerField()), + ( + "publication", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="pex_coverages", + to="journals.publication", + ), + ), + ( + "subsidy", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="pex_coverages", + to="finances.subsidy", + ), + ), + ], + options={ + "verbose_name": "PEX coverage", + "verbose_name_plural": "PEX coverages", + }, + ), + migrations.AddConstraint( + model_name="publicationexpenditurecoverage", + constraint=models.UniqueConstraint( + fields=("subsidy", "publication"), name="unique_subsidy_publication" + ), + ), + ] diff --git a/scipost_django/finances/models/__init__.py b/scipost_django/finances/models/__init__.py index a370b148d4ff1a0efca1f3a54905585b3613f4fe..c1669c99e7df453c2f0a73ee77c213dce8a80170 100644 --- a/scipost_django/finances/models/__init__.py +++ b/scipost_django/finances/models/__init__.py @@ -8,6 +8,8 @@ from .periodic_report import ( PeriodicReport, ) +from .pex_coverage import PublicationExpenditureCoverage + from .pubfrac import PubFrac from .pubfrac_compensation import PubFracCompensation diff --git a/scipost_django/finances/models/pex_coverage.py b/scipost_django/finances/models/pex_coverage.py new file mode 100644 index 0000000000000000000000000000000000000000..4f68c987eed695787e76b203f002b5ae8a205e54 --- /dev/null +++ b/scipost_django/finances/models/pex_coverage.py @@ -0,0 +1,44 @@ +__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} " + "from {self.subsidy.organization}" + ) diff --git a/scipost_django/finances/models/pubfrac_compensation.py b/scipost_django/finances/models/pubfrac_compensation.py index 9539d973b17ea5463a6e54cb4e6c27044675c48e..0aa5c48ab22f3c2de6c79d9d5addedc7336f2645 100644 --- a/scipost_django/finances/models/pubfrac_compensation.py +++ b/scipost_django/finances/models/pubfrac_compensation.py @@ -30,4 +30,5 @@ class PubFracCompensation(models.Model): fields=["subsidy", "pubfrac"], name="unique_subsidy_pubfrac" ), ] - verbose_name_plural = "PubFrac Compensations" + verbose_name = "PubFrac compensation" + verbose_name_plural = "PubFrac compensations" diff --git a/scipost_django/journals/models/publication.py b/scipost_django/journals/models/publication.py index 888d0ca1f1143e6c4b6c117a5bd80211f1cb1fbd..2f92b0959b4c8074d7ac6d4a56946a10ae963071 100644 --- a/scipost_django/journals/models/publication.py +++ b/scipost_django/journals/models/publication.py @@ -421,8 +421,8 @@ class Publication(models.Model): ) @property - def expenditure(self): - """The expenditure (as defined by the Journal) to produce this Publication.""" + def expenditures(self): + """The expenditures (as defined by the Journal) to produce this Publication.""" return self.get_journal().cost_per_publication(self.publication_date.year) @property @@ -430,16 +430,31 @@ 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): - """Uncompensated part of expenditures for this Publication.""" + """Compensated part of expenditures for this Publication.""" qs = PubFracCompensation.objects.filter(pubfrac__publication=self) - return qs.aggregate(Sum("amount"))["amount__sum"] + return qs.aggregate(Sum("amount"))["amount__sum"] if qs.exists() else 0 @property def uncompensated_expenditures(self): - """Compensated part of expenditures for this Publication.""" - return self.expenditure - self.compensated_expenditures + """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): diff --git a/scipost_django/journals/templates/journals/publication_detail.html b/scipost_django/journals/templates/journals/publication_detail.html index 4ccc19f61a18fb24e7393010665173c624790df9..200b5c0e4bd29a42f5803036755c8c19990e06d9 100644 --- a/scipost_django/journals/templates/journals/publication_detail.html +++ b/scipost_django/journals/templates/journals/publication_detail.html @@ -172,9 +172,10 @@ {% if 'edadmin' in user_roles %} <h3 class="mt-4"> - PubFracs, Compensations and Arrears + Expenditures and Balance </h3> - <table class="table mt-2"> + <table class="table mt-2 caption-top"> + <caption>PubFracs, Compensations and Arrears</caption> <thead class="table-light"> <tr> <th>Organization</th> @@ -187,7 +188,7 @@ <tbody> {% for pubfrac in publication.pubfracs.all %} <tr> - <td>{{ pubfrac.organization }}</td> + <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> @@ -209,7 +210,7 @@ </ul> </td> {% with pubfrac.arrears as arrears %} - <td class="{% if arrears == 0 %}bg-success{% elif arrears < pubfrac.cf_value %}bg-warning{% else %}bg-danger{% endif %} bg-opacity-25"> + <td class="{% if arrears == 0 %}bg-success{{% else %}bg-danger{% endif %} bg-opacity-25"> €{{ pubfrac.arrears }} </td> {% endwith %} @@ -218,13 +219,10 @@ <tr> <th>Totals</th> <td>1</td> - <td>{{ publication.expenditure }}</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> - {% elif publication.uncompensated_expenditures < publication.expenditure %} - <td class="bg-warning bg-opacity-25">{{ publication.compensated_expenditures }}</td> - <td class="bg-warning 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> @@ -232,6 +230,49 @@ </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> + </tr> + {% endwith %} + </tbody> + </table> {% endif %} {% if publication.status == 'draft' and perms.scipost.can_draft_publication %}