diff --git a/edi_oca/__manifest__.py b/edi_oca/__manifest__.py index 124eb84d80..137567c55c 100644 --- a/edi_oca/__manifest__.py +++ b/edi_oca/__manifest__.py @@ -9,7 +9,7 @@ Define backends, exchange types, exchange records, basic automation and views for handling EDI exchanges. """, - "version": "14.0.1.19.0", + "version": "14.0.1.20.0", "website": "https://github.com/OCA/edi", "development_status": "Beta", "license": "LGPL-3", @@ -35,6 +35,7 @@ "views/edi_backend_type_views.xml", "views/edi_exchange_record_views.xml", "views/edi_exchange_type_views.xml", + "views/edi_exchange_type_rule_views.xml", "views/menuitems.xml", "templates/exchange_chatter_msg.xml", "templates/exchange_mixin_buttons.xml", diff --git a/edi_oca/migrations/14.0.1.20.0/post-migrate.py b/edi_oca/migrations/14.0.1.20.0/post-migrate.py new file mode 100644 index 0000000000..8c8b6a8b91 --- /dev/null +++ b/edi_oca/migrations/14.0.1.20.0/post-migrate.py @@ -0,0 +1,35 @@ +# Copyright 2023 Camptocamp SA (http://www.camptocamp.com) +# @author Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging + +from odoo import SUPERUSER_ID, api, tools + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + if not version: + return + + bkp_table = "exc_type_model_rel_bkp" + if not tools.sql.table_exists(cr, bkp_table): + return + + # Use backup table (created by pre-migrate step) to create type rules + env = api.Environment(cr, SUPERUSER_ID, {}) + query = """ + SELECT * FROM exc_type_model_rel_bkp + """ + cr.execute(query) + res = cr.dictfetchall() + model = env["edi.exchange.type.rule"] + for item in res: + kind = "form_btn" if item.pop("form_btn", False) else "custom" + vals = dict(item, name="Default", kind=kind) + rec = model.create(vals) + rec.type_id.button_wipe_deprecated_rule_fields() + + cr.execute("DROP TABLE exc_type_model_rel_bkp") + _logger.info("edi.exchange.type.rule created") diff --git a/edi_oca/migrations/14.0.1.20.0/pre-migrate.py b/edi_oca/migrations/14.0.1.20.0/pre-migrate.py new file mode 100644 index 0000000000..61419f1d88 --- /dev/null +++ b/edi_oca/migrations/14.0.1.20.0/pre-migrate.py @@ -0,0 +1,42 @@ +# Copyright 2023 Camptocamp SA (http://www.camptocamp.com) +# @author Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging + +from odoo import tools + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + if not version: + return + + # Backup old style rules to be used later on post migrate + old_table = "edi_exchange_type_ir_model_rel" + if not tools.sql.table_exists(cr, old_table): + return + bkp_table = "exc_type_model_rel_bkp" + if tools.sql.table_exists(cr, bkp_table): + return + + bkp_query = """ + CREATE TABLE IF NOT EXISTS + exc_type_model_rel_bkp + AS + SELECT + rel.ir_model_id as model_id, + type.id as type_id, + type.enable_domain as enable_domain, + type.enable_snippet as enable_snippet, + type.model_manual_btn as form_btn + FROM + edi_exchange_type type, + edi_exchange_type_ir_model_rel rel + WHERE + rel.edi_exchange_type_id = type.id; + """ + cr.execute(bkp_query) + + _logger.info("edi.exchange.type old style rules backed up") diff --git a/edi_oca/models/__init__.py b/edi_oca/models/__init__.py index b3961f232f..f40b0abe1e 100644 --- a/edi_oca/models/__init__.py +++ b/edi_oca/models/__init__.py @@ -3,4 +3,5 @@ from . import edi_exchange_record from . import edi_exchange_consumer_mixin from . import edi_exchange_type +from . import edi_exchange_type_rule from . import edi_id_mixin diff --git a/edi_oca/models/edi_exchange_consumer_mixin.py b/edi_oca/models/edi_exchange_consumer_mixin.py index a5430e635b..d8e3538683 100644 --- a/edi_oca/models/edi_exchange_consumer_mixin.py +++ b/edi_oca/models/edi_exchange_consumer_mixin.py @@ -58,34 +58,45 @@ def _compute_edi_config(self): record.edi_has_form_config = any([x.get("form") for x in config.values()]) def _edi_get_exchange_type_config(self): - exchange_types = ( - self.env["edi.exchange.type"] + # TODO: move this machinery to the rule model + rules = ( + self.env["edi.exchange.type.rule"] .sudo() - .search([("model_ids.model", "=", self._name)]) + .search([("model_id.model", "=", self._name)]) ) result = {} - for exchange_type in exchange_types: + for rule in rules: + exchange_type = rule.type_id eval_ctx = dict( self._get_eval_context(), record=self, exchange_type=exchange_type ) - domain = safe_eval.safe_eval(exchange_type.enable_domain or "[]", eval_ctx) + domain = safe_eval.safe_eval(rule.enable_domain or "[]", eval_ctx) if not self.filtered_domain(domain): continue - if exchange_type.enable_snippet: + if rule.enable_snippet: safe_eval.safe_eval( - exchange_type.enable_snippet, eval_ctx, mode="exec", nocopy=True + rule.enable_snippet, eval_ctx, mode="exec", nocopy=True ) if not eval_ctx.get("result", False): continue - result[exchange_type.id] = self._edi_get_exchange_type_conf(exchange_type) + result[rule.id] = self._edi_get_exchange_type_rule_conf(rule) return result @api.model - def _edi_get_exchange_type_conf(self, exchange_type): - conf = {"form": {}} - if exchange_type.model_manual_btn: - conf.update({"form": {"btn": {"label": exchange_type.name}}}) + def _edi_get_exchange_type_rule_conf(self, rule): + conf = { + "form": {}, + "type": { + "id": rule.type_id.id, + "name": rule.type_id.name, + }, + } + if rule.kind == "form_btn": + label = rule.form_btn_label or rule.type_id.name + conf.update( + {"form": {"btn": {"label": label, "tooltip": rule.form_btn_tooltip}}} + ) return conf def _get_eval_context(self): diff --git a/edi_oca/models/edi_exchange_type.py b/edi_oca/models/edi_exchange_type.py index f5a8af86bc..db09a74198 100644 --- a/edi_oca/models/edi_exchange_type.py +++ b/edi_oca/models/edi_exchange_type.py @@ -111,25 +111,33 @@ class EDIExchangeType(models.Model): """, ) advanced_settings = Serialized(default={}, compute="_compute_advanced_settings") - model_ids = fields.Many2many( - "ir.model", - help="""Modules to be checked for manual EDI generation""", - ) - enable_domain = fields.Char( - string="Enable on domain", help="""Filter domain to be checked on Models""" + rule_ids = fields.One2many( + comodel_name="edi.exchange.type.rule", + inverse_name="type_id", + help="Rules to handle exchanges and UI automatically", ) - enable_snippet = fields.Char( - string="Enable on snippet", - help="""Snippet of code to be checked on Models, - You can use `record` and `exchange_type` here. - It will be executed if variable result has been defined as True - """, + # Deprecated fields for rules - begin + # These fields have been deprecated in + # https://github.com/OCA/edi/pull/797 + # but are kept for backward compat. + # If you can stop using them now. + # Anyway, annoying warning messages will be logged. + # See inverse methods. + # NOTE: old configurations are migrated automatically on upgrade + # Yet, if you have data files they might be broken + # if we delete these fields. + model_ids = fields.Many2many( + "ir.model", inverse="_inverse_deprecated_rules_model_ids" ) + enable_domain = fields.Char(inverse="_inverse_deprecated_rules_enable_domain") + enable_snippet = fields.Char(inverse="_inverse_deprecated_rules_enable_snippet") model_manual_btn = fields.Boolean( - string="Manual button on form", - help="Automatically display a button on related models' form." - # TODO: "Button settings can be configured via advanced settings." + inverse="_inverse_deprecated_rules_model_manual_btn" + ) + deprecated_rule_fields_still_used = fields.Boolean( + compute="_compute_deprecated_rule_fields_still_used" ) + # Deprecated fields for rules - end quick_exec = fields.Boolean( string="Quick execution", help="When active, records of this type will be processed immediately " @@ -240,3 +248,115 @@ def is_partner_enabled(self, partner): if exc_type.partner_ids: return partner.id in exc_type.partner_ids.ids return True + + # API to support deprecated model rules fields - begin + def _inverse_deprecated_rules_warning(self): + _fields = ", ".join( + ["model_ids", "enable_domain", "enable_snippet", "model_manual_btn"] + ) + _logger.warning( + "The fields %s are deprecated, " + "please stop using them in favor of edi.exchange.type.rule", + _fields, + ) + + def _inverse_deprecated_rules_model_ids(self): + if self.env.context.get("deprecated_rule_fields_bypass_inverse"): + return + self._inverse_deprecated_rules_warning() + for rec in self: + for model in rec.model_ids: + rule = rec._get_rule_by_model(model) + if not rule: + _logger.warning( + "New rule for %s created from deprecated `model_ids`", + model.model, + ) + rec.rule_ids += rec._inverse_deprecated_rules_create(model) + rules_to_delete = rec.rule_ids.browse() + for rule in rec.rule_ids: + if rule.model_id not in rec.model_ids: + _logger.warning( + "Rule for %s deleted from deprecated `model_ids`", + rule.model_id.model, + ) + rules_to_delete |= rule + rules_to_delete.unlink() + + def _inverse_deprecated_rules_enable_domain(self): + if self.env.context.get("deprecated_rule_fields_bypass_inverse"): + return + self._inverse_deprecated_rules_warning() + for rec in self: + for model in rec.model_ids: + rule = rec._get_rule_by_model(model) + if rule: + _logger.warning( + "Rule for %s domain updated from deprecated `enable_domain`", + model.model, + ) + rule.enable_domain = rec.enable_domain + + def _inverse_deprecated_rules_enable_snippet(self): + if self.env.context.get("deprecated_rule_fields_bypass_inverse"): + return + self._inverse_deprecated_rules_warning() + for rec in self: + for model in rec.model_ids: + rule = rec._get_rule_by_model(model) + if rule: + _logger.warning( + "Rule for %s snippet updated from deprecated `enable_snippet`", + model.model, + ) + rule.enable_snippet = rec.enable_snippet + + def _inverse_deprecated_rules_model_manual_btn(self): + if self.env.context.get("deprecated_rule_fields_bypass_inverse"): + return + self._inverse_deprecated_rules_warning() + for rec in self: + for model in rec.model_ids: + rule = rec._get_rule_by_model(model) + if rule: + _logger.warning( + "Rule for %s btn updated from deprecated `model_manual_btn`", + model.model, + ) + rule.kind = "form_btn" if self.model_manual_btn else "custom" + + def _get_rule_by_model(self, model): + return self.rule_ids.filtered(lambda x: x.model_id == model) + + def _inverse_deprecated_rules_create(self, model): + kind = "form_btn" if self.model_manual_btn else "custom" + vals = { + "type_id": self.id, + "model_id": model.id, + "kind": kind, + "name": "Default", + "enable_snippet": self.enable_snippet, + "enable_domain": self.enable_domain, + } + return self.rule_ids.create(vals) + + @api.depends("model_ids", "enable_domain", "enable_snippet", "model_manual_btn") + def _compute_deprecated_rule_fields_still_used(self): + for rec in self: + rec.deprecated_rule_fields_still_used = ( + rec._deprecated_rule_fields_still_used() + ) + + def _deprecated_rule_fields_still_used(self): + for fname in ("model_ids", "enable_snippet", "enable_domain"): + if self[fname]: + return True + + def button_wipe_deprecated_rule_fields(self): + _fields = ["model_ids", "enable_domain", "enable_snippet", "model_manual_btn"] + deprecated_vals = {}.fromkeys(_fields, None) + self.with_context(deprecated_rule_fields_bypass_inverse=True).write( + deprecated_vals + ) + + # API to support deprecated model rules fields - end diff --git a/edi_oca/models/edi_exchange_type_rule.py b/edi_oca/models/edi_exchange_type_rule.py new file mode 100644 index 0000000000..8098493721 --- /dev/null +++ b/edi_oca/models/edi_exchange_type_rule.py @@ -0,0 +1,62 @@ +# Copyright 2023 Camptocamp SA +# @author Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + +KIND_HELP = """ +* Form button: show a button on the related model form + when conditions from domain and snippet are satisfied + +* Custom: let devs handle a custom behavior with specific developments +""" + + +class EDIExchangeTypeRule(models.Model): + """ + Define rules for exchange types. + """ + + _name = "edi.exchange.type.rule" + _description = "EDI Exchange type rule" + + active = fields.Boolean(default=True) + name = fields.Char(required=True) + type_id = fields.Many2one( + comodel_name="edi.exchange.type", + required=True, + ondelete="cascade", + ) + model_id = fields.Many2one( + comodel_name="ir.model", + help="Apply to this model", + ondelete="cascade", + ) + model = fields.Char(related="model_id.model") # Tech field + enable_domain = fields.Char( + string="Enable on domain", help="Filter domain to be checked on Models" + ) + enable_snippet = fields.Char( + string="Enable on snippet", + help="""Snippet of code to be checked on Models, + You can use `record` and `exchange_type` here. + It will be executed if variable result has been defined as True + """, + ) + kind = fields.Selection( + selection=[ + ("form_btn", "Form button"), + ("custom", "Custom"), + ], + required=True, + default="form_btn", + help=KIND_HELP, + ) + form_btn_label = fields.Char( + string="Form button label", translate=True, help="Type name used by default" + ) + form_btn_tooltip = fields.Text( + string="Form button tooltip", + translate=True, + help="Help message visible as tooltip on button h-over", + ) diff --git a/edi_oca/readme/CONFIGURE.rst b/edi_oca/readme/CONFIGURE.rst index c65c8e743b..acf6a0d286 100644 --- a/edi_oca/readme/CONFIGURE.rst +++ b/edi_oca/readme/CONFIGURE.rst @@ -27,3 +27,24 @@ snippet of code. After defining this fields, we will automatically see buttons on the view to generate the exchange records. This configuration is useful to define a way of generation managed by user. + + +Exchange type rules configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Exchange types can be further configured with rules. +You can use rules to: + +1. make buttons automatically appear in forms +2. define your own custom logic + +Go to an exchange type and go to the tab "Model rules". +There you can add one or more rule, one per model. +On each rule you can define a domain or a snippet to activate it. +In case of a "Form button" kind, if the domain and/ the snippet is/are satisfied, +a form btn will appear on the top of the form. +This button can be used by the end user to manually generate an exchange. +If there's more than a backend and the exchange type has not a backend set, +a wizard will appear asking to select a backend to be used for the exchange. + +In case of "Custom" kind, you'll have to define your own logic to do something. diff --git a/edi_oca/security/ir_model_access.xml b/edi_oca/security/ir_model_access.xml index 19dfacd511..83970fb3c5 100644 --- a/edi_oca/security/ir_model_access.xml +++ b/edi_oca/security/ir_model_access.xml @@ -27,6 +27,15 @@ + + access_edi_exchange_type_rule manager + + + + + + + access_edi_exchange_record manager @@ -63,6 +72,15 @@ + + access_edi_exchange_type_rule user + + + + + + + access_edi_exchange_record user diff --git a/edi_oca/static/src/xml/widget_edi.xml b/edi_oca/static/src/xml/widget_edi.xml index ebcf9ad3ff..278610eca4 100644 --- a/edi_oca/static/src/xml/widget_edi.xml +++ b/edi_oca/static/src/xml/widget_edi.xml @@ -2,13 +2,14 @@
- - - - + + + +