diff --git a/__init__.py b/__init__.py index 0650744f6bc69b9f0b865e8c7174c813a5f5995e..b046ff82fc33987dc39e441795c2abe2b2aae54e 100644 --- a/__init__.py +++ b/__init__.py @@ -1 +1 @@ -from . import models +from . import models, wizard diff --git a/__manifest__.py b/__manifest__.py index e45df1cd0a07444c7cbe0bd1c94b4b785bec6523..98efaefb451812d0f4d91ed57f4d23b1cd3adc08 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -11,6 +11,7 @@ # views # views menu # wizard + "wizard/acc_operation_import_wizard_views.xml", ], "assets": { "web._assets_primary_variables": [], diff --git a/models/acc_operation.py b/models/acc_operation.py index 15f61894ed56788270c950549833c566c2e8107d..cd9a948880f20c54556caf6b33c0e5c903074478 100644 --- a/models/acc_operation.py +++ b/models/acc_operation.py @@ -1,7 +1,7 @@ # 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 os.path import splitext from dateutil.relativedelta import relativedelta from odoo import _, models @@ -54,8 +54,10 @@ class AccOperation(models.Model): else: files.mapped("name") for file in files: - ext_file = file.name[-3:] - if ext_file != "csv": + file_suffix = splitext(file.name)[1] + file_name = splitext(file.name)[0] + + if file_suffix != ".csv": raise ValidationError( _( "Le fichier %s ne semble pas être au bon format. " @@ -64,7 +66,7 @@ class AccOperation(models.Model): % file.name ) - data_filename = file.name.split("_") + data_filename = file_name.split("_") if len(data_filename) != 5: raise ValidationError( @@ -78,11 +80,23 @@ class AccOperation(models.Model): date_begin_str = data_filename[1] date_end_str = data_filename[2] + + # Contrôles sur le type de données CDC + if data_filename[3] not in ["Prod", "Conso", "Autoconso", "Surplus"]: + raise UserError( + _( + "Le fichier %s ne correspond à aucun type " + "de courbes de charge. il doit contenir Prod, Conso, " + "Autoconso ou Surplus." + ) + % file.name + ) + computed_data_type = data_filename[3].lower() # Contrôle sur le type de données - ext_file = data_filename[4][:3] - if ext_file != "CDC": + file_suffix = data_filename[4] + if file_suffix != "CDC": raise UserError( _( "Le fichier %s n'a pas le bon type de format, " @@ -91,22 +105,6 @@ class AccOperation(models.Model): % file.name ) - # Contrôles sur le type de données CDC - if computed_data_type != "prod" and computed_data_type != "surplus": - computed_data_type = data_filename[3].lower()[:-1] - if ( - computed_data_type != "cons" - and computed_data_type != "autocons" - ): - raise UserError( - _( - "Le fichier %s ne correspond à aucun type " - "de courbes de charge. il doit contenir Prod, Conso, " - "Autoconso ou Surplus." - ) - % file.name - ) - # Contrôle PRM id_prm = data_filename[0] # Vérification existance PRM diff --git a/wizard/__init__.py b/wizard/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b3220ebfb2b78033d522533648e2413087e923ff --- /dev/null +++ b/wizard/__init__.py @@ -0,0 +1 @@ +from . import acc_operation_import_wizard diff --git a/wizard/acc_operation_import_wizard.py b/wizard/acc_operation_import_wizard.py new file mode 100644 index 0000000000000000000000000000000000000000..b1071673b95cae1eb783adcff237e3246f7db10a --- /dev/null +++ b/wizard/acc_operation_import_wizard.py @@ -0,0 +1,177 @@ +# Copyright 2021- Le Filament (https://le-filament.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +import base64 +import csv +import io +from datetime import datetime, timedelta + +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models + +from odoo.addons.api_connector.tools.date_utils import local_to_utc + + +class AccOperationImportWizard(models.TransientModel): + _name = "acc.operation.import.wizard" + _description = "Wizard: Import des données" + + # ------------------------------------------------------ + # Fields declaration + # ------------------------------------------------------ + operation_id = fields.Many2one("acc.operation", "Opération liée") + message = fields.Text( + string="Message Logs", + ) + attachment_ids = fields.Many2many( + comodel_name="ir.attachment", string="Documents à importer" + ) + + # ------------------------------------------------------ + # SQL Constraints + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Default methods + # ------------------------------------------------------ + @api.model + def default_get(self, fields_list): + # OVERRIDE + res = super().default_get(fields_list) + self._context.get("active_ids") + return res + + # ------------------------------------------------------ + # Computed fields / Search Fields + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Onchange / Constraints + # ------------------------------------------------------ + + # ------------------------------------------------------ + # CRUD methods (ORM overrides) + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Actions + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Business methods + # ------------------------------------------------------ + def delete_existing_data(self, data_filename, counter_id, message): + """ + deleting existing data depends of wich curves type you import + implemented in each modules + """ + message += "Delete data not implemented" + return message + + def create_curve(self, curve_data): + """ + creating curve depends of wich curves type you import + implemented in each modules + """ + pass + + def update_partner_id(self, data_filename, counter_id): + pass + + def valid_import(self): + message = "" + message += "<h1>Début Import manuelle: " + str(fields.Datetime.now()) + "</h1>" + for file in self.attachment_ids: + message += ( + "<p><strong>Fichier " + + file.name + + "</strong><br/>Début Import ... <br/>" + ) + data_filename = file.name.split("_") + id_pdm = data_filename[0] + + counter_id = self.env["acc.counter"].search([("name", "=", id_pdm)]) + data_filename[3] = data_filename[3].lower() + + if data_filename[3] not in ["prod", "surplus"]: + data_filename[3] = data_filename[3][:-1] + + message = self.delete_existing_data( + data_filename=data_filename, counter_id=counter_id, message=message + ) + + file_decode = io.StringIO(base64.b64decode(file.datas).decode("UTF-8")) + file_decode.seek(0) + + file_reader = [] + csv_reader = csv.reader(file_decode, delimiter=";") + file_reader.extend(csv_reader) + + # Create Data for the CDC + message += "Lecture et import des données ... <br/>" + for row in file_reader: + power_data = row[1:] + # check case of csv line ending by delimiter + if power_data[-1] == "": + power_data.pop() + + slot_datetime_tz = datetime.strptime(row[0], "%d/%m/%Y %H:%M") + slot_datetime_utc = local_to_utc(slot_datetime_tz, "Europe/Paris") + + # if 2 data it's a 30 min step file if 4 it's a 15 min one + if len(power_data) == 4: + timestep = 15 + else: + timestep = 30 + + for index, power in enumerate(power_data): + if index == 0: + timestamp = slot_datetime_utc + else: + timestamp = timestamp + timedelta(minutes=timestep) + + self.create_curve( + { + "name": file.name, + "acc_operation_id": self.operation_id.id, + "acc_counter_id": counter_id.id, + "comp_data_type": data_filename[3], + "power": power, + "date_slot": timestamp, + "timestep": timestep, + } + ) + + self.update_partner_id(data_filename, counter_id) + + message += "Fin de l'Import des données OK<br/>" + # Suppression du fichier après création des enregistrements + message += "Suppression du fichiers " + file.name + " ...<br/>" + file.unlink() + message += "Suppression OK </p>" + + message += "<h1>Fin Import manuelle: " + str(fields.Datetime.now()) + "</h1>" + # Logs information logs + log_id = self.env["acc.logs"].create( + { + "name": "Import du " + str(fields.Date.today()) + " manuelle", + "date_launched": fields.Datetime.now(), + "type_log": "manual", + "message": message, + "acc_operation_id": self.operation_id.id, + } + ) + + view_id = self.env.ref("oacc.acc_logs_form").id + return { + "name": "LOGS", + "view_type": "form", + "view_mode": "form", + "views": [(view_id, "form")], + "res_model": "acc.logs", + "view_id": view_id, + "type": "ir.actions.act_window", + "res_id": log_id.id, + "target": "new", + "flags": {"initial_mode": "view"}, + } diff --git a/wizard/acc_operation_import_wizard_views.xml b/wizard/acc_operation_import_wizard_views.xml new file mode 100644 index 0000000000000000000000000000000000000000..53a9b3c75792ea132718ee34401a34131df71543 --- /dev/null +++ b/wizard/acc_operation_import_wizard_views.xml @@ -0,0 +1,36 @@ +<?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_import_wizard_form" model="ir.ui.view"> + <field name="name">acc.operation.import.wizard.form</field> + <field name="model">acc.operation.import.wizard</field> + <field name="arch" type="xml"> + <form string="Import des fichiers"> + <div> + <field + class="o_field_header" + name="message" + readonly="1" + widget="html" + /> + </div> + <group> + <field name="operation_id" invisible="1" /> + <field name="attachment_ids" invisible="1" /> + </group> + <footer> + <button + name="valid_import" + type="object" + string="Valider l'import" + class="oe_highlight" + /> + <button special="cancel" string="Annuler" /> + </footer> + </form> + </field> + </record> + +</odoo>