From 30c316c5a8f082fbd99c2b40a1ab322d4327bcc5 Mon Sep 17 00:00:00 2001 From: thien Date: Thu, 18 Jul 2024 15:30:10 +0700 Subject: [PATCH] [ADD] edi_oca: Add new model edi.configuration --- edi_oca/README.rst | 1 + edi_oca/__manifest__.py | 2 + edi_oca/data/edi_configuration.xml | 18 ++ edi_oca/models/__init__.py | 1 + edi_oca/models/edi_backend.py | 21 +- edi_oca/models/edi_configuration.py | 193 ++++++++++++++++++ edi_oca/models/edi_exchange_consumer_mixin.py | 44 +++- edi_oca/models/edi_exchange_record.py | 3 + edi_oca/readme/CONTRIBUTORS.rst | 1 + edi_oca/security/ir_model_access.xml | 18 ++ edi_oca/static/description/index.html | 12 +- edi_oca/tests/__init__.py | 1 + edi_oca/tests/fake_components.py | 26 +++ edi_oca/tests/fake_models.py | 67 +++++- edi_oca/tests/test_edi_configuration.py | 163 +++++++++++++++ edi_oca/views/edi_configuration_views.xml | 107 ++++++++++ edi_oca/views/menuitems.xml | 7 + edi_purchase_oca/data/edi_configuration.xml | 23 +++ edi_purchase_oca/models/res_partner.py | 19 ++ .../static/description/index.html | 1 - edi_purchase_oca/views/res_partner_view.xml | 16 ++ 21 files changed, 727 insertions(+), 17 deletions(-) create mode 100644 edi_oca/data/edi_configuration.xml create mode 100644 edi_oca/models/edi_configuration.py create mode 100644 edi_oca/tests/test_edi_configuration.py create mode 100644 edi_oca/views/edi_configuration_views.xml create mode 100644 edi_purchase_oca/data/edi_configuration.xml create mode 100644 edi_purchase_oca/models/res_partner.py create mode 100644 edi_purchase_oca/views/res_partner_view.xml diff --git a/edi_oca/README.rst b/edi_oca/README.rst index acde589e405..488fe835632 100644 --- a/edi_oca/README.rst +++ b/edi_oca/README.rst @@ -166,6 +166,7 @@ Contributors * Simone Orsi * Enric Tobella +* Thien Vo Maintainers ~~~~~~~~~~~ diff --git a/edi_oca/__manifest__.py b/edi_oca/__manifest__.py index 6bdcbe77b1a..64c4b6a17bc 100644 --- a/edi_oca/__manifest__.py +++ b/edi_oca/__manifest__.py @@ -29,6 +29,7 @@ "data/sequence.xml", "data/job_channel.xml", "data/job_function.xml", + "data/edi_configuration.xml", "security/res_groups.xml", "security/ir_model_access.xml", "views/edi_backend_views.xml", @@ -36,6 +37,7 @@ "views/edi_exchange_record_views.xml", "views/edi_exchange_type_views.xml", "views/edi_exchange_type_rule_views.xml", + "views/edi_configuration_views.xml", "views/menuitems.xml", "templates/exchange_chatter_msg.xml", "templates/exchange_mixin_buttons.xml", diff --git a/edi_oca/data/edi_configuration.xml b/edi_oca/data/edi_configuration.xml new file mode 100644 index 00000000000..f65eb6286fb --- /dev/null +++ b/edi_oca/data/edi_configuration.xml @@ -0,0 +1,18 @@ + + + + Send Via Email + False + send_via_email + on_email_send + record._edi_send_via_email() + + + + + Send Via EDI + False + send_via_edi + record._edi_send_via_edi(conf.type_id) + + diff --git a/edi_oca/models/__init__.py b/edi_oca/models/__init__.py index f40b0abe1e5..c5223ae7f6f 100644 --- a/edi_oca/models/__init__.py +++ b/edi_oca/models/__init__.py @@ -5,3 +5,4 @@ from . import edi_exchange_type from . import edi_exchange_type_rule from . import edi_id_mixin +from . import edi_configuration diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py index 504bcd5b72d..7c544ddc1d9 100644 --- a/edi_oca/models/edi_backend.py +++ b/edi_oca/models/edi_backend.py @@ -377,6 +377,18 @@ def _cron_check_output_exchange_sync(self, **kw): for backend in self: backend._check_output_exchange_sync(**kw) + def exchange_generate_send(self, recordset, skip_generate=False, skip_send=False): + for rec in recordset: + if skip_generate: + job1 = rec + else: + job1 = rec.delayable().action_exchange_generate() + if not skip_send: + # Chain send job. + # Raise prio to max to send the record out as fast as possible. + job1.on_done(rec.delayable(priority=0).action_exchange_send()) + job1.delay() + def _check_output_exchange_sync( self, skip_send=False, skip_sent=True, record_ids=None ): @@ -396,13 +408,8 @@ def _check_output_exchange_sync( "EDI Exchange output sync: found %d new records to process.", len(new_records), ) - for rec in new_records: - job1 = rec.delayable().action_exchange_generate() - if not skip_send: - # Chain send job. - # Raise prio to max to send the record out as fast as possible. - job1.on_done(rec.delayable(priority=0).action_exchange_send()) - job1.delay() + if new_records: + self.exchange_generate_send(new_records, skip_send=skip_send) if skip_send: return diff --git a/edi_oca/models/edi_configuration.py b/edi_oca/models/edi_configuration.py new file mode 100644 index 00000000000..3b5fc04a869 --- /dev/null +++ b/edi_oca/models/edi_configuration.py @@ -0,0 +1,193 @@ +# Copyright 2024 Camptocamp SA +# @author Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import datetime + +import pytz + +from odoo import _, api, exceptions, fields, models +from odoo.tools import DotDict, safe_eval + + +def date_to_datetime(dt): + """Convert date to datetime.""" + if isinstance(dt, datetime.date): + return datetime.datetime.combine(dt, datetime.datetime.min.time()) + return dt + + +def to_utc(dt): + """Convert date or datetime to UTC.""" + # Gracefully convert to datetime if needed 1st + return date_to_datetime(dt).astimezone(pytz.UTC) + + +class EdiConfiguration(models.Model): + _name = "edi.configuration" + _description = """ + This model is used to configure EDI (Electronic Data Interchange) flows. + It allows users to create their own configurations, which can be tailored + to meet the specific needs of their business processes. + """ + + name = fields.Char(string="Name", required=True) + active = fields.Boolean(default=True) + code = fields.Char(required=True, copy=False, index=True, unique=True) + description = fields.Char(help="Describe what the conf is for") + backend_id = fields.Many2one(string="Backend", comodel_name="edi.backend") + type_id = fields.Many2one( + string="Exchange Type", + comodel_name="edi.exchange.type", + ondelete="cascade", + auto_join=True, + index=True, + ) + model = fields.Many2one( + "ir.model", + string="Model", + help="Model the conf applies to. Leave blank to apply for all models", + ) + model_name = fields.Char(related="model.model", store=True) + trigger = fields.Selection( + [ + ("on_record_write", "Update Record"), + ("on_record_create", "Create Record"), + ("on_email_send", "Send Email"), + ], + string="Trigger", + default=False, + ) + snippet_before_do = fields.Text( + string="Snippet Before Do", + help="Snippet to validate the state and collect records to do", + ) + snippet_do = fields.Text( + string="Snippet Do", + help="""Used to do something specific here. + Receives: operation, edi_action, vals, old_vals.""", + ) + + @api.constrains("backend_id", "type_id") + def _constrains_backend(self): + for rec in self: + if rec.type_id.backend_id: + if rec.type_id.backend_id != rec.backend_id: + raise exceptions.ValidationError( + _("Backend must match with exchange type's backend!") + ) + else: + if rec.type_id.backend_type_id != rec.backend_id.backend_type_id: + raise exceptions.ValidationError( + _("Backend type must match with exchange type's backend type!") + ) + + # TODO: This function is also available in `edi_exchange_template`. + # Consider adding this to util or mixin + def _code_snippet_valued(self, snippet): + snippet = snippet or "" + return bool( + [ + not line.startswith("#") + for line in (snippet.splitlines()) + if line.strip("") + ] + ) + + @staticmethod + def _date_to_string(dt, utc=True): + if not dt: + return "" + if utc: + dt = to_utc(dt) + return fields.Date.to_string(dt) + + @staticmethod + def _datetime_to_string(dt, utc=True): + if not dt: + return "" + if utc: + dt = to_utc(dt) + return fields.Datetime.to_string(dt) + + def _time_utils(self): + return { + "datetime": safe_eval.datetime, + "dateutil": safe_eval.dateutil, + "time": safe_eval.time, + "utc_now": fields.Datetime.now(), + "date_to_string": self._date_to_string, + "datetime_to_string": self._datetime_to_string, + "time_to_string": lambda dt: dt.strftime("%H:%M:%S") if dt else "", + "first_of": fields.first, + } + + def _get_code_snippet_eval_context(self): + """Prepare the context used when evaluating python code + + :returns: dict -- evaluation context given to safe_eval + """ + ctx = { + "uid": self.env.uid, + "user": self.env.user, + "DotDict": DotDict, + "conf": self, + } + ctx.update(self._time_utils()) + return ctx + + def _evaluate_code_snippet(self, snippet, **render_values): + if not self._code_snippet_valued(snippet): + return {} + eval_ctx = dict(render_values, **self._get_code_snippet_eval_context()) + safe_eval.safe_eval(snippet, eval_ctx, mode="exec", nocopy=True) + result = eval_ctx.get("result", {}) + if not isinstance(result, dict): + return {} + return result + + def edi_exec_snippet_before_do(self, record, **kwargs): + self.ensure_one() + # Execute snippet before do + vals_before_do = self._evaluate_code_snippet( + self.snippet_before_do, record=record, **kwargs + ) + + # Prepare data + vals = { + "todo": vals_before_do.get("todo", True), + "snippet_do_vars": vals_before_do.get("snippet_do_vars", False), + "event_only": vals_before_do.get("event_only", False), + "tracked_fields": vals_before_do.get("tracked_fields", False), + "edi_action": vals_before_do.get("edi_action", False), + } + return vals + + def edi_exec_snippet_do(self, record, **kwargs): + self.ensure_one() + + old_value = kwargs.get("old_vals", {}).get(record.id, {}) + new_value = kwargs.get("vals", {}).get(record.id, {}) + vals = { + "todo": True, + "record": record, + "operation": kwargs.get("operation", False), + "edi_action": kwargs.get("edi_action", False), + "old_value": old_value, + "vals": new_value, + } + if self.snippet_before_do: + before_do_vals = self.edi_exec_snippet_before_do(record, **kwargs) + vals.update(before_do_vals) + if vals["todo"]: + return self._evaluate_code_snippet(self.snippet_do, **vals) + return True + + def edi_get_conf(self, trigger, backend=None): + domain = [("trigger", "=", trigger)] + backend_ids = self.mapped("type_id.backend_id.id") + if backend: + domain.append(("backend_id", "=", backend.id)) + elif backend_ids: + domain.append(("backend_id", "in", backend_ids)) + return self.filtered_domain(domain) diff --git a/edi_oca/models/edi_exchange_consumer_mixin.py b/edi_oca/models/edi_exchange_consumer_mixin.py index af67a98fe8e..e54deea7b8f 100644 --- a/edi_oca/models/edi_exchange_consumer_mixin.py +++ b/edi_oca/models/edi_exchange_consumer_mixin.py @@ -7,7 +7,7 @@ from lxml import etree -from odoo import api, fields, models +from odoo import _, api, fields, models from odoo.tools import safe_eval from odoo.addons.base_sparse_field.models.fields import Serialized @@ -291,3 +291,45 @@ def _edi_set_origin(self, exc_record): def _edi_get_origin(self): self.ensure_one() return self.origin_exchange_record_id + + def _edi_send_via_edi(self, exchange_type): + exchange_record = self._edi_create_exchange_record(exchange_type) + exchange_record.action_exchange_generate_send() + msg = _("EDI auto: output generated.") + exchange_record._notify_related_record(msg) + exchange_record._trigger_edi_event("generated") + + def _edi_send_via_email( + self, ir_action=None, subtype_ref=None, partner_method=None, partners=None + ): + # Default action if not provided + if ir_action is None: + # `action_send_email` is just an action name I created + # to be able to generalize across models. + if hasattr(self, "action_send_email"): + ir_action = self.action_send_email() + else: + return False + # Retrieve context and composer model + ctx = ir_action.get("context", {}) + composer_model = self.env[ir_action["res_model"]].with_context(ctx) + + # Determine subtype and partner_ids dynamically based on model-specific logic + subtype = subtype_ref and self.env.ref(subtype_ref) or None + if not subtype: + return False + + composer = composer_model.create({"subtype_id": subtype.id}) + composer.onchange_template_id_wrapper() + + # Dynamically retrieve partners based on the provided method or fallback to parameter + if partner_method and hasattr(self, partner_method): + composer.partner_ids = getattr(self, partner_method)().ids + elif partners: + composer.partner_ids = partners.ids + else: + return False + + # Send the email + composer.send_mail() + return True diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index f5243e7128f..d436a7cfb54 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -308,6 +308,9 @@ def action_exchange_generate(self, **kw): self.ensure_one() return self.backend_id.exchange_generate(self, **kw) + def action_exchange_generate_send(self): + return self.backend_id.exchange_generate_send(self) + def action_exchange_send(self): self.ensure_one() return self.backend_id.exchange_send(self) diff --git a/edi_oca/readme/CONTRIBUTORS.rst b/edi_oca/readme/CONTRIBUTORS.rst index 4945a3dc401..033aac71242 100644 --- a/edi_oca/readme/CONTRIBUTORS.rst +++ b/edi_oca/readme/CONTRIBUTORS.rst @@ -1,2 +1,3 @@ * Simone Orsi * Enric Tobella +* Thien Vo diff --git a/edi_oca/security/ir_model_access.xml b/edi_oca/security/ir_model_access.xml index 83970fb3c5a..5ffa4e7a539 100644 --- a/edi_oca/security/ir_model_access.xml +++ b/edi_oca/security/ir_model_access.xml @@ -113,4 +113,22 @@ [(1, '=', 1)] + + access_edi_configuration manager + + + + + + + + + access_edi_configuration user + + + + + + + diff --git a/edi_oca/static/description/index.html b/edi_oca/static/description/index.html index f263f4a038f..f692c9664e5 100644 --- a/edi_oca/static/description/index.html +++ b/edi_oca/static/description/index.html @@ -8,11 +8,10 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. -Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +274,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: gray; } /* line numbers */ +pre.code .ln { color: grey; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +300,7 @@ span.pre { white-space: pre } -span.problematic, pre.problematic { +span.problematic { color: red } span.section-subtitle { @@ -516,14 +515,13 @@

Contributors

Maintainers

This module is maintained by the OCA.

- -Odoo Community Association - +Odoo Community Association

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

diff --git a/edi_oca/tests/__init__.py b/edi_oca/tests/__init__.py index 30692700a2a..1373da895b3 100644 --- a/edi_oca/tests/__init__.py +++ b/edi_oca/tests/__init__.py @@ -13,3 +13,4 @@ from . import test_security from . import test_quick_exec from . import test_exchange_type_deprecated_fields +from . import test_edi_configuration diff --git a/edi_oca/tests/fake_components.py b/edi_oca/tests/fake_components.py index f764c5e7567..1602547f529 100644 --- a/edi_oca/tests/fake_components.py +++ b/edi_oca/tests/fake_components.py @@ -134,3 +134,29 @@ class FakeInputValidate(FakeComponentMixin): def validate(self, value=None): self._fake_it() return + + +class FakeConfigurationListener(FakeComponentMixin): + _name = "fake.configuration.listener" + _inherit = "base.event.listener" + _apply_on = ["edi.exchange.consumer.test"] + + def on_record_write_configuration(self, record, fields=None, **kwargs): + trigger = "on_record_write" + if kwargs.get("vals", False): + for rec in record: + confs = record.edi_config_ids.edi_get_conf(trigger) + for conf in confs: + conf.edi_exec_snippet_do(rec, **kwargs) + return True + + def on_record_create_configuration(self, record, fields=None, **kwargs): + trigger = "on_record_create" + val_list = kwargs.get("vals", False) + if val_list: + for rec, vals in zip(record, val_list): + kwargs["vals"] = {rec.id: vals} + confs = rec.edi_config_ids.edi_get_conf(trigger) + for conf in confs: + conf.edi_exec_snippet_do(rec, **kwargs) + return True diff --git a/edi_oca/tests/fake_models.py b/edi_oca/tests/fake_models.py index 44bd3b73fe8..cb878903678 100644 --- a/edi_oca/tests/fake_models.py +++ b/edi_oca/tests/fake_models.py @@ -2,7 +2,7 @@ # @author: Enric Tobella # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from odoo import fields, models +from odoo import api, fields, models class EdiExchangeConsumerTest(models.Model): @@ -11,6 +11,71 @@ class EdiExchangeConsumerTest(models.Model): _description = "Model used only for test" name = fields.Char() + edi_config_ids = fields.Many2many( + string="EDI Purchase Config Ids", + comodel_name="edi.configuration", + relation="test_edi_configuration_rel", + column1="record_id", + column2="conf_id", + domain="[('model_name', '=', 'edi.exchange.consumer.test')]", + ) def _get_edi_exchange_record_name(self, exchange_record): return self.id + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + operation = "create" + + new_records = self.browse() + new_vals_list = [] + + for rec, vals in zip(records, vals_list): + if not rec._edi_configuration_skip(operation): + new_records |= rec + new_vals_list.append(vals) + + if new_records: + self._event("on_record_create_configuration").notify( + new_records, + operation=operation, + vals=new_vals_list, + ) + return records + + def write(self, vals): + operation = "write" + new_records = self.browse() + + for rec in self: + if not rec._edi_configuration_skip(operation): + new_records |= rec + + old_vals = {} + for record in new_records: + old_vals[record.id] = {field: record[field] for field in vals.keys()} + + res = super().write(vals) + + new_values = {} + for record in new_records: + new_values[record.id] = {field: record[field] for field in vals.keys()} + + if new_values: + self._event("on_record_write_configuration").notify( + new_records, + operation=operation, + old_vals=old_vals, + vals=new_values, + ) + return res + + def _edi_configuration_skip(self, operation): + skip_reason = None + if self.env.context.get("edi_skip_configuration"): + skip_reason = "edi_skip_configuration ctx key found" + # TODO: Add more skip cases + if skip_reason: + return True + return False diff --git a/edi_oca/tests/test_edi_configuration.py b/edi_oca/tests/test_edi_configuration.py new file mode 100644 index 00000000000..6e5fa0c1839 --- /dev/null +++ b/edi_oca/tests/test_edi_configuration.py @@ -0,0 +1,163 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import os +import unittest + +from odoo_test_helper import FakeModelLoader + +from .common import EDIBackendCommonComponentRegistryTestCase +from .fake_components import ( + FakeConfigurationListener, + FakeOutputChecker, + FakeOutputGenerator, + FakeOutputSender, +) + + +# This clashes w/ some setup (eg: run tests w/ pytest when edi_storage is installed) +# If you still want to run `edi` tests w/ pytest when this happens, set this env var. +@unittest.skipIf(os.getenv("SKIP_EDI_CONSUMER_CASE"), "Consumer test case disabled.") +class TestEDIConfigurations(EDIBackendCommonComponentRegistryTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._build_components( + cls, + FakeOutputGenerator, + FakeOutputSender, + FakeOutputChecker, + FakeConfigurationListener, + ) + vals = { + "model": cls.partner._name, + "res_id": cls.partner.id, + } + cls.record = cls.backend.create_record("test_csv_output", vals) + + def setUp(self): + super().setUp() + FakeOutputGenerator.reset_faked() + FakeOutputSender.reset_faked() + FakeOutputChecker.reset_faked() + self.consumer_record = self.env["edi.exchange.consumer.test"].create( + { + "name": "Test Consumer", + "edi_config_ids": [ + (4, self.create_config.id), + (4, self.write_config.id), + ], + } + ) + + @classmethod + def _setup_records(cls): + super()._setup_records() + # Load fake models ->/ + cls.loader = FakeModelLoader(cls.env, cls.__module__) + cls.loader.backup_registry() + from .fake_models import EdiExchangeConsumerTest + + cls.loader.update_registry((EdiExchangeConsumerTest,)) + cls.exchange_type_out.exchange_filename_pattern = "{record.id}" + cls.edi_configuration = cls.env["edi.configuration"] + cls.create_config = cls.edi_configuration.create( + { + "name": "Create Config", + "active": True, + "code": "create_config", + "backend_id": cls.backend.id, + "type_id": cls.exchange_type_out.id, + "trigger": "on_record_create", + "model": cls.env["ir.model"]._get_id("edi.exchange.consumer.test"), + "snippet_do": "record._edi_send_via_edi(conf.type_id)", + } + ) + cls.write_config = cls.edi_configuration.create( + { + "name": "Write Config 1", + "active": True, + "code": "write_config", + "backend_id": cls.backend.id, + "type_id": cls.exchange_type_out.id, + "trigger": "on_record_write", + "model": cls.env["ir.model"]._get_id("edi.exchange.consumer.test"), + "snippet_do": "record._edi_send_via_edi(conf.type_id)", + } + ) + + @classmethod + def tearDownClass(cls): + cls.loader.restore_registry() + super().tearDownClass() + + def test_edi_send_via_edi_config(self): + # Check configuration on create + self.consumer_record.refresh() + exchange_record = self.consumer_record.exchange_record_ids + self.assertEqual(len(exchange_record), 1) + self.assertEqual(exchange_record.type_id, self.exchange_type_out) + self.assertEqual(exchange_record.edi_exchange_state, "output_sent") + # Write the existed consumer record + self.consumer_record.name = "Fixed Consumer" + # check Configuration on write + self.consumer_record.refresh() + exchange_record = self.consumer_record.exchange_record_ids - exchange_record + self.assertEqual(len(exchange_record), 1) + self.assertEqual(exchange_record.type_id, self.exchange_type_out) + self.assertEqual(exchange_record.edi_exchange_state, "output_sent") + + def test_edi_code_snippet(self): + expected_value = { + "todo": True, + "snippet_do_vars": { + "a": 1, + "b": 2, + }, + "event_only": True, + "tracked_fields": ["state"], + "edi_action": "new_action", + } + # Simulate the snippet_before_do + self.write_config.snippet_before_do = "result = " + str(expected_value) + # Execute with the raw data + vals = self.write_config.edi_exec_snippet_before_do( + self.consumer_record, + tracked_fields=[], + edi_action="generate", + ) + # Check the new vals after execution + self.assertEqual(vals, expected_value) + + # Check the snippet_do + expected_value = { + "change_state": True, + "snippet_do_vars": { + "a": 1, + "b": 2, + }, + "record": self.consumer_record, + "tracked_fields": ["state"], + } + snippet_do = """\n +old_state = old_value.get("state", False)\n +new_state = vals.get("state", False)\n +result = {\n + "change_state": True if old_state and new_state and old_state != new_state else False,\n + "snippet_do_vars": snippet_do_vars,\n + "record": record,\n + "tracked_fields": tracked_fields,\n +} + """ + self.write_config.snippet_do = snippet_do + # Execute with the raw data + record_id = self.consumer_record.id + vals = self.write_config.edi_exec_snippet_do( + self.consumer_record, + tracked_fields=[], + edi_action="generate", + old_vals={record_id: dict(state="draft")}, + vals={record_id: dict(state="confirmed")}, + ) + # Check the new vals after execution + self.assertEqual(vals, expected_value) diff --git a/edi_oca/views/edi_configuration_views.xml b/edi_oca/views/edi_configuration_views.xml new file mode 100644 index 00000000000..4ec399add75 --- /dev/null +++ b/edi_oca/views/edi_configuration_views.xml @@ -0,0 +1,107 @@ + + + + edi.configuration.view.search + edi.configuration + + + + + + + + + + + + + + edi.configuration + + + + + + + + + + + + + + + edi.configuration + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + EDI Configuration + ir.actions.act_window + edi.configuration + tree,form + [] + + + + + form + + + + + + tree + + +
diff --git a/edi_oca/views/menuitems.xml b/edi_oca/views/menuitems.xml index 3842809d315..9a2b1fd8f19 100644 --- a/edi_oca/views/menuitems.xml +++ b/edi_oca/views/menuitems.xml @@ -82,4 +82,11 @@ sequence="40" action="act_open_edi_exchange_type_rule_view" /> + diff --git a/edi_purchase_oca/data/edi_configuration.xml b/edi_purchase_oca/data/edi_configuration.xml new file mode 100644 index 00000000000..68cb404eab6 --- /dev/null +++ b/edi_purchase_oca/data/edi_configuration.xml @@ -0,0 +1,23 @@ + + + + Purchase Confirmation Config + on_button_confirm_purchase_order + on_button_confirm_purchase_order + + + result={ + "snippet_var_do": {} + } + + + + + Send EDI Quoctation Config + send_via_email_rfq + send_via_email_rfq + record._edi_send_via_email(ir_action=record.action_rfq_send()) + + diff --git a/edi_purchase_oca/models/res_partner.py b/edi_purchase_oca/models/res_partner.py new file mode 100644 index 00000000000..1fdeaf3861b --- /dev/null +++ b/edi_purchase_oca/models/res_partner.py @@ -0,0 +1,19 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + edi_purchase_conf_ids = fields.Many2many( + string="EDI Purchase Config Ids", + comodel_name="edi.configuration", + relation="res_partner_edi_configuration_rel", + column1="partner_id", + column2="conf_id", + # TODO: Domain for Purchase model + domain="[('model_name', '=', 'purchase.order')]" + ) diff --git a/edi_purchase_oca/static/description/index.html b/edi_purchase_oca/static/description/index.html index 844cb403382..c7cf8ca0ff3 100644 --- a/edi_purchase_oca/static/description/index.html +++ b/edi_purchase_oca/static/description/index.html @@ -1,4 +1,3 @@ - diff --git a/edi_purchase_oca/views/res_partner_view.xml b/edi_purchase_oca/views/res_partner_view.xml new file mode 100644 index 00000000000..d4d4f53e6c6 --- /dev/null +++ b/edi_purchase_oca/views/res_partner_view.xml @@ -0,0 +1,16 @@ + + + + res.partner.form.inherit.sales_purchases + res.partner + + + + + + + + + +