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] =?UTF-8?q?[FIX]=20erreur=20lors=20de=20la=20tentative=20d?= =?UTF-8?q?e=20cr=C3=A9ation=20du=20PRM=20et/ou=20de=20la=20nouvelle=20p?= =?UTF-8?q?=C3=A9riode,=20=C3=A0=20v=C3=A9rifier=20manuellement=20:=20Impo?= =?UTF-8?q?ssible=20de=20cr=C3=A9er=20une=20nouvelle=20p=C3=A9riode=20pour?= =?UTF-8?q?=20le=20mod=C3=A8le=20Gestion=20des=20prix,=20il=20y=20a=20d?= =?UTF-8?q?=C3=A9j=C3=A0=20une=20p=C3=A9riode=20qui=20d=C3=A9marre=20le=20?= =?UTF-8?q?2025-06-02=20et=20qui=20chevauche=20la=20p=C3=A9riode=20existan?= =?UTF-8?q?te?= 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