From 66be8575405ebeeb4c2ef54e7bda01d2b4a7f1a8 Mon Sep 17 00:00:00 2001
From: benjamin <benjamin@le-filament.com>
Date: Thu, 4 Jul 2024 12:09:26 +0200
Subject: [PATCH] [UPD] report template & rmovee cancel state & add validation
 date

---
 .pre-commit-config.yaml                    |  35 +++---
 models/scop_cotisation_cg_followup.py      |  16 ++-
 models/scop_cotisation_cg_followup_line.py |  41 ++++++-
 templates/report_scop_followup.xml         | 132 ++++++++++++++++-----
 views/scop_cotisation_cg_followup.xml      |   9 +-
 views/scop_cotisation_cg_followup_line.xml |  22 +++-
 6 files changed, 197 insertions(+), 58 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 732d0c4..4acca68 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,6 +6,8 @@ exclude: |
   ^setup/|/static/description/index\.html$|
   # We don't want to mess with tool-generated files
   .svg$|/tests/([^/]+/)?cassettes/|^.copier-answers.yml$|^.github/|
+  # Maybe reactivate this when all README files include prettier ignore tags?
+  ^README\.md$|
   # Library files can have extraneous formatting (even minimized)
   /static/(src/)?lib/|
   # Repos using Sphinx to generate docs don't need prettying
@@ -25,8 +27,13 @@ repos:
         entry: found forbidden files; remove them
         language: fail
         files: "\\.rej$"
+      - id: en-po-files
+        name: en.po files cannot exist
+        entry: found a en.po file
+        language: fail
+        files: '[a-zA-Z0-9_]*/i18n/en\.po$'
   - repo: https://github.com/oca/maintainer-tools
-    rev: 7d8a9f9ad73db0976fb03cbee43d953bc29b89e9
+    rev: ab1d7f6
     hooks:
       # update the NOT INSTALLABLE ADDONS section above
       - id: oca-update-pre-commit-excluded-addons
@@ -48,7 +55,7 @@ repos:
     hooks:
       - id: black
   - repo: https://github.com/pre-commit/mirrors-prettier
-    rev: v2.6.2
+    rev: v2.1.2
     hooks:
       - id: prettier
         name: prettier (with plugin-xml)
@@ -59,7 +66,7 @@ repos:
           - --plugin=@prettier/plugin-xml
         files: \.(css|htm|html|js|json|jsx|less|md|scss|toml|ts|xml|yaml|yml)$
   - repo: https://github.com/pre-commit/mirrors-eslint
-    rev: v8.15.0
+    rev: v7.8.1
     hooks:
       - id: eslint
         verbose: true
@@ -67,7 +74,7 @@ repos:
           - --color
           - --fix
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.2.0
+    rev: v3.2.0
     hooks:
       - id: trailing-whitespace
         # exclude autogenerated files
@@ -89,37 +96,33 @@ repos:
       - id: mixed-line-ending
         args: ["--fix=lf"]
   - repo: https://github.com/asottile/pyupgrade
-    rev: v2.32.1
+    rev: v2.7.2
     hooks:
       - id: pyupgrade
         args: ["--keep-percent-format"]
   - repo: https://github.com/PyCQA/isort
-    rev: 5.10.1
+    rev: 5.12.0
     hooks:
       - id: isort
         name: isort except __init__.py
         args:
           - --settings=.
         exclude: /__init__\.py$
-  - repo: https://gitlab.com/PyCQA/flake8
-    rev: 3.9.2
+  - repo: https://github.com/PyCQA/flake8
+    rev: 3.8.3
     hooks:
       - id: flake8
         name: flake8
         additional_dependencies: ["flake8-bugbear==20.1.4"]
-  - repo: https://github.com/PyCQA/pylint
-    rev: v2.11.1
+  - repo: https://github.com/OCA/pylint-odoo
+    rev: 7.0.2
     hooks:
-      - id: pylint
+      - id: pylint_odoo
         name: pylint with optional checks
         args:
           - --rcfile=.pylintrc
           - --exit-zero
         verbose: true
-        additional_dependencies: &pylint_deps
-          - pylint-odoo==5.0.5
-      - id: pylint
-        name: pylint with mandatory checks
+      - id: pylint_odoo
         args:
           - --rcfile=.pylintrc-mandatory
-        additional_dependencies: *pylint_deps
diff --git a/models/scop_cotisation_cg_followup.py b/models/scop_cotisation_cg_followup.py
index ccf2e7f..f100779 100644
--- a/models/scop_cotisation_cg_followup.py
+++ b/models/scop_cotisation_cg_followup.py
@@ -17,6 +17,7 @@ class ScopCotisationCgFollowup(models.Model):
     date = fields.Date(
         "Date Limite de la Relance", states={"done": [("readonly", True)]}
     )
+    date_validation = fields.Date("Date de validation de la Relance")
     threshold_amount = fields.Monetary(
         "Seuil de relance",
         default=0,
@@ -34,7 +35,7 @@ class ScopCotisationCgFollowup(models.Model):
         string="Année minimale",
     )
     state = fields.Selection(
-        [("draft", "Brouillon"), ("done", "Validé"), ("cancel", "Annulé")],
+        [("draft", "Brouillon"), ("done", "Validé")],
         string="Statut",
         default="draft",
         tracking=2,
@@ -49,7 +50,7 @@ class ScopCotisationCgFollowup(models.Model):
         string="Nombre de coopératives", compute="_compute_partner_count"
     )
     total_amount = fields.Monetary(
-        string="Montant à recouvrir",
+        string="Montant recouvré",
         compute="_compute_total_amount",
         currency_field="company_currency_id",
     )
@@ -135,10 +136,6 @@ class ScopCotisationCgFollowup(models.Model):
         for line in followup_line_ids:
             # update partner followup_level
             line.partner_id.followup_level = line.followup_level
-            # TODO:
-            #  - création courrier + chargement sur Alfresco
-            #  - envouer un mail (contact COTI ?)
-            #  - autres actions à définir en fonction du niveau
 
             # Store invoices history in JSON
             amount_history = {}
@@ -162,10 +159,11 @@ class ScopCotisationCgFollowup(models.Model):
                 )
             line.amount_history = amount_history
 
-        self.state = "done"
+        self.update({"date_validation": fields.Date.today(), "state": "done"})
 
-    def cancel_followup(self):
-        self.state = "cancel"
+    def reset_to_draft(self):
+        self.ensure_one()
+        self.update({"date_validation": False, "state": "draft"})
 
     def create_followup(self):
         """
diff --git a/models/scop_cotisation_cg_followup_line.py b/models/scop_cotisation_cg_followup_line.py
index 99c7375..66d3fa5 100644
--- a/models/scop_cotisation_cg_followup_line.py
+++ b/models/scop_cotisation_cg_followup_line.py
@@ -2,6 +2,7 @@
 # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
 
 import ast
+from datetime import datetime
 
 from odoo import _, api, fields, models
 from odoo.exceptions import UserError
@@ -16,6 +17,7 @@ class ScopCotisationCgFollowupLine(models.Model):
         comodel_name="scop.cotisation.cg.followup",
         string="Relance",
         required=True,
+        ondelete="cascade",
         index=True,
     )
     followup_date = fields.Date(related="followup_id.date", string="Date")
@@ -157,11 +159,16 @@ class ScopCotisationCgFollowupLine(models.Model):
     # ------------------------------------------------------
     def print_followup(self):
         return self.env.ref(
-            "cgscop_cotisation_cg_followup.cgscop_followup_report").report_action(self)
+            "cgscop_cotisation_cg_followup.cgscop_followup_report"
+        ).report_action(self)
 
     def delete_followup_line(self):
         self.unlink()
 
+    def action_exclude_line(self):
+        for line in self.browse(self.env.context["active_ids"]):
+            line.update({"is_excluded": True})
+
     # ------------------------------------------------------
     # Override ORM
     # ------------------------------------------------------
@@ -195,3 +202,35 @@ class ScopCotisationCgFollowupLine(models.Model):
                     )
                 )
         return super(ScopCotisationCgFollowupLine, self).unlink()
+
+    # ------------------------------------------------------
+    # Report functions
+    # ------------------------------------------------------
+    def get_report_invoices(self):
+        """
+        Retournes les impayés depuis la date de validation
+        """
+        self.ensure_one()
+        domain_odoo = [
+            ("invoice_date_due", "<=", self.followup_id.date_validation),
+            ("is_contribution", "=", True),
+            ("state", "=", "posted"),
+            ("move_type", "=", "out_invoice"),
+            ("bordereau_id", "!=", False),
+            ("bordereau_id.state", "!=", "cancelled"),
+            ("partner_id", "=", self.partner_id.id),
+            ("amount_residual_signed", ">", 0),
+            ("id", "not in", self.invoice_ids.ids),
+        ]
+        if self.followup_id.threshold_year:
+            domain_odoo.append(
+                (
+                    "invoice_date_due",
+                    ">",
+                    datetime(int(self.followup_id.threshold_year), 1, 1),
+                )
+            )
+
+        # Get Odoo move line & partners
+        move_ids = self.env["account.move"].search(domain_odoo)
+        return move_ids + self.invoice_ids
diff --git a/templates/report_scop_followup.xml b/templates/report_scop_followup.xml
index 79cae31..e507983 100644
--- a/templates/report_scop_followup.xml
+++ b/templates/report_scop_followup.xml
@@ -5,6 +5,12 @@
         <template id="report_scop_followup_document">
             <t t-call="web.external_layout">
                 <t t-set="o" t-value="o.with_context(lang='fr')" />
+                <t t-set="all_invoices" t-value="o.get_report_invoices()" />
+                <t
+                    t-set="total_amount"
+                    t-value="sum(all_invoices.mapped('amount_residual_signed')) + sum(o.riga_contribution_ids.mapped('amount_due'))"
+                />
+
                 <div class="page" style="font-size: 16px;">
                     <!-- Adresse -->
                     <div class="row">
@@ -43,8 +49,7 @@
                     <div class="row pt32">
                         <div class="col-12" style="text-align: justify;">
                             <p>
-                                Objet : Relance pour cotisation impayée<br
-                                />
+                                Objet : Relance pour cotisation impayée<br />
                                 N° adhérent : <t
                                     t-esc="str(o.partner_id.member_number_int)"
                                 /><br />
@@ -61,15 +66,20 @@
                             </p>
                             <p>
                                 Sauf erreur ou omission de notre part, votre relevé de compte cotisant à la date du
-                                <span t-field="o.followup_id.date" /> présente un solde débiteur de
-                                <span t-field="o.amount_due" t-options='{"widget": "monetary","display_currency": company.currency_id}' />
+                                <span
+                                    t-field="o.followup_id.date"
+                                /> présente un solde débiteur de
+                                <span
+                                    t-esc="total_amount"
+                                    t-options='{"widget": "monetary","display_currency": company.currency_id}'
+                                />.
                             </p>
 
                             <table class="table table-sm table-stripped">
                                 <thead>
                                     <tr>
                                         <th>Année</th>
-                                        <th>Trimestre</th>
+                                        <th>Trimestre(s)</th>
                                         <th>Type de cotisation</th>
                                         <th>Montant total</th>
                                         <th>Montant réglé</th>
@@ -77,41 +87,109 @@
                                     </tr>
                                 </thead>
                                 <tbody>
-                                    <tr t-foreach="o.invoice_ids" t-as="line">
-                                        <td><span t-esc="line.year" /></td>
-                                        <td><span t-field="line.cotiz_quarter" /></td>
-                                        <td><span t-field="line.type_contribution_id.name" /></td>
-                                        <td class="text-right"><span t-field="line.amount_called" /></td>
-                                        <td class="text-right"><span t-esc="line.amount_called - line.amount_residual_signed" t-options='{"widget": "monetary","display_currency": company.currency_id}' /></td>
-                                        <td class="text-right"><span t-field="line.amount_residual_signed" /></td>
-                                    </tr>
-                                    <tr t-foreach="o.riga_contribution_ids.sorted(key='year', reverse=True)" t-as="line">
-                                        <td><span t-field="line.year" /></td>
-                                        <td><span t-field="line.quarter" /></td>
-                                        <td><span t-field="line.type_contribution_id.name" /></td>
-                                        <td class="text-right"><span t-field="line.amount" t-options='{"widget": "monetary","display_currency": company.currency_id}' /></td>
-                                        <td class="text-right"><span t-esc="line.amount_paid" t-options='{"widget": "monetary","display_currency": company.currency_id}' /></td>
-                                        <td class="text-right"><span t-field="line.amount_due" t-options='{"widget": "monetary","display_currency": company.currency_id}' /></td>
-                                    </tr>
+                                    <t
+                                        t-set="year_invoices"
+                                        t-value="sorted(list(set(all_invoices.mapped('year'))), reverse=True)"
+                                    />
+
+                                    <t
+                                        t-set="type_contrib_invoices"
+                                        t-value="list(set(all_invoices.mapped('type_contribution_id')))"
+                                    />
+                                    <t t-foreach="year_invoices" t-as="year">
+                                        <tr
+                                            t-foreach="type_contrib_invoices"
+                                            t-as="contrib"
+                                        >
+                                            <t
+                                                t-set="line"
+                                                t-value="all_invoices.filtered(lambda c: c.year == year and c.type_contribution_id == contrib)"
+                                            />
+                                            <t t-if="line">
+                                                <td><span t-esc="year" /></td>
+                                                <td><span
+                                                        t-esc="', '.join(sorted(line.mapped(lambda c: c.cotiz_quarter or '')))"
+                                                    /></td>
+                                                <td><span t-esc="contrib.name" /></td>
+                                                <td class="text-right"><span
+                                                        t-esc="sum(line.mapped('amount_called'))"
+                                                        t-options='{"widget": "monetary","display_currency": company.currency_id}'
+                                                    /></td>
+                                                <td class="text-right"><span
+                                                        t-esc="sum(line.mapped(lambda c: c.amount_called - c.amount_residual_signed))"
+                                                        t-options='{"widget": "monetary","display_currency": company.currency_id}'
+                                                    /></td>
+                                                <td class="text-right"><span
+                                                        t-esc="sum(line.mapped('amount_residual_signed'))"
+                                                        t-options='{"widget": "monetary","display_currency": company.currency_id}'
+                                                    /></td>
+                                            </t>
+                                        </tr>
+                                    </t>
+
+                                    <t
+                                        t-set="year_riga"
+                                        t-value="list(set(o.riga_contribution_ids.sorted(key='year', reverse=True).mapped('year')))"
+                                    />
+                                    <t
+                                        t-set="type_contrib_riga"
+                                        t-value="list(set(o.riga_contribution_ids.mapped('type_contribution_id')))"
+                                    />
+                                    <t t-foreach="year_riga" t-as="year">
+                                        <tr
+                                            t-foreach="type_contrib_riga"
+                                            t-as="contrib"
+                                        >
+                                            <t
+                                                t-set="line"
+                                                t-value="o.riga_contribution_ids.filtered(lambda c: c.year == year and c.type_contribution_id == contrib)"
+                                            />
+                                            <t t-if="line">
+                                                <td><span t-esc="year" /></td>
+                                                <td><span
+                                                        t-esc="', '.join(sorted(line.mapped('quarter')))"
+                                                    /></td>
+                                                <td><span t-esc="contrib.name" /></td>
+                                                <td class="text-right"><span
+                                                        t-esc="sum(line.mapped('amount'))"
+                                                        t-options='{"widget": "monetary","display_currency": company.currency_id}'
+                                                    /></td>
+                                                <td class="text-right"><span
+                                                        t-esc="sum(line.mapped('amount_paid'))"
+                                                        t-options='{"widget": "monetary","display_currency": company.currency_id}'
+                                                    /></td>
+                                                <td class="text-right"><span
+                                                        t-esc="sum(line.mapped('amount_due'))"
+                                                        t-options='{"widget": "monetary","display_currency": company.currency_id}'
+                                                    /></td>
+
+                                            </t>
+                                        </tr>
+                                    </t>
                                 </tbody>
                                 <tfoot>
                                     <th colspan="5" class="text-right">Total impayé</th>
-                                    <td class="text-right"><span t-field="o.amount_due" t-options='{"widget": "monetary","display_currency": company.currency_id}' /></td>
+                                    <td class="text-right"><span
+                                            t-esc="total_amount"
+                                            t-options='{"widget": "monetary","display_currency": company.currency_id}'
+                                        /></td>
                                 </tfoot>
                             </table>
 
-                            <p class="pt32 font-italic">
+                            <p class="pt32">
                                 Ces cotisations sont à régler par virement à l’ordre de la CGSCOP (IBAN : FR76 4255 9100 0008 0024 8837 710). Il convient de préciser dans le libellé votre numéro d’adhérent et/ou le nom de votre coopérative ainsi que la période réglée.
                             </p>
-                            <p class="font-italic">
+                            <p class="">
                                 Pour une gestion simplifiée de vos cotisations, nous pouvons mettre en place le prélèvement automatique trimestriel. Vous êtes informés avant chaque prélèvement par courriel.
                                 <br />
-                                Vous pouvez faire une demande de mandat SEPA à <a href="mailto:administratif.cg@scop.coop">administratif.cg@scop.coop</a>.
+                                Vous pouvez faire une demande de mandat SEPA à <a
+                                    href="mailto:administratif.cg@scop.coop"
+                                >administratif.cg@scop.coop</a>.
                             </p>
-                            <p class="font-italic">
+                            <p class="">
                                 Si vous rencontrez des difficultés ou si vous souhaitez des renseignements complémentaires sur le calcul de ces cotisations, nous vous invitons à contacter votre délégué.e au sein de votre Union Régionale.
                             </p>
-                            <p class="font-italic">
+                            <p class="">
                                 Nous vous rappelons que les cotisations constituent les principales ressources de notre mouvement, et lui donnent les moyens d’agir en toute indépendance au service de ses adhérents, notamment au travers des outils financiers et des équipes de délégués de votre Union Régionale.
                             </p>
                             <p>
diff --git a/views/scop_cotisation_cg_followup.xml b/views/scop_cotisation_cg_followup.xml
index fedf9e6..e268c91 100644
--- a/views/scop_cotisation_cg_followup.xml
+++ b/views/scop_cotisation_cg_followup.xml
@@ -17,11 +17,11 @@
                             attrs="{'invisible': [('state', '!=', 'draft')]}"
                         />
                         <button
-                            name="cancel_followup"
+                            name="reset_to_draft"
                             type="object"
-                            string="Annuler la relance"
-                            confirm="Êtes-vous certain(e) de vouloir annuler cette relance ?"
-                            attrs="{'invisible': [('state', '!=', 'draft')]}"
+                            string="Remettre en brouillon"
+                            confirm="Êtes-vous certain(e) de vouloir remettre en brouillon cette relance ?"
+                            attrs="{'invisible': [('state', '!=', 'done')]}"
                         />
                         <button
                             name="action_report_followup_per_ur"
@@ -77,6 +77,7 @@
                         <group string="Critères de la relance">
                             <group>
                                 <field name="date" required="1" />
+                                <field name="date_validation" readonly="1" />
                                 <field name="threshold_year" />
                                 <field name="threshold_amount" widget="monetary" />
                                 <div class="text-muted" colspan="2">
diff --git a/views/scop_cotisation_cg_followup_line.xml b/views/scop_cotisation_cg_followup_line.xml
index eccce18..aa7d775 100644
--- a/views/scop_cotisation_cg_followup_line.xml
+++ b/views/scop_cotisation_cg_followup_line.xml
@@ -174,13 +174,18 @@
             <field name="model">scop.cotisation.cg.followup.line</field>
             <field name="arch" type="xml">
                 <tree string="Détail Relances">
+                    <field name="followup_state" invisible="1" />
                     <field name="followup_id" />
                     <field name="member_number" />
                     <field name="partner_id" />
                     <field name="ur_id" />
                     <field name="followup_level" />
                     <field name="amount_due" />
-                    <field name="is_excluded" widget="boolean_toggle" />
+                    <field
+                        name="is_excluded"
+                        widget="boolean_toggle"
+                        attrs="{'readonly': [('followup_state', '=', 'done')]}"
+                    />
                     <button
                         name="print_followup"
                         type="object"
@@ -225,6 +230,21 @@
             <field name="view_mode">tree,pivot,graph,form</field>
         </record>
 
+        <!-- Action server -->
+        <record id="action_exclude_followup_line" model="ir.actions.server">
+            <field name="name">Exclure les relances</field>
+            <field name="model_id" ref="model_scop_cotisation_cg_followup_line" />
+            <field
+                name="binding_model_id"
+                ref="cgscop_cotisation_cg_followup.model_scop_cotisation_cg_followup_line"
+            />
+            <field name="binding_view_types">list</field>
+            <field name="state">code</field>
+            <field name="code">
+                action = model.action_exclude_line()
+            </field>
+        </record>
+
         <!-- Menu -->
         <menuitem
             name="Détail des Relances"
-- 
GitLab