implement calculating subtotals for MoU Milestones for subsets of bugs
[utils.git] / src / budget_sync / write_budget_markdown.py
index 427f97f8d07ec2fcc5305193ada517571cafe668..ce2e246e7bb27e7b95a69deb97241065d4273c97 100644 (file)
@@ -1,9 +1,12 @@
+from collections import defaultdict
 from pathlib import Path
 from typing import Dict, List, Any, Optional
 from io import StringIO
 import enum
-from budget_sync.budget_graph import BudgetGraph, Node, Payment, PayeeState
-from budget_sync.config import Person, Milestone, Config
+from budget_sync.budget_graph import BudgetGraph, Node, Payment, PayeeState, PaymentSummary
+from budget_sync.config import Person, Milestone
+from budget_sync.money import Money
+from budget_sync.ordered_set import OrderedSet
 from budget_sync.util import BugStatus
 
 
@@ -68,14 +71,22 @@ class MarkdownWriter:
                              self.last_headers, headers)
         assert headers == self.last_headers
 
-    def write_node(self,
-                   headers: List[str],
-                   node: Node,
-                   payment: Optional[Payment]):
+    def write_node_header(self,
+                          headers: List[str],
+                          node: Optional[Node]):
         self.write_headers(headers)
+        if node is None:
+            print("* None", file=self.buffer)
+            return
         summary = markdown_escape(node.bug.summary)
         print(f"* [Bug #{node.bug.id}]({node.bug_url}):\n  {summary}",
               file=self.buffer)
+
+    def write_node(self,
+                   headers: List[str],
+                   node: Node,
+                   payment: Optional[Payment]):
+        self.write_node_header(headers, node)
         if payment is not None:
             if node.fixed_budget_excluding_subtasks \
                     != node.budget_excluding_subtasks:
@@ -113,7 +124,11 @@ class MarkdownWriter:
 
 def _markdown_for_person(person: Person,
                          payments_dict: Dict[Milestone, List[Payment]],
-                         assigned_nodes: List[Node]) -> str:
+                         assigned_nodes: List[Node],
+                         nodes_subset: Optional[OrderedSet[Node]] = None,
+                         ) -> str:
+    def node_included(node: Node) -> bool:
+        return nodes_subset is None or node in nodes_subset
     writer = MarkdownWriter()
     print(f"<!-- autogenerated by budget-sync -->", file=writer.buffer)
     writer.write_headers([f"\n# {person.full_name} ({person.identifier})\n"])
@@ -129,6 +144,8 @@ def _markdown_for_person(person: Person,
     def write_display_status_chunk(display_status: DisplayStatus):
         display_status_header = f"\n## {display_status.value}\n"
         for node in displayed_nodes_dict[display_status]:
+            if not node_included(node):
+                continue
             if display_status == DisplayStatus.Completed:
                 payment_found = False
                 for payment in node.payments.values():
@@ -153,21 +170,48 @@ def _markdown_for_person(person: Person,
 
     for payee_state in PayeeState:
         if payee_state == PayeeState.NotYetSubmitted:
-            display_status_header = f"## Payment not yet submitted"
+            display_status_header = "\n## Payment not yet submitted\n"
+            subtotals_header = ("\n#### MoU Milestone subtotals for not "
+                                "yet submitted payments\n")
         elif payee_state == PayeeState.Submitted:
-            display_status_header = f"## Submitted to NLNet but not yet paid"
+            display_status_header = ("\n## Submitted to NLNet but "
+                                     "not yet paid\n")
+            subtotals_header = ("\n#### MoU Milestone subtotals for "
+                                "submitted but not yet paid payments\n")
         else:
             assert payee_state == PayeeState.Paid
-            display_status_header = f"## Paid by NLNet"
-        display_status_header = "\n%s\n" % display_status_header
+            display_status_header = "\n## Paid by NLNet\n"
+            subtotals_header = ("\n#### MoU Milestone subtotals for paid "
+                                "payments\n")
         for milestone, payments_list in payments_dict.items():
             milestone_header = f"\n### {milestone.identifier}\n"
+            mou_subtotals: Dict[Optional[Node], Money] = defaultdict(Money)
+            headers = [status_tracking_header,
+                       display_status_header,
+                       milestone_header]
             for payment in payments_list:
-                if payment.state == payee_state:
-                    writer.write_node(headers=[status_tracking_header,
-                                               display_status_header,
-                                               milestone_header],
+                node = payment.node
+                if payment.state == payee_state and node_included(node):
+                    mou_subtotals[node.closest_bug_in_mou] += payment.amount
+                    writer.write_node(headers=headers,
                                       node=payment.node, payment=payment)
+            headers.append(subtotals_header)
+            for node, subtotal in mou_subtotals.items():
+                writer.write_node_header(headers, node)
+                if node is None:
+                    budget = ""
+                elif node.fixed_budget_including_subtasks \
+                        != node.budget_including_subtasks:
+                    budget = (" out of total including subtasks of "
+                              f"&euro;{node.fixed_budget_including_subtasks}"
+                              " (budget is fixed from amount appearing in "
+                              "bug report, which is "
+                              f"&euro;{node.budget_including_subtasks})")
+                else:
+                    budget = (" out of total including subtasks of "
+                              f"&euro;{node.fixed_budget_including_subtasks}")
+                print(f"    * subtotal &euro;{subtotal}{budget}",
+                      file=writer.buffer)
 
     # write_display_status_chunk(DisplayStatus.NotYetStarted)
 
@@ -175,11 +219,21 @@ def _markdown_for_person(person: Person,
 
 
 def write_budget_markdown(budget_graph: BudgetGraph,
-                          output_dir: Path):
+                          output_dir: Path,
+                          nodes_subset: Optional[OrderedSet[Node]] = None):
     output_dir.mkdir(parents=True, exist_ok=True)
     for person, payments_dict in budget_graph.payments.items():
         markdown = _markdown_for_person(person,
                                         payments_dict,
-                                        budget_graph.assigned_nodes[person])
+                                        budget_graph.assigned_nodes[person],
+                                        nodes_subset)
         output_file = output_dir.joinpath(person.output_markdown_file)
         output_file.write_text(markdown, encoding="utf-8")
+
+
+def markdown_for_person(budget_graph: BudgetGraph, person: Person,
+                        nodes_subset: Optional[OrderedSet[Node]] = None,
+                        ) -> str:
+    return _markdown_for_person(person, budget_graph.payments[person],
+                                budget_graph.assigned_nodes[person],
+                                nodes_subset)