diff --git a/README.rst b/README.rst index de02ce987cef9288c60958988d1a3ff29f1f006f..15952e40aed8f1e2f825eb6bcb96a6f3f85c93da 100644 --- a/README.rst +++ b/README.rst @@ -3,13 +3,14 @@ :alt: License: AGPL-3 -========================== -OACC - Gestion portail CDC -========================== +================================ +OACC - Affichage courbes portail +================================ Ce module hérite du module *portal_oacc* et permet: - Création des pages portail pour la visualisation des courbes de charge + - Export des courbes de charge Description @@ -23,11 +24,16 @@ Exemple Credits ======= +Le développement de ce module a été financé par / The development of this module has been financially supported by: + - ENERCOOP Midi-Pyrénées (https://enercoop.fr) Contributors ------------ +* Benjamin Rivier <benjamin@le-filament.com> * Juliana Poudou <juliana@le-filament.com> +* Julien Ortet <julien@le-filament.com> +* Rémi Cazenave <remi@le-filament.com> Maintainer diff --git a/__manifest__.py b/__manifest__.py index 7176cfb699989c4116dabe2fff9d3723646f343b..6fddf8e32e3299ddeddecd823cb6883705cb1271 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -5,10 +5,11 @@ "website": "https://le-filament.com", "version": "16.0.1.0.1", "license": "AGPL-3", - "depends": ["web", "oacc_portal", "oacc_overview_cdc"], + "depends": ["web", "oacc_portal"], "data": [ # datas # views + "views/acc_operation_views.xml", # templates "templates/portal_layout.xml", "templates/operation_templates_page.xml", diff --git a/controllers/main.py b/controllers/main.py index 18def2422d68d88b9bc69531f8192a3de1d402a9..b2a873d1aa93ef88a4b1b0e6354bf3a014be6465 100644 --- a/controllers/main.py +++ b/controllers/main.py @@ -12,7 +12,7 @@ class CustomerPortal(CustomerPortal): # Si des données existent, récupérer les dates min/max/début/fin # de l'opération if is_data_cdc: - vals["data_values"] = operation.get_values_init_graph(partner_id) + vals["data_values"] = operation._get_values_init_graph(partner_id) else: vals["data_values"] = { @@ -146,7 +146,7 @@ class CustomerPortal(CustomerPortal): @http.route( ["/chart/update_json"], type="json", - auth="public", + auth="user", methods=["POST"], website=True, csrf=False, @@ -169,7 +169,7 @@ class CustomerPortal(CustomerPortal): """ # TODO: add check that prm_id and partner_id are allowed for this user operation = request.env["acc.operation"].browse(operation_id) - vals = operation.graph_view_global( + vals = operation._graph_view_global( start_date, end_date, partner_id, prm_id, data_type ) @@ -197,8 +197,9 @@ class CustomerPortal(CustomerPortal): This route is called : - When click on button export """ + # TODO: add check that prm_id and partner_id are allowed for this user operation = request.env["acc.operation"].sudo().browse(int(operation_id)) - file_values = operation.export_cdc( + file_values = operation._export_cdc( start_date, end_date, partner_id, prm_id, data_type ) diff --git a/models/__init__.py b/models/__init__.py index c9f67d1029451187faac73b30248b565bd1340d9..b9c11d9d929c89108a38db6f9596ab8bfd372d52 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -1 +1,2 @@ +from . import acc_enedis_cdc from . import acc_operation diff --git a/models/acc_enedis_cdc.py b/models/acc_enedis_cdc.py new file mode 100644 index 0000000000000000000000000000000000000000..42c445d618be7969068d74625ff408c0a90312da --- /dev/null +++ b/models/acc_enedis_cdc.py @@ -0,0 +1,565 @@ +# Copyright 2021- Le Filament (https://le-filament.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +from datetime import datetime + +from dateutil.relativedelta import relativedelta + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError +from odoo.osv import expression +from odoo.tools import date_utils + + +class AccEnedisCdc(models.Model): + _inherit = "acc.enedis.cdc" + + # ------------------------------------------------------ + # Actions + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Business methods + # ------------------------------------------------------ + @api.model + def _get_last_month_power_tot(self, operation_id): + """ + Fonction retournant la consommation totale, l'autoconsommation totale + et la production totale du dernier mois. + :return: la somme de la conso totale de tous les consommateurs, + la somme de l autoconso totale de tous les consommateurs, + la sommme de la prod totale de tous les producteurs + la date de début de mois + """ + # Get last date slot recorded + last_record = self._get_last_cdc_record(operation_id) + start_date, end_date = self._get_interval("month", last_record) + end_date_month = end_date + relativedelta(days=1) + query = ( + self._select_clause( + date_slot="month", curve_types=["cons", "prod", "autocons"] + ) + + self._from_clause() + + self._where_clause( + operation_id=operation_id, + start_date=start_date, + end_date=end_date_month, + ) + + self._group_clause(date_slot="month") + + self._order_clause() + ) + self.env.cr.execute(query) + raw_data = self.env.cr.fetchone() + return raw_data[1], raw_data[2], raw_data[3], start_date + + @api.model + def _get_last_cdc_record(self, operation_id, partner_id=None): + """ + @param: int operation_id identifiant de l'opération + int partner_id identifiant du contact + @returns: last acc.enedis.cdc record in operations + """ + domain = [("acc_operation_id", "=", operation_id)] + + if partner_id: + domain = expression.AND([domain, [("partner_id", "=", partner_id)]]) + # Get last date slot recorded + last_record = self.sudo().search( + domain, + limit=1, + order="date_slot DESC", + ) + + if not last_record: + raise ValidationError(_("L'opération ne possède pas de données")) + + return last_record + + @api.model + def _get_interval(self, scale, cdc_record): + """ + Fonction retournant une date de début et une date de fin. + Ces dates sont calculées en fonction de l'échelle choisie et du dernier + élément enregistré + - day: la date de début et la date de fin = dernier jour de données + - week: date de fin = fin du jour du dernier jour de données + date de début = date de fin moins 7 jours + - month: date de fin = fin du mois du dernier jour de données + date de début = début du mois du dernier jour de données + - year: date de fin = fin du mois du dernier jour de données + date de début = début de l'année du dernier jour de données + :param str scale: type d'affichage des graphes + (day/week/month/year) + :param object cdc_record: courbe de charge (table acc.enedis.cdc) + @returns: une date de début et une date de fin + """ + + # Convert end datetime to timezone + cdc_datetime = fields.Datetime.context_timestamp(self, cdc_record.date_slot) + start_month, end_month = date_utils.get_month(cdc_datetime) + + if scale == "year": + end_date = end_month + start_date = start_month.replace(month=1) + elif scale == "month": + end_date = end_month + start_date = start_month + elif scale == "week": + end_date = cdc_datetime.date() + start_date = cdc_datetime.date() - relativedelta(days=6) + elif scale == "day": + end_date = cdc_datetime.date() + start_date = cdc_datetime.date() + else: + raise ValueError( + _( + "La période d'affichage est incorrecte : %s, " + "valeurs attendues : day/week/month/year" + ) + % scale + ) + + return start_date, end_date + + @api.model + def _get_step_from_date(self, start_date, end_date): + """ + Fonction retournant le pas des courbes en fonction de 2 dates. + :return: + display_hourly_curves (boolean) : whether or not hourly curves + should be displayed + step: hour/month/year + step_display_curve: hour/day/month/year + """ + display_hourly_curves = False + step = "hour" + step_display_curve = "hour" + # Calculate delta between 2 dates + delta = (end_date - start_date).days + if delta > 1 and delta <= 31: + step_display_curve = "day" + display_hourly_curves = True + elif delta > 31 and delta <= 366: + step = "month" + step_display_curve = "month" + elif delta > 366: + step = "year" + step_display_curve = "year" + + return display_hourly_curves, step, step_display_curve + + @api.model + def _select_clause(self, date_slot, curve_types): + """ + Function to build SELECT section of query for retrieving curves + @param + 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 ("minute", "hour", "day", "month", "year"): + raise ValidationError(_("Incorrect date_slot in SELECT section")) + result = f""" + SELECT date_trunc('{date_slot}', + cdc.date_slot AT TIME ZONE 'UTC' AT TIME ZONE 'Europe/Paris' + ) AS date_slot + """ + for curve_type in curve_types: + if curve_type in ("cons", "autocons", "prod", "surplus"): + result += f""" + , (SUM(CASE WHEN cdc.comp_data_type = '{curve_type}' + THEN cdc.power ELSE 0 END)) /2 / 1000 as {curve_type} + """ + elif curve_type == "allocons": + result += """ + ,(SUM(CASE + WHEN cdc.comp_data_type = 'cons' + THEN cdc.power ELSE 0 END) + - SUM(CASE + WHEN cdc.comp_data_type = 'autocons' + THEN cdc.power ELSE 0 END)) / 2 / 1000 as allocons + """ + elif curve_type == "autoprod": + result += """ + ,(SUM(CASE + WHEN cdc.comp_data_type = 'prod' + THEN cdc.power ELSE 0 END) + - SUM(CASE + WHEN cdc.comp_data_type = 'surplus' + THEN cdc.power ELSE 0 END)) / 2 / 1000 as autoprod + """ + return result + + @api.model + def _from_clause(self): + """ + Function to build FROM section of query for retrieving curves + """ + return """ + FROM + acc_enedis_cdc cdc + INNER JOIN acc_operation ope + ON ope.id = cdc.acc_operation_id + """ + + @api.model + def _where_clause( + self, + operation_id, + start_date, + end_date, + prm_id=None, + extra_curve_type=None, + partner_id=None, + ): + """ + Function to build WHERE section of query for retrieving curves + @param + int operation_id : id of operation for which curves should be retrieved + 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) + (allowed values : 'cons', 'autocons', 'allocons', + 'prod', 'surplus', 'autoprod') + int partner_id : id of partner to be retrieved (optional) + """ + if ( + not isinstance(operation_id, int) + and not isinstance(start_date, datetime) + and not isinstance(end_date, datetime) + ): + raise ValidationError(_("WHERE clause parameters incorrect")) + + start_datetime = self._convert_time(start_date) + end_datetime = self._convert_time(end_date) + result = f""" + WHERE cdc.acc_operation_id = {operation_id} + AND cdc.date_slot >= '{start_datetime}' + AND cdc.date_slot < '{end_datetime}' + """ + + 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", + "allocons", + "prod", + "surplus", + "autoprod", + ): + result = f"{result} OR cdc.comp_data_type = '{extra_curve_type}' )" + else: + result = f"{result})" + + return result + + @api.model + def _group_clause(self, date_slot): + """ + Function to build GROUP BY section of query for retrieving curves + @param + char date_slot : granularity + (one of "minute", "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}', + cdc.date_slot AT TIME ZONE 'UTC' AT TIME ZONE 'Europe/Paris') + """ + + @api.model + def _order_clause(self): + return "ORDER BY date_slot ASC;" + + @api.model + def _cdc_by_query_cons( + self, + operation_id, + slot_type, + start_date, + end_date, + prm_id=None, + partner_id=None, + curve_types=None, + ): + """ + Fonction permettant de récupérer les données pour les consommateurs + + :param: int operation_id: opération concernée + 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) + + """ + if curve_types is None: + curve_types = ["cons", "autocons", "prod"] + query = ( + self._select_clause(date_slot=slot_type, curve_types=curve_types) + + self._from_clause() + + self._where_clause( + operation_id, + start_date, + end_date, + prm_id, + "prod" if "prod" in curve_types else None, + partner_id, + ) + + "AND cdc.comp_data_type IN %s" + + self._group_clause(date_slot=slot_type) + + self._order_clause() + ) + self.env.cr.execute(query, (tuple(curve_types),)) + return self.env.cr.fetchall() + + @api.model + def _get_cdc_by_query_cons( + self, + operation_id, + 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: int operation_id : Opération concernée + 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: resultat de la requête + (labels et data pour les charts à afficher) + """ + label = [] + data_autocons = [] + data_allocons = [] + data_cons = [] + data_prod = [] + + raw_data = self._cdc_by_query_cons( + operation_id, slot_type, start_date, end_date, prm_id, partner_id + ) + + for row in raw_data: + label.append(row[0]) + data_cons.append({"x": row[0], "y": round(row[1], 2)}) + data_autocons.append({"x": row[0], "y": round(row[2], 2)}) + data_allocons.append({"x": row[0], "y": round(row[1] - row[2], 2)}) + data_prod.append({"x": row[0], "y": round(row[3], 2)}) + + cdc_cons = { + "label": label, + "autocons": data_autocons, + "allocons": data_allocons, + "cons": data_cons, + "prod": data_prod, + } + return cdc_cons + + @api.model + def _get_cdc_by_query_daily_histo_cons( + self, + operation_id, + 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: int operation_id : Opération concernée + 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 + :return: un dictionnaire de données + (labels et data pour les charts à afficher) + """ + label_histo = [] + data_autocons_histo = [] + data_allocons_histo = [] + + query = ( + self._select_clause(date_slot="day", curve_types=["autocons", "allocons"]) + + self._from_clause() + + self._where_clause( + operation_id=operation_id, + start_date=start_date, + end_date=end_date, + prm_id=prm_id, + partner_id=partner_id, + ) + + self._group_clause(date_slot="day") + + self._order_clause() + ) + + self.env.cr.execute(query) + raw_data = self.env.cr.fetchall() + for row in raw_data: + data_autocons_histo.append(round(row[1], 2)) + data_allocons_histo.append(round(row[2], 2)) + label_histo.append(row[0]) + + cdc_cons = { + "autocons_histo": data_autocons_histo, + "allocons_histo": data_allocons_histo, + "label_histo": label_histo, + } + return cdc_cons + + @api.model + def _cdc_by_query_prod( + self, + operation_id, + slot_type, + start_date, + end_date, + prm_id=None, + partner_id=None, + curve_types=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: int operation_id : Opération concernée + 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) + """ + if curve_types is None: + curve_types = ["autoprod", "surplus"] + query = ( + self._select_clause(date_slot=slot_type, curve_types=curve_types) + + self._from_clause() + + self._where_clause( + operation_id=operation_id, + start_date=start_date, + end_date=end_date, + prm_id=prm_id, + partner_id=partner_id, + ) + + self._group_clause(date_slot=slot_type) + + self._order_clause() + ) + + self.env.cr.execute(query) + return self.env.cr.fetchall() + + @api.model + def _get_cdc_by_query_prod( + self, + operation_id, + 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: int operation_id : Opération concernée + 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( + operation_id, 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)}) + data_surplus.append({"x": row[0], "y": round(row[2], 2)}) + + cdc_prod = { + "label": label, + "autocons_prod": data_autocons, + "surplus": data_surplus, + } + return cdc_prod + + @api.model + def _get_cdc_by_query_daily_histo_prod( + self, operation_id, 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: int operation_id : Opération concernée + 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_histo = [] + data_autocons_prod_histo = [] + data_surplus_histo = [] + + query = ( + self._select_clause(date_slot="day", curve_types=["autoprod", "surplus"]) + + self._from_clause() + + self._where_clause( + operation_id=operation_id, + start_date=start_date, + end_date=end_date, + prm_id=prm_id, + partner_id=partner_id, + ) + + self._group_clause(date_slot="day") + + self._order_clause() + ) + self.env.cr.execute(query) + raw_data = self.env.cr.fetchall() + for row in raw_data: + label_histo.append(row[0]) + data_autocons_prod_histo.append(round(row[1], 2)) + data_surplus_histo.append(round(row[2], 2)) + + cdc_jour = { + "label_histo": label_histo, + "autocons_prod_histo": data_autocons_prod_histo, + "surplus_histo": data_surplus_histo, + } + return cdc_jour + + # ------------------------------------------------------ + # Functions to manage route + # ------------------------------------------------------ diff --git a/models/acc_operation.py b/models/acc_operation.py index daa382ff2b40732087773dd4f781b5a558b1cffc..56027620cd9546b678fb48aa1e3fd684579ed171 100644 --- a/models/acc_operation.py +++ b/models/acc_operation.py @@ -1,6 +1,13 @@ # Copyright 2021- Le Filament (https://le-filament.com) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -from odoo import models +from datetime import datetime +from io import StringIO + +from dateutil.relativedelta import relativedelta + +from odoo import fields, http, models + +from ..tools import export_cdc class AccOperation(models.Model): @@ -33,31 +40,189 @@ class AccOperation(models.Model): # ------------------------------------------------------ # Actions # ------------------------------------------------------ + def action_view_curves(self): + """ + Action qui ouvre le portail sur l'opération + :return: Vue Qweb + """ + self.ensure_one() + if self.id: + return { + "type": "ir.actions.act_url", + "url": "/operation/%s" % (self.id), + "target": "new", + } + + def _graph_view_global( + self, + start_date=None, + end_date=None, + partner_id=None, + prm_id=None, + data_type=None, + ): + """ + Fonction appelée pour l'affichage des courbes globales + sur le portail + :param: date start_date: date début + date end_date: date de fin + int partner_id: données uniquement de ce contact + int prm_id: données uniquement de ce PRM (mutuellement exclusif de + partner_id) + char data_type: type de courbes à afficher: + - "pmo" : vue globale (cons + prod sans filtrage) + - "cons" : vue mon suivi conso (avec filtrage possible) + - "prod" : vue mon suivi production (avec filtrage possible) + @returns: dictionnaire pour la construction des graphes + """ + self.ensure_one() + result_graph = {} + Cdc_model = self.env["acc.enedis.cdc"] + start_date = datetime.strptime(start_date, "%d/%m/%Y") + end_date = datetime.strptime(end_date, "%d/%m/%Y") + relativedelta(days=1) + ( + display_hourly_curves, + step_curve, + step_display_curve, + ) = Cdc_model._get_step_from_date(start_date=start_date, end_date=end_date) + + chart_data = {} + if data_type == "cons" or data_type == "pmo": + chart_data_cons = Cdc_model._get_cdc_by_query_cons( + self.id, step_curve, start_date, end_date, prm_id, partner_id + ) + if display_hourly_curves: + chart_data_histo = Cdc_model._get_cdc_by_query_daily_histo_cons( + self.id, start_date, end_date, prm_id, partner_id + ) + chart_data_cons.update(chart_data_histo) + chart_data.update(chart_data_cons) + if data_type == "prod" or data_type == "pmo": + chart_data_prod = Cdc_model._get_cdc_by_query_prod( + self.id, step_curve, start_date, end_date, prm_id, partner_id + ) + if display_hourly_curves: + chart_data_histo = Cdc_model._get_cdc_by_query_daily_histo_prod( + self.id, start_date, end_date, prm_id, partner_id + ) + chart_data_prod.update(chart_data_histo) + chart_data.update(chart_data_prod) + + result_graph["chart_data"] = chart_data + result_graph.update(self.get_date_min_max(partner_id)) + result_graph.update( + { + "start_date": start_date, + "end_date": end_date, + "scale": step_display_curve, + "is_curve_line": display_hourly_curves, + } + ) + 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.env["acc.enedis.cdc"]._cdc_by_query_cons( + self.id, + "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.env["acc.enedis.cdc"]._cdc_by_query_prod( + self.id, + "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} # ------------------------------------------------------ # Business methods # ------------------------------------------------------ - def get_values_init_graph(self, partner_id=None): + def get_date_min_max(self, partner_id=None): + self.ensure_one() + last_record = self.env["acc.enedis.cdc"]._get_last_cdc_record( + self.id, partner_id + ) + date_max = fields.Datetime.context_timestamp( + self, last_record.date_slot + ).strftime("%d/%m/%Y") + if partner_id: + first_date = self.env["acc.counter.period"].search( + [("acc_operation_id", "=", self.id), ("partner_id", "=", partner_id)], + order="start_date ASC", + limit=1, + ) + date_min = first_date.start_date.strftime("%d/%m/%Y") + else: + date_min = self.date_start_contract.strftime("%d/%m/%Y") + return {"date_min": date_min, "date_max": date_max} + + def _get_values_init_graph(self, partner_id=None): self.ensure_one() values = {} - last_record = self.get_last_cdc_record(partner_id) + Cdc_model = self.env["acc.enedis.cdc"] + last_record = Cdc_model._get_last_cdc_record(self.id, partner_id) - date_day_start, date_day_end = self.get_interval("day", last_record) + date_day_start, date_day_end = Cdc_model._get_interval("day", last_record) date_day_start = date_day_start.strftime("%d/%m/%Y") date_day_end = date_day_end.strftime("%d/%m/%Y") - date_week_start, date_week_end = self.get_interval("week", last_record) + date_week_start, date_week_end = Cdc_model._get_interval("week", last_record) date_week_start = date_week_start.strftime("%d/%m/%Y") date_week_end = date_week_end.strftime("%d/%m/%Y") - date_month_start, date_month_end = self.get_interval("month", last_record) + date_month_start, date_month_end = Cdc_model._get_interval("month", last_record) date_month_start = date_month_start.strftime("%d/%m/%Y") date_month_end = date_month_end.strftime("%d/%m/%Y") - date_year_start, date_year_end = self.get_interval("year", last_record) + date_year_start, date_year_end = Cdc_model._get_interval("year", last_record) date_year_start = date_year_start.strftime("%d/%m/%Y") date_year_end = date_year_end.strftime("%d/%m/%Y") diff --git a/static/src/js/operation_chart.js b/static/src/js/operation_chart.js index 8749de3d05fdad7fdd03faf5b78c268568ea93cf..a9fb5d423e5d74a46bdfeefb606bff9420cdc3a5 100644 --- a/static/src/js/operation_chart.js +++ b/static/src/js/operation_chart.js @@ -109,16 +109,15 @@ odoo.define("oacc_portal_overview_cdc.operation_chart", function (require) { var ctx_histo_prod = self.$(".histo_chart_prod"); this._rpc({ - model: "acc.operation", - method: "graph_view_global", - args: [ - self.operation, - self.first_day, - self.last_day, - self.partner_id, - self.prm_id, - self.data_type, - ], + route: "/chart/update_json", + params: { + operation_id: self.operation, + start_date: self.first_day, + end_date: self.last_day, + partner_id: self.partner_id, + prm_id: self.prm_id, + data_type: self.data_type, + }, }).then(function (data) { self.scale = data.scale; self.chartData = data.chart_data; @@ -659,19 +658,18 @@ odoo.define("oacc_portal_overview_cdc.operation_chart", function (require) { }, _exportChartData: function (title_name) { - var self = this; var url = "/chart/export_cdc?operation_id=" + self.operation; url = url + "&start_date=" + self.first_day; url = url + "&end_date=" + self.last_day; - url = url + "&data_type=" + self.data_type - if(self.partner_id) { + url = url + "&data_type=" + self.data_type; + if (self.partner_id) { url = url + "&partner_id=" + self.partner_id; } - if(self.prm_id) { - url = url + "&prm_id=" + self.prm_id + if (self.prm_id) { + url = url + "&prm_id=" + self.prm_id; } - window.open(url, "_blank") + window.open(url, "_blank"); }, _updateDataTemplate: function (data) { 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..34563d4d6803c3a411e06d59a7a6c0cd98c7d39a --- /dev/null +++ b/tools/export_cdc.py @@ -0,0 +1,181 @@ +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 diff --git a/views/acc_operation_views.xml b/views/acc_operation_views.xml new file mode 100644 index 0000000000000000000000000000000000000000..364c9bcce57800c41b8c81a253f5689c89d7147a --- /dev/null +++ b/views/acc_operation_views.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8" ?> +<!-- Copyright 2021- Le Filament (https://le-filament.com) + License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> +<odoo> + + <record id="acc_operation_form_view" model="ir.ui.view"> + <field name="name">cdc.acc_operation_form_view.api.form.inherit</field> + <field name="model">acc.operation</field> + <field name="inherit_id" ref="oacc.acc_operation_form_view" /> + <field name="arch" type="xml"> + <header position="inside"> + <button + string="Voir les courbes" + type="object" + class="btn-primary" + name="action_view_curves" + /> + </header> + </field> + </record> + +</odoo>