From b48c9b79bd522644e9e643f3c814250df9b486dd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Laporte?= <stephane.laporte@enercoop.org>
Date: Thu, 12 Jun 2025 17:44:25 +0200
Subject: [PATCH 1/2] =?UTF-8?q?[FIX]=20erreur=20lors=20de=20la=20tentative?=
 =?UTF-8?q?=20de=20cr=C3=A9ation=20du=20PRM=20et/ou=20de=20la=20nouvelle?=
 =?UTF-8?q?=20p=C3=A9riode,=20=C3=A0=20v=C3=A9rifier=20manuellement=20:=20?=
 =?UTF-8?q?Impossible=20de=20cr=C3=A9er=20une=20nouvelle=20p=C3=A9riode=20?=
 =?UTF-8?q?pour=20le=20mod=C3=A8le=20Gestion=20des=20prix,=20il=20y=20a=20?=
 =?UTF-8?q?d=C3=A9j=C3=A0=20une=20p=C3=A9riode=20qui=20d=C3=A9marre=20le?=
 =?UTF-8?q?=202025-06-02=20et=20qui=20chevauche=20la=20p=C3=A9riode=20exis?=
 =?UTF-8?q?tante?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 models/acc_operation.py | 506 ++++++++++++++++++++++++++--------------
 1 file changed, 325 insertions(+), 181 deletions(-)

diff --git a/models/acc_operation.py b/models/acc_operation.py
index b68a2e7..57f09a8 100644
--- a/models/acc_operation.py
+++ b/models/acc_operation.py
@@ -1,10 +1,22 @@
 # Copyright 2021- Le Filament (https://le-filament.com)
 # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
-from datetime import date
+import json
+from datetime import date, datetime
 
 from odoo import _, api, fields, models
 from odoo.exceptions import AccessError, ValidationError
 from odoo.tools import html_sanitize
+from odoo.osv import expression
+
+from enum import Enum
+
+class GetPerimeterEvent(Enum):
+    NOT_SET = 1
+    NO_CHANGE = 2
+    PRM_IN = 3
+    PRM_OUT = 4
+    DATE_CHANGE = 5
+
 
 
 class AccOperation(models.Model):
@@ -133,40 +145,154 @@ class AccOperation(models.Model):
     # ------------------------------------------------------
     # API functions
     # ------------------------------------------------------
+
+    def _gen_log_table(self, tLogDatas, log_type, header):
+        message = ""
+
+        n = sum(1 for item in tLogDatas if item.get('log_type') == log_type)
+        if n == 0: return message
+
+        message += f"<h5>{header}</h5>"
+        message += "<table class='table table-striped'>"
+        message += "<thead>"
+        message += "<th>PRM</th><th>type</th><th>début</th><th>fin</th><th>action à prendre</th>"
+        message += "</thead>"
+        for tLogData in tLogDatas:
+            if (tLogData['log_type'] != log_type): continue
+            end_date = '' if tLogData['end'] is False else tLogData['end'].strftime('%d/%m/%Y')
+            message += (f"<tr>")
+            message += (f"<td class='text-nowrap'>{tLogData['id']}</td>")
+            message += (f"<td class='text-nowrap'>{'injection' if tLogData['prm_type'] == 'PROD' else 'soutirage'}</td>")
+            message += (f"<td class='text-nowrap'>{tLogData['start']:%d/%m/%Y}</td>")
+            message += f"<td class='text-nowrap' style='width: 10em;'>{end_date}</td>"
+            message += ("<td>")
+            if 'action' in tLogData:
+                message += ("<ul>")
+                for v in tLogData['action']:
+                    message += (f"<li>{v}</li>")
+                message += ("</ul>")
+            message += (f"</td>")
+            message += (f"</tr>")
+        message += "</table>"
+        return message
+
+    def _gen_log_tables(self, tLogDatas):
+        message = ""
+        # MULTI
+        message += self._gen_log_table(tLogDatas, GetPerimeterEvent.PRM_IN, 'Entrées de PRM: des actions sont requises')
+        message += self._gen_log_table(tLogDatas, GetPerimeterEvent.DATE_CHANGE, 'Dates modifiées: des actions sont requises')
+        message += self._gen_log_table(tLogDatas, GetPerimeterEvent.PRM_OUT, 'Sorties de PRM: pour information, ne requiert pas d\'action de votre part')
+        message += self._gen_log_table(tLogDatas, GetPerimeterEvent.NO_CHANGE, 'Pas de changement sur ces PRM: pour information, ne requiert pas d\'action de votre part')
+        return message
+
+    def gen_log_prm_list(self, tLogDatas, prm_type):
+        t_ids = [
+            item["id"] for item in tLogDatas
+            if item.get("log_type") in (
+                GetPerimeterEvent.NO_CHANGE,
+                GetPerimeterEvent.PRM_IN,
+                GetPerimeterEvent.PRM_OUT
+            )
+            and item.get("prm_type") == prm_type
+        ]
+        header = ("Liste complète des " + str(len(t_ids))+ " PRM d'injection"
+                  if prm_type == "PROD" else "Liste complète des " + str(len(t_ids))+ " PRM de soutirage")
+        message = f"<h5>{header}</h5>"
+        message += "<table class='table table-striped'>"
+        for id in t_ids:
+            message += (f"<tr>")
+            message += (f"<td>{id}</td>")
+            message += (f"</tr>")
+        message += "</table>"
+        return message
+
+    def gen_log_prm_lists(self, tLogDatas):
+        message = ""
+        message += self.gen_log_prm_list(tLogDatas, "PROD")
+        message += self.gen_log_prm_list(tLogDatas, "CONS")
+        return message
+
+    def clean_database(self):
+        o_counters = self.env["acc.counter"].search([("name", ">=", "90000000000001"),("name", "<=", "90000000000010")])
+        #print(o_counters)
+        o_counter_periods = o_counters.period_ids
+        #print(o_counter_periods)
+        o_prices = self.env["acc.price.conf"].search([("acc_injection_counter_id", "in", o_counters.ids)])
+        #print(o_prices)
+        o_prices.unlink()
+        o_prices = self.env["acc.price.conf"].search([("acc_delivery_counter_id", "in", o_counters.ids)])
+        #print(o_prices)
+        o_prices.unlink()
+        o_counter_periods.unlink()
+        o_counters.unlink()
+
+    def debug_database(self):
+        o_counters = self.env["acc.counter"].search([("name", ">=", "90000000000001"),("name", "<=", "90000000000010")])
+        for o_counter in o_counters:
+            print('added counter ' + str(o_counter.id) + ' ' + o_counter.name)
+        o_counter_periods = o_counters.period_ids
+        for o_counter_period in o_counter_periods:
+            print('added counter period ' + str(o_counter_period.id) + ' ' + o_counter_period.acc_counter_id.name + ' ' + str(o_counter_period.start_date) + ' ' + str(o_counter_period.end_date))
+        o_prices = self.env["acc.price.conf"].search([("acc_injection_counter_id", "in", o_counters.ids)])
+        for o_price in o_prices:
+            print('added price ' + str(o_price.id) + ' ' + o_price.acc_injection_counter_id.name + ' ' + o_price.acc_delivery_counter_id.name + ' ' + str(o_price.start_date) + ' ' + str(o_price.end_date) + ' ' + str(o_price.price))
+        o_prices = self.env["acc.price.conf"].search([("acc_delivery_counter_id", "in", o_counters.ids)])
+        for o_price in o_prices:
+            print('added price ' + str(o_price.id) + ' ' + o_price.acc_injection_counter_id.name + ' ' + o_price.acc_delivery_counter_id.name + ' ' + str(o_price.start_date) + ' ' + str(o_price.end_date) + ' ' + str(o_price.price))
+
+
     def _perimeter(self, from_cron=False):
         """
         Récupère les données de l'opération concernant le périmètre:
          - liste des PRM
          - date de début opération
 
-        @returns: log_id created with details
+        @returns: log_id created with detailsnv
+
+        Format du flux reçu
+            {
+                'agreement_id': 'ACC00000119',
+                'usage_points': [
+                    {
+                        'usage_point_id': '30002521267277',
+                        'type': 'PROD / CONS',
+                        'start': '2022-02-01',
+                        'end': '2025-06-16 / 9999-12-31'
+                    },
         """
-        # TODO : refactor, too complex
-        self._check_access_api()
         message = ""
+
         message += (
-            "<h1>Appel Enedis Périmètre "
+            "<p>Début appel API Enedis pour mise à jour du périmètre "
             + self.name
-            + " du "
+            + " : "
             + str(fields.Datetime.now())
-            + "</h1>"
+            + "</p>"
         )
-        message += "<p><strong>Appel API ...<br/>"
-        try:
-            perimeter_data = self._get_perimeter()
-        except ValidationError as e:
-            if from_cron:
-                log_id = self.create_log(message=str(e), type_log="cron")
-                log_id.send_api_error_mail()
-            raise e from e
+        if False:
+            self._check_access_api()
+            message += "<p>Appel API ...</p>"
+            try:
+                perimeter_data = self._get_perimeter()
+            except ValidationError as e:
+                if from_cron:
+                    log_id = self.create_log(message=str(e), type_log="cron")
+                    log_id.send_api_error_mail()
+                raise e from e
+
+            message += "<p>Appel API terminé</p>"
+        else:
+            self.clean_database()
+            with open('/home/stephane.laporte/Téléchargements/enedis_perimeter_ecoe_castelet.txt', 'r') as file:
+                sJsonData = file.read()
+            sJsonData = sJsonData.replace("'", '"')
+            perimeter_data = json.loads(sJsonData)
 
-        message += "<p><strong>Appel API terminé<br/>" "Traitement des données ...<br/>"
         usage_points = perimeter_data.get("usage_points")
 
         counter_used = []
 
-        list_injection = []
-        list_soutirage = []
+        tLogDatas = []
 
         for usage_point in sorted(
             usage_points, key=lambda p: date.fromisoformat(p["start"])
@@ -182,163 +308,157 @@ class AccOperation(models.Model):
                 usage_point_prm_type = "delivery"
             elif usage_point["type"] == "PROD":
                 usage_point_prm_type = "injection"
-            message += (
-                "<br/>PRM "
-                + usage_point["type"]
-                + " : "
-                + usage_point["usage_point_id"]
-                + " - Dates Enedis : "
-                + usage_point["start"]
-                + " - "
-                + usage_point["end"]
-                + "<br/>"
-            )
-            counter_id = self.env["acc.counter"].search(
-                [
-                    ("name", "=", usage_point["usage_point_id"]),
-                ]
-            )
-            if counter_id and len(counter_id) == 1:
-                message += "Ce PRM existe déjà dans Elocoop<br/>"
-                counter_period_ids = counter_id.period_ids.filtered(
-                    lambda p: p.prm_type == usage_point_prm_type
-                )
-                if counter_period_ids.filtered(
-                    lambda p: p.start_date == usage_point_start
-                    and p.end_date == usage_point_end
-                ):
-                    message += (
-                        "Les dates de début et de fin sont identiques à celles "
-                        "déjà enregistrées dans Elocoop.<br/>"
-                    )
-                elif counter_period_ids.filtered(
-                    lambda p: p.start_date == usage_point_start
-                ):
-                    counter_period_ids.filtered(
-                        lambda p: p.start_date == usage_point_start
-                    ).end_date = usage_point_end
-                    message += (
-                        "période existante avec la même date de début, mais date de "
-                        "fin différente, mise à jour date de fin<br/>"
-                    )
-                else:
-                    try:
-                        self.env["acc.counter.period"].create(
-                            {
-                                "acc_counter_id": counter_id.id,
-                                "prm_type": usage_point_prm_type,
-                                "acc_operation_id": self.id,
-                                "start_date": usage_point_start,
-                                "end_date": usage_point_end,
-                            }
-                        )
-
-                        counter_used.append(counter_id.name)
-                        message += (
-                            "De nouvelles dates sont renvoyées par Enedis : veuillez"
-                            " renseigner le participant lié dans l’onglet "
-                            "'Points de soutirage / Point d’injection' "
-                            "(en fonction du cas)<br/>"
-                        )
-                    except ValidationError as e:
-                        message += (
-                            "<strong>erreur lors de la tentative de création d'une "
-                            "nouvelle période, à vérifier manuellement :</strong><br/>"
-                            + str(e)
-                            + "<br/>"
-                        )
-
-            elif len(counter_id) > 1:
-                message += "Plusieurs PRMs trouvés avec ce numéro - pas de modif<br/>"
-            else:
-                message += "PRM n'existe pas : Création ...<br/>"
-                if usage_point_prm_type == "injection":
-                    # Si la date de l'opération n'est pas renseignée ou
-                    # après la date de démarrage du point d'injection
-                    # alors on force la date à celle du point d'injection
-                    if (
-                        not self.date_start_contract
-                        or self.date_start_contract > usage_point_start
-                    ):
-                        self.date_start_contract = usage_point_start
-                try:
-                    counter_id = self.env["acc.counter"].create(
-                        {
-                            "name": usage_point["usage_point_id"],
-                        }
-                    )
-                    counter_used.append(counter_id.name)
-                    self.env["acc.counter.period"].create(
-                        {
-                            "acc_counter_id": counter_id.id,
-                            "prm_type": usage_point_prm_type,
-                            "acc_operation_id": self.id,
-                            "start_date": usage_point_start,
-                            "end_date": usage_point_end,
-                            "sale_price": self.sale_price_by_default,
-                        }
-                    )
-
-                    self.check_sale_price_conf(
-                        counter_id=counter_id, periode_start_date=usage_point_start
-                    )
-
-                    # If delivery counter add to first priority group if exist
-                    if usage_point_prm_type == "delivery":
-                        if self.check_priority_groups(counter=counter_id):
-                            message += (
-                                "Ajout du nouveau PRM au premier groupe "
-                                "de priorité<br/>"
-                            )
-
-                    message += "Fin de la création du PRM<br/>"
-
-                except ValidationError as e:
-                    message += (
-                        "<strong>erreur lors de la tentative de création du PRM et/ou "
-                        "de la nouvelle période, à vérifier manuellement :<strong><br/>"
-                        + str(e)
-                        + "<br/>"
-                    )
-
-            if usage_point_prm_type == "injection":
-                if usage_point["usage_point_id"] not in list_injection:
-                    list_injection.append(usage_point["usage_point_id"])
-            if usage_point_prm_type == "delivery":
-                if usage_point["usage_point_id"] not in list_soutirage:
-                    list_soutirage.append(usage_point["usage_point_id"])
-
-        message += "<p>Liste complète des PRMs : </br>PRM Injection</br>"
+            # else ? on génère une exception ?
+
+            tLogData = {
+                'log_type': GetPerimeterEvent.NOT_SET,
+                'id': usage_point["usage_point_id"],
+                'prm_type': usage_point["type"],
+                'start': usage_point_start,
+                'end': usage_point_end,       # date() or False
+            }
 
-        i = 1
-        for inj in list_injection:
-            message += str(i) + " - " + inj + "<br/>"
-            i += 1
-        message += "Total: " + str(len(list_injection)) + "</br>"
+            counter_id = self.env["acc.counter"].search([
+                ("name", "=", usage_point["usage_point_id"]),
+            ])
+            if counter_id:
+                (counter_used, message) = self.update_existing_counter(counter_id, usage_point_prm_type, usage_point_start, usage_point_end, counter_used, message, tLogData)
+            #elif len(counter_id) > 1:
+            # il y a une contrainte d'unicité sur le champ name
+            # par contre si le champ acc_operation_id ne correspond pas on aurait un problème
+            else:
+                (counter_used, message) = self.add_new_counter(usage_point, usage_point_prm_type, usage_point_start, usage_point_end, counter_used, message, tLogData)
 
-        message += "<br/>PRM Soutirage<br/>"
-        i = 1
-        for inj in list_soutirage:
-            message += str(i) + " - " + inj + "<br/>"
-            i += 1
-        message += "Total: " + str(len(list_soutirage)) + "</br>"
+            tLogDatas.append(tLogData)
 
         message += (
-            "<h1>Fin appel API Périmètre: " + str(fields.Datetime.now()) + "</h1>"
+            "<p>Fin appel API Enedis pour mise à jour du périmètre: " + str(fields.Datetime.now()) + "</p>"
         )
 
-        updated_objects = "<br/>".join(counter_used)
+        message += self._gen_log_tables(tLogDatas)
+        message += self.gen_log_prm_lists(tLogDatas)
+
+        t_counters = [item['id'] for item in tLogDatas if item.get('log_type') in (GetPerimeterEvent.PRM_IN, GetPerimeterEvent.PRM_OUT, GetPerimeterEvent.DATE_CHANGE)]
+        updated_objects = "<br/>".join(t_counters)
 
         log_id = self.create_log(
             message=message,
             type_log="cron" if from_cron else "api",
             updated_objects=html_sanitize(updated_objects),
         )
-        if from_cron and counter_used:
+        if from_cron and (len(t_counters) > 0):
             log_id.send_new_prm_email()
 
+        self.debug_database()
+
         return log_id
 
+    def update_existing_counter(self, counter_id, usage_point_prm_type, usage_point_start, usage_point_end, counter_used, message, tLogData):
+        counter_period_ids = counter_id.period_ids.filtered(
+            lambda p: p.prm_type == usage_point_prm_type
+        )
+        if counter_period_ids.filtered(
+            lambda p: p.start_date == usage_point_start
+                      and p.end_date == usage_point_end
+        ):
+            tLogData['message'] = 'Pas de changement'
+            tLogData['log_type'] = GetPerimeterEvent.NO_CHANGE
+        elif counter_period_ids.filtered(
+            lambda p: p.start_date == usage_point_start
+        ):
+            counter_period_ids.filtered(
+                lambda p: p.start_date == usage_point_start
+            ).end_date = usage_point_end
+            counter_used.append(counter_id.name)
+            tLogData['message'] = 'Changement de la date de sortie du PRM'
+            tLogData['log_type'] = GetPerimeterEvent.PRM_OUT
+        else:
+            try:
+                self.env["acc.counter.period"].create({
+                    "acc_counter_id": counter_id.id,
+                    "prm_type": usage_point_prm_type,
+                    "acc_operation_id": self.id,
+                    "start_date": usage_point_start,
+                    "end_date": usage_point_end,
+                })
+
+                counter_used.append(counter_id.name)
+                tLogData['message'] = 'Changement de dates'
+                tLogData['log_type'] = GetPerimeterEvent.DATE_CHANGE
+                tLogData['action'] = ['Renseigner le participant lié dans l’onglet ' + (
+                    'Points d\'injection' if usage_point_prm_type == 'injection' else 'Points de soutirage')]
+            except ValidationError as e:
+                message += (
+                    "<strong>erreur lors de la tentative de création d'une "
+                    "nouvelle période, à vérifier manuellement :</strong><br/>"
+                    + str(e)
+                    + "<br/>"
+                )
+        return counter_used, message
+
+    def add_new_counter(self, usage_point, usage_point_prm_type, usage_point_start, usage_point_end, counter_used, message, tLogData):
+        tLogData['message'] = 'Entrée du PRM'
+        tLogData['log_type'] = GetPerimeterEvent.PRM_IN
+        if usage_point_prm_type == "injection":
+            # Si la date de l'opération n'est pas renseignée ou
+            # postérieure à la date de démarrage du point d'injection
+            # alors on force celle-ci à la date de démarrage du point d'injection
+            self.update_date_start_contract(usage_point_start)
+        try:
+            # ajout compteur
+            # le type est initialisé à posteriori lorsque les périodes sont mises à jour
+            counter_id = self.env["acc.counter"].create({
+                "name": usage_point["usage_point_id"]
+            })
+            counter_used.append(counter_id.name)
+
+            # ajout période compteur
+            counter_period_id = self.env["acc.counter.period"].create({
+                "acc_counter_id": counter_id.id,
+                "prm_type": usage_point_prm_type,
+                "acc_operation_id": self.id,
+                "start_date": usage_point_start,
+                "end_date": usage_point_end,
+                "sale_price": self.sale_price_by_default,
+            })
+
+            # ajout acc.price.conf se fait désormais lors de la création de la période
+
+            self.check_sale_price_conf(
+                counter_id=counter_id, periode_start_date=usage_point_start
+            )
+
+            # If delivery counter add to first priority group if exist
+            b_added = False
+            if usage_point_prm_type == "delivery":
+                if self.check_priority_groups(counter=counter_id):
+                    b_added = True
+
+            # ajout au premier groupe de priorité par défaut en fonction du type de clé de répartition de l'opération se fait désormais lors de la création de la période
+
+            tLogData['action'] = ['Renseigner le participant lié dans l’onglet ' + (
+                'Points d\'injection' if usage_point_prm_type == 'injection' else 'Points de soutirage')]
+            # TODO afficher uniquement si on a des groupes de priorité pour cette opération
+            if b_added:
+                tLogData[
+                    'action'].append('Vérifier le paramétrage de la clé de répartition si nécessaire, le nouveau PRM a été ajouté par défaut au premier groupe de priorité')
+
+        except ValidationError as e:
+            message += (
+                "<strong>erreur lors de la tentative de création du PRM et/ou "
+                "de la nouvelle période, à vérifier manuellement :</strong><br/>"
+                + str(e)
+                + "<br/>"
+            )
+        return counter_used, message
+
+    def update_date_start_contract(self, usage_point_start):
+        if (
+            not self.date_start_contract
+            or self.date_start_contract > usage_point_start
+        ):
+            self.date_start_contract = usage_point_start
+
     # ------------------------------------------------------
     # Business methods
     # ------------------------------------------------------
@@ -355,22 +475,29 @@ class AccOperation(models.Model):
             return True
         return False
 
-    def check_sale_price_conf(self, counter_id, periode_start_date):
-        """
-        create sale price conf on new counter
-        """
-        if counter_id.type in ["del", "del_inj"]:
-            inj_periods = self.env["acc.counter.period"].search(
-                [("acc_operation_id", "=", self.id), ("prm_type", "=", "injection")]
+    def check_sale_price_conf_for_delivery_counter(self, counter_id, periode_start_date):
+        if self.use_default_sale_price:
+
+            domain = [("acc_operation_id", "=", self.id), ("prm_type", "=", "injection")]
+            date_end_domain = expression.OR(
+                [
+                    [("end_date", ">=", periode_start_date)],
+                    [("end_date", "=", False)],
+                ]
             )
+            domain = expression.AND([domain, date_end_domain])
+            inj_periods = self.env["acc.counter.period"].search(domain)
 
             for inj_period in inj_periods:
                 if counter_id.type == "del_inj":
                     price = self.sale_price_by_default
                 else:
                     price = inj_period.sale_price
+                    if price == 0.0:
+                        # ex: compteur d'injection ajouté récemment
+                        price = self.sale_price_by_default
 
-                if self.use_default_sale_price and price > 0.0:
+                if price > 0.0:
                     self.env["acc.price.conf"].create(
                         {
                             "start_date": inj_period.start_date,
@@ -382,20 +509,37 @@ class AccOperation(models.Model):
                         }
                     )
 
-        elif counter_id.type in ["inj"]:
-            del_periods = self.env["acc.counter.period"].search(
-                [("acc_operation_id", "=", self.id), ("prm_type", "=", "delivery")]
+    def check_sale_price_conf_for_injection_counter(self, counter_id, periode_start_date):
+
+        if self.use_default_sale_price and self.sale_price_by_default > 0.0:
+            domain = [("acc_operation_id", "=", self.id), ("prm_type", "=", "delivery")]
+            date_end_domain = expression.OR(
+                [
+                    [("end_date", ">=", periode_start_date)],
+                    [("end_date", "=", False)],
+                ]
             )
+            domain = expression.AND([domain, date_end_domain])
+            del_periods = self.env["acc.counter.period"].search(domain)
 
             for del_period in del_periods:
-                if self.use_default_sale_price and self.sale_price_by_default > 0.0:
-                    self.env["acc.price.conf"].create(
-                        {
-                            "start_date": periode_start_date,
-                            "acc_operation_id": self.id,
-                            "acc_injection_counter_id": counter_id.id,
-                            "acc_delivery_counter_id": del_period.acc_counter_id.id,
-                            "price": self.sale_price_by_default,
-                            "type": "sale",
-                        }
-                    )
+                self.env["acc.price.conf"].create(
+                    {
+                        "start_date": periode_start_date,
+                        "acc_operation_id": self.id,
+                        "acc_injection_counter_id": counter_id.id,
+                        "acc_delivery_counter_id": del_period.acc_counter_id.id,
+                        "price": self.sale_price_by_default,
+                        "type": "sale",
+                    }
+                )
+
+    def check_sale_price_conf(self, counter_id, periode_start_date):
+        """
+        create sale price conf on new counter
+        """
+        if counter_id.type in ["del", "del_inj"]:
+            self.check_sale_price_conf_for_delivery_counter(counter_id, periode_start_date)
+
+        elif counter_id.type in ["inj"]:
+            self.check_sale_price_conf_for_injection_counter(counter_id, periode_start_date)
-- 
GitLab


From 60cdbfd48e560f41a4ab5f21769a890c1932a298 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Laporte?= <stephane.laporte@enercoop.org>
Date: Thu, 26 Jun 2025 15:20:09 +0200
Subject: [PATCH 2/2] [UPD] get perimeter exception

---
 data/mail_template_data.xml |  93 +-----
 models/acc_logs.py          |  14 +-
 models/acc_operation.py     | 584 ++++++++++++++++++++++--------------
 3 files changed, 382 insertions(+), 309 deletions(-)

diff --git a/data/mail_template_data.xml b/data/mail_template_data.xml
index 92798cf..dbfae80 100644
--- a/data/mail_template_data.xml
+++ b/data/mail_template_data.xml
@@ -3,8 +3,8 @@
      License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
 <odoo>
     <!--Email template -->
-    <record id="email_template_new_prm" model="mail.template">
-        <field name="name">Logs périmètres: Nouveau PRM ou période</field>
+    <record id="email_template_enedis_perimeter" model="mail.template">
+        <field name="name">Compte-rendu suite à la mise à jour du périmètre</field>
         <field name="model_id" ref="oacc.model_acc_logs" />
         <field name="email_from">"Elocoop" &lt;bonjour@elo.coop></field>
         <field
@@ -14,90 +14,23 @@
         <field name="reply_to">"Elocoop" &lt;support@elo.coop></field>
         <field
             name="subject"
-        >Action requise - entrées/sorties de PRM de l’opération {{object.acc_operation_id.name}}</field>
+        >Opération {{object.acc_operation_id.name}} : Compte-rendu suite à la mise à jour du périmètre</field>
         <field
             name="description"
-        >Envoi notification nouveau PRM ou nouvelle période détecté.</field>
+        >Compte-rendu suite à la mise à jour du périmètre</field>
         <field name="body_html" type="html">
             <div style="margin: 0px; padding: 0px;">
-                    Bonjour,<br /><br />
-                Vous recevez ce mail car vous êtes administrateur de l’opération d’autoconsommation collective : <t
-                    t-out="object.acc_operation_id.name"
-                >operation</t> (<t
-                    t-out="object.acc_operation_id.description"
-                >oacc</t>)<br />
-                    D’après les données Enedis, des nouveaux PRM ont été ajoutés et/ou retirés de l’opération ou ont changé de propriétaire.<br
-                /><br />
-                    Numéros des PRM concernées :<br />
-                        <t
-                    t-out="object.updated_objects"
-                    t-options="{'widget':'html'}"
-                />
-
-                <br />
-                <br />
-
-                Si des PRM sont ajoutés, afin qu’ils soient bien pris en compte dans Elocoop, nous vous remercions de renseigner les informations des participants liés à ces PRM dans votre espace d’administration Elocoop, de cette manière :<br
-                />
-                <ul>
-                    <li
-                    >Si ce n’est pas encore fait, créer le participant dans l’onglet Participants</li>
-                    <li
-                    >Lier le nouveau PRM à ce participant dans l’onglet Points de soutirage ou Points d’injection</li>
-                    <li>Indiquer les prix de vente pour ce PRM si nécessaire</li>
-                    <li
-                    >Donner accès au portail Elocoop au participant si nécessaire</li>
-                </ul>
-
-                Nous restons disponibles pour toute question à l’adresse bonjour@elo.coop.<br
-                /><br />
-                Bonne journée,<br /><br />
-
-                Le service client Elocoop<br />
-            </div>
-        </field>
-    </record>
-
-    <record id="email_template_api_error" model="mail.template">
-        <field name="name">Logs Périmètres: Erreur API Enedis</field>
-        <field name="model_id" ref="oacc.model_acc_logs" />
-        <field name="email_from">"Elocoop" &lt;bonjour@elo.coop></field>
-        <field name="partner_to" />
-        <field name="email_to">"Elocoop" &lt;support@elo.coop></field>
-        <field name="reply_to">"Elocoop" &lt;support@elo.coop></field>
-        <field
-            name="subject"
-        >Action requise - erreur d’API Enedis pour l’opération {{object.acc_operation_id.name}}</field>
-        <field
-            name="description"
-        >Envoi notification suite à une erreur api enedis.</field>
-        <field name="body_html" type="html">
-            <div style="margin: 0px; padding: 0px;">
-                    Bonjour,<br /><br />
-                    Vous recevez ce mail car vous êtes administrateur de l’opération d’autoconsommation collective: <t
-                    t-out="object.acc_operation_id.name"
-                >operation</t> (<t
-                    t-out="object.acc_operation_id.description"
-                >oacc</t>).<br />
-                    Lors de la mise à jour automatique du périmètre et des données Enedis, l’API a renvoyé l’erreur suivante :<br
-                /><br />
-                        <t t-out="object.message">operation</t><br /><br />
-                Nous vous remercions de vérifier que :<br />
-                <ul>
-                    <li
-                    >Vos identifiants Enedis sont corrects dans l’onglet Autres Informations de votre espace d’administration Elocoop</li>
-                    <li>L’identifiant <t
-                            t-out="object.acc_operation_id.name"
-                        >operation</t> est correct sur la page de votre opération.</li>
-                    <li
-                    >Nous informer en retour de ce mail lorsque cela est fait afin que nous vérifions la bonne configuration de l’API</li>
-                </ul>
-
+                Bonjour,<br /><br />
+                Vous recevez ce mail car vous êtes administrateur de
+                l’opération d’autoconsommation collective :
+                <t t-out="object.acc_operation_id.name">name</t>
+                (<t t-out="object.acc_operation_id.description">description</t>)<br />
+                Veuillez prendre connaissance des informations qui suivent.
                 Nous restons disponibles pour toute question à l’adresse bonjour@elo.coop.<br
-                /><br />
-                Bonne journée,<br /><br />
-
+                />
                 Le service client Elocoop<br />
+                <br />
+                <t t-out="object.message" t-options="{'widget':'html'}" /><br />
             </div>
         </field>
     </record>
diff --git a/models/acc_logs.py b/models/acc_logs.py
index 4fa0d5a..d9f4de6 100644
--- a/models/acc_logs.py
+++ b/models/acc_logs.py
@@ -33,20 +33,12 @@ class AccLogs(models.Model):
     # ------------------------------------------------------
     # Actions
     # ------------------------------------------------------
-    def send_new_prm_email(self):
+    def send_enedis_perimeter_email(self):
         """
-        send email for new prm
+        send email for new prm or update or exception raising
         """
         self.ensure_one()
-        template_id = self.env.ref("oacc_perimeter_api.email_template_new_prm")
-        template_id.send_mail(self.id)
-
-    def send_api_error_mail(self):
-        """
-        send api error email
-        """
-        self.ensure_one()
-        template_id = self.env.ref("oacc_perimeter_api.email_template_api_error")
+        template_id = self.env.ref("oacc_perimeter_api.email_template_enedis_perimeter")
         template_id.send_mail(self.id)
 
     # ------------------------------------------------------
diff --git a/models/acc_operation.py b/models/acc_operation.py
index 57f09a8..3125a30 100644
--- a/models/acc_operation.py
+++ b/models/acc_operation.py
@@ -1,14 +1,16 @@
 # Copyright 2021- Le Filament (https://le-filament.com)
 # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
-import json
-from datetime import date, datetime
+import traceback
+from datetime import date
+from enum import Enum
 
 from odoo import _, api, fields, models
-from odoo.exceptions import AccessError, ValidationError
-from odoo.tools import html_sanitize
+from odoo.exceptions import AccessError, UserError, ValidationError
 from odoo.osv import expression
+from odoo.tools import html_sanitize
+
+from odoo.addons.api_enedis_acc.tools.elo_enums import EloCode
 
-from enum import Enum
 
 class GetPerimeterEvent(Enum):
     NOT_SET = 1
@@ -16,7 +18,7 @@ class GetPerimeterEvent(Enum):
     PRM_IN = 3
     PRM_OUT = 4
     DATE_CHANGE = 5
-
+    EXCEPTION = 6
 
 
 class AccOperation(models.Model):
@@ -50,7 +52,7 @@ class AccOperation(models.Model):
     # ------------------------------------------------------
     # Actions
     # ------------------------------------------------------
-    def create_log(self, message, type_log="api", updated_objects="", call="perimetre"):
+    def create_log(self, message, type_log="api", call="perimetre"):
         if call == "perimetre":
             title = "Appel API Enedis Périmètre "
         else:
@@ -61,7 +63,6 @@ class AccOperation(models.Model):
                 "name": title + self.name + " du " + str(fields.Date.today()),
                 "date_launched": fields.Datetime.now(),
                 "type_log": type_log,
-                "updated_objects": updated_objects,
                 "message": message,
                 "acc_operation_id": self.id,
             }
@@ -69,7 +70,7 @@ class AccOperation(models.Model):
 
     def get_perimeter(self):
         """
-        call from user, for manual perimeter
+        called by user to update perimeter
         """
         self.ensure_one()
 
@@ -98,6 +99,7 @@ class AccOperation(models.Model):
                 "flags": {"initial_mode": "view"},
             }
 
+        # TODO Pourquoi on dit que tout va bien si admin
         return {
             "type": "ir.actions.client",
             "tag": "display_notification",
@@ -146,63 +148,137 @@ class AccOperation(models.Model):
     # API functions
     # ------------------------------------------------------
 
+    def _gen_error_table(selfself, tLogDatas):
+        message = ""
+        n = sum(
+            1
+            for item in tLogDatas
+            if item.get("log_type") == GetPerimeterEvent.EXCEPTION
+        )
+        if n == 0:
+            return message
+
+        message += (
+            "<h5>Erreurs survenues pendant le "
+            "traitement des données provenant de Enedis</h5>"
+        )
+        message += "<table class='table table-bordered table-striped'>"
+        message += "<thead>"
+        message += "<th>description</th><th>action à prendre</th>"
+        message += "</thead>"
+        for tLogData in tLogDatas:
+            if tLogData["log_type"] != GetPerimeterEvent.EXCEPTION:
+                continue
+            message += "<tr>"
+            data = (tLogData["data"] + "<br/>") if "data" in tLogData else ""
+            message += (
+                f"<td>{tLogData['message']}<br/>{data}{tLogData['stack_trace']}</td>"
+            )
+            message += "<td>"
+            if "action" in tLogData:
+                message += "<ul>"
+                for v in tLogData["action"]:
+                    message += f"<li>{v}</li>"
+                message += "</ul>"
+            message += "</td>"
+            message += "</tr>"
+        message += "</table>"
+        return message
+
     def _gen_log_table(self, tLogDatas, log_type, header):
         message = ""
 
-        n = sum(1 for item in tLogDatas if item.get('log_type') == log_type)
-        if n == 0: return message
+        n = sum(1 for item in tLogDatas if item.get("log_type") == log_type)
+        if n == 0:
+            return message
 
         message += f"<h5>{header}</h5>"
         message += "<table class='table table-striped'>"
         message += "<thead>"
-        message += "<th>PRM</th><th>type</th><th>début</th><th>fin</th><th>action à prendre</th>"
+        message += (
+            "<th>PRM</th><th>type</th><th>début</th><th>fin</th>"
+            "<th>action à prendre</th>"
+        )
         message += "</thead>"
         for tLogData in tLogDatas:
-            if (tLogData['log_type'] != log_type): continue
-            end_date = '' if tLogData['end'] is False else tLogData['end'].strftime('%d/%m/%Y')
-            message += (f"<tr>")
-            message += (f"<td class='text-nowrap'>{tLogData['id']}</td>")
-            message += (f"<td class='text-nowrap'>{'injection' if tLogData['prm_type'] == 'PROD' else 'soutirage'}</td>")
-            message += (f"<td class='text-nowrap'>{tLogData['start']:%d/%m/%Y}</td>")
+            if tLogData["log_type"] != log_type:
+                continue
+            end_date = (
+                "" if tLogData["end"] is False else tLogData["end"].strftime("%d/%m/%Y")
+            )
+            message += "<tr>"
+            message += f"<td class='text-nowrap'>{tLogData['id']}</td>"
+            message += (
+                f"<td class='text-nowrap'>"
+                f"{'injection' if tLogData['prm_type'] == 'PROD' else 'soutirage'}"
+                f"</td>"
+            )
+            message += f"<td class='text-nowrap'>{tLogData['start']:%d/%m/%Y}</td>"
             message += f"<td class='text-nowrap' style='width: 10em;'>{end_date}</td>"
-            message += ("<td>")
-            if 'action' in tLogData:
-                message += ("<ul>")
-                for v in tLogData['action']:
-                    message += (f"<li>{v}</li>")
-                message += ("</ul>")
-            message += (f"</td>")
-            message += (f"</tr>")
+            message += "<td>"
+            if "action" in tLogData:
+                message += "<ul>"
+                for v in tLogData["action"]:
+                    message += f"<li>{v}</li>"
+                message += "</ul>"
+            message += "</td>"
+            message += "</tr>"
         message += "</table>"
         return message
 
     def _gen_log_tables(self, tLogDatas):
         message = ""
-        # MULTI
-        message += self._gen_log_table(tLogDatas, GetPerimeterEvent.PRM_IN, 'Entrées de PRM: des actions sont requises')
-        message += self._gen_log_table(tLogDatas, GetPerimeterEvent.DATE_CHANGE, 'Dates modifiées: des actions sont requises')
-        message += self._gen_log_table(tLogDatas, GetPerimeterEvent.PRM_OUT, 'Sorties de PRM: pour information, ne requiert pas d\'action de votre part')
-        message += self._gen_log_table(tLogDatas, GetPerimeterEvent.NO_CHANGE, 'Pas de changement sur ces PRM: pour information, ne requiert pas d\'action de votre part')
+        message += self._gen_log_table(
+            tLogDatas,
+            GetPerimeterEvent.PRM_IN,
+            "Entrées de PRM: des actions sont requises",
+        )
+        message += self._gen_log_table(
+            tLogDatas,
+            GetPerimeterEvent.DATE_CHANGE,
+            "Dates modifiées: des actions sont requises",
+        )
+        message += self._gen_log_table(
+            tLogDatas,
+            GetPerimeterEvent.PRM_OUT,
+            "Sorties de PRM: "
+            "pour information, ne requiert pas d'action de votre part",
+        )
+        message += self._gen_log_table(
+            tLogDatas,
+            GetPerimeterEvent.NO_CHANGE,
+            "Pas de changement sur ces PRM: "
+            "pour information, ne requiert pas d'action de votre part",
+        )
         return message
 
     def gen_log_prm_list(self, tLogDatas, prm_type):
+        message = ""
         t_ids = [
-            item["id"] for item in tLogDatas
-            if item.get("log_type") in (
+            item["id"]
+            for item in tLogDatas
+            if item.get("log_type")
+            in (
                 GetPerimeterEvent.NO_CHANGE,
                 GetPerimeterEvent.PRM_IN,
-                GetPerimeterEvent.PRM_OUT
+                GetPerimeterEvent.PRM_OUT,
             )
             and item.get("prm_type") == prm_type
         ]
-        header = ("Liste complète des " + str(len(t_ids))+ " PRM d'injection"
-                  if prm_type == "PROD" else "Liste complète des " + str(len(t_ids))+ " PRM de soutirage")
-        message = f"<h5>{header}</h5>"
+        n = len(t_ids)
+        if n == 0:
+            return message
+        header = (
+            "Pour information, liste des " + str(n) + " PRM d'injection"
+            if prm_type == "PROD"
+            else "Pour information, liste des " + str(n) + " PRM de soutirage"
+        )
+        message += f"<h5>{header}</h5>"
         message += "<table class='table table-striped'>"
-        for id in t_ids:
-            message += (f"<tr>")
-            message += (f"<td>{id}</td>")
-            message += (f"</tr>")
+        for cid in t_ids:
+            message += "<tr>"
+            message += f"<td>{cid}</td>"
+            message += "</tr>"
         message += "</table>"
         return message
 
@@ -212,35 +288,6 @@ class AccOperation(models.Model):
         message += self.gen_log_prm_list(tLogDatas, "CONS")
         return message
 
-    def clean_database(self):
-        o_counters = self.env["acc.counter"].search([("name", ">=", "90000000000001"),("name", "<=", "90000000000010")])
-        #print(o_counters)
-        o_counter_periods = o_counters.period_ids
-        #print(o_counter_periods)
-        o_prices = self.env["acc.price.conf"].search([("acc_injection_counter_id", "in", o_counters.ids)])
-        #print(o_prices)
-        o_prices.unlink()
-        o_prices = self.env["acc.price.conf"].search([("acc_delivery_counter_id", "in", o_counters.ids)])
-        #print(o_prices)
-        o_prices.unlink()
-        o_counter_periods.unlink()
-        o_counters.unlink()
-
-    def debug_database(self):
-        o_counters = self.env["acc.counter"].search([("name", ">=", "90000000000001"),("name", "<=", "90000000000010")])
-        for o_counter in o_counters:
-            print('added counter ' + str(o_counter.id) + ' ' + o_counter.name)
-        o_counter_periods = o_counters.period_ids
-        for o_counter_period in o_counter_periods:
-            print('added counter period ' + str(o_counter_period.id) + ' ' + o_counter_period.acc_counter_id.name + ' ' + str(o_counter_period.start_date) + ' ' + str(o_counter_period.end_date))
-        o_prices = self.env["acc.price.conf"].search([("acc_injection_counter_id", "in", o_counters.ids)])
-        for o_price in o_prices:
-            print('added price ' + str(o_price.id) + ' ' + o_price.acc_injection_counter_id.name + ' ' + o_price.acc_delivery_counter_id.name + ' ' + str(o_price.start_date) + ' ' + str(o_price.end_date) + ' ' + str(o_price.price))
-        o_prices = self.env["acc.price.conf"].search([("acc_delivery_counter_id", "in", o_counters.ids)])
-        for o_price in o_prices:
-            print('added price ' + str(o_price.id) + ' ' + o_price.acc_injection_counter_id.name + ' ' + o_price.acc_delivery_counter_id.name + ' ' + str(o_price.start_date) + ' ' + str(o_price.end_date) + ' ' + str(o_price.price))
-
-
     def _perimeter(self, from_cron=False):
         """
         Récupère les données de l'opération concernant le périmètre:
@@ -260,204 +307,296 @@ class AccOperation(models.Model):
                         'end': '2025-06-16 / 9999-12-31'
                     },
         """
+        tLogDatas = []
         message = ""
+        try:
+            message += (
+                "<p>Début mise à jour du périmètre de l'opération "
+                + self.name
+                + " : "
+                + str(fields.Datetime.now())
+                + "</p>"
+            )
 
-        message += (
-            "<p>Début appel API Enedis pour mise à jour du périmètre "
-            + self.name
-            + " : "
-            + str(fields.Datetime.now())
-            + "</p>"
-        )
-        if False:
             self._check_access_api()
-            message += "<p>Appel API ...</p>"
-            try:
-                perimeter_data = self._get_perimeter()
-            except ValidationError as e:
-                if from_cron:
-                    log_id = self.create_log(message=str(e), type_log="cron")
-                    log_id.send_api_error_mail()
-                raise e from e
-
-            message += "<p>Appel API terminé</p>"
-        else:
-            self.clean_database()
-            with open('/home/stephane.laporte/Téléchargements/enedis_perimeter_ecoe_castelet.txt', 'r') as file:
-                sJsonData = file.read()
-            sJsonData = sJsonData.replace("'", '"')
-            perimeter_data = json.loads(sJsonData)
 
-        usage_points = perimeter_data.get("usage_points")
+            message += (
+                "<p>Début appel API Enedis :" + str(fields.Datetime.now()) + "</p>"
+            )
+
+            perimeter_data = self._get_perimeter()
 
-        counter_used = []
+            message += "<p>Fin appel API Enedis :" + str(fields.Datetime.now()) + "</p>"
 
-        tLogDatas = []
+            usage_points = perimeter_data.get("usage_points")
 
-        for usage_point in sorted(
-            usage_points, key=lambda p: date.fromisoformat(p["start"])
-        ):
-            usage_point_start = date.fromisoformat(usage_point["start"])
-            usage_point_end = (
-                date.fromisoformat(usage_point["end"])
-                if usage_point["end"] != "9999-12-31"
-                else False
+            sorted_usage_points = sorted(
+                usage_points, key=lambda p: date.fromisoformat(p["start"])
             )
-            usage_point_prm_type = False
-            if usage_point["type"] == "CONS":
-                usage_point_prm_type = "delivery"
-            elif usage_point["type"] == "PROD":
-                usage_point_prm_type = "injection"
-            # else ? on génère une exception ?
 
+            for usage_point in sorted_usage_points:
+                try:
+                    usage_point_start = date.fromisoformat(usage_point["start"])
+                    usage_point_end = (
+                        date.fromisoformat(usage_point["end"])
+                        if usage_point["end"] != "9999-12-31"
+                        else False
+                    )
+                    if usage_point["type"] == "CONS":
+                        usage_point_prm_type = "delivery"
+                    elif usage_point["type"] == "PROD":
+                        usage_point_prm_type = "injection"
+                    else:
+                        raise UserError(_("type incorrect."))
+                except Exception:
+                    tLogData = {
+                        "log_type": GetPerimeterEvent.EXCEPTION,
+                        "message": "Ces données provenant de Enedis sont mal formées.",
+                        "data": str(usage_point),
+                        "stack_trace": traceback.format_exc(),
+                        "action": ["Contacter le service client Elocoop"],
+                    }
+                    tLogDatas.append(tLogData)
+                    continue
+
+                tLogData = {
+                    "id": usage_point["usage_point_id"],
+                    "prm_type": usage_point["type"],
+                    "start": usage_point_start,
+                    "end": usage_point_end,  # date() or False
+                }
+
+                counter_id = self.env["acc.counter"].search(
+                    [
+                        ("name", "=", usage_point["usage_point_id"]),
+                    ]
+                )
+                if counter_id:
+                    try:
+                        self.update_existing_counter(
+                            counter_id,
+                            usage_point_prm_type,
+                            usage_point_start,
+                            usage_point_end,
+                            tLogData,
+                        )
+                    except Exception:
+                        tLogData = {
+                            "log_type": GetPerimeterEvent.EXCEPTION,
+                            "message": "Une erreur est survenue lors de "
+                            "la création de la période.",
+                            "data": str(usage_point),
+                            "stack_trace": traceback.format_exc(),
+                            "action": ["Contacter le service client Elocoop"],
+                        }
+                        tLogDatas.append(tLogData)
+                        continue
+                # elif len(counter_id) > 1:
+                # il y a une contrainte d'unicité sur le champ name
+                # par contre si le champ acc_operation_id
+                # ne correspond pas on aurait un problème
+                else:
+                    try:
+                        self.add_new_counter(
+                            usage_point,
+                            usage_point_prm_type,
+                            usage_point_start,
+                            usage_point_end,
+                            tLogData,
+                        )
+                    except Exception:
+                        tLogData = {
+                            "log_type": GetPerimeterEvent.EXCEPTION,
+                            "message": "Une erreur est survenue lors de "
+                            "la création du PRM ou de la période associée.",
+                            "data": str(usage_point),
+                            "stack_trace": traceback.format_exc(),
+                            "action": ["Contacter le service client Elocoop"],
+                        }
+                        tLogDatas.append(tLogData)
+                        continue
+
+                tLogDatas.append(tLogData)
+
+            message += (
+                "<p>Fin mise à jour du périmètre: "
+                + str(fields.Datetime.now())
+                + "</p>"
+            )
+
+        except Exception as e:
             tLogData = {
-                'log_type': GetPerimeterEvent.NOT_SET,
-                'id': usage_point["usage_point_id"],
-                'prm_type': usage_point["type"],
-                'start': usage_point_start,
-                'end': usage_point_end,       # date() or False
+                "log_type": GetPerimeterEvent.EXCEPTION,
+                "message": "Une erreur est survenue lors du "
+                "traitement des données provenant de Enedis.",
+                "stack_trace": traceback.format_exc(),
             }
-
-            counter_id = self.env["acc.counter"].search([
-                ("name", "=", usage_point["usage_point_id"]),
-            ])
-            if counter_id:
-                (counter_used, message) = self.update_existing_counter(counter_id, usage_point_prm_type, usage_point_start, usage_point_end, counter_used, message, tLogData)
-            #elif len(counter_id) > 1:
-            # il y a une contrainte d'unicité sur le champ name
-            # par contre si le champ acc_operation_id ne correspond pas on aurait un problème
+            code = getattr(e, "code", 0)
+            if code == EloCode.MISSING_ENEDIS_ID:
+                tLogData["action"] = [
+                    "Vérifier que les identifiants Enedis "
+                    "sont corrects dans l’onglet Autres Informations "
+                    "de votre espace d’administration Elocoop",
+                    f"Vérifier que l’identifiant de "
+                    f"l'opération {self.name} est correct",
+                    "Dès que vous avez tout vérifié, "
+                    "nous en informer en répondant à cet mail",
+                ]
             else:
-                (counter_used, message) = self.add_new_counter(usage_point, usage_point_prm_type, usage_point_start, usage_point_end, counter_used, message, tLogData)
-
+                tLogData["action"] = ["Contacter le service client Elocoop"]
             tLogDatas.append(tLogData)
 
-        message += (
-            "<p>Fin appel API Enedis pour mise à jour du périmètre: " + str(fields.Datetime.now()) + "</p>"
+        t_counters = [
+            item["id"]
+            for item in tLogDatas
+            if item.get("log_type")
+            in (
+                GetPerimeterEvent.PRM_IN,
+                GetPerimeterEvent.PRM_OUT,
+                GetPerimeterEvent.DATE_CHANGE,
+            )
+        ]
+        n_exceptions = sum(
+            1 for d in tLogDatas if d.get("log_type") == GetPerimeterEvent.EXCEPTION
         )
 
+        message += self._gen_error_table(tLogDatas)
         message += self._gen_log_tables(tLogDatas)
         message += self.gen_log_prm_lists(tLogDatas)
 
-        t_counters = [item['id'] for item in tLogDatas if item.get('log_type') in (GetPerimeterEvent.PRM_IN, GetPerimeterEvent.PRM_OUT, GetPerimeterEvent.DATE_CHANGE)]
-        updated_objects = "<br/>".join(t_counters)
-
         log_id = self.create_log(
-            message=message,
+            message=html_sanitize(message),
             type_log="cron" if from_cron else "api",
-            updated_objects=html_sanitize(updated_objects),
         )
-        if from_cron and (len(t_counters) > 0):
-            log_id.send_new_prm_email()
-
-        self.debug_database()
-
+        # envoyer le mail si une exception se produit et pas uniquement
+        # si une modification est détectée au niveau des PRM
+        if from_cron and ((len(t_counters) > 0) or (n_exceptions > 0)):
+            log_id.send_enedis_perimeter_email()
         return log_id
 
-    def update_existing_counter(self, counter_id, usage_point_prm_type, usage_point_start, usage_point_end, counter_used, message, tLogData):
+    def update_existing_counter(
+        self,
+        counter_id,
+        usage_point_prm_type,
+        usage_point_start,
+        usage_point_end,
+        tLogData,
+    ):
         counter_period_ids = counter_id.period_ids.filtered(
             lambda p: p.prm_type == usage_point_prm_type
         )
         if counter_period_ids.filtered(
             lambda p: p.start_date == usage_point_start
-                      and p.end_date == usage_point_end
-        ):
-            tLogData['message'] = 'Pas de changement'
-            tLogData['log_type'] = GetPerimeterEvent.NO_CHANGE
-        elif counter_period_ids.filtered(
-            lambda p: p.start_date == usage_point_start
+            and p.end_date == usage_point_end
         ):
+            tLogData["message"] = "Pas de changement"
+            tLogData["log_type"] = GetPerimeterEvent.NO_CHANGE
+        elif counter_period_ids.filtered(lambda p: p.start_date == usage_point_start):
             counter_period_ids.filtered(
                 lambda p: p.start_date == usage_point_start
             ).end_date = usage_point_end
-            counter_used.append(counter_id.name)
-            tLogData['message'] = 'Changement de la date de sortie du PRM'
-            tLogData['log_type'] = GetPerimeterEvent.PRM_OUT
+            tLogData["message"] = "Changement de la date de sortie du PRM"
+            tLogData["log_type"] = GetPerimeterEvent.PRM_OUT
         else:
-            try:
-                self.env["acc.counter.period"].create({
+            self.env["acc.counter.period"].create(
+                {
                     "acc_counter_id": counter_id.id,
                     "prm_type": usage_point_prm_type,
                     "acc_operation_id": self.id,
                     "start_date": usage_point_start,
                     "end_date": usage_point_end,
-                })
-
-                counter_used.append(counter_id.name)
-                tLogData['message'] = 'Changement de dates'
-                tLogData['log_type'] = GetPerimeterEvent.DATE_CHANGE
-                tLogData['action'] = ['Renseigner le participant lié dans l’onglet ' + (
-                    'Points d\'injection' if usage_point_prm_type == 'injection' else 'Points de soutirage')]
-            except ValidationError as e:
-                message += (
-                    "<strong>erreur lors de la tentative de création d'une "
-                    "nouvelle période, à vérifier manuellement :</strong><br/>"
-                    + str(e)
-                    + "<br/>"
-                )
-        return counter_used, message
-
-    def add_new_counter(self, usage_point, usage_point_prm_type, usage_point_start, usage_point_end, counter_used, message, tLogData):
-        tLogData['message'] = 'Entrée du PRM'
-        tLogData['log_type'] = GetPerimeterEvent.PRM_IN
+                }
+            )
+            tLogData["message"] = "Changement des dates"
+            tLogData["log_type"] = GetPerimeterEvent.DATE_CHANGE
+            tLogData["action"] = [
+                "Ajouter, si ce n'est pas déjà le cas, le participant "
+                "dans l'onglet Participants de l'opération",
+                "Lier le PRM et le participant dans l’onglet "
+                + (
+                    "Points d'injection"
+                    if usage_point_prm_type == "injection"
+                    else "Points de soutirage"
+                ),
+                "Indiquer si nécessaire le prix de vente pour ce PRM",
+                "Vérifier le paramétrage de la clé de répartition le cas échéant",
+                "Donner éventuellement au participant l'accès au portail Elocoop",
+            ]
+
+    def add_new_counter(
+        self,
+        usage_point,
+        usage_point_prm_type,
+        usage_point_start,
+        usage_point_end,
+        tLogData,
+    ):
+        tLogData["message"] = "Entrée du PRM"
+        tLogData["log_type"] = GetPerimeterEvent.PRM_IN
         if usage_point_prm_type == "injection":
             # Si la date de l'opération n'est pas renseignée ou
             # postérieure à la date de démarrage du point d'injection
             # alors on force celle-ci à la date de démarrage du point d'injection
             self.update_date_start_contract(usage_point_start)
-        try:
-            # ajout compteur
-            # le type est initialisé à posteriori lorsque les périodes sont mises à jour
-            counter_id = self.env["acc.counter"].create({
-                "name": usage_point["usage_point_id"]
-            })
-            counter_used.append(counter_id.name)
-
-            # ajout période compteur
-            counter_period_id = self.env["acc.counter.period"].create({
+
+        # ajout compteur
+        # le type est initialisé à posteriori lorsque les périodes sont mises à jour
+        counter_id = self.env["acc.counter"].create(
+            {"name": usage_point["usage_point_id"]}
+        )
+
+        # ajout période compteur
+        self.env["acc.counter.period"].create(
+            {
                 "acc_counter_id": counter_id.id,
                 "prm_type": usage_point_prm_type,
                 "acc_operation_id": self.id,
                 "start_date": usage_point_start,
                 "end_date": usage_point_end,
                 "sale_price": self.sale_price_by_default,
-            })
-
-            # ajout acc.price.conf se fait désormais lors de la création de la période
-
-            self.check_sale_price_conf(
-                counter_id=counter_id, periode_start_date=usage_point_start
-            )
-
-            # If delivery counter add to first priority group if exist
-            b_added = False
-            if usage_point_prm_type == "delivery":
-                if self.check_priority_groups(counter=counter_id):
-                    b_added = True
+            }
+        )
 
-            # ajout au premier groupe de priorité par défaut en fonction du type de clé de répartition de l'opération se fait désormais lors de la création de la période
+        # ajout acc.price.conf se fait désormais lors de la création de la période
 
-            tLogData['action'] = ['Renseigner le participant lié dans l’onglet ' + (
-                'Points d\'injection' if usage_point_prm_type == 'injection' else 'Points de soutirage')]
-            # TODO afficher uniquement si on a des groupes de priorité pour cette opération
-            if b_added:
-                tLogData[
-                    'action'].append('Vérifier le paramétrage de la clé de répartition si nécessaire, le nouveau PRM a été ajouté par défaut au premier groupe de priorité')
+        self.check_sale_price_conf(
+            counter_id=counter_id, periode_start_date=usage_point_start
+        )
 
-        except ValidationError as e:
-            message += (
-                "<strong>erreur lors de la tentative de création du PRM et/ou "
-                "de la nouvelle période, à vérifier manuellement :</strong><br/>"
-                + str(e)
-                + "<br/>"
+        # If delivery counter add to first priority group if exist
+        b_added = False
+        if usage_point_prm_type == "delivery":
+            if self.check_priority_groups(counter=counter_id):
+                b_added = True
+
+        # ajout au premier groupe de priorité par défaut
+        # en fonction du type de clé de répartition de l'opération
+        # se fera désormais lors de la création de la période
+
+        tLogData["action"] = [
+            "Ajouter, si ce n'est pas déjà le cas, "
+            "le participant dans l'onglet Participants de l'opération",
+            "Lier le nouveau PRM et le participant dans l’onglet "
+            + (
+                "Points d'injection"
+                if usage_point_prm_type == "injection"
+                else "Points de soutirage"
+            ),
+            "Indiquer si nécessaire le prix de vente pour ce PRM",
+        ]
+        if b_added:
+            tLogData["action"].append(
+                "Vérifier le paramétrage de la clé de répartition si nécessaire, "
+                "le nouveau PRM a été ajouté par défaut au premier groupe de priorité",
             )
-        return counter_used, message
+        tLogData["action"].append(
+            "Donner éventuellement au participant l'accès au portail Elocoop",
+        )
 
     def update_date_start_contract(self, usage_point_start):
-        if (
-            not self.date_start_contract
-            or self.date_start_contract > usage_point_start
-        ):
+        if not self.date_start_contract or self.date_start_contract > usage_point_start:
             self.date_start_contract = usage_point_start
+            # ? à quel moment on enregistre en base ?
 
     # ------------------------------------------------------
     # Business methods
@@ -475,10 +614,14 @@ class AccOperation(models.Model):
             return True
         return False
 
-    def check_sale_price_conf_for_delivery_counter(self, counter_id, periode_start_date):
+    def check_sale_price_conf_for_delivery_counter(
+        self, counter_id, periode_start_date
+    ):
         if self.use_default_sale_price:
-
-            domain = [("acc_operation_id", "=", self.id), ("prm_type", "=", "injection")]
+            domain = [
+                ("acc_operation_id", "=", self.id),
+                ("prm_type", "=", "injection"),
+            ]
             date_end_domain = expression.OR(
                 [
                     [("end_date", ">=", periode_start_date)],
@@ -509,8 +652,9 @@ class AccOperation(models.Model):
                         }
                     )
 
-    def check_sale_price_conf_for_injection_counter(self, counter_id, periode_start_date):
-
+    def check_sale_price_conf_for_injection_counter(
+        self, counter_id, periode_start_date
+    ):
         if self.use_default_sale_price and self.sale_price_by_default > 0.0:
             domain = [("acc_operation_id", "=", self.id), ("prm_type", "=", "delivery")]
             date_end_domain = expression.OR(
@@ -539,7 +683,11 @@ class AccOperation(models.Model):
         create sale price conf on new counter
         """
         if counter_id.type in ["del", "del_inj"]:
-            self.check_sale_price_conf_for_delivery_counter(counter_id, periode_start_date)
+            self.check_sale_price_conf_for_delivery_counter(
+                counter_id, periode_start_date
+            )
 
         elif counter_id.type in ["inj"]:
-            self.check_sale_price_conf_for_injection_counter(counter_id, periode_start_date)
+            self.check_sale_price_conf_for_injection_counter(
+                counter_id, periode_start_date
+            )
-- 
GitLab