diff --git a/models/acc_operation.py b/models/acc_operation.py index bd228c5012a25675a6d785ea40f243a1f38b2c89..ce31e0a27a58e79b2c754468e1d17519de97c113 100644 --- a/models/acc_operation.py +++ b/models/acc_operation.py @@ -1,10 +1,12 @@ # Copyright 2021- Le Filament (https://le-filament.com) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +from io import StringIO +from ..tools import export_cdc from datetime import datetime from dateutil.relativedelta import relativedelta -from odoo import _, fields, models +from odoo import _, fields, models, http from odoo.exceptions import ValidationError from odoo.osv import expression from odoo.tools import date_utils @@ -69,6 +71,7 @@ class AccOperation(models.Model): @returns: last acc.enedis.cdc record in operations """ domain = [("acc_operation_id", "in", self.ids)] + if partner_id: domain = expression.AND([domain, [("partner_id", "=", partner_id)]]) # Get last date slot recorded @@ -162,12 +165,12 @@ class AccOperation(models.Model): """ Function to build SELECT section of query for retrieving curves @param - char date_slot : granularity (one of "hour", "day", "month", "year") + char date_slot : granularity (one of "minute", "hour", "day", "month", "year") [char] curve_types : list of type of curves (allowed values : 'cons', 'autocons', 'allocons', 'prod', 'surplus', 'autoprod') """ - if date_slot not in ("hour", "day", "month", "year"): + if date_slot not in ("minute", "hour", "day", "month", "year"): raise ValidationError(_("Incorrect date_slot in SELECT section")) result = f""" SELECT date_trunc('{date_slot}', @@ -227,7 +230,7 @@ class AccOperation(models.Model): date start_date : first date to be retrieved date end_date : last date to be retrieved int prm_id : id of PRM to be retrieved (optional) - char extra_curve_type : extra curve to be retrieved (optional) + [char] extra_curve_type : extra curve to be retrieved (optional) (allowed values : 'cons', 'autocons', 'allocons', 'prod', 'surplus', 'autoprod') int partner_id : id of partner to be retrieved (optional) @@ -246,7 +249,13 @@ class AccOperation(models.Model): AND cdc.date_slot >= '{start_datetime}' AND cdc.date_slot < '{end_datetime}' """ - if prm_id and isinstance(prm_id, int): + + if partner_id and isinstance(partner_id, int): + result += f" AND ((cdc.partner_id = {partner_id} " + if prm_id and isinstance(prm_id, int): + result += f" AND cdc.acc_counter_id = {prm_id}) " + else: + result = f"{result})" if extra_curve_type and extra_curve_type in ( "cons", "autocons", @@ -255,25 +264,19 @@ class AccOperation(models.Model): "surplus", "autoprod", ): - result += f""" - AND (cdc.comp_data_type = '{extra_curve_type}' - OR cdc.acc_counter_id = {prm_id}) - """ + result = f"{result} OR cdc.comp_data_type = '{extra_curve_type}' )" else: - result += f""" - AND cdc.acc_counter_id = {prm_id} - """ - if partner_id and isinstance(partner_id, int): - result += f" AND cdc.partner_id = {partner_id} " + result = f"{result})" + return result def _group_clause(self, date_slot): """ Function to build GROUP BY section of query for retrieving curves @param - char date_slot : granularity (one of "hour", "day", "month", "year") + char date_slot : granularity (one of "minute", "hour", "day", "month", "year") """ - if date_slot not in ("hour", "day", "month", "year"): + if date_slot not in ("minute", "hour", "day", "month", "year"): raise ValidationError(_("Incorrect date_slot in GROUP BY section")) return f""" GROUP BY date_trunc('{date_slot}', @@ -283,6 +286,43 @@ class AccOperation(models.Model): def _order_clause(self): return "ORDER BY date_slot ASC;" + def _cdc_by_query_cons( + self, + slot_type, + start_date, + end_date, + prm_id=None, + partner_id=None, + curve_types=["cons", "autocons", "prod"], + ): + """ + Fonction permettant de récupérer les données pour les consommateurs + + :param: char slot_type: type de slot pour la query ("minute", "hour", "month" ou + "year") + datetime start_date: date début + datetime end_date: date de fin + int prm_id : PRM de soutirage à récupérer + int partner_id: contact associé à la courbe + [char] curve_types: type de données + @returns: resultat de la requête + (labels et data pour les charts à afficher) + + """ + in_search = ", ".join(f"'{curve}'" for curve in curve_types) + query = ( + self._select_clause(date_slot=slot_type, curve_types=curve_types) + + self._from_clause() + + self._where_clause( + self.id, start_date, end_date, prm_id, "prod", partner_id + ) + + f"AND cdc.comp_data_type IN ({in_search})" + + self._group_clause(date_slot=slot_type) + + self._order_clause() + ) + self.env.cr.execute(query) + return self.env.cr.fetchall() + def get_cdc_by_query_cons( self, slot_type, start_date, end_date, prm_id=None, partner_id=None ): @@ -290,13 +330,13 @@ class AccOperation(models.Model): Fonction permettant de récupérer les données pour la construction des chart pour une ou des opérations données pour les consommateurs - :param: char slot_type: type de slot pour la query ("hour", "month" ou + :param: char slot_type: type de slot pour la query ("minute", "hour", "month" ou "year") datetime start_date: date début datetime end_date: date de fin int prm_id : PRM de soutirage à récupérer int partner_id: contact associé à la courbe - @returns: un dictionnaire de données + @returns: resultat de la requête (labels et data pour les charts à afficher) """ self.ensure_one() @@ -307,20 +347,10 @@ class AccOperation(models.Model): data_cons = [] data_prod = [] - query = ( - self._select_clause( - date_slot=slot_type, curve_types=["cons", "autocons", "prod"] - ) - + self._from_clause() - + self._where_clause( - self.id, start_date, end_date, prm_id, "prod", partner_id - ) - + "AND cdc.comp_data_type IN ('autocons', 'cons', 'prod')" - + self._group_clause(date_slot=slot_type) - + self._order_clause() + raw_data = self._cdc_by_query_cons( + slot_type, start_date, end_date, prm_id, partner_id ) - self.env.cr.execute(query) - raw_data = self.env.cr.fetchall() + for row in raw_data: label.append(row[0]) data_cons.append({"x": row[0], "y": round(row[1], 2)}) @@ -338,7 +368,11 @@ class AccOperation(models.Model): return cdc_cons def get_cdc_by_query_daily_histo_cons( - self, start_date, end_date, prm_id=None, partner_id=None + self, + start_date, + end_date, + prm_id=None, + partner_id=None, ): """ Fonction permettant de récupérer les données pour la construction @@ -382,29 +416,31 @@ class AccOperation(models.Model): } return cdc_cons - def get_cdc_by_query_prod( - self, slot_type, start_date, end_date, prm_id=None, partner_id=None + def _cdc_by_query_prod( + self, + slot_type, + start_date, + end_date, + prm_id=None, + partner_id=None, + curve_types=["autoprod", "surplus"], ): """ Fonction permettant de récupérer les données pour la construction des chart pour une ou des opérations données pour les consommateurs - :param: char slot_type: type de slot pour la query ("hour", "month" ou + :param: char slot_type: type de slot pour la query ("minute", "hour", "month" ou "year") datetime start_date: date début datetime end_date: date de fin int prm_id : PRM d'injection à récupérer int partner_id: contact associé à la courbe + [char] curve_types: type de données :return: un dictionnaire de données (labels et data pour les charts à afficher) """ - label = [] - data_autocons = [] - data_surplus = [] query = ( - self._select_clause( - date_slot=slot_type, curve_types=["autoprod", "surplus"] - ) + self._select_clause(date_slot=slot_type, curve_types=curve_types) + self._from_clause() + self._where_clause( operation_id=self.id, @@ -418,7 +454,31 @@ class AccOperation(models.Model): ) self.env.cr.execute(query) - raw_data = self.env.cr.fetchall() + return self.env.cr.fetchall() + + def get_cdc_by_query_prod( + self, slot_type, start_date, end_date, prm_id=None, partner_id=None + ): + """ + Fonction permettant de récupérer les données pour la construction des + chart pour une ou des opérations données pour les consommateurs + :param: char slot_type: type de slot pour la query ("minute", "hour", "month" ou + "year") + datetime start_date: date début + datetime end_date: date de fin + int prm_id : PRM d'injection à récupérer + int partner_id: contact associé à la courbe + :return: un dictionnaire de données + (labels et data pour les charts à afficher) + """ + label = [] + data_autocons = [] + data_surplus = [] + + raw_data = self._cdc_by_query_prod( + slot_type, start_date, end_date, prm_id, partner_id + ) + for row in raw_data: label.append(row[0]) data_autocons.append({"x": row[0], "y": round(row[1], 2)}) @@ -558,3 +618,60 @@ class AccOperation(models.Model): } ) return result_graph + + def export_cdc(self, start_date, end_date, partner_id, prm_id, data_type): + """ + + :param start_date: + :param end_date: + :param partner_id: + :param prm_id: + :param data_type: + :return: + """ + start = datetime.strptime(start_date, "%d/%m/%Y") + end = datetime.strptime(end_date, "%d/%m/%Y") + relativedelta(days=1) + + filename = export_cdc.make_filename( + self, start_date, end_date, partner_id, prm_id, data_type + ) + header = export_cdc.make_header_lines(self, partner_id, prm_id, data_type) + if data_type in ["cons"]: + data_file = export_cdc.make_cons_data( + self._cdc_by_query_cons( + "minute", + start, + end, + prm_id, + partner_id, + curve_types=["cons", "autocons", "allocons"], + ) + ) + if data_type in ["prod"]: + data_file = export_cdc.make_prod_data( + self._cdc_by_query_prod( + "minute", + start, + end, + prm_id, + partner_id, + curve_types=["autoprod", "surplus", "prod"], + ) + ) + + fout = StringIO() + for h_line in header: + fout.write(f"{h_line}\n") + for d_line in data_file: + fout.write(f"{d_line}\n") + + fout.seek(0) + data = fout.read() + fout.close() + + csv_http_headers = [ + ("Content-Type", "text/csv;charset=utf8"), + ("Content-Disposition", http.content_disposition(filename)), + ] + + return {"data": data, "name": filename, "headers": csv_http_headers} diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tools/export_cdc.py b/tools/export_cdc.py new file mode 100644 index 0000000000000000000000000000000000000000..425484ef52571a57b86c5436f85ceef5bae4df51 --- /dev/null +++ b/tools/export_cdc.py @@ -0,0 +1,186 @@ +import string +import unicodedata + + +PROD_HEADER = [ + "Horodatage", + "Production (W)", + "Production (kWh)", + "Surplus (kWh)", + "Production autoconsommee (kWh)", +] +CONS_HEADER = [ + "Horodatage", + "Consommation (W)", + "Consommation (kWh)", + "Alloconsommation(kWh)", + "Autoconsommation (kWh)", +] + + +def make_header_lines(operation, partner_id, prm_id, data_type): + """ + Données Elocoop de production de l'opération [nom opé] - [contact ou PRM] + + :param operation: id de l operation + :param start_date: date de debut + :param end_date: date de fin + :param partner_id: id du partner + :param prm_id: id du prm + :param data_type: type de données (consomation ou production) + :return: + """ + header = [] + if prm_id: + desc = f" - {get_prm_name_from_id(operation, prm_id)}" + elif partner_id: + desc = f" - {get_partner_name_from_id(operation, partner_id)}" + else: + desc = "" + + if data_type in ["cons"]: + header.append( + f"Données Elocoop de consommation de l'opération {operation.name}{desc}" + ) + header.append(";".join(CONS_HEADER)) + elif data_type in ["prod"]: + header.append( + f"Données Elocoop de production de l'opération {operation.name}{desc}" + ) + header.append(";".join(PROD_HEADER)) + + return header + + +def make_filename( + operation, start_date, end_date, partner_id, prm_id, data_type, file_type="csv" +): + """ + Genere le nom du fichier exporte sous la forme + Elocoop_[nom opé]_[date_debut]_[date_fin]_[production ou consommation]_[contact ou PRM].csv + :param operation: id de l operation + :param start_date: date de debut + :param end_date: date de fin + :param partner_id: id du partner + :param prm_id: id du prm + :param data_type: type de données (consomation ou production) + :param file_type: type de fichier par defaut csv + :return: + """ + if prm_id: + desc = f"_{clean_for_title(get_prm_name_from_id(operation, prm_id))}" + elif partner_id: + desc = f"_{clean_for_title(get_partner_name_from_id(operation, partner_id))}" + else: + desc = "" + + filename = ( + f"Elocoop_{clean_for_title(operation.name)}_" + f"{start_date.replace('/', '')}_{end_date.replace('/', '')}_" + f"{'consommation' if data_type == 'cons' else 'production'}{desc}.{file_type}" + ) + return filename + + +def make_cons_data(raw_data): + """ + make data file with cons hearder + horodatage cons w cons kwh allocons autocans + :param raw_data: + :return: + """ + data_file_lines = [] + rounding = 3 + for row in raw_data: + data_file_lines.append( + ";".join( + [ + # horodatage + row[0].strftime("%d/%m/%Y %H:%M"), + # consommation en watt + str(round(row[1] * 2000, rounding)), + # consommation en kwh + str(round(row[1], rounding)), + # allocons + str(round(row[3], rounding)), + # autocons + str(round(row[2], rounding)), + ] + ) + ) + + return data_file_lines + + +def make_prod_data(raw_data): + """ + make data file with prod hearder + horodatage prod w prod kwh surplus autocons + :param raw_data: + :return: + """ + data_file_lines = [] + rounding = 3 + for row in raw_data: + data_file_lines.append( + ";".join( + [ + # horodatage + row[0].strftime("%d/%m/%Y %H:%M"), + # production en watt + str(round(row[3] * 2000, rounding)), + # production en kwh + str(round(row[3], rounding)), + # surplus + str(round(row[2], rounding)), + # autocons + str(round(row[1], rounding)), + ] + ) + ) + + return data_file_lines + + +def clean_for_title(word): + """ + remplace les espace et caracteres speciaux par '_' + retire les accents + + :param word: chaine de caractere d entrée + :return: chaine de caractere "nettoyée" + """ + if isinstance(word, str): + char = list(string.punctuation) + char.append(" ") + + for c in char: + word = word.replace(c, "_") + + return "".join( + ( + c + for c in unicodedata.normalize("NFD", word) + if unicodedata.category(c) != "Mn" + ) + ) + + +def get_prm_name_from_id(operation, prm_id): + """ + :param operation: operation + :param prm_id: id du prm + :return: chaine avec le nom du prm + """ + + return operation.env["acc.counter"].browse(int(prm_id)).name + + +def get_partner_name_from_id(operation, partner_id): + """ + :param operation: operation + :param partner_id: id du partner + :return: chaine avec le nom du partner + """ + + return operation.env["res.partner"].browse(int(partner_id)).name