diff --git a/__manifest__.py b/__manifest__.py index 2cb8596feea56970af84e38fb35a07532eb4742b..260d55a887ba5f5558502c068ea972b88e51ae3f 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -3,19 +3,30 @@ "summary": "Module de gestion des clés de répartitions enedis OACC", "author": "Le Filament", "website": "https://le-filament.com", - "version": "16.0.1.0.0", + "version": "16.0.2.0.0", "license": "AGPL-3", "depends": ["oacc", "api_enedis_acc", "queue_job"], "data": [ "security/ir.model.access.csv", # datas # wizard - "wizard/acc_repartition_keys_wizard_views.xml", + "wizard/acc_repartition_keys_file_wizard_views.xml", + "wizard/acc_repartition_keys_compute_wizard_views.xml", # views "views/acc_operation_views.xml", "views/acc_repartition_keys_views.xml", + "views/acc_priority_group_counter_views.xml", + "views/acc_priority_group_views.xml", + "views/acc_repartition_counter_views.xml", # views menu + "views/menu_views.xml", ], + "assets": { + "web.assets_backend": [ + "/oacc_repartition_keys/static/src/js/kanban_button.js", + "/oacc_repartition_keys/static/src/xml/kanban_button.xml", + ], + }, "installable": True, "auto_install": False, } diff --git a/migrations/16.0.2.0.0/pre-migration.py b/migrations/16.0.2.0.0/pre-migration.py new file mode 100644 index 0000000000000000000000000000000000000000..52618619973d447cd4a050e43668f645ad208303 --- /dev/null +++ b/migrations/16.0.2.0.0/pre-migration.py @@ -0,0 +1,14 @@ +# Copyright 2023- Le Filament (https://le-filament.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + # Rename models to preserve fields during upgrade + openupgrade.rename_models( + env.cr, [("acc.repartition.keys", "acc.repartition.keys.file")] + ) + openupgrade.rename_tables( + env.cr, [("acc_repartition_keys", "acc_repartition_keys_file")] + ) diff --git a/models/__init__.py b/models/__init__.py index 183168978d7da2e5bc4b899455e45738c23fa71c..d790d852cab00995333cca45298f2e5a0269c708 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -1,2 +1,6 @@ from . import acc_operation from . import acc_repartition_keys +from . import acc_repartition_keys_file +from . import acc_priority_group +from . import acc_priority_group_counter +from . import acc_repartition_counter diff --git a/models/acc_operation.py b/models/acc_operation.py index cdead838e9f723bd4e686fec594390bae77e7a2f..f43c4c39e400f88459bf159eea59ee29bdcf5919 100644 --- a/models/acc_operation.py +++ b/models/acc_operation.py @@ -1,10 +1,15 @@ # Copyright 2021- Le Filament (https://le-filament.com) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -import logging +import base64 +import csv +from collections import OrderedDict +from datetime import date, datetime +from io import StringIO -from odoo import fields, models +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError -_logger = logging.getLogger(__name__) +from odoo.addons.api_connector.tools.date_utils import utc_to_local class AccOperation(models.Model): @@ -13,13 +18,47 @@ class AccOperation(models.Model): # ------------------------------------------------------ # Fields declaration # ------------------------------------------------------ - keys_repartition_ids = fields.One2many( + keys_file_repartition_ids = fields.One2many( + comodel_name="acc.repartition.keys.file", + inverse_name="operation_id", + string="keys files repartition", + required=False, + groups="oacc.group_operation_superadmin", + ) + + repartition_keys_id = fields.One2many( comodel_name="acc.repartition.keys", inverse_name="operation_id", - string="keys repartition", + string="Repartition key", required=False, groups="oacc.group_operation_superadmin", ) + + acc_priority_group_ids = fields.One2many( + comodel_name="acc.priority.group", + inverse_name="acc_operation_id", + string="Groupe de priorité", + ) + + # uniquement pour des question d affichage + type_algo = fields.Selection( + [ + ("prorata", "Dynamique par défaut"), + ("static", "Statique"), + ("dyn_perso_send", "Dynamique personalisée - envoi de fichier"), + ("dyn_perso_compute", "Dynamique personalisée - calcul automatisé"), + ], + string="Type de clé de répartition", + default="prorata", + required=True, + ) + + algo_description = fields.Text( + string="Description de l algorithme", + default="La clé de répartition est calculée automatiquement chaque mois, " + "au prorata de la consommation de chacun des consommateurs", + ) + # ------------------------------------------------------ # SQL Constraints # ------------------------------------------------------ @@ -27,6 +66,82 @@ class AccOperation(models.Model): # ------------------------------------------------------ # Default methods # ------------------------------------------------------ + def compute_repartition(self): + """ + compute task wich compute keys and generate csv file + """ + self.generate() + + def delete_keys(self): + repartitions = self.env["acc.repartition.keys"].search( + [("operation_id", "=", self.id)] + ) + for repartition in repartitions: + keys = self.env["acc.repartition.counter"].search( + [("acc_repartition_id", "=", repartition.id)] + ) + keys.unlink() + + for repartition in repartitions: + repartition.unlink() + + def generate(self): + """ + generate repartition keys + """ + + if not self.acc_priority_group_ids: + raise ValidationError( + _("Aucune priorité n'est définie pour cette opération.") + ) + + self.delete_keys() + + repartition = self.env["acc.repartition.keys"].create({"operation_id": self.id}) + + data = None + for piority in self.acc_priority_group_ids: + data = piority.compute(data) + + if not data: + raise ValidationError( + _("Pas de données brute pour le mois précedent présente") + ) + # todo : ajouter la verification de coherence des données + for slot in data: + affect = data.get(slot).get("affect") + if affect: + total_affecte = sum(affect.values()) + for counter in affect: + counter_id = self.env["acc.counter"].search( + [("name", "=", counter)] + ) + if total_affecte == 0: + weight = 0.0 + else: + weight = round((affect.get(counter) * 100) / total_affecte, 8) + self.env["acc.repartition.counter"].create( + { + "acc_repartition_id": repartition.id, + "time_slot": slot, + "weight": weight, + "acc_counter_id": counter_id.id, + } + ) + else: + # aucune consommation n a été affecté, on envoi des clé a zero + for counter in data.get(slot).get("conso"): + counter_id = self.env["acc.counter"].search( + [("name", "=", counter)] + ) + self.env["acc.repartition.counter"].create( + { + "acc_repartition_id": repartition.id, + "time_slot": slot, + "weight": 0, + "acc_counter_id": counter_id.id, + } + ) # ------------------------------------------------------ # Computed fields / Search Fields @@ -36,6 +151,20 @@ class AccOperation(models.Model): # Onchange / Constraints # ------------------------------------------------------ + @api.onchange("type_algo") + def on_change_algo(self): + desc = { + "prorata": "La clé de répartition est calculée automatiquement chaque mois," + " au prorata de la consommation de chacun des consommateurs.", + "static": "La clé de répartition est calculée automatiquement chaque mois, " + "en fonction des coefficients de répartition communiqués " + "initialement à Enedis.", + "dyn_perso_send": "", + "dyn_perso_compute": "", + } + + self.algo_description = desc.get(self.type_algo) + # ------------------------------------------------------ # CRUD methods (ORM overrides) # ------------------------------------------------------ @@ -43,3 +172,159 @@ class AccOperation(models.Model): # ------------------------------------------------------ # Actions # ------------------------------------------------------ + def action_view_repartition_algo_priority_group(self): + """ + Action pour accéder aux algo de calcul defini + """ + + action = self.env["ir.actions.actions"]._for_xml_id( + "oacc_repartition_keys.acc_priority_group_counter_act_window" + ) + + action["context"] = { + "default_acc_operation_id": self.id, + } + action["domain"] = [("acc_operation_id", "=", self.id)] + action["res_id"] = self.id + + return action + + def action_send_repartition_keys(self): + """ + Action d envoi des clés calculées a Enedis + """ + repartition = self.env["acc.repartition.keys"].search( + [("operation_id", "=", self.id)] + ) + + if not repartition: + raise UserError(_("Clés de répartition non générées")) + + keys = ( + self.env["acc.repartition.counter"] + .search([("acc_repartition_id", "=", repartition.id)]) + .mapped("time_slot") + ) + + horodatages = list(OrderedDict.fromkeys(keys)) + + for time_slot in horodatages: + keys = self.env["acc.repartition.counter"].search( + [ + ("acc_repartition_id", "=", repartition.id), + ("time_slot", "=", time_slot), + ] + ) + body = [] + for key in keys: + body.append({"id": key.acc_counter_id.name, "key": key.weight}) + data = {"timestamp": time_slot.strftime("%Y%m%dT%H%M%SZ"), "body": body} + job_description = ( + f"{self.name} - Send repartition key at {data.get('timestamp')}" + ) + try: + self.with_delay(description=job_description).send_repartition_key( + key=data + ) + except ValidationError as exc: + raise UserError(_(str(exc))) from exc + + def get_repartition_data_for_csv(self): + """ + generate data for csv + """ + repartition = self.env["acc.repartition.keys"].search( + [("operation_id", "=", self.id)] + ) + + keys = self.env["acc.repartition.counter"].search( + [("acc_repartition_id", "=", repartition.id)] + ) + if not keys: + raise ValidationError(_("Clés de répartition non générées")) + + counters = keys.mapped("acc_counter_id") + keys = keys.mapped("time_slot") + + horodatages = list(OrderedDict.fromkeys(keys)) + + hearders = ["Horodate"] + [c.name for c in counters] + data = [hearders] + for time_slot in horodatages: + line = [utc_to_local(time_slot, "Europe/Paris")] + for counter in counters: + w = ( + self.env["acc.repartition.counter"] + .search( + [ + ("acc_counter_id", "=", counter.id), + ("time_slot", "=", time_slot), + ] + ) + .weight + ) + line.append(str(w).replace(".", ",")) + + data.append(line) + + return data + + def export_repartition(self): + """ + Genere un fichier csv des repartitions + :param operation_id: operation + :return: file + """ + + csv_data = self.get_repartition_data_for_csv() + + try: + start_date = csv_data[1][0] + end_date = csv_data[-1][0] + + d = csv_data[2][0] - start_date + except IndexError as e: + raise ValidationError(_("Pas de données")) from e + ts = int(d.total_seconds() / 60) + if isinstance(start_date, datetime) and isinstance(end_date, datetime): + filename = ( + f"{self.name}_{ts}_" + f"{start_date.strftime('%d%m%Y')}_{end_date.strftime('%d%m%Y')}" + ) + else: + filename = f"cle_de_repartition_{self.name}" + + return self.create_csv(filename, csv_data) + + # ------------------------------------------------------ + # Common function + # ------------------------------------------------------ + + def create_csv(self, filename, lines_to_export): + fp = StringIO() + export_file = csv.writer(fp, delimiter=";", quoting=csv.QUOTE_NONE) + # Add header line + for line in lines_to_export: + # Format date value + line_values = [ + value + if not isinstance(value, date) + else value.strftime("%d-%m-%Y %H:%M") + for value in line + ] + export_file.writerow(line_values) + + fp.seek(0) + data = fp.read() + fp.close() + + filename = filename + ".csv" + # sauvegarde du fichier de cles + self.env["acc.repartition.keys.file"].create( + { + "csv_file": base64.b64encode(str.encode(data)), + "filename": filename, + "operation_id": self.id, + "date_send": datetime.now(), + } + ) diff --git a/models/acc_priority_group.py b/models/acc_priority_group.py new file mode 100644 index 0000000000000000000000000000000000000000..4aeec6f8456135608694f0053b7564c428129aa8 --- /dev/null +++ b/models/acc_priority_group.py @@ -0,0 +1,176 @@ +from odoo import api, fields, models + + +class AccPriorityGroup(models.Model): + _name = "acc.priority.group" + _description = "Groupe de clés de répartition" + _rec_name = "display_name" + _order = "sequence, id" + + # ------------------------------------------------------ + # Fields declaration + # ------------------------------------------------------ + + acc_operation_id = fields.Many2one("acc.operation", "Opération", required=True) + sequence = fields.Integer(required=True, default=1) + type_algo = fields.Selection( + [ + ("prorata", "Répartition au prorata"), + ], + string="Répartition au sein du groupe", + default="prorata", + required=True, + ) + display_name = fields.Char( + compute="_compute_display_name", store=True, readonly=True + ) + acc_priority_group_counter_ids = fields.One2many( + comodel_name="acc.priority.group.counter", + inverse_name="acc_priority_group_id", + string="Affectation de compteur par groupe de priorité", + required=True, + ) + + counter_datas = fields.Json(compute="_compute_counter_datas") + # ------------------------------------------------------ + # SQL Constraints + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Default methods + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Computed fields / Search Fields + # ------------------------------------------------------ + + @api.depends("type_algo", "sequence") + def _compute_display_name(self): + for prio_group in self: + prio_group.display_name = f"Priorité {str(prio_group.sequence)}" + + def _compute_counter_datas(self): + for priority in self: + priority.counter_datas = ( + priority.acc_priority_group_counter_ids.acc_counter_id.mapped( + lambda q: { + "name": q.name, + "partner": q.partner_id.name, + "street": q.street, + } + ) + ) + + # ------------------------------------------------------ + # Onchange / Constraints + # ------------------------------------------------------ + @api.onchange("sequence") + def on_change_algo(self): + self._compute_display_name() + + # ------------------------------------------------------ + # CRUD methods (ORM overrides) + # ------------------------------------------------------ + @api.model + def unlink(self, _id): + self = self.env["acc.priority.group"].browse(_id) + current_operation_id = self.env.context.get("active_id") + existing_groups = self.env["acc.priority.group"].search( + [("acc_operation_id", "=", current_operation_id), ("id", "!=", _id)] + ) + seq = 1 + for group in existing_groups: + group.sequence = seq + seq += 1 + + return super().unlink() + + @api.model_create_multi + def create(self, vals_list): + """ + si c est le premier groupe cree on y affecte tout les compteurs + """ + current_operation_id = self.env.context.get("active_id") + + existing_groups = self.env["acc.priority.group"].search( + [ + ("acc_operation_id", "=", current_operation_id), + ] + ) + + res = super().create(vals_list) + + if existing_groups: + res.sequence = max(existing_groups.mapped("sequence")) + 1 + return res + + counters_to_affect = self.env["acc.counter"].search( + [ + ("acc_operation_id", "=", res.acc_operation_id.id), + ("type", "in", ["del", "del_inj"]), + ] + ) + for counter in counters_to_affect: + self.env["acc.priority.group.counter"].create( + { + "acc_priority_group_id": res.id, + "acc_operation_id": res.acc_operation_id.id, + "acc_counter_id": counter.id, + "counter_street": counter.street, + "counter_owner": counter.partner_id.name, + } + ) + return res + + # ------------------------------------------------------ + # Actions + # ------------------------------------------------------ + + def compute(self, data=None): + compute_algo = { + "prorata": self._prorata, + } + if data is None: + data = self.env["acc.enedis.raw.cdc"].get_repartition_data( + operation_id=self.acc_operation_id + ) + + d = compute_algo[self.type_algo](data) + + return d + + def get_conso_sum(self, data_slot): + conso = 0.0 + for counter in self.acc_priority_group_counter_ids.acc_counter_id.mapped( + "name" + ): + consok = data_slot.get("conso").get(counter) + if consok: + conso += consok + return conso + + def _prorata(self, data): + for slot in data: + prod = data.get(slot).get("prod_totale") + priority_counters_conso_sum = self.get_conso_sum(data.get(slot)) + + total_affecte = 0.0 + if not data.get(slot).get("affect"): + data[slot]["affect"] = {} + + for counter in self.acc_priority_group_counter_ids.acc_counter_id.mapped( + "name" + ): + if prod == 0 or priority_counters_conso_sum == 0: + part_a_affecter = 0.0 + else: + conso_k = data.get(slot).get("conso").get(counter) + part_a_affecter = min( + conso_k, prod * (conso_k / priority_counters_conso_sum) + ) + data[slot]["affect"][counter] = part_a_affecter + total_affecte += part_a_affecter + + data[slot]["prod_totale"] = prod - total_affecte + + return data diff --git a/models/acc_priority_group_counter.py b/models/acc_priority_group_counter.py new file mode 100644 index 0000000000000000000000000000000000000000..a7575036357d4d0a49c73639578e4737f3ace0a7 --- /dev/null +++ b/models/acc_priority_group_counter.py @@ -0,0 +1,101 @@ +from odoo import api, fields, models +from odoo.osv import expression + + +class AccPriorityGroupCounter(models.Model): + _name = "acc.priority.group.counter" + _description = "Clé de répartition par groupe de priorité" + _rec_name = "acc_counter_id" + + # ------------------------------------------------------ + # Fields declaration + # ------------------------------------------------------ + + acc_priority_group_id = fields.Many2one( + "acc.priority.group", + "Groupe de priorité", + required=True, + ondelete="cascade", + group_expand="_group_expand_acc_priority_group_id", + ) + acc_operation_id = fields.Many2one( + related="acc_priority_group_id.acc_operation_id", store=True, readonly=True + ) + acc_counter_id = fields.Many2one( + "acc.counter", + "PRM", + required=True, + ondelete="cascade", + domain="[('acc_operation_id', '=', acc_operation_id)," + "('type', 'in', ['del', 'del_inj'])]", + ) + counter_street = fields.Char( + related="acc_counter_id.street", store=True, readonly=True + ) + counter_owner = fields.Char( + related="acc_counter_id.partner_id.name", store=True, readonly=True + ) + acc_counter_id_domain = fields.Binary( + string="Counter domain", compute="_compute_acc_counter_id_domain" + ) + + # ------------------------------------------------------ + # SQL Constraints + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Default methods + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Computed fields / Search Fields + # ------------------------------------------------------ + @api.depends("acc_counter_id", "acc_priority_group_id") + def _compute_acc_counter_id_domain(self): + for counter in self: + domain = [ + ("acc_operation_id", "=", counter.acc_operation_id.id), + ("type", "in", ["del", "del_inj"]), + ] + extended_domain = expression.AND( + [ + domain, + [ + ( + "id", + "not in", + counter.acc_priority_group_id.acc_priority_group_counter_ids.acc_counter_id.ids, + ) + ], + ] + ) + counter.acc_counter_id_domain = extended_domain + + # ------------------------------------------------------ + # Onchange / Constraints + # ------------------------------------------------------ + # @api.constrains("acc_counter_id_domain", "acc_counter_id") + # def _check_only_one_counter_per_group(self): + # """ + # Vérification qu'il n'y a pas plusieurs fois le meme compteur dans un groupe + # """ + # + # domain = expression.AND([self.acc_counter_id_domain, ]) + # + # s = self.env["acc.counter"].search(self.acc_counter_id_domain) + # l = s.ids + # c = self.acc_counter_id.id + # + # if c not in l: + # raise UserError("Compteur déjà affecté a ce groupe") + + # ------------------------------------------------------ + # CRUD methods (ORM overrides) + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Actions + # ------------------------------------------------------ + + def _group_expand_acc_priority_group_id(self, groups, domain, order): + return self.env["acc.priority.group"].search(domain=domain, order=order) diff --git a/models/acc_repartition_counter.py b/models/acc_repartition_counter.py new file mode 100644 index 0000000000000000000000000000000000000000..e88844c81e8d3d8860784ce63e25944bbb6474ee --- /dev/null +++ b/models/acc_repartition_counter.py @@ -0,0 +1,42 @@ +from odoo import api, fields, models + + +class AccRepartitionCounter(models.Model): + _name = "acc.repartition.counter" + _description = "Priorité par compteur" + + acc_repartition_id = fields.Many2one("acc.repartition.keys", "Clé", required=True) + weight = fields.Float(string="Répartition en pourcentage", required=False) + time_slot = fields.Datetime("Horodatage de la clé") + acc_counter_id = fields.Many2one("acc.counter", string="Compteur", required=True) + acc_operation_id = fields.Many2one( + comodel_name="acc.operation", compute="_compute_operation_id", store=True + ) + + # ------------------------------------------------------ + # SQL Constraints + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Default methods + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Computed fields / Search Fields + # ------------------------------------------------------ + @api.depends("acc_repartition_id") + def _compute_operation_id(self): + for key in self: + key.acc_operation_id = key.acc_repartition_id.operation_id + + # ------------------------------------------------------ + # Onchange / Constraints + # ------------------------------------------------------ + + # ------------------------------------------------------ + # CRUD methods (ORM overrides) + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Actions + # ------------------------------------------------------ diff --git a/models/acc_repartition_keys.py b/models/acc_repartition_keys.py index 234cd0eeda7e05845ffbf9a5bdcab066d41bcdbe..7aeb704c30ae7dd607af21f823716bdbaddf54e2 100644 --- a/models/acc_repartition_keys.py +++ b/models/acc_repartition_keys.py @@ -4,14 +4,16 @@ from odoo import fields, models class AccRepartitionKeys(models.Model): _name = "acc.repartition.keys" _description = "clés de repartition" - _order = "date_send DESC, id DESC" # ------------------------------------------------------ # Fields declaration - csv_file = fields.Binary("Contenu du fichier CSV", required=True) - filename = fields.Char("Nom du fichier", required=True) - date_send = fields.Date("Date de l'envoi des clés", required=True, default=None) + # ------------------------------------------------------ operation_id = fields.Many2one("acc.operation", "Opération", required=True) + acc_repartition_counter_ids = fields.One2many( + comodel_name="acc.repartition.counter", + inverse_name="acc_repartition_id", + string="Compteur", + ) # ------------------------------------------------------ # SQL Constraints diff --git a/models/acc_repartition_keys_file.py b/models/acc_repartition_keys_file.py new file mode 100644 index 0000000000000000000000000000000000000000..8cc99ef602552d5b6b548a3a7ecb8cb17d289708 --- /dev/null +++ b/models/acc_repartition_keys_file.py @@ -0,0 +1,42 @@ +from odoo import fields, models + + +class AccRepartitionKeysFile(models.Model): + _name = "acc.repartition.keys.file" + _description = "clés de repartition" + _order = "date_send DESC, id DESC" + + # ------------------------------------------------------ + # Fields declaration + csv_file = fields.Binary("Contenu du fichier CSV", required=True) + filename = fields.Char("Nom du fichier", required=True) + date_send = fields.Date("Date de l'envoi des clés", default=None) + operation_id = fields.Many2one("acc.operation", "Opération", required=True) + + # ------------------------------------------------------ + # SQL Constraints + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Default methods + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Computed fields / Search Fields + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Onchange / Constraints + # ------------------------------------------------------ + + # ------------------------------------------------------ + # CRUD methods (ORM overrides) + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Actions + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Business methods + # ------------------------------------------------------ diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv index d2f220c5a1e1675964c06edb5df0db0fdc7c4247..01de11f6c621533d85fc6fa0b821c0f03c799a9c 100644 --- a/security/ir.model.access.csv +++ b/security/ir.model.access.csv @@ -1,4 +1,14 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink -"access_acc_repartition_keys_group_operation_superadmin","acc_repartition_keys group_operation_superadmin","model_acc_repartition_keys","oacc.group_operation_superadmin",1,1,1,0 -"access_acc_repartition_keys_wizard_group_operation_superadmin","acc_repartition_keys_wizard group_operation_superadmin","model_acc_repartition_keys_wizard","oacc.group_operation_superadmin",1,1,1,1 +"access_acc_repartition_keys_file_group_operation_superadmin","acc_repartition_keys_file group_operation_superadmin","model_acc_repartition_keys_file","oacc.group_operation_superadmin",1,1,1,0 +"access_acc_repartition_keys_operation_superadmin","acc_repartition_keys group_operation_superadmin","model_acc_repartition_keys","oacc.group_operation_superadmin",1,1,1,1 +"access_acc_repartition_keys_file_wizard_group_operation_superadmin","acc_repartition_keys_file_wizard group_operation_superadmin","model_acc_repartition_keys_file_wizard","oacc.group_operation_superadmin",1,1,1,1 +"access_acc_priority_group_counter_operation_superadmin","acc_priority_group_counter group_operation_superadmin","model_acc_priority_group_counter","oacc.group_operation_superadmin",1,1,1,1 +"access_acc_priority_group_operation_superadmin","acc_priority_group group_operation_superadmin","model_acc_priority_group","oacc.group_operation_superadmin",1,1,1,1 +"access_acc_repartition_counter_group_operation_superadmin","acc_repartition_counter group_operation_superadmin","model_acc_repartition_counter","oacc.group_operation_superadmin",1,1,1,1 +"access_acc_repartition_keys_compute_wizard_group_operation_superadmin","acc_repartition_keys_compute_wizard group_operation_superadmin","model_acc_repartition_keys_compute_wizard","oacc.group_operation_admin",1,1,1,1 +"access_acc_priority_group_counter_operation_admin","acc_priority_group_counter group_operation_admin","model_acc_priority_group_counter","oacc.group_operation_admin",1,1,1,1 +"access_acc_priority_group_operation_admin","acc_priority_group group_operation_admin","model_acc_priority_group","oacc.group_operation_admin",1,1,1,1 +"access_acc_repartition_counter_group_operation_admin","acc_repartition_counter group_operation_admin","model_acc_repartition_counter","oacc.group_operation_admin",1,1,1,1 +"access_acc_repartition_keys_file_wizard_group_operation_admin","acc_repartition_keys_file_wizard group_operation_admin","model_acc_repartition_keys_file_wizard","oacc.group_operation_admin",1,1,1,1 +"access_acc_repartition_keys_file_group_operation_admin","acc_repartition_keys_file group_operation_admin","model_acc_repartition_keys_file","oacc.group_operation_admin",1,1,1,1 diff --git a/static/src/js/kanban_button.js b/static/src/js/kanban_button.js new file mode 100644 index 0000000000000000000000000000000000000000..b1f668a77b60d7b896e94a1d423f497d5a61f8d0 --- /dev/null +++ b/static/src/js/kanban_button.js @@ -0,0 +1,28 @@ +/** @odoo-module */ +import {KanbanController} from "@web/views/kanban/kanban_controller"; +import {registry} from "@web/core/registry"; +import {kanbanView} from "@web/views/kanban/kanban_view"; +export class PriorityGroupKanbanController extends KanbanController { + setup() { + super.setup(); + } + CreatePriorityGroupClick() { + console.log(this.props.context); + this.actionService.doAction({ + type: "ir.actions.act_window", + res_model: "acc.priority.group", + name: "Creer un groupe de priorité", + view_mode: "form", + view_type: "form", + views: [[false, "form"]], + target: "new", + res_id: false, + context: this.props.context || {}, + }); + } +} +registry.category("views").add("button_in_kanban", { + ...kanbanView, + Controller: PriorityGroupKanbanController, + buttonTemplate: "button_priority_group.KanbanView.Buttons", +}); diff --git a/static/src/xml/kanban_button.xml b/static/src/xml/kanban_button.xml new file mode 100644 index 0000000000000000000000000000000000000000..8bd0ca43cd035d76dfb26e3736000febc2572e55 --- /dev/null +++ b/static/src/xml/kanban_button.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<odoo> + <t + t-name="button_priority_group.KanbanView.Buttons" + t-inherit="web.KanbanView.Buttons" + > + <xpath expr="//*[@class='btn btn-primary o-kanban-button-new']" position="after"> + <button + type="button" + class="btn btn-primary" + style="margin-left: 10px;" + t-on-click="CreatePriorityGroupClick" + > + Creer un groupe de priorité + </button> + </xpath> + </t> +</odoo> diff --git a/views/acc_operation_views.xml b/views/acc_operation_views.xml index a99f61a30356a0a3b396515236b67dc3111e7362..54dffadb2be0a366f717e3c5a4cc8320f3ab49e3 100644 --- a/views/acc_operation_views.xml +++ b/views/acc_operation_views.xml @@ -9,13 +9,40 @@ <field name="arch" type="xml"> <page name="other_infos" position="before"> <page string="Clés de répartition" name="keys"> + <div> + <field name="type_algo" /> + + <field name="algo_description" readonly="1" /> + </div> + <separator /> <button - string="Importer un fichier" + string="Calcul clés de répartition" type="action" - name="%(oacc_repartition_keys.acc_repartition_keys_wizard_action)d" + name="%(oacc_repartition_keys.acc_repartition_keys_compute_wizard_action)d" class="btn-primary" + attrs="{'invisible': [('type_algo', 'in', ['prorata', 'static'])]}" + groups="oacc.group_operation_superadmin" /> - <field name="keys_repartition_ids" mode="tree"> + + <button + string="Algorithme de clé de répartition" + type="object" + name="action_view_repartition_algo_priority_group" + class="btn-primary" + context="{'default_acc_operation_id': active_id}" + attrs="{'invisible': [('type_algo', 'in', ['prorata', 'static', 'dyn_perso_send'])]}" + groups="oacc.group_operation_admin" + /> + <separator /> + <button + string="Importer un fichier de clés" + type="action" + name="%(oacc_repartition_keys.acc_repartition_keys_file_wizard_action)d" + class="btn-primary" + attrs="{'invisible': [('type_algo', 'not in', ['dyn_perso_send'])]}" + groups="oacc.group_operation_admin" + /> + <field name="keys_file_repartition_ids" mode="tree"> <tree create="0" editable="bottom" delete="0"> <field name="date_send" readonly="1" /> <field name="filename" invisible="1" /> @@ -32,4 +59,5 @@ </page> </field> </record> + </odoo> diff --git a/views/acc_priority_group_counter_views.xml b/views/acc_priority_group_counter_views.xml new file mode 100644 index 0000000000000000000000000000000000000000..246eec47dda447148edf9feab05e96c53b44a13d --- /dev/null +++ b/views/acc_priority_group_counter_views.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="utf-8" ?> +<odoo> + <record id="acc_priority_group_counter_act_window" model="ir.actions.act_window"> + <field name="name">Clé de répartition par groupe de priorité</field> + <field name="res_model">acc.priority.group.counter</field> + <field name="view_mode">kanban</field> + </record> + + <record id="acc_operation_priority_group_counter_form" model="ir.ui.view"> + <field name="name">acc.operation.priority.group.counter.form</field> + <field name="model">acc.priority.group.counter</field> + <field name="arch" type="xml"> + <form> + <field name="acc_priority_group_id" invisible="1" /> + <field name="acc_operation_id" invisible="1" /> + <field name="acc_counter_id_domain" invisible="1" /> + <field + name="acc_counter_id" + options="{'no_create_edit': True, 'no_create': True}" + domain="acc_counter_id_domain" + /> + </form> + </field> + </record> + + + <record id="acc_operation_priority_group_counter_kanban" model="ir.ui.view"> + <field name="name">acc.operation.priority.group.counter.kanban</field> + <field name="model">acc.priority.group.counter</field> + <field name="arch" type="xml"> + <kanban + default_group_by="acc_priority_group_id" + class="o_kanban_small_column" + on_create="quick_create" + quick_create_view="oacc_repartition_keys.acc_operation_priority_group_counter_form" + archivable="false" + sample="1" + js_class="button_in_kanban" + group_create="false" + > + <field name="acc_counter_id" /> + <field name="acc_priority_group_id" /> + <field name="counter_street" /> + <field name="counter_owner" /> + + <templates> + <t t-name="kanban-box"> + <div t-attf-class="oe_kanban_global_click oe_kanban_card"> + <div class="oe_kanban_content"> + <div class="o_kanban_record_top"> + <div class="o_kanban_record_headings"> + <a + type="delete" + style="position: absolute; right: 5px; top: 5px;" + >X</a> + <strong class="o_kanban_record_title"> + <field + name="acc_counter_id" + options="{'no_open': True}" + /> + </strong> + </div> + </div> + <div class="o_kanban_record_body"> + <field name="counter_owner" /> + <br /> + <field name="counter_street" /> + </div> + </div> + <div class="clearfix" /> + </div> + </t> + </templates> + </kanban> + </field> + </record> + +</odoo> diff --git a/views/acc_priority_group_views.xml b/views/acc_priority_group_views.xml new file mode 100644 index 0000000000000000000000000000000000000000..ea7fe4d557335ef563991117e602b4b08656dd69 --- /dev/null +++ b/views/acc_priority_group_views.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8" ?> +<odoo> + <record id="acc_priority_group_act_window" model="ir.actions.act_window"> + <field name="name">Groupes de priorités</field> + <field name="res_model">acc.priority.group</field> + <field name="view_mode">kanban,form</field> + </record> + + <record id="acc_priority_group_form" model="ir.ui.view"> + <field name="name">acc.priority.group.form</field> + <field name="model">acc.priority.group</field> + <field name="arch" type="xml"> + <form> + <group> + <field name="acc_operation_id" readonly="1" /> + + <field name="type_algo" /> + </group> + </form> + </field> + </record> + +</odoo> diff --git a/views/acc_repartition_counter_views.xml b/views/acc_repartition_counter_views.xml new file mode 100644 index 0000000000000000000000000000000000000000..1438b38be0893de2bb61b90950633618aba922e8 --- /dev/null +++ b/views/acc_repartition_counter_views.xml @@ -0,0 +1,121 @@ +<?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_repartition_counter_filter" model="ir.ui.view"> + <field name="name">acc.acc_repartition_counter.filter</field> + <field name="model">acc.repartition.counter</field> + <field name="arch" type="xml"> + <search string="Recherche"> + <field name="acc_repartition_id" string="Opération" /> + <field name="acc_counter_id" string="PRM" /> + <field name="time_slot" string="Horodatage" /> + <separator /> + <group expand="0" string="Group By"> + <filter + name="group_operation" + string="Opération" + context="{'group_by':'acc_operation_id'}" + /> + <filter + name="group_time_slot" + string="Horodatage" + context="{'group_by':'time_slot'}" + /> + <filter + name="group_counter" + string="PRMs" + context="{'group_by':'acc_counter_id'}" + /> + </group> + </search> + </field> + </record> + + <record id="acc_repartition_counter_form_view" model="ir.ui.view"> + <field name="name">acc.acc_repartition_counter.form</field> + <field name="model">acc.repartition.counter</field> + <field name="arch" type="xml"> + <form string="CDC Enedis"> + <sheet> + <div class="oe_title"> + <label for="time_slot" /> + <h1> + <field name="time_slot" /> + </h1> + </div> + <group> + <group> + <field + name="acc_operation_id" + options="{'no_create_edit': True, 'no_create': True}" + /> + <field + name="acc_counter_id" + options="{'no_create_edit': True, 'no_create': True}" + /> + <field + name="time_slot" + options="{'no_create_edit': True, 'no_create': True}" + /> + <field + name="weight" + options="{'no_create_edit': True, 'no_create': True}" + /> + </group> + <group> + <field name="time_slot" /> + <field name="acc_counter_id" /> + <field name="weight" /> + </group> + </group> + </sheet> + </form> + </field> + </record> + + <record id="acc_repartition_counter_tree_view" model="ir.ui.view"> + <field name="name">acc.acc_repartition_counter.tree</field> + <field name="model">acc.repartition.counter</field> + <field name="arch" type="xml"> + <tree> + <field + name="acc_operation_id" + options="{'no_create_edit': True, 'no_create': True}" + /> + <field name="time_slot" /> + <field + name="acc_counter_id" + options="{'no_create_edit': True, 'no_create': True}" + /> + <field name="weight" /> + </tree> + </field> + </record> + + <record id="acc_repartition_counter_pivot_view" model="ir.ui.view"> + <field name="name">acc.acc_repartition_counter.pivot</field> + <field name="model">acc.repartition.counter</field> + <field name="arch" type="xml"> + <pivot string="Clés de repartition" sample="1"> + <field name="acc_operation_id" type="row" /> + <field name="time_slot" type="col" /> + <field name="weight" type="measure" /> + </pivot> + </field> + </record> + + <record id="acc_repartition_counter_act_window" model="ir.actions.act_window"> + <field name="name">CLES CALCULEES</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">acc.repartition.counter</field> + <field name="view_mode">pivot,tree,form</field> + <field name="help" type="html"> + <p class="o_view_nocontent_smiling_face"> + Créer une entrée de données + </p> + </field> + </record> + +</odoo> diff --git a/views/acc_repartition_keys_views.xml b/views/acc_repartition_keys_views.xml index 2c5f15a5abe194ef29807a5aad2e224f9009b98a..7be5b5bfe5974359b367ac0f6602fb16506cdd06 100644 --- a/views/acc_repartition_keys_views.xml +++ b/views/acc_repartition_keys_views.xml @@ -1,21 +1,25 @@ <?xml version="1.0" encoding="utf-8" ?> <odoo> -<!-- <data>--> - <record id="acc_keys_repartition_action" model="ir.actions.act_window"> - <field name="name">Keys</field> - <field name="res_model">acc.repartition.keys</field> + <data> + +<!-- KEYS FILES--> + <record id="acc_keys_repartition_file_action" model="ir.actions.act_window"> + <field name="name">Keys file</field> + <field name="res_model">acc.repartition.keys.file</field> <field name="view_mode">tree,form</field> </record> - <record id="acc_keys_repartition_tree" model="ir.ui.view"> - <field name="name">acc.repartition.keys.tree</field> - <field name="model">acc.repartition.keys</field> + + <record id="acc_keys_file_repartition_tree" model="ir.ui.view"> + <field name="name">acc.repartition.keys.file.tree</field> + <field name="model">acc.repartition.keys.file</field> <field name="arch" type="xml"> - <tree string="Keys" create="false"> + <tree string="Keys file" create="false"> <field name="date_send" /> <field name="csv_file" /> <field name="operation_id" /> </tree> </field> </record> -<!-- </data>--> + + </data> </odoo> diff --git a/views/menu_views.xml b/views/menu_views.xml new file mode 100644 index 0000000000000000000000000000000000000000..3bc4ea9f143543817d847b1a23be57c1d2e3cda2 --- /dev/null +++ b/views/menu_views.xml @@ -0,0 +1,26 @@ +<?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> + <menuitem + id="menu_repartition_principal" + parent="oacc.menu_acc" + name="Clés de répartition" + sequence="15" + groups="oacc.group_operation_superadmin" + /> +<!-- <menuitem--> +<!-- id="menu_send_key"--> +<!-- parent="menu_repartition_principal"--> +<!-- name="Clés envoyées"--> +<!-- sequence="30"--> +<!-- action="oacc_repartition_keys.acc_keys_repartition_file_action"--> +<!-- />--> + <menuitem + id="menu_computed_key" + parent="menu_repartition_principal" + name="Clés calculées" + sequence="40" + action="acc_repartition_counter_act_window" + /> +</odoo> diff --git a/wizard/__init__.py b/wizard/__init__.py index cbd8c61e5e0dbd922961fa9b0b9117dc317a3bc3..7747d99f28e7bcd354835f7a631b898d55a4677d 100644 --- a/wizard/__init__.py +++ b/wizard/__init__.py @@ -1 +1,2 @@ -from . import acc_repartition_keys_wizard +from . import acc_repartition_keys_file_wizard +from . import acc_repartition_keys_compute_wizard diff --git a/wizard/acc_repartition_keys_compute_wizard.py b/wizard/acc_repartition_keys_compute_wizard.py new file mode 100644 index 0000000000000000000000000000000000000000..e4e830eec0a5840f0706670c634aea2e71194f3c --- /dev/null +++ b/wizard/acc_repartition_keys_compute_wizard.py @@ -0,0 +1,74 @@ +# Copyright 2021- Le Filament (https://le-filament.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo import _, fields, models +from odoo.exceptions import UserError, ValidationError + + +class AccRepartitionKeysComputeWizard(models.TransientModel): + _name = "acc.repartition.keys.compute.wizard" + _description = "Import fichier de clés de répartition" + + # ------------------------------------------------------ + # Default methods + # ------------------------------------------------------ + def _default_operation_id(self): + return self.env.context.get("active_id") + + # ------------------------------------------------------ + # Fields declaration + # ------------------------------------------------------ + operation_id = fields.Many2one( + "acc.operation", "Opération", default=_default_operation_id + ) + + # ------------------------------------------------------ + # SQL Constraints + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Computed fields / Search Fields + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Onchange / Constraints + # ------------------------------------------------------ + + # ------------------------------------------------------ + # CRUD methods (ORM overrides) + # ------------------------------------------------------ + def ignore(self): + return {"type": "ir.actions.act_window_close"} + + # ------------------------------------------------------ + # Actions + # ------------------------------------------------------ + + # ------------------------------------------------------ + # Business methods + # ------------------------------------------------------ + + def send_keys(self): + self.operation_id.action_send_repartition_keys() + + def get_csv(self): + job_description = f"{self.operation_id.name} - Generate repartition CSV " + try: + self.operation_id.with_delay( + description=job_description + ).export_repartition() + except ValidationError as exc: + raise UserError(_(str(exc))) from exc + + return {"type": "ir.actions.act_window_close"} + + def compute_keys(self): + job_description = f"{self.operation_id.name} - Generate repartition key " + try: + self.operation_id.with_delay( + description=job_description + ).compute_repartition() + except ValidationError as exc: + raise UserError(_(str(exc))) from exc + + return {"type": "ir.actions.act_window_close"} diff --git a/wizard/acc_repartition_keys_compute_wizard_views.xml b/wizard/acc_repartition_keys_compute_wizard_views.xml new file mode 100644 index 0000000000000000000000000000000000000000..2c9aecb352d83a328f50cc8e0df39459dcac3397 --- /dev/null +++ b/wizard/acc_repartition_keys_compute_wizard_views.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8" ?> +<odoo> + <record id="acc_repartition_keys_compute_wizard_form" model="ir.ui.view"> + <field name="name">acc.repartition.keys.compute.wizard.form</field> + <field name="model">acc.repartition.keys.compute.wizard</field> + <field name="arch" type="xml"> + <form string="Calcul des clés de repartition"> + <button + class="btn btn-primary" + name="compute_keys" + type="object" + string="Lancer le calcul" + /> + + <footer> + <button + class="btn btn-primary" + name="send_keys" + type="object" + string="Envoyer les clés" + /> + <button + class="btn btn-primary" + name="get_csv" + type="object" + string="Génerer CSV" + /> + <button + class="btn btn-primary" + name="ignore" + type="object" + string="Ignorer" + /> + </footer> + </form> + </field> + </record> + + <record + id="acc_repartition_keys_compute_wizard_action" + model="ir.actions.act_window" + > + <field name="name">Calcul des clés de repartition</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">acc.repartition.keys.compute.wizard</field> + <field name="view_mode">form</field> + <field name="view_id" ref="acc_repartition_keys_compute_wizard_form" /> + <field name="target">new</field> + </record> +</odoo> diff --git a/wizard/acc_repartition_keys_wizard.py b/wizard/acc_repartition_keys_file_wizard.py similarity index 91% rename from wizard/acc_repartition_keys_wizard.py rename to wizard/acc_repartition_keys_file_wizard.py index 2f4fa917668408399ace667eeb5e62b1d7cbefc9..879495dd3d6623a0d5b274827dcbc218ecd1fd22 100644 --- a/wizard/acc_repartition_keys_wizard.py +++ b/wizard/acc_repartition_keys_file_wizard.py @@ -9,9 +9,9 @@ from odoo.exceptions import UserError, ValidationError from ..tools.key_file import RepartitionKeyEntryFile -class AccRepartitionKeysWizard(models.TransientModel): - _name = "acc.repartition.keys.wizard" - _description = "clés de répartition" +class AccRepartitionKeysFileWizard(models.TransientModel): + _name = "acc.repartition.keys.file.wizard" + _description = "Import fichier de clés de répartition" # ------------------------------------------------------ # Default methods @@ -100,7 +100,10 @@ class AccRepartitionKeysWizard(models.TransientModel): data_to_send = entry_file_handler.data_to_send(send_empty_key=True) for key in data_to_send: - job_description = f"{self.operation_id.name} - Send repartition key at {key.get('timestamp')}" + job_description = ( + f"{self.operation_id.name} - Send repartition key " + f"at {key.get('timestamp')}" + ) try: self.operation_id.with_delay( description=job_description @@ -108,7 +111,7 @@ class AccRepartitionKeysWizard(models.TransientModel): except ValidationError as exc: raise UserError(_(str(exc))) from exc - self.env["acc.repartition.keys"].create( + self.env["acc.repartition.keys.file"].create( { "csv_file": self.csv_file, "filename": self.filename, diff --git a/wizard/acc_repartition_keys_wizard_views.xml b/wizard/acc_repartition_keys_file_wizard_views.xml similarity index 57% rename from wizard/acc_repartition_keys_wizard_views.xml rename to wizard/acc_repartition_keys_file_wizard_views.xml index 81b5da0a99e588979e36e1c7216f77cd3ce5a2d0..fc167d55fb546686a2499c54c856923934a81de9 100644 --- a/wizard/acc_repartition_keys_wizard_views.xml +++ b/wizard/acc_repartition_keys_file_wizard_views.xml @@ -1,10 +1,10 @@ <?xml version="1.0" encoding="utf-8" ?> <odoo> - <record id="acc_repartition_keys_wizard_form" model="ir.ui.view"> - <field name="name">acc.repartition.keys.wizard.form</field> - <field name="model">acc.repartition.keys.wizard</field> + <record id="acc_repartition_keys_file_wizard_form" model="ir.ui.view"> + <field name="name">acc.repartition.keys.file.wizard.form</field> + <field name="model">acc.repartition.keys.file.wizard</field> <field name="arch" type="xml"> - <form string="Création clés de repartition"> + <form string="Importation fichier clés de repartition"> <field name="filename" invisible="1" /> <field widget="binary" name="csv_file" filename="filename" /> <footer> @@ -25,12 +25,12 @@ </field> </record> - <record id="acc_repartition_keys_wizard_action" model="ir.actions.act_window"> - <field name="name">Création clés de repartition</field> + <record id="acc_repartition_keys_file_wizard_action" model="ir.actions.act_window"> + <field name="name">Importation fichier clés de repartition</field> <field name="type">ir.actions.act_window</field> - <field name="res_model">acc.repartition.keys.wizard</field> + <field name="res_model">acc.repartition.keys.file.wizard</field> <field name="view_mode">form</field> - <field name="view_id" ref="acc_repartition_keys_wizard_form" /> + <field name="view_id" ref="acc_repartition_keys_file_wizard_form" /> <field name="target">new</field> </record> </odoo>