From 21dafcb96ca342136d9db993694fb5cc472fc180 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jean-S=C3=A9bastien=20Caux?= <git@jscaux.org>
Date: Sun, 17 Mar 2024 19:47:59 +0100
Subject: [PATCH] Rework PubFrac presentation on organization detail page

---
 scipost_django/organizations/models.py        | 139 ++++++++---
 .../organizations/_organization_card.html     | 233 +++++++++++++++---
 2 files changed, 309 insertions(+), 63 deletions(-)

diff --git a/scipost_django/organizations/models.py b/scipost_django/organizations/models.py
index c60de9b9d..d9232b8bb 100644
--- a/scipost_django/organizations/models.py
+++ b/scipost_django/organizations/models.py
@@ -453,70 +453,151 @@ class Organization(models.Model):
         """
         pubyears = range(int(timezone.now().strftime("%Y")), 2015, -1)
         rep = {}
-        cumulative_balance = 0
+        cumulative_nap = 0
+        cumulative_pubfracs = 0
         cumulative_expenditures = 0
-        cumulative_contribution = 0
+        cumulative_self_compensated = 0
+        cumulative_ally_compensated = 0
+        cumulative_uncompensated = 0
+        cumulative_undivided_expenditures = 0
+        cumulative_undivided_compensated = 0
+        cumulative_undivided_uncompensated = 0
+        cumulative_subsidy_income = 0
+        cumulative_reserved = 0
+        cumulative_balance = 0
         pf = self.pubfracs.all()
+        publications_all = self.get_publications()
         for year in pubyears:
             rep[str(year)] = {}
-            contribution = self.total_subsidies_in_year(year)
-            rep[str(year)]["contribution"] = contribution
+            subsidy_income = self.total_subsidies_in_year(year)
+            rep[str(year)]["subsidy_income"] = subsidy_income
+            year_nap = 0
+            year_undivided_expenditures = 0
+            year_pubfracs = 0
             year_expenditures = 0
-            rep[str(year)]["expenditures"] = {}
+            year_self_compensated = 0
+            year_ally_compensated = 0
+            year_uncompensated = 0
+            year_undivided_compensated = 0
+            year_undivided_uncompensated = 0
+            year_reserved = 0
+            rep[str(year)]["expenditures"] = {
+                "per_journal": {},
+            }
             pfy = pf.filter(publication__publication_date__year=year)
+            publications_year = publications_all.filter(publication_date__year=year)
             jl1 = [
                 j
-                for j in pfy.values_list(
-                    "publication__in_journal__doi_label",
+                for j in publications_year.values_list(
+                    "in_journal__doi_label",
                     flat=True,
                 )
                 if j
             ]
             jl2 = [
                 j
-                for j in pfy.values_list(
-                    "publication__in_issue__in_journal__doi_label",
+                for j in publications_year.values_list(
+                    "in_issue__in_journal__doi_label",
                     flat=True,
                 )
                 if j
             ]
             jl3 = [
                 j
-                for j in pfy.values_list(
-                    "publication__in_issue__in_volume__in_journal__doi_label",
+                for j in publications_year.values_list(
+                    "in_issue__in_volume__in_journal__doi_label",
                     flat=True,
                 )
                 if j
             ]
             journal_labels = set(jl1 + jl2 + jl3)
             for journal_label in journal_labels:
-                qs = pfy.filter(
-                    publication__doi_label__istartswith=journal_label + "."
-                )
+                qs = pfy.filter(publication__doi_label__istartswith=journal_label + ".")
                 nap = qs.count()
-                sumpf = qs.aggregate(Sum("fraction"))["fraction__sum"]
-                costperpaper = get_object_or_404(
-                    Journal, doi_label=journal_label
-                ).cost_per_publication(year)
-                expenditures = int(costperpaper * sumpf)
-                if sumpf > 0:
-                    rep[str(year)]["expenditures"][journal_label] = {
+                sum_pf = qs.aggregate(Sum("fraction"))["fraction__sum"] or 0
+                journal = get_object_or_404(Journal, doi_label=journal_label)
+                costperpaper = journal.cost_per_publication(year)
+                expenditures = int(costperpaper * sum_pf)
+                self_compensated = int(
+                    qs.filter(compensated_by__organization=self).aggregate(
+                        Sum("cf_value")
+                    )["cf_value__sum"]
+                    or 0
+                )
+                ally_compensated = int(
+                    qs.filter(compensated_by__isnull=False)
+                    .exclude(compensated_by__organization=self)
+                    .aggregate(Sum("cf_value"))["cf_value__sum"]
+                    or 0
+                )
+                uncompensated = expenditures - self_compensated - ally_compensated
+                undivided_compensated = int(
+                    sum(
+                        p.compensated_expenditures
+                        for p in self.get_publications(year=year, journal=journal)
+                    )
+                )
+                undivided_uncompensated = nap * costperpaper - undivided_compensated
+                if sum_pf > 0 or undivided_uncompensated > 0:
+                    rep[str(year)]["expenditures"]["per_journal"][journal_label] = {
                         "costperpaper": costperpaper,
                         "nap": nap,
-                        "undivided_expenditures": nap * costperpaper,
-                        "pubfracs": float(sumpf),
+                        "pubfracs": float(sum_pf),
                         "expenditures": expenditures,
+                        "self_compensated": self_compensated,
+                        "ally_compensated": ally_compensated,
+                        "uncompensated": uncompensated,
+                        "undivided_expenditures": nap * costperpaper,
+                        "undivided_compensated": undivided_compensated,
+                        "undivided_uncompensated": undivided_uncompensated,
                     }
+                year_nap += nap
+                year_undivided_expenditures += nap * costperpaper
+                year_pubfracs += float(sum_pf)
                 year_expenditures += expenditures
-            rep[str(year)]["expenditures"]["total"] = year_expenditures
-            rep[str(year)]["balance"] = contribution - year_expenditures
+                year_self_compensated += self_compensated
+                year_ally_compensated += ally_compensated
+                year_uncompensated += uncompensated
+                year_undivided_compensated += undivided_compensated
+                year_undivided_uncompensated += undivided_uncompensated
+            rep[str(year)]["expenditures"]["total"] = {
+                "nap": year_nap,
+                "pubfracs": year_pubfracs,
+                "expenditures": year_expenditures,
+                "self_compensated": year_self_compensated,
+                "ally_compensated": year_ally_compensated,
+                "uncompensated": year_uncompensated,
+                "undivided_expenditures": year_undivided_expenditures,
+                "undivided_compensated": year_undivided_compensated,
+                "undivided_uncompensated": year_undivided_uncompensated,
+            }
+            rep[str(year)]["reserved"] = subsidy_income - year_self_compensated
+            rep[str(year)]["balance"] = subsidy_income - year_expenditures
+            cumulative_nap += year_nap
+            cumulative_undivided_expenditures += year_undivided_expenditures
+            cumulative_pubfracs += year_pubfracs
             cumulative_expenditures += year_expenditures
-            cumulative_contribution += contribution
-            cumulative_balance += contribution - year_expenditures
+            cumulative_self_compensated += year_self_compensated
+            cumulative_ally_compensated += year_ally_compensated
+            cumulative_uncompensated += year_uncompensated
+            cumulative_undivided_compensated += year_undivided_compensated
+            cumulative_undivided_uncompensated += year_undivided_uncompensated
+            cumulative_subsidy_income += subsidy_income
+            cumulative_balance += subsidy_income - year_expenditures
+            cumulative_reserved += subsidy_income - year_self_compensated
         rep["cumulative"] = {
-            "balance": cumulative_balance,
+            "nap": cumulative_nap,
+            "undivided_expenditures": cumulative_undivided_expenditures,
+            "pubfracs": cumulative_pubfracs,
             "expenditures": cumulative_expenditures,
-            "contribution": cumulative_contribution,
+            "self_compensated": cumulative_self_compensated,
+            "ally_compensated": cumulative_ally_compensated,
+            "uncompensated": cumulative_uncompensated,
+            "undivided_compensated": cumulative_undivided_compensated,
+            "undivided_uncompensated": cumulative_undivided_uncompensated,
+            "subsidy_income": cumulative_subsidy_income,
+            "reserved": cumulative_reserved,
+            "balance": cumulative_balance,
         }
         return rep
 
diff --git a/scipost_django/organizations/templates/organizations/_organization_card.html b/scipost_django/organizations/templates/organizations/_organization_card.html
index a6d0b11cf..1c6993869 100644
--- a/scipost_django/organizations/templates/organizations/_organization_card.html
+++ b/scipost_django/organizations/templates/organizations/_organization_card.html
@@ -207,11 +207,13 @@
 
 	<div class="tab-pane pt-4" id="support-{{ org.id }}" role="tabpanel" aria-labelledby="support-{{ org.id }}-tab">
 	  <h3 class="highlight">Support history</h3>
+	  {% if 'finadmin' in user_roles %}
+	    <a href="{% url "finances:subsidy_sponsorship_create" organization_id=org.id %}">Create a new sponsorship agreement</a>
+	  {% endif %}
 	  {% if org.subsidy_set.obtained|length > 0 or org.children.all|length > 0 %}
 	    {% if org.subsidy_set.obtained|length > 0 %}
-	      <p>List of the subsidies (in one form or another) which SciPost has received from this Organization. Click on a row to see more details.</p>
-      <a href="{% url "finances:subsidy_sponsorship_create" organization_id=org.id %}">Create a new sponsorship agreement</a>
-	      <table class="table table-hover mb-5">
+	      <table class="table table-hover mb-5 caption-top">
+		<caption>List of the subsidies (in one form or another) which SciPost has received from this Organization. Click on a row to see more details.</caption>
 		<thead class="table-light">
 		  <tr>
 		    <th>Type</th>
@@ -252,66 +254,224 @@
 	      <li><a href="{{ org.parent.get_absolute_url }}">{{ org.parent }}</a></li>
 	    </ul>
 	  {% endif %}
+
 	  <h3 class="highlight mt-4">Balance of SciPost expenditures versus support received</h3>
-	  <table class="table">
+
+
+
+
+
+
+	  <table class="table caption-top">
+	    <caption>Based on PubFracs for {{ organization }}</caption>
 	    <thead class="table-light">
 	      <tr>
 		<th>Year (click to toggle details)</th>
-		<th class="text-end">Total expenditures<br>by SciPost (&euro;)</th>
-		<th class="text-end">This Organization's<br>contribution to SciPost (&euro;)</th>
-		<th class="text-end">Balance (&euro;)</th>
+		<th class="text-end">NAP</th>
+		<th class="text-end">PubFrac<br>expenditures</th>
+		<th class="text-end">Subsidy<br>support</th>
+		<th class="text-end">Compensations (self)</th>
+		<th class="text-end">Reserved</th>
+		<th class="text-end">Compensations (Allies)</th>
+		<th class="text-end">Balance</th>
 	      </tr>
 	    </thead>
 	    <tbody>
 	      <tr class="table-light">
 		<td>Cumulative</td>
-		<td class="text-end">{{ balance.cumulative.expenditures }}</td>
-		<td class="text-end">{{ balance.cumulative.contribution }}</td>
-		<td class="text-end">{{ balance.cumulative.balance }}</td>
+		<td class="text-end">{{ balance.cumulative.nap }}</td>
+		<td class="text-end">&euro;{{ balance.cumulative.expenditures }}</td>
+		<td class="text-end">&euro;{{ balance.cumulative.subsidy_income }}</td>
+		<td class="text-end">&euro;{{ balance.cumulative.self_compensated }}</td>
+		<td class="text-end">&euro;{{ balance.cumulative.reserved }}</td>
+		<td class="text-end">&euro;{{ balance.cumulative.ally_compensated }}</td>
+		<td class="text-end bg-{% if balance.cumulative.balance < 0 %}danger{% else %}success{% endif %} bg-opacity-25">
+		  &euro;{{ balance.cumulative.balance }}
+		</td>
 	      </tr>
+	      {% now "Y" as current_year %}
 	      {% for year in pubyears %}
 		{% for key, val in balance.items %}
 		  {% if year == key|add:"0" %}
 		    <tr>
-		      <td><a class="mx-1 my-0 p-0" data-bs-toggle="collapse" href="#details{{ year }}" role="button" aria-expanded="false" aria-controls="details{{ year }}">{{ key }}</a></td>
+		      <td>
+			<a class="mx-1 my-0 p-0" data-bs-toggle="collapse" href="#details-{{ year }}" role="button" aria-expanded="false" aria-controls="details-{{ year }}">{{ key }}{% if key == current_year %}&emsp;(ongoing){% endif %}</a>
+		      </td>
+		      <td class="text-end">{{ val.expenditures.total.nap }}</td>
 		      <td class="text-end">
-			{{ val.expenditures.total }}
+			&euro;{{ val.expenditures.total.expenditures }}
+		      </td>
+		      <td class="text-end">&euro;{{ val.subsidy_income }}</td>
+		      <td class="text-end">
+			&euro;{{ val.expenditures.total.self_compensated }}
+		      </td>
+		      <td class="text-end">&euro;{{ val.reserved }}</td>
+		      <td class="text-end">
+			&euro;{{ val.expenditures.total.ally_compensated }}
+		      </td>
+		      <td class="text-end bg-{% if val.balance < 0 %}danger{% else %}success{% endif %} bg-opacity-25">
+			&euro;{{ val.balance }}
 		      </td>
-		      <td class="text-end">{{ val.contribution }}</td>
-		      <td class="text-end">{{ val.balance }}</td>
 		    </tr>
-		    <tr class="collapse" id="details{{ year }}">
-		      <td colspan="4">
-			<div class="p-2">
-			  <table class="table table-bordered">
-			    <thead class="table-dark">
+		    <tr class="collapse" id="details-{{ year }}">
+		      <td class="pe-0" colspan="8">
+			<div class="ms-4 me-0 p-2 border border-secondary">
+
+			  <p>The following table give an overview of expenditures and their compensation, compiled for all Publications which are associated to {{ organization }} for {{ year }}.</p>
+
+			  <p>You can see the list of associated publications and their PubFracs under the <em>Publications & PubFracs</em> tab.</p>
+			  <p>The data presented here uses PubFracs directly associated to {{ organization }}.</p>
+
+			  <table class="table table-bordered caption-top mb-0">
+			    <caption>Expenditures ({{ organization }})</caption>
+			    <thead class="table-secondary">
 			      <tr>
-				<th class="align-top">Journal<br></th>
-				<th class="text-end">SciPost Expenditure<br>per publication</th>
-				<th class="text-end">NAP<br>for this Org</th>
-				<th class="text-end">
-				  Full Expenditures (unshared)<br>by SciPost for these Publications
-				</th>
-				<th class="text-end">Sum of PubFracs<br>for this Org</th>
-				<th class="text-end">Expenditures share<br>for this Org</th>
+				<th>Journal</th>
+				<th class="text-end">APEX</th>
+				<th class="text-end">NAP</th>
+				<th class="text-end">PubFracs</th>
+				<th class="text-end">Expenditures share</th>
+				<th class="text-end">Compensations<br>(Organization)</th>
+				<th class="text-end">Compensations<br>(Allies)</th>
+				<th class="text-end">Balance<br>(uncompensated)</th>
 			      </tr>
 			    </thead>
 			    <tbody>
-			    {% for journal, journaldata in val.expenditures.items %}
-			      {% if journal != 'total' %}
+			      {% for journal, journaldata in val.expenditures.per_journal.items %}
 				<tr>
 				  <td>{{ journal }}</td>
-				  <td class="text-end">{{ journaldata.costperpaper }}</td>
+				  <td class="text-end">&euro;{{ journaldata.costperpaper }}</td>
 				  <td class="text-end">{{ journaldata.nap }}</td>
-				  <td class="text-end">{{ journaldata.undivided_expenditures }}</td>
 				  <td class="text-end">{{ journaldata.pubfracs }}</td>
-				  <td class="text-end">{{ journaldata.expenditures }}</td>
+				  <td class="text-end">&euro;{{ journaldata.expenditures }}</td>
+				  <td class="text-end">&euro;{{ journaldata.self_compensated }}</td>
+				  <td class="text-end">&euro;{{ journaldata.ally_compensated }}</td>
+				  {% if journaldata.uncompensated > 0 %}
+				    <td class="bg-danger bg-opacity-25 text-end">-&euro;{{ journaldata.uncompensated }}</td>
+				  {% else %}
+				    <td class="bg-success bg-opacity-25 text-end">&euro;{{ journaldata.uncompensated }}</td>
+				  {% endif %}
 				</tr>
-			      {% endif %}
+			      {% endfor %}
+			    </tbody>
+			  </table>
+			  <details class="ms-2 mb-2">
+			    <summary>
+			      Info on this table
+			    </summary>
+			    <p class="mt-1">This <strong>Expenditures (Organization-level)</strong> table compiles the expenditures by SciPost to publish all papers which are associated to this Organization for {{ year }}, weighed by this Organization's PubFracs for these individual papers. The next columns detail the compensations (through Subsidies) obtained from this Organization, or from other (ally) Organizations on its behalf. Any negative balance hits SciPost's reserve budget.</p>
+			  </details>
+
+			</div>
+		      </td>
+		    </tr>
+		  {% endif %}
+		{% endfor %}
+	      {% endfor %}
+	    </tbody>
+	  </table>
+
+
+
+
+	  <table class="table caption-top">
+	    <caption>Undivided: totals for all this Organization's associated publications</caption>
+	    <thead class="table-light">
+	      <tr>
+		<th>Year (click to toggle details)</th>
+		<th class="text-end">NAP</th>
+		<th class="text-end">Undivided<br>expenditures</th>
+		<th class="text-end">Compensations</th>
+		<th class="text-end">Uncompensated</th>
+	      </tr>
+	    </thead>
+	    <tbody>
+	      <tr class="table-light">
+		<td>Cumulative</td>
+		<td class="text-end">{{ balance.cumulative.nap }}</td>
+		<td class="text-end">&euro;{{ balance.cumulative.undivided_expenditures }}</td>
+		<td class="text-end">&euro;{{ balance.cumulative.undivided_compensated }}</td>
+		{% if balance.cumulative.undivided_uncompensated > 0 %}
+		  <td class="text-end bg-danger bg-opacity-25">
+		    -&euro;{{ balance.cumulative.undivided_uncompensated }}
+		  </td>
+		{% else %}
+		  <td class="text-end bg-success bg-opacity-25">
+		    &euro;{{ balance.cumulative.undivided_uncompensated }}
+		  </td>
+		{% endif %}
+	      </tr>
+	      {% now "Y" as current_year %}
+	      {% for year in pubyears %}
+		{% for key, val in balance.items %}
+		  {% if year == key|add:"0" %}
+		    <tr>
+		      <td>
+			<a class="mx-1 my-0 p-0" data-bs-toggle="collapse" href="#details-undivided-{{ year }}" role="button" aria-expanded="false" aria-controls="details-undivided-{{ year }}">{{ key }}{% if key == current_year %}&emsp;(ongoing){% endif %}</a>
+		      </td>
+		      <td class="text-end">{{ val.expenditures.total.nap }}</td>
+		      <td class="text-end">&euro;{{ val.expenditures.total.undivided_expenditures }}</td>
+		      <td class="text-end">
+			&euro;{{ val.expenditures.total.undivided_compensated }}
+		      </td>
+		      {% if val.expenditures.total.undivided_uncompensated > 0 %}
+			<td class="text-end bg-danger bg-opacity-25">
+			  -&euro;{{ val.expenditures.total.undivided_uncompensated }}
+			</td>
+		      {% else %}
+			<td class="text-end bg-success bg-opacity-25">
+			  &euro;{{ val.expenditures.total.undivided_uncompensated }}
+			</td>
+		      {% endif %}
+		    </tr>
+		    <tr class="collapse" id="details-undivided-{{ year }}">
+		      <td class="pe-0" colspan="8">
+			<div class="ms-4 me-0 p-2 border border-secondary">
+
+			  <p>The following table give an overview of expenditures and their compensation, compiled for all Publications which are associated to {{ organization }} for {{ year }}.</p>
+
+			  <p>You can see the list of associated publications and their PubFracs under the <em>Publications & PubFracs</em> tab.</p>
+			  <p>The data presented here uses data on <strong>all</strong> Publications which are directly associated to {{ organization }}.</p>
+
+			  <table class="table table-bordered caption-top mb-0">
+			    <caption>Expenditures (SciPost)</caption>
+			    <thead class="table-secondary">
+			      <tr>
+				<th>Journal</th>
+				<th class="text-end">APEX</th>
+				<th class="text-end">Org<br>NAP</th>
+				<th class="text-end">
+				  Undivided expenditures<br>for these Publications
+				</th>
+				<th class="text-end">Compensations<br>received</th>
+				<th class="text-end">Balance<br>(uncompensated)</th>
+			      </tr>
+			    </thead>
+			    <tbody>
+			    {% for journal, journaldata in val.expenditures.per_journal.items %}
+			      <tr>
+				<td>{{ journal }}</td>
+				<td class="text-end">&euro;{{ journaldata.costperpaper }}</td>
+				<td class="text-end">{{ journaldata.nap }}</td>
+				<td class="text-end">&euro;{{ journaldata.undivided_expenditures }}</td>
+				<td class="text-end">&euro;{{ journaldata.undivided_compensated }}</td>
+				{% if journaldata.undivided_uncompensated > 0 %}
+				  <td class="bg-danger bg-opacity-25 text-end">-&euro;{{ journaldata.undivided_uncompensated }}</td>
+				{% else %}
+				  <td class="bg-success bg-opacity-25 text-end">&euro;{{ journaldata.undivided_uncompensated }}</td>
+				{% endif %}
+			      </tr>
 			    {% endfor %}
 			    </tbody>
 			  </table>
-			  <p>You can see the associated publications and their PubFracs under the <em>Publications & PubFracs</em> tab.</p>
+			  <details class="ms-2 mb-2">
+			    <summary>
+			      Info on this table
+			    </summary>
+			    <p class="mt-1">This <strong>Expenditures (SciPost-level)</strong> table compiles the (total) expenditures by SciPost to publish all papers which are associated to this Organization for {{ year }}. Compensations received from any Organization for these papers is summed up. This sum would equal the expenditures if all Organizations sponsored us according to their PubFrac share. Any uncompensated balance hits SciPost's reserve budget.</p>
+			  </details>
+
+
 			</div>
 		      </td>
 		    </tr>
@@ -322,6 +482,11 @@
 	  </table>
 	</div>
 
+
+
+
+
+
 	{% if perms.scipost.can_manage_organizations or perms.scipost.can_add_contactperson or "can_view_org_contacts" in user_org_perms %}
 	  <div class="tab-pane pt-4" id="contacts-{{ org.id }}" role="tabpanel" aria-labelledby="contacts-{{ org.id }}-tab">
 	    <h3>Contacts (with explicit role)</h3>
-- 
GitLab