From 69d4d04abb88294e23cac30e2fb050e8eb1d8d6f Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 19 Aug 2022 15:22:18 +0200 Subject: [PATCH 01/70] edi: fix consumer mixin test --- edi_oca/tests/test_consumer_mixin.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/edi_oca/tests/test_consumer_mixin.py b/edi_oca/tests/test_consumer_mixin.py index 50c433210e..708c8e085f 100644 --- a/edi_oca/tests/test_consumer_mixin.py +++ b/edi_oca/tests/test_consumer_mixin.py @@ -71,20 +71,24 @@ def tearDownClass(cls): super().tearDownClass() def test_mixin(self): - self.assertEqual(0, self.consumer_record.exchange_record_count) + self.assertEqual(self.consumer_record.exchange_record_count, 0) vals = { "model": self.consumer_record._name, "res_id": self.consumer_record.id, } exchange_record = self.backend.create_record("test_csv_output", vals) self.consumer_record.refresh() - self.assertEqual(1, self.consumer_record.exchange_record_count) + self.assertEqual(self.consumer_record.exchange_record_count, 1) action = self.consumer_record.action_view_edi_records() self.consumer_record.refresh() self.assertEqual( exchange_record, self.env["edi.exchange.record"].search(action["domain"]) ) - self.consumer_record._has_exchange_record(exchange_record.type_id, self.backend) + self.assertTrue( + self.consumer_record._has_exchange_record( + exchange_record.type_id, self.backend + ) + ) def test_expected_configuration(self): # no btn enabled From b6507c3ae341a5f07e4c30cc4805d90761f765ed Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 19 Aug 2022 15:23:27 +0200 Subject: [PATCH 02/70] edi: improve consumer mixin w/ origin You can now track the original exchange record that originated the document in the 1st place. --- edi_oca/models/edi_exchange_consumer_mixin.py | 13 +++++++++++++ edi_oca/tests/test_consumer_mixin.py | 12 ++++++++++++ 2 files changed, 25 insertions(+) diff --git a/edi_oca/models/edi_exchange_consumer_mixin.py b/edi_oca/models/edi_exchange_consumer_mixin.py index b9d724e684..e54e46e302 100644 --- a/edi_oca/models/edi_exchange_consumer_mixin.py +++ b/edi_oca/models/edi_exchange_consumer_mixin.py @@ -19,6 +19,12 @@ class EDIExchangeConsumerMixin(models.AbstractModel): _name = "edi.exchange.consumer.mixin" _description = "Abstract record where exchange records can be assigned" + origin_exchange_record_id = fields.Many2one( + string="EDI origin record", + comodel_name="edi.exchange.record", + ondelete="set null", + help="EDI record that originated this document.", + ) exchange_record_ids = fields.One2many( "edi.exchange.record", inverse_name="res_id", @@ -232,3 +238,10 @@ def get_edi_access(self, doc_ids, operation, model_name=False): else: check_operation = operation return check_operation + + def _edi_set_origin(self, exc_record): + self.sudo().update({"origin_exchange_record_id": exc_record.id}) + + def _edi_get_origin(self): + self.ensure_one() + return self.origin_exchange_record_id diff --git a/edi_oca/tests/test_consumer_mixin.py b/edi_oca/tests/test_consumer_mixin.py index 708c8e085f..8008e85a8c 100644 --- a/edi_oca/tests/test_consumer_mixin.py +++ b/edi_oca/tests/test_consumer_mixin.py @@ -90,6 +90,18 @@ def test_mixin(self): ) ) + def test_origin(self): + vals = { + "model": self.consumer_record._name, + "res_id": self.consumer_record.id, + } + exchange_record = self.backend.create_record("test_csv_output", vals) + self.consumer_record._edi_set_origin(exchange_record) + self.assertEqual( + self.consumer_record.origin_exchange_record_id, exchange_record + ) + self.assertEqual(self.consumer_record._edi_get_origin(), exchange_record) + def test_expected_configuration(self): # no btn enabled self.assertFalse(self.consumer_record.edi_has_form_config) From 3298408d1b849624d30ccf18d947ba815f611df0 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 19 Aug 2022 15:24:38 +0200 Subject: [PATCH 03/70] edi: add test for create child/ack --- edi_oca/tests/test_record.py | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/edi_oca/tests/test_record.py b/edi_oca/tests/test_record.py index 14fc301303..186eb66f77 100644 --- a/edi_oca/tests/test_record.py +++ b/edi_oca/tests/test_record.py @@ -1,4 +1,5 @@ # Copyright 2020 ACSONE +# Copyright 2022 Camptocamp SA # @author: Simone Orsi # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). @@ -130,3 +131,55 @@ def test_with_delay_override(self): self.assertTrue(isinstance(delayed, DelayableRecordset)) self.assertEqual(delayed.recordset, record) self.assertEqual(delayed.delayable.channel, "root.parent_test_chan.test_chan") + + def test_create_child(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + } + record0 = self.backend.create_record("test_csv_output", vals) + record1 = record0.exchange_create_child_record() + record2 = record0.exchange_create_child_record() + record3 = record2.exchange_create_child_record(model="sale.order", res_id=1) + record0.invalidate_cache() + record2.invalidate_cache() + self.assertIn(record1, record0.related_exchange_ids) + self.assertIn(record2, record0.related_exchange_ids) + self.assertIn(record3, record2.related_exchange_ids) + self.assertRecordValues( + record1 + record2 + record3, + [ + { + "parent_id": record0.id, + "model": "res.partner", + "res_id": self.partner.id, + }, + { + "parent_id": record0.id, + "model": "res.partner", + "res_id": self.partner.id, + }, + {"parent_id": record2.id, "model": "sale.order", "res_id": 1}, + ], + ) + + def test_create_ack(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + } + record0 = self.backend.create_record("test_csv_output", vals) + ack = record0.exchange_create_ack_record() + record0.invalidate_cache() + self.assertIn(ack, record0.related_exchange_ids) + self.assertRecordValues( + ack, + [ + { + "parent_id": record0.id, + "model": "res.partner", + "res_id": self.partner.id, + "type_id": self.exchange_type_out_ack.id, + }, + ], + ) From 00f45ad55388152cc3fc10ae96c7ce42564740eb Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 19 Aug 2022 15:43:36 +0200 Subject: [PATCH 04/70] edi: add 'ack for' on type Ease finding which type the current one is an ack for. --- edi_oca/models/edi_exchange_record.py | 2 +- edi_oca/models/edi_exchange_type.py | 13 ++++++++++++- edi_oca/tests/test_exchange_type.py | 14 ++++++++++++++ edi_oca/views/edi_exchange_type_views.xml | 3 +++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index b2aa6f117f..e9c123e7c9 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -89,7 +89,7 @@ class EDIExchangeRecord(models.Model): ack_exchange_id = fields.Many2one( string="ACK exchange", comodel_name="edi.exchange.record", - help="ACK for this exchange", + help="ACK generated for current exchange.", compute="_compute_ack_exchange_id", store=True, ) diff --git a/edi_oca/models/edi_exchange_type.py b/edi_oca/models/edi_exchange_type.py index 172b143840..76bee13607 100644 --- a/edi_oca/models/edi_exchange_type.py +++ b/edi_oca/models/edi_exchange_type.py @@ -8,7 +8,7 @@ from pytz import timezone, utc from odoo import _, api, exceptions, fields, models -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT, groupby from odoo.addons.base_sparse_field.models.fields import Serialized from odoo.addons.http_routing.models.ir_http import slugify @@ -63,6 +63,11 @@ class EDIExchangeType(models.Model): help="Identify the type of the ack. " "If this field is valued it means an hack is expected.", ) + ack_for_type_ids = fields.Many2many( + string="Ack for exchange type", + comodel_name="edi.exchange.type", + compute="_compute_ack_for_type_ids", + ) advanced_settings_edit = fields.Text( string="Advanced YAML settings", help=""" @@ -140,6 +145,12 @@ def _load_advanced_settings(self): # This would help documenting core and custom keys. return yaml.safe_load(self.advanced_settings_edit or "") or {} + def _compute_ack_for_type_ids(self): + ack_for = self.search([("ack_type_id", "in", self.ids)]) + by_type_id = dict(groupby(ack_for, lambda x: x.ack_type_id.id)) + for rec in self: + rec.ack_for_type_ids = [x.id for x in by_type_id.get(rec.id, [])] + def get_settings(self): return self.advanced_settings diff --git a/edi_oca/tests/test_exchange_type.py b/edi_oca/tests/test_exchange_type.py index a44e9dd560..c0c0df73f0 100644 --- a/edi_oca/tests/test_exchange_type.py +++ b/edi_oca/tests/test_exchange_type.py @@ -9,6 +9,20 @@ class EDIExchangeTypeTestCase(EDIBackendCommonTestCase): + def test_ack_for(self): + self.assertEqual(self.exchange_type_out.ack_type_id, self.exchange_type_out_ack) + new_type = self.exchange_type_out.copy({"code": "just_a_test"}) + self.assertEqual(new_type.ack_type_id, self.exchange_type_out_ack) + self.exchange_type_out_ack.refresh() + self.assertIn( + self.exchange_type_out.id, + self.exchange_type_out_ack.ack_for_type_ids.ids, + ) + self.assertIn( + new_type.id, + self.exchange_type_out_ack.ack_for_type_ids.ids, + ) + def test_advanced_settings(self): settings = """ components: diff --git a/edi_oca/views/edi_exchange_type_views.xml b/edi_oca/views/edi_exchange_type_views.xml index adb28cdb96..2e4596cf14 100644 --- a/edi_oca/views/edi_exchange_type_views.xml +++ b/edi_oca/views/edi_exchange_type_views.xml @@ -8,6 +8,8 @@ + + @@ -34,6 +36,7 @@ + From 65df16221d6523099b7714ae2e0e3877eca1cb64 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 19 Aug 2022 15:45:15 +0200 Subject: [PATCH 05/70] edi: improve exc type search --- edi_oca/views/edi_exchange_type_views.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/edi_oca/views/edi_exchange_type_views.xml b/edi_oca/views/edi_exchange_type_views.xml index 2e4596cf14..30066f3a99 100644 --- a/edi_oca/views/edi_exchange_type_views.xml +++ b/edi_oca/views/edi_exchange_type_views.xml @@ -81,6 +81,17 @@ + + + Date: Mon, 22 Aug 2022 15:49:23 +0200 Subject: [PATCH 06/70] edi: fix ack record compute You can produce or receive more than one ack for a given exchange. If that's the case, make sure only the newest is considered. --- edi_oca/models/edi_exchange_record.py | 6 ++++-- edi_oca/tests/test_record.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index e9c123e7c9..79d10e67b9 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -148,8 +148,10 @@ def _compute_ack_exchange_id(self): def _get_ack_record(self): if not self.type_id.ack_type_id: return None - return self.related_exchange_ids.filtered( - lambda x: x.type_id == self.type_id.ack_type_id + return fields.first( + self.related_exchange_ids.filtered( + lambda x: x.type_id == self.type_id.ack_type_id + ).sorted("id", reverse=True) ) def _compute_ack_expected(self): diff --git a/edi_oca/tests/test_record.py b/edi_oca/tests/test_record.py index 186eb66f77..7b7dd38ed7 100644 --- a/edi_oca/tests/test_record.py +++ b/edi_oca/tests/test_record.py @@ -183,3 +183,5 @@ def test_create_ack(self): }, ], ) + ack2 = record0.exchange_create_ack_record() + self.assertEqual(record0.ack_exchange_id, ack2) From b691e9c4d7073214b9786c9f507ed1006d542878 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 22 Aug 2022 15:50:06 +0200 Subject: [PATCH 07/70] edi: fix exchange ordering In case exchange_on is the same, use ID to get the latest on top. --- edi_oca/models/edi_exchange_record.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index 79d10e67b9..33f00c6dd6 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -17,7 +17,7 @@ class EDIExchangeRecord(models.Model): _name = "edi.exchange.record" _inherit = "mail.thread" _description = "EDI exchange Record" - _order = "exchanged_on desc" + _order = "exchanged_on desc, id desc" _rec_name = "identifier" identifier = fields.Char(required=True, index=True, readonly=True) From 7be31c4c78f111d8d778c6d8b35f2836686face6 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 25 Aug 2022 17:29:00 +0200 Subject: [PATCH 08/70] edi: fix record and type copy on fields --- edi_oca/models/edi_exchange_record.py | 10 ++++++---- edi_oca/models/edi_exchange_type.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index 33f00c6dd6..2222777aee 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -20,8 +20,8 @@ class EDIExchangeRecord(models.Model): _order = "exchanged_on desc, id desc" _rec_name = "identifier" - identifier = fields.Char(required=True, index=True, readonly=True) - external_identifier = fields.Char(index=True, readonly=True) + identifier = fields.Char(required=True, index=True, readonly=True, copy=False) + external_identifier = fields.Char(index=True, readonly=True, copy=False) type_id = fields.Many2one( string="Exchange type", comodel_name="edi.exchange.type", @@ -38,9 +38,10 @@ class EDIExchangeRecord(models.Model): required=False, readonly=True, model_field="model", + copy=False, ) related_name = fields.Char(compute="_compute_related_name", compute_sudo=True) - exchange_file = fields.Binary(attachment=True) + exchange_file = fields.Binary(attachment=True, copy=False) exchange_filename = fields.Char( compute="_compute_exchange_filename", readonly=False, store=True ) @@ -53,6 +54,7 @@ class EDIExchangeRecord(models.Model): edi_exchange_state = fields.Selection( string="Exchange state", readonly=True, + copy=False, default="new", selection=[ # Common states @@ -72,7 +74,7 @@ class EDIExchangeRecord(models.Model): ("input_processed_error", "Error on process"), ], ) - exchange_error = fields.Text(readonly=True) + exchange_error = fields.Text(string="Exchange error", readonly=True, copy=False) # Relations w/ other records parent_id = fields.Many2one( comodel_name="edi.exchange.record", diff --git a/edi_oca/models/edi_exchange_type.py b/edi_oca/models/edi_exchange_type.py index 76bee13607..d535170dc2 100644 --- a/edi_oca/models/edi_exchange_type.py +++ b/edi_oca/models/edi_exchange_type.py @@ -45,7 +45,7 @@ class EDIExchangeType(models.Model): comodel_name="queue.job.channel", ) name = fields.Char(required=True) - code = fields.Char(required=True) + code = fields.Char(required=True, copy=False) direction = fields.Selection( selection=[("input", "Input"), ("output", "Output")], required=True ) From d3f229f7138a67187e83222165add9f56e2b4b99 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Sat, 27 Aug 2022 14:44:53 +0200 Subject: [PATCH 09/70] edi: fix error msg typo --- edi_oca/models/edi_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py index 8a9531e1f9..31810d1581 100644 --- a/edi_oca/models/edi_backend.py +++ b/edi_oca/models/edi_backend.py @@ -252,7 +252,7 @@ def _check_exchange_generate(self, exchange_record, force=False): ) if exchange_record.exchange_file: raise exceptions.UserError( - _("Exchabge record ID=%d already has a file to process!") + _("Exchange record ID=%d already has a file to process!") % exchange_record.id ) From b4041e166732d0dbe21da88d033b4db93dc1e8dc Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Sat, 27 Aug 2022 14:45:26 +0200 Subject: [PATCH 10/70] edi: add exc.type.set_settings method --- edi_oca/models/edi_exchange_type.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/edi_oca/models/edi_exchange_type.py b/edi_oca/models/edi_exchange_type.py index d535170dc2..1facb1f53e 100644 --- a/edi_oca/models/edi_exchange_type.py +++ b/edi_oca/models/edi_exchange_type.py @@ -154,6 +154,9 @@ def _compute_ack_for_type_ids(self): def get_settings(self): return self.advanced_settings + def set_settings(self, val): + self.advanced_settings_edit = val + @api.constrains("backend_id", "backend_type_id") def _check_backend(self): for rec in self: From 6ad8b26a1653023a76ec7cd1808c0f422c585bb6 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Sat, 27 Aug 2022 14:46:19 +0200 Subject: [PATCH 11/70] edi: improve chatter msg w/ type detail --- edi_oca/templates/exchange_chatter_msg.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/edi_oca/templates/exchange_chatter_msg.xml b/edi_oca/templates/exchange_chatter_msg.xml index 909abd7961..d76a94078d 100644 --- a/edi_oca/templates/exchange_chatter_msg.xml +++ b/edi_oca/templates/exchange_chatter_msg.xml @@ -17,6 +17,18 @@
+ + + Type: + + + + +
State: From d5dea74652826bdca400f90b40d2cf5e135c7a37 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 25 Aug 2022 17:35:08 +0200 Subject: [PATCH 12/70] edi_exchange_template: improve time utils on render ctx --- .../models/edi_exchange_template_mixin.py | 41 +++++++++++++++++-- .../models/edi_exchange_template_output.py | 3 +- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/edi_exchange_template_oca/models/edi_exchange_template_mixin.py b/edi_exchange_template_oca/models/edi_exchange_template_mixin.py index 8a8e1cc5a3..f3a61e51f5 100644 --- a/edi_exchange_template_oca/models/edi_exchange_template_mixin.py +++ b/edi_exchange_template_oca/models/edi_exchange_template_mixin.py @@ -13,6 +13,19 @@ _logger = logging.getLogger(__name__) +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 EDIExchangeTemplateMixin(models.AbstractModel): """Define a common ground for EDI exchange templates.""" @@ -77,22 +90,42 @@ def _utc_now(): @staticmethod def _date_to_string(dt, utc=True): + if not dt: + return "" if utc: - dt = dt.astimezone(pytz.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 _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, + } + ctx.update(self._time_utils()) + return ctx + + def _time_utils(self): return { "datetime": safe_eval.datetime, "dateutil": safe_eval.dateutil, "time": safe_eval.time, - "uid": self.env.uid, - "user": self.env.user, - "DotDict": DotDict, + "utc_now": self._utc_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 "", } def _evaluate_code_snippet(self, **render_values): diff --git a/edi_exchange_template_oca/models/edi_exchange_template_output.py b/edi_exchange_template_oca/models/edi_exchange_template_output.py index d33a9601ab..23723f7b15 100644 --- a/edi_exchange_template_oca/models/edi_exchange_template_output.py +++ b/edi_exchange_template_oca/models/edi_exchange_template_output.py @@ -94,12 +94,11 @@ def _get_render_values(self, exchange_record, **kw): "record": exchange_record.record, "backend": exchange_record.backend_id, "template": self, - "utc_now": self._utc_now, - "date_to_string": self._date_to_string, "render_edi_template": self._render_template, "get_info_provider": self._get_info_provider, "info": {}, } + values.update(self._time_utils()) values.update(self._evaluate_code_snippet(**values)) values.update(kw) return values From 009e74bfd9d4df634da1b296c9bbbf6248216d11 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Sun, 28 Aug 2022 12:23:49 +0200 Subject: [PATCH 13/70] edi: fix _cron_check_output_exchange_sync Skip 'output_sent' records by default. Reason: most of the times when a message is sent out you don't care about its state anymore. Any subsequent update from outside should come with a new input record instead. Yet, if for any reason this feature is required you can still enable it by passing 'skip_sent=False'. --- edi_oca/models/edi_backend.py | 18 ++++++++++++------ edi_oca/tests/test_edi_backend_cron.py | 2 +- edi_storage_oca/models/edi_backend.py | 6 ++++++ .../tests/test_edi_backend_storage.py | 3 +++ 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py index 31810d1581..22a412dc12 100644 --- a/edi_oca/models/edi_backend.py +++ b/edi_oca/models/edi_backend.py @@ -346,13 +346,14 @@ def _cron_check_output_exchange_sync(self, **kw): backend._check_output_exchange_sync(**kw) # TODO: consider splitting cron in 2 (1 for receiving, 1 for processing) - def _check_output_exchange_sync(self, skip_send=False): + def _check_output_exchange_sync(self, skip_send=False, skip_sent=True): """Lookup for pending output records and take care of them. First work on records that need output generation. Then work on records waiting for a state update. :param skip_send: only generate missing output. + :param skip_sent: ignore records that were already sent. """ # Generate output files new_records = self.exchange_record_model.search( @@ -368,7 +369,7 @@ def _check_output_exchange_sync(self, skip_send=False): if skip_send: return pending_records = self.exchange_record_model.search( - self._output_pending_records_domain() + self._output_pending_records_domain(skip_sent=skip_sent) ) _logger.info( "EDI Exchange output sync: found %d pending records to process.", @@ -393,10 +394,15 @@ def _output_new_records_domain(self): ("exchange_file", "=", False), ] - def _output_pending_records_domain(self): - """Domain for output records needing to be sent or have errors or - ack to handle.""" - states = ("output_pending", "output_sent", "output_sent_and_error") + def _output_pending_records_domain(self, skip_sent=True): + """Domain for pending output records. + + Records might be waiting to be sent or have errors or have ack to handle.""" + states = ("output_pending", "output_sent_and_error") + if not skip_sent: + # If you want to update sent records + # you'll have to provide a `check` component. + states += ("output_sent",) return [ ("type_id.direction", "=", "output"), ("backend_id", "=", self.id), diff --git a/edi_oca/tests/test_edi_backend_cron.py b/edi_oca/tests/test_edi_backend_cron.py index ebba9c867f..b1c4e1011f 100644 --- a/edi_oca/tests/test_edi_backend_cron.py +++ b/edi_oca/tests/test_edi_backend_cron.py @@ -87,7 +87,7 @@ def test_exchange_generate_output_ready_auto_send(self): self.record1.edi_exchange_state = "output_sent" self.backend.with_context( fake_update_values={"edi_exchange_state": "output_sent_and_processed"} - )._cron_check_output_exchange_sync() + )._cron_check_output_exchange_sync(skip_sent=False) for rec in self.records - self.record1: self.assertEqual(rec.edi_exchange_state, "new") self.assertEqual(self.record1.edi_exchange_state, "output_sent_and_processed") diff --git a/edi_storage_oca/models/edi_backend.py b/edi_storage_oca/models/edi_backend.py index 3669eabf92..e396ae69df 100644 --- a/edi_storage_oca/models/edi_backend.py +++ b/edi_storage_oca/models/edi_backend.py @@ -167,3 +167,9 @@ def _storage_get_input_filenames(self, exchange_type): def _storage_new_exchange_record_vals(self, file_name): return {"exchange_filename": file_name, "edi_exchange_state": "input_pending"} + + def _check_output_exchange_sync(self, skip_send=False, skip_sent=True): + # Do not skip sent records when dealing w/ storage related exchanges, + # because we want to update the file state + # depending on where they are in the external folder. + return super()._check_output_exchange_sync(skip_sent=not self.storage_id) diff --git a/edi_storage_oca/tests/test_edi_backend_storage.py b/edi_storage_oca/tests/test_edi_backend_storage.py index 0d4793d3f9..fb37ae7115 100644 --- a/edi_storage_oca/tests/test_edi_backend_storage.py +++ b/edi_storage_oca/tests/test_edi_backend_storage.py @@ -148,6 +148,8 @@ def test_cron_full_flow(self): "model": partner2._name, "res_id": partner2.id, "exchange_filename": "rec2.csv", + "exchange_file": rec1.exchange_file, + "edi_exchange_state": rec1.edi_exchange_state, } ) rec3 = self.record.copy( @@ -156,6 +158,7 @@ def test_cron_full_flow(self): "res_id": partner3.id, "exchange_filename": "rec3.csv", "edi_exchange_state": "output_sent_and_error", + "exchange_file": rec1.exchange_file, } ) mocked_paths = { From d772bc511985037bf3ccf3035f3c441667da4b9b Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 5 Sep 2022 14:07:42 +0200 Subject: [PATCH 14/70] edi: fix backward compat usage of _has_exchange_record_domain In this commit https://github.com/OCA/edi/commit/0a9b6d5ef18f9d6c7d493ebefee8e579c697a165 I unified the way these kind of methods were used. Was breaking older implementations where the method was called directly. --- edi_oca/models/edi_exchange_consumer_mixin.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/edi_oca/models/edi_exchange_consumer_mixin.py b/edi_oca/models/edi_exchange_consumer_mixin.py index e54e46e302..08413a5503 100644 --- a/edi_oca/models/edi_exchange_consumer_mixin.py +++ b/edi_oca/models/edi_exchange_consumer_mixin.py @@ -178,11 +178,15 @@ def _has_exchange_record(self, exchange_type, backend=False, extra_domain=False) def _has_exchange_record_domain( self, exchange_type, backend=False, extra_domain=False ): + if isinstance(exchange_type, str): + # Backward compat: allow passing the code when this method is called directly + type_leaf = [("type_id.code", "=", exchange_type)] + else: + type_leaf = [("type_id", "=", exchange_type.id)] domain = [ ("model", "=", self._name), ("res_id", "=", self.id), - ("type_id", "=", exchange_type.id), - ] + ] + type_leaf if backend is None: backend = exchange_type.backend_id if backend: From f948c6afb2d7bc123b1cdcfcde5591e8a54613ef Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 2 Sep 2022 14:45:08 +0200 Subject: [PATCH 15/70] edi: allow search consumers by exc type --- edi_oca/models/edi_exchange_consumer_mixin.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/edi_oca/models/edi_exchange_consumer_mixin.py b/edi_oca/models/edi_exchange_consumer_mixin.py index 08413a5503..e2476b1ab2 100644 --- a/edi_oca/models/edi_exchange_consumer_mixin.py +++ b/edi_oca/models/edi_exchange_consumer_mixin.py @@ -25,6 +25,14 @@ class EDIExchangeConsumerMixin(models.AbstractModel): ondelete="set null", help="EDI record that originated this document.", ) + origin_exchange_type_id = fields.Many2one( + string="EDI origin exchange type", + comodel_name="edi.exchange.type", + ondelete="set null", + related="origin_exchange_record_id.type_id", + # Store it to ease searching by type + store=True, + ) exchange_record_ids = fields.One2many( "edi.exchange.record", inverse_name="res_id", From 5e532c656c5dfc4deb4d748a61bfb902b6961099 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 2 Sep 2022 14:47:50 +0200 Subject: [PATCH 16/70] edi: fix action_view_edi_records As we are reusing the existing window action we must remove default filters otherwise some related exchange records will be hidden by default. --- edi_oca/models/edi_exchange_consumer_mixin.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/edi_oca/models/edi_exchange_consumer_mixin.py b/edi_oca/models/edi_exchange_consumer_mixin.py index e2476b1ab2..7f59b61806 100644 --- a/edi_oca/models/edi_exchange_consumer_mixin.py +++ b/edi_oca/models/edi_exchange_consumer_mixin.py @@ -221,6 +221,15 @@ def action_view_edi_records(self): xmlid = "edi_oca.act_open_edi_exchange_record_view" action = self.env["ir.actions.act_window"]._for_xml_id(xmlid) action["domain"] = [("model", "=", self._name), ("res_id", "=", self.id)] + # Purge default search filters from ctx to avoid hiding records + ctx = action.get("context", {}) + if isinstance(ctx, str): + ctx = safe_eval.safe_eval(ctx, self.env.context) + action["context"] = { + k: v for k, v in ctx.items() if not k.startswith("search_default_") + } + # Drop ID otherwise the context will be loaded from the action's record :S + action.pop("id") return action @api.model From 939997f0d821d6087204012091d7e529cb61ed9c Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 2 Sep 2022 14:52:55 +0200 Subject: [PATCH 17/70] edi: improve consumer record count perf --- edi_oca/models/edi_exchange_consumer_mixin.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/edi_oca/models/edi_exchange_consumer_mixin.py b/edi_oca/models/edi_exchange_consumer_mixin.py index 7f59b61806..f07e84ff8a 100644 --- a/edi_oca/models/edi_exchange_consumer_mixin.py +++ b/edi_oca/models/edi_exchange_consumer_mixin.py @@ -213,8 +213,14 @@ def _get_exchange_record(self, exchange_type, backend=False, extra_domain=False) @api.depends("exchange_record_ids") def _compute_exchange_record_count(self): - for record in self: - record.exchange_record_count = len(record.exchange_record_ids) + data = self.env["edi.exchange.record"].read_group( + [("res_id", "in", self.ids)], + ["res_id"], + ["res_id"], + ) + mapped_data = {x["res_id"]: x["res_id_count"] for x in data} + for rec in self: + rec.exchange_record_count = mapped_data.get(rec.id, 0) def action_view_edi_records(self): self.ensure_one() From ce22980c43467cd229c5bae287c3249b3a2d9ba4 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 2 Sep 2022 16:07:50 +0200 Subject: [PATCH 18/70] edi: fix record missing indexes --- edi_oca/models/edi_exchange_record.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index 2222777aee..b9a16389b0 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -28,6 +28,7 @@ class EDIExchangeRecord(models.Model): required=True, ondelete="cascade", auto_join=True, + index=True, ) direction = fields.Selection(related="type_id.direction") backend_id = fields.Many2one(comodel_name="edi.backend", required=True) @@ -56,6 +57,7 @@ class EDIExchangeRecord(models.Model): readonly=True, copy=False, default="new", + index=True, selection=[ # Common states ("new", "New"), From f46dcd80fe4442b922c09aba6b66c80d1ad73a03 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 2 Sep 2022 16:11:01 +0200 Subject: [PATCH 19/70] edi: imp backend check input/output Accept `record_ids` to work w/ specific records. --- edi_oca/models/edi_backend.py | 44 ++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py index 22a412dc12..2f463b66ec 100644 --- a/edi_oca/models/edi_backend.py +++ b/edi_oca/models/edi_backend.py @@ -346,7 +346,9 @@ def _cron_check_output_exchange_sync(self, **kw): backend._check_output_exchange_sync(**kw) # TODO: consider splitting cron in 2 (1 for receiving, 1 for processing) - def _check_output_exchange_sync(self, skip_send=False, skip_sent=True): + def _check_output_exchange_sync( + self, skip_send=False, skip_sent=True, record_ids=None + ): """Lookup for pending output records and take care of them. First work on records that need output generation. @@ -357,7 +359,7 @@ def _check_output_exchange_sync(self, skip_send=False, skip_sent=True): """ # Generate output files new_records = self.exchange_record_model.search( - self._output_new_records_domain() + self._output_new_records_domain(record_ids=record_ids) ) _logger.info( "EDI Exchange output sync: found %d new records to process.", @@ -369,7 +371,9 @@ def _check_output_exchange_sync(self, skip_send=False, skip_sent=True): if skip_send: return pending_records = self.exchange_record_model.search( - self._output_pending_records_domain(skip_sent=skip_sent) + self._output_pending_records_domain( + skip_sent=skip_sent, record_ids=record_ids + ) ) _logger.info( "EDI Exchange output sync: found %d pending records to process.", @@ -384,17 +388,20 @@ def _check_output_exchange_sync(self, skip_send=False, skip_sent=True): self._exchange_check_ack_needed(pending_records) - def _output_new_records_domain(self): + def _output_new_records_domain(self, record_ids=None): """Domain for output records needing output content generation.""" - return [ + domain = [ ("backend_id", "=", self.id), ("type_id.exchange_file_auto_generate", "=", True), ("type_id.direction", "=", "output"), ("edi_exchange_state", "=", "new"), ("exchange_file", "=", False), ] + if record_ids: + domain.append(("id", "in", record_ids)) + return domain - def _output_pending_records_domain(self, skip_sent=True): + def _output_pending_records_domain(self, skip_sent=True, record_ids=None): """Domain for pending output records. Records might be waiting to be sent or have errors or have ack to handle.""" @@ -403,11 +410,14 @@ def _output_pending_records_domain(self, skip_sent=True): # If you want to update sent records # you'll have to provide a `check` component. states += ("output_sent",) - return [ + domain = [ ("type_id.direction", "=", "output"), ("backend_id", "=", self.id), ("edi_exchange_state", "in", states), ] + if record_ids: + domain.append(("id", "in", record_ids)) + return domain def _exchange_output_check_state(self, exchange_record): component = self._get_component(exchange_record, "check") @@ -546,14 +556,14 @@ def _cron_check_input_exchange_sync(self, **kw): # TODO: add tests # TODO: consider splitting cron in 2 (1 for receiving, 1 for processing) - def _check_input_exchange_sync(self, **kw): + def _check_input_exchange_sync(self, record_ids=None, **kw): """Lookup for pending input records and take care of them. First work on records that need to receive input. Then work on records waiting to be processed. """ pending_records = self.exchange_record_model.search( - self._input_pending_records_domain() + self._input_pending_records_domain(record_ids=record_ids) ) _logger.info( "EDI Exchange input sync: found %d pending records to receive.", @@ -563,7 +573,7 @@ def _check_input_exchange_sync(self, **kw): rec.with_delay().action_exchange_receive() pending_process_records = self.exchange_record_model.search( - self._input_pending_process_records_domain() + self._input_pending_process_records_domain(record_ids=record_ids) ) _logger.info( "EDI Exchange input sync: found %d pending records to process.", @@ -575,21 +585,27 @@ def _check_input_exchange_sync(self, **kw): # TODO: test it! self._exchange_check_ack_needed(pending_process_records) - def _input_pending_records_domain(self): - return [ + def _input_pending_records_domain(self, record_ids=None): + domain = [ ("backend_id", "=", self.id), ("type_id.direction", "=", "input"), ("edi_exchange_state", "=", "input_pending"), ("exchange_file", "=", False), ] + if record_ids: + domain.append(("id", "in", record_ids)) + return domain - def _input_pending_process_records_domain(self): + def _input_pending_process_records_domain(self, record_ids=None): states = ("input_received", "input_processed_error") - return [ + domain = [ ("backend_id", "=", self.id), ("type_id.direction", "=", "input"), ("edi_exchange_state", "in", states), ] + if record_ids: + domain.append(("id", "in", record_ids)) + return domain def _exchange_check_ack_needed(self, pending_records): ack_pending_records = pending_records.filtered(lambda x: x.needs_ack()) From ed0d5869d90ced8e4b68fdae35b3132a2289df79 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 2 Sep 2022 16:11:42 +0200 Subject: [PATCH 20/70] edi: improve cron names --- edi_oca/data/cron.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/edi_oca/data/cron.xml b/edi_oca/data/cron.xml index 9ad1bd7111..e182b10acd 100644 --- a/edi_oca/data/cron.xml +++ b/edi_oca/data/cron.xml @@ -5,7 +5,7 @@ model="ir.cron" forcecreate="True" > - EDI exchange check status + EDI exchange check output sync 1 @@ -22,7 +22,7 @@ model="ir.cron" forcecreate="True" > - EDI exchange input status + EDI exchange check input sync 1 From 3efaa1e93c1623c1e70ed8cacf5b66446d4785c5 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 5 Sep 2022 14:04:28 +0200 Subject: [PATCH 21/70] edi: exchange type add TODO --- edi_oca/models/edi_exchange_type.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/edi_oca/models/edi_exchange_type.py b/edi_oca/models/edi_exchange_type.py index 1facb1f53e..6f9865c9db 100644 --- a/edi_oca/models/edi_exchange_type.py +++ b/edi_oca/models/edi_exchange_type.py @@ -52,6 +52,10 @@ class EDIExchangeType(models.Model): exchange_filename_pattern = fields.Char(default="{record_name}-{type.code}-{dt}") # TODO make required if exchange_filename_pattern is exchange_file_ext = fields.Char() + # TODO: this flag should be probably deprecated + # because when an exchange w/o file is pending + # there's no reason not to generate it. + # Also this could be controlled more generally w/ edi auto settings. exchange_file_auto_generate = fields.Boolean( help="Auto generate output for records missing their payload. " "If active, a cron will take care of generating the output when not set yet. " From 3987fb53d9cf688f25232e917cae7c97181c5705 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 2 Sep 2022 16:08:33 +0200 Subject: [PATCH 22/70] edi: add test for exc.record.action_retry --- edi_oca/tests/test_record.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/edi_oca/tests/test_record.py b/edi_oca/tests/test_record.py index 7b7dd38ed7..18041e980e 100644 --- a/edi_oca/tests/test_record.py +++ b/edi_oca/tests/test_record.py @@ -185,3 +185,16 @@ def test_create_ack(self): ) ack2 = record0.exchange_create_ack_record() self.assertEqual(record0.ack_exchange_id, ack2) + + def test_retry(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + } + record0 = self.backend.create_record("test_csv_output", vals) + self.assertFalse(record0.retryable) + record0.edi_exchange_state = "output_error_on_send" + self.assertTrue(record0.retryable) + record0.action_retry() + self.assertEqual(record0.edi_exchange_state, "output_pending") + self.assertFalse(record0.retryable) From 491d19b69274add9ef65d479ec3654335a3300a4 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 2 Sep 2022 16:09:10 +0200 Subject: [PATCH 23/70] edi: fix exc.record._compute_retryable --- edi_oca/models/edi_exchange_record.py | 1 + 1 file changed, 1 insertion(+) diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index b9a16389b0..d7779302e3 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -173,6 +173,7 @@ def needs_ack(self): "input_processed_error": "input_received", } + @api.depends("edi_exchange_state") def _compute_retryable(self): for rec in self: rec.retryable = rec.edi_exchange_state in self._rollback_state_mapping From 9847e98a5423defe501391104b47c7794ec917bf Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 2 Sep 2022 16:47:53 +0200 Subject: [PATCH 24/70] edi: add quick exec option by type Allow scheduling jobs immediately based on type configuration. Very often you want an action to be executed right away. You still get a job, meaning that the action is not sync but the job will be scheduled immediately instead of waiting for the cron to run. --- edi_oca/models/edi_exchange_record.py | 21 ++++- edi_oca/models/edi_exchange_type.py | 5 + edi_oca/tests/__init__.py | 1 + edi_oca/tests/test_quick_exec.py | 108 ++++++++++++++++++++++ edi_oca/tests/test_record.py | 6 +- edi_oca/views/edi_exchange_type_views.xml | 1 + 6 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 edi_oca/tests/test_quick_exec.py diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index d7779302e3..549f67ca0b 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -219,11 +219,28 @@ def name_get(self): @api.model def create(self, vals): vals["identifier"] = self._get_identifier() - return super().create(vals) + rec = super().create(vals) + if rec._quick_exec_enabled(): + rec._execute_next_action() + return rec def _get_identifier(self): return self.env["ir.sequence"].next_by_code("edi.exchange") + def _quick_exec_enabled(self): + if self.env.context.get("edi__skip_quick_exec"): + return False + return self.type_id.quick_exec + + def _execute_next_action(self): + # The backend already knows how to handle records + # according to their direction and status. + # Let it decide. + if self.type_id.direction == "output": + self.backend_id._check_output_exchange_sync(record_ids=self.ids) + else: + self.backend_id._check_input_exchange_sync(record_ids=self.ids) + @api.constrains("backend_id", "type_id") def _constrain_backend(self): for rec in self: @@ -308,6 +325,8 @@ def _retry_exchange_action(self): self.message_post( body=_("Action retry: state moved back to '%s'") % display_state ) + if self._quick_exec_enabled(): + self._execute_next_action() return True def action_open_related_record(self): diff --git a/edi_oca/models/edi_exchange_type.py b/edi_oca/models/edi_exchange_type.py index 6f9865c9db..861670068f 100644 --- a/edi_oca/models/edi_exchange_type.py +++ b/edi_oca/models/edi_exchange_type.py @@ -129,6 +129,11 @@ class EDIExchangeType(models.Model): help="Automatically display a button on related models' form." # TODO: "Button settings can be configured via advanced settings." ) + quick_exec = fields.Boolean( + string="Quick execution", + help="When active, records of this type will be processed immediately " + "without waiting for the cron to pass by.", + ) _sql_constraints = [ ( diff --git a/edi_oca/tests/__init__.py b/edi_oca/tests/__init__.py index e3cb82d443..3f4193ce03 100644 --- a/edi_oca/tests/__init__.py +++ b/edi_oca/tests/__init__.py @@ -11,3 +11,4 @@ from . import test_consumer_mixin from . import test_edi_backend_cron from . import test_security +from . import test_quick_exec diff --git a/edi_oca/tests/test_quick_exec.py b/edi_oca/tests/test_quick_exec.py new file mode 100644 index 0000000000..51471a3c0b --- /dev/null +++ b/edi_oca/tests/test_quick_exec.py @@ -0,0 +1,108 @@ +# Copyright 2022 Camptocamp SA +# @author: Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import base64 + +import mock + +from .common import EDIBackendCommonComponentRegistryTestCase +from .fake_components import ( + FakeInputProcess, + FakeOutputChecker, + FakeOutputGenerator, + FakeOutputSender, +) + +LOGGERS = ("odoo.addons.edi_oca.models.edi_backend", "odoo.addons.queue_job.delay") + + +class EDIQuickExecTestCase(EDIBackendCommonComponentRegistryTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._build_components( + cls, + FakeOutputGenerator, + FakeOutputSender, + FakeOutputChecker, + FakeInputProcess, + ) + cls.partner2 = cls.env.ref("base.res_partner_10") + cls.partner3 = cls.env.ref("base.res_partner_12") + + def setUp(self): + super().setUp() + FakeOutputGenerator.reset_faked() + FakeOutputSender.reset_faked() + FakeOutputChecker.reset_faked() + FakeInputProcess.reset_faked() + + def test_quick_exec_on_create_no_call(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + } + model = self.env["edi.exchange.record"] + # quick exec is off, we should not get any call + with mock.patch.object(type(model), "_execute_next_action") as mocked: + record0 = self.backend.create_record("test_csv_output", vals) + mocked.assert_not_called() + self.assertEqual(record0.edi_exchange_state, "new") + # enabled but bypassed + self.exchange_type_out.exchange_file_auto_generate = True + self.exchange_type_out.quick_exec = True + with mock.patch.object(type(model), "_execute_next_action") as mocked: + record0 = self.backend.with_context( + edi__skip_quick_exec=True + ).create_record("test_csv_output", vals) + # quick exec is off, we should not get any call + mocked.assert_not_called() + self.assertEqual(record0.edi_exchange_state, "new") + + def test_quick_exec_on_create_out(self): + self.exchange_type_out.exchange_file_auto_generate = True + self.exchange_type_out.quick_exec = True + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + } + record0 = self.backend.create_record("test_csv_output", vals) + # File generated and sent! + self.assertEqual(record0.edi_exchange_state, "output_sent") + self.assertTrue(FakeOutputGenerator.check_called_for(record0)) + self.assertEqual( + record0._get_file_content(), FakeOutputGenerator._call_key(record0) + ) + + def test_quick_exec_on_create_in(self): + self.exchange_type_in.quick_exec = True + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + "exchange_file": base64.b64encode(b"1234"), + "edi_exchange_state": "input_received", + } + record0 = self.backend.create_record("test_csv_input", vals) + self.assertEqual(record0.edi_exchange_state, "input_processed") + self.assertTrue(FakeInputProcess.check_called_for(record0)) + + def test_quick_exec_on_retry(self): + self.exchange_type_in.quick_exec = True + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + "edi_exchange_state": "input_processed_error", + "exchange_file": base64.b64encode(b"1234"), + } + record0 = self.backend.with_context(edi__skip_quick_exec=True).create_record( + "test_csv_input", vals + ) + self.assertEqual(record0.edi_exchange_state, "input_processed_error") + self.assertTrue(record0.retryable) + # get record w/ a clean context + record0 = self.backend.exchange_record_model.browse(record0.id) + record0.action_retry() + # The file has been rolled back and processed right away + self.assertEqual(record0.edi_exchange_state, "input_processed") + self.assertTrue(FakeInputProcess.check_called_for(record0)) diff --git a/edi_oca/tests/test_record.py b/edi_oca/tests/test_record.py index 18041e980e..ee2efe3680 100644 --- a/edi_oca/tests/test_record.py +++ b/edi_oca/tests/test_record.py @@ -3,6 +3,7 @@ # @author: Simone Orsi # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import mock from freezegun import freeze_time from odoo import exceptions, fields @@ -195,6 +196,9 @@ def test_retry(self): self.assertFalse(record0.retryable) record0.edi_exchange_state = "output_error_on_send" self.assertTrue(record0.retryable) - record0.action_retry() + with mock.patch.object(type(record0), "_execute_next_action") as mocked: + record0.action_retry() + # quick exec is off, we should not get any call + mocked.assert_not_called() self.assertEqual(record0.edi_exchange_state, "output_pending") self.assertFalse(record0.retryable) diff --git a/edi_oca/views/edi_exchange_type_views.xml b/edi_oca/views/edi_exchange_type_views.xml index 30066f3a99..3e8c882cda 100644 --- a/edi_oca/views/edi_exchange_type_views.xml +++ b/edi_oca/views/edi_exchange_type_views.xml @@ -38,6 +38,7 @@ +
From fc2d1722be6893d697cfb70909027021c5be4976 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 6 Sep 2022 12:05:16 +0200 Subject: [PATCH 25/70] edi_storage: adapt _check_output_exchange_sync override --- edi_storage_oca/models/edi_backend.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/edi_storage_oca/models/edi_backend.py b/edi_storage_oca/models/edi_backend.py index e396ae69df..e3eb2d700f 100644 --- a/edi_storage_oca/models/edi_backend.py +++ b/edi_storage_oca/models/edi_backend.py @@ -168,8 +168,10 @@ def _storage_get_input_filenames(self, exchange_type): def _storage_new_exchange_record_vals(self, file_name): return {"exchange_filename": file_name, "edi_exchange_state": "input_pending"} - def _check_output_exchange_sync(self, skip_send=False, skip_sent=True): + def _check_output_exchange_sync(self, **kw): # Do not skip sent records when dealing w/ storage related exchanges, # because we want to update the file state # depending on where they are in the external folder. - return super()._check_output_exchange_sync(skip_sent=not self.storage_id) + if self.storage_id: + kw["skip_sent"] = False + return super()._check_output_exchange_sync(**kw) From af9c347cbde57cd6f9f40590192094e92dbd2cc4 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 16 Sep 2022 14:19:54 +0200 Subject: [PATCH 26/70] edi: drop ack record auto create Creating the ack was kind of... an hack :) When I added this behavior we didn't have yet the storage machinery that looks automatically for a new file. Also for webservices seems not relevant since we can configure endpoints that can create the ack when needed. All in all, I don't think this feature is needed. Worse, it adds some overhead when a record gets created and can generate write conflicts when exchanges are processed immediately overlapping with the creation of the ack record. --- edi_oca/models/edi_backend.py | 14 -------------- edi_oca/tests/test_edi_backend_cron.py | 5 ----- 2 files changed, 19 deletions(-) diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py index 2f463b66ec..edf959c530 100644 --- a/edi_oca/models/edi_backend.py +++ b/edi_oca/models/edi_backend.py @@ -386,8 +386,6 @@ def _check_output_exchange_sync( # TODO: run in job as well? self._exchange_output_check_state(rec) - self._exchange_check_ack_needed(pending_records) - def _output_new_records_domain(self, record_ids=None): """Domain for output records needing output content generation.""" domain = [ @@ -582,9 +580,6 @@ def _check_input_exchange_sync(self, record_ids=None, **kw): for rec in pending_process_records: rec.with_delay().action_exchange_process() - # TODO: test it! - self._exchange_check_ack_needed(pending_process_records) - def _input_pending_records_domain(self, record_ids=None): domain = [ ("backend_id", "=", self.id), @@ -607,15 +602,6 @@ def _input_pending_process_records_domain(self, record_ids=None): domain.append(("id", "in", record_ids)) return domain - def _exchange_check_ack_needed(self, pending_records): - ack_pending_records = pending_records.filtered(lambda x: x.needs_ack()) - _logger.info( - "EDI Exchange output sync: found %d records needing ack record.", - len(ack_pending_records), - ) - for rec in ack_pending_records: - rec.with_delay().exchange_create_ack_record() - def _find_existing_exchange_records( self, exchange_type, extra_domain=None, count_only=False ): diff --git a/edi_oca/tests/test_edi_backend_cron.py b/edi_oca/tests/test_edi_backend_cron.py index b1c4e1011f..ec497a4d11 100644 --- a/edi_oca/tests/test_edi_backend_cron.py +++ b/edi_oca/tests/test_edi_backend_cron.py @@ -75,8 +75,6 @@ def test_exchange_generate_new_auto_send(self): rec._get_file_content(), FakeOutputGenerator._call_key(rec) ) self.assertTrue(FakeOutputSender.check_called_for(rec)) - # TODO: test better? - self.assertTrue(rec.ack_exchange_id) @mute_logger(*LOGGERS) def test_exchange_generate_output_ready_auto_send(self): @@ -94,6 +92,3 @@ def test_exchange_generate_output_ready_auto_send(self): self.assertTrue(FakeOutputGenerator.check_not_called_for(self.record1)) self.assertTrue(FakeOutputSender.check_not_called_for(self.record1)) self.assertTrue(FakeOutputChecker.check_called_for(self.record1)) - - # TODO: test better? - self.assertTrue(self.record1.ack_exchange_id) From 94cc74ba47b4426d68efc54dc68c5bad18713c9c Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 5 Oct 2022 11:43:53 +0200 Subject: [PATCH 27/70] edi: fix _get_component ctx propagation The env_ctx that might come from a exchange type settings was not propagated to the exchange record which is made directly available to the work ctx of the component. Before this change, if you had any method on the exchange record or on the related record relying on a ctx key, the ctx key was not propagated to all records' context. --- edi_oca/models/edi_backend.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py index edf959c530..084bdd9ca5 100644 --- a/edi_oca/models/edi_backend.py +++ b/edi_oca/models/edi_backend.py @@ -54,20 +54,21 @@ class EDIBackend(models.Model): active = fields.Boolean(default=True) def _get_component(self, exchange_record, key): - candidates = self._get_component_usage_candidates(exchange_record, key) - work_ctx = {"exchange_record": exchange_record} - # Inject work context from advanced settings record_conf = self._get_component_conf_for_record(exchange_record, key) - work_ctx.update(record_conf.get("work_ctx", {})) - match_attrs = self._component_match_attrs(exchange_record, key) - # Model is not granted to be there - model = exchange_record.model or self._name # Load additional ctx keys if any collection = self - # TODO: document this + # TODO: document/test this env_ctx = record_conf.get("env_ctx", {}) if env_ctx: collection = collection.with_context(**env_ctx) + exchange_record = exchange_record.with_context(**env_ctx) + work_ctx = {"exchange_record": exchange_record} + # Inject work context from advanced settings + work_ctx.update(record_conf.get("work_ctx", {})) + # Model is not granted to be there + model = exchange_record.model or self._name + candidates = self._get_component_usage_candidates(exchange_record, key) + match_attrs = self._component_match_attrs(exchange_record, key) return collection._find_component( model, candidates, From e48264bb4ff239e582650eca2a18b6ba07f685ae Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 3 Oct 2022 11:14:18 +0200 Subject: [PATCH 28/70] edi: add 'as_bytes' option to _get_file_content When sending the file data, we might want to use its bytes version. This new flag gives an option for it. --- edi_oca/models/edi_exchange_record.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index 549f67ca0b..003b05f899 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -197,13 +197,16 @@ def _set_file_content( output_string = bytes(output_string, encoding) self[field_name] = base64.b64encode(output_string) - def _get_file_content(self, field_name="exchange_file", binary=True): + def _get_file_content( + self, field_name="exchange_file", binary=True, as_bytes=False + ): """Handy method to not have to convert b64 back and forth.""" self.ensure_one() if not self[field_name]: return "" if binary: - return base64.b64decode(self[field_name]).decode() + res = base64.b64decode(self[field_name]) + return res.decode() if not as_bytes else res return self[field_name] def name_get(self): From 5f76400b1118232203ca6c5397ad67ff4d81228d Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Tue, 16 May 2023 15:10:52 +0200 Subject: [PATCH 29/70] edi_oca: avoid backend mismatch --- edi_oca/models/edi_backend.py | 4 +++- edi_oca/tests/test_exchange_type.py | 19 +++++++++++++++++++ edi_oca/tests/test_record.py | 16 ++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py index 084bdd9ca5..9fc48b7e9a 100644 --- a/edi_oca/models/edi_backend.py +++ b/edi_oca/models/edi_backend.py @@ -178,8 +178,10 @@ def _get_exchange_type_domain(self, code): return [ ("code", "=", code), "|", - ("backend_type_id", "=", self.backend_type_id.id), ("backend_id", "=", self.id), + "&", + ("backend_type_id", "=", self.backend_type_id.id), + ("backend_id", "=", False), ] def _delay_action(self, rec): diff --git a/edi_oca/tests/test_exchange_type.py b/edi_oca/tests/test_exchange_type.py index c0c0df73f0..f640ebbd61 100644 --- a/edi_oca/tests/test_exchange_type.py +++ b/edi_oca/tests/test_exchange_type.py @@ -23,6 +23,25 @@ def test_ack_for(self): self.exchange_type_out_ack.ack_for_type_ids.ids, ) + def test_same_code_same_backend(self): + with self.assertRaises(Exception) as err: + self.exchange_type_in.copy({"code": "test_csv_input"}) + err_msg = err.exception.args[0] + self.assertTrue( + err_msg.startswith("duplicate key value violates unique constraint") + ) + + def test_same_code_different_backend(self): + new_backend = self.backend.copy() + new_type = self.exchange_type_in.copy( + {"backend_id": new_backend.id, "code": "test_csv_input"} + ) + self.assertEqual(new_type.code, self.exchange_type_in.code) + self.assertEqual( + new_type.backend_type_id, self.exchange_type_in.backend_type_id + ) + self.assertNotEqual(new_type.backend_id, self.exchange_type_in.backend_id) + def test_advanced_settings(self): settings = """ components: diff --git a/edi_oca/tests/test_record.py b/edi_oca/tests/test_record.py index ee2efe3680..f93957a279 100644 --- a/edi_oca/tests/test_record.py +++ b/edi_oca/tests/test_record.py @@ -46,6 +46,22 @@ def test_record_validate_state(self): err.exception.name, "Exchange state must respect direction!" ) + def test_record_same_type_code(self): + # Two record.exchange.type sharing same code "test_csv_input" + # Record should be created with the right backend + new_backend = self.backend.copy() + self.exchange_type_in.copy( + {"backend_id": new_backend.id, "code": "test_csv_input"} + ) + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + } + rec1 = self.backend.create_record("test_csv_input", vals) + rec2 = new_backend.create_record("test_csv_input", vals) + self.assertEqual(rec1.backend_id, self.backend) + self.assertEqual(rec2.backend_id, new_backend) + def test_record_exchange_date(self): vals = { "model": self.partner._name, From a345ab294dc8329040b75afb5c51387c37485ffb Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Tue, 16 May 2023 11:03:31 +0200 Subject: [PATCH 30/70] edi_oca: avoid ghost exchange records --- edi_oca/models/edi_exchange_record.py | 4 ++-- edi_oca/tests/test_security.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index 003b05f899..1a3ac02840 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -422,8 +422,8 @@ def _search( count=False, access_rights_uid=access_rights_uid, ) - if self.env.is_superuser(): - # rules do not apply for the superuser + if self.env.is_system(): + # rules do not apply to group "Settings" return len(ids) if count else ids if not ids: diff --git a/edi_oca/tests/test_security.py b/edi_oca/tests/test_security.py index 04e158e801..b568cd1b9a 100644 --- a/edi_oca/tests/test_security.py +++ b/edi_oca/tests/test_security.py @@ -171,6 +171,33 @@ def test_rule_no_search(self): .search_count([("id", "=", exchange_record.id)]), ) + def test_search_no_record(self): + # Consumer record no longer exists: + # exchange_record is hidden in search + exchange_record = self.create_record() + exchange_record.res_id = -1 + self.user.write({"groups_id": [(4, self.group.id)]}) + self.assertEqual( + 0, + self.env["edi.exchange.record"] + .with_user(self.user) + .search_count([("id", "=", exchange_record.id)]), + ) + + def test_search_no_record_admin(self): + # Consumer record no longer exists: + # user with group "Settings" has access + exchange_record = self.create_record() + exchange_record.res_id = -1 + admin_group = self.env.ref("base.group_system") + self.user.write({"groups_id": [(4, admin_group.id)]}) + self.assertEqual( + 1, + self.env["edi.exchange.record"] + .with_user(self.user) + .search_count([("id", "=", exchange_record.id)]), + ) + @mute_logger("odoo.addons.base.models.ir_model") def test_no_group_no_write(self): exchange_record = self.create_record() From f3dd0cc29ddbbed4204f64fd7386bc233defeea1 Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Wed, 17 May 2023 10:17:24 +0200 Subject: [PATCH 31/70] edi_oca: log warning for deleted records --- edi_oca/models/edi_exchange_record.py | 24 +++++++++++++++++++++--- edi_oca/tests/test_security.py | 18 +++++++++++++----- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index 1a3ac02840..bced9dc793 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -4,10 +4,13 @@ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import base64 +import logging from collections import defaultdict from odoo import _, api, exceptions, fields, models +_logger = logging.getLogger(__name__) + class EDIExchangeRecord(models.Model): """ @@ -423,9 +426,14 @@ def _search( access_rights_uid=access_rights_uid, ) if self.env.is_system(): - # rules do not apply to group "Settings" + # restrictions do not apply to group "Settings" return len(ids) if count else ids + # TODO highlight orphaned EDI records in UI: + # - self.model + self.res_id are set + # - self.record returns empty recordset + # Remark: self.record is @property, not field + if not ids: return 0 if count else [] orig_ids = ids @@ -452,11 +460,21 @@ def _search( for model, targets in model_data.items(): if not self.env[model].check_access_rights("read", False): continue - target_ids = list(targets) + recs = self.env[model].browse(list(targets)) + missing = recs - recs.exists() + if missing: + for res_id in missing.ids: + _logger.warning( + "Deleted record %s,%s is referenced by edi.exchange.record %s", + model, + res_id, + list(targets[res_id]), + ) + recs = recs - missing allowed = ( self.env[model] .with_context(active_test=False) - ._search([("id", "in", target_ids)]) + ._search([("id", "in", recs.ids)]) ) for target_id in allowed: result += list(targets[target_id]) diff --git a/edi_oca/tests/test_security.py b/edi_oca/tests/test_security.py index b568cd1b9a..f063a07561 100644 --- a/edi_oca/tests/test_security.py +++ b/edi_oca/tests/test_security.py @@ -177,12 +177,20 @@ def test_search_no_record(self): exchange_record = self.create_record() exchange_record.res_id = -1 self.user.write({"groups_id": [(4, self.group.id)]}) - self.assertEqual( - 0, - self.env["edi.exchange.record"] - .with_user(self.user) - .search_count([("id", "=", exchange_record.id)]), + logger_name = "odoo.addons.edi_oca.models.edi_exchange_record" + expected_msg = ( + f"WARNING:{logger_name}:" + f"Deleted record {exchange_record.model},{exchange_record.res_id} " + f"is referenced by edi.exchange.record [{exchange_record.id}]" ) + with self.assertLogs(logger_name, "WARNING") as watcher: + self.assertEqual( + 0, + self.env["edi.exchange.record"] + .with_user(self.user) + .search_count([("id", "=", exchange_record.id)]), + ) + self.assertEqual(watcher.output, [expected_msg]) def test_search_no_record_admin(self): # Consumer record no longer exists: From 1fe8c904c19cc81ab5c72332950598237d2a0639 Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Wed, 17 May 2023 07:46:10 +0200 Subject: [PATCH 32/70] edi_oca: assertion was not executed --- edi_oca/tests/test_record.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/edi_oca/tests/test_record.py b/edi_oca/tests/test_record.py index f93957a279..60b9b08033 100644 --- a/edi_oca/tests/test_record.py +++ b/edi_oca/tests/test_record.py @@ -35,16 +35,14 @@ def test_record_identifier(self): self.assertNotEqual(new_record.identifier, record.identifier) def test_record_validate_state(self): - with self.assertRaises(exceptions.ValidationError) as err: + expected_err = "Exchange state must respect direction!" + with self.assertRaises(exceptions.ValidationError, msg=expected_err): vals = { "model": self.partner._name, "res_id": self.partner.id, "edi_exchange_state": "output_pending", } self.backend.create_record("test_csv_input", vals) - self.assertEqual( - err.exception.name, "Exchange state must respect direction!" - ) def test_record_same_type_code(self): # Two record.exchange.type sharing same code "test_csv_input" From 97b820ef76a6ca5652110c206992bb49ce008fd8 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 17 May 2023 13:22:28 +0200 Subject: [PATCH 33/70] edi: unify action complete notification Now all main framework actions call the same notification method when complete. --- edi_oca/models/edi_backend.py | 11 ++++++----- edi_oca/models/edi_exchange_record.py | 11 +++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py index 9fc48b7e9a..0a4af288b7 100644 --- a/edi_oca/models/edi_backend.py +++ b/edi_oca/models/edi_backend.py @@ -203,6 +203,7 @@ def exchange_generate(self, exchange_record, store=True, force=False, **kw): self.ensure_one() self._check_exchange_generate(exchange_record, force=force) output = self._exchange_generate(exchange_record, **kw) + message = None if output and store: if not isinstance(output, bytes): output = output.encode() @@ -228,7 +229,7 @@ def exchange_generate(self, exchange_record, store=True, force=False, **kw): exchange_record.update( {"edi_exchange_state": state, "exchange_error": error} ) - exchange_record._notify_related_record(message) + exchange_record.notify_action_complete("generate", message=message) return output def _check_exchange_generate(self, exchange_record, force=False): @@ -311,8 +312,7 @@ def exchange_send(self, exchange_record): "exchanged_on": fields.Datetime.now(), } ) - if message: - exchange_record._notify_related_record(message) + exchange_record.notify_action_complete("send", message=message) return res def _swallable_exceptions(self): @@ -450,6 +450,7 @@ def exchange_process(self, exchange_record): return False state = exchange_record.edi_exchange_state error = False + message = None try: self._exchange_process(exchange_record) except self._swallable_exceptions() as err: @@ -476,6 +477,7 @@ def exchange_process(self, exchange_record): exchange_record._notify_error("process_ko") elif state == "input_processed": exchange_record._notify_done() + exchange_record.notify_action_complete("process", message=message) return res def _exchange_process(self, exchange_record): @@ -528,8 +530,7 @@ def exchange_receive(self, exchange_record): "exchanged_on": fields.Datetime.now(), } ) - if message: - exchange_record._notify_related_record(message) + exchange_record.notify_action_complete("receive", message=message) return res def _exchange_receive_check(self, exchange_record): diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index bced9dc793..793669c071 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -353,6 +353,17 @@ def action_open_related_exchanges(self): action["domain"] = [("id", "in", self.related_exchange_ids.ids)] return action + def notify_action_complete(self, action, message=None): + """Notify current record that an edi action has been completed. + + Implementers should take care of calling this method + if they work on records w/o calling edi_backend methods (eg: action_send). + + Implementers can hook to this method to do something after any action ends. + """ + if message: + self._notify_related_record(message) + def _notify_related_record(self, message, level="info"): """Post notification on the original record.""" if not hasattr(self.record, "message_post_with_view"): From d7fd2b2e6e8a732f852a2f528be395b6121b33b1 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 17 May 2023 13:27:50 +0200 Subject: [PATCH 34/70] edi: trigger generic event on action complete --- edi_oca/models/edi_exchange_record.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index 793669c071..547320020e 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -364,6 +364,9 @@ def notify_action_complete(self, action, message=None): if message: self._notify_related_record(message) + # Trigger generic action complete event + self._trigger_edi_event(f"{action}_complete") + def _notify_related_record(self, message, level="info"): """Post notification on the original record.""" if not hasattr(self.record, "message_post_with_view"): From 7b692a7ddce0523f273f2ae2950d85562e001829 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 18 May 2023 11:32:37 +0200 Subject: [PATCH 35/70] edi: trigger generic event on related record --- edi_oca/models/edi_exchange_record.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index 547320020e..2ea8656234 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -364,8 +364,12 @@ def notify_action_complete(self, action, message=None): if message: self._notify_related_record(message) - # Trigger generic action complete event - self._trigger_edi_event(f"{action}_complete") + # Trigger generic action complete event on exchange record + event_name = f"{action}_complete" + self._trigger_edi_event(event_name) + if self.record: + # Trigger specific event on related record + self._trigger_edi_event(event_name, target=self.record) def _notify_related_record(self, message, level="info"): """Post notification on the original record.""" @@ -388,10 +392,11 @@ def _trigger_edi_event_make_name(self, name, suffix=None): suffix=("_" + suffix) if suffix else "", ) - def _trigger_edi_event(self, name, suffix=None, **kw): + def _trigger_edi_event(self, name, suffix=None, target=None, **kw): """Trigger a component event linked to this backend and edi exchange.""" name = self._trigger_edi_event_make_name(name, suffix=suffix) - self._event(name).notify(self, **kw) + target = target or self + target._event(name).notify(self, **kw) def _notify_done(self): self._notify_related_record(self._exchange_status_message("process_ok")) From d9fba111e6b43d756dfe39b46fa7c80b9a52a63e Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 18 May 2023 11:30:55 +0200 Subject: [PATCH 36/70] edi: mark every session w/ 'edi_framework' ctx key --- edi_oca/models/edi_backend.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/edi_oca/models/edi_backend.py b/edi_oca/models/edi_backend.py index 0a4af288b7..654ca9c591 100644 --- a/edi_oca/models/edi_backend.py +++ b/edi_oca/models/edi_backend.py @@ -58,10 +58,9 @@ def _get_component(self, exchange_record, key): # Load additional ctx keys if any collection = self # TODO: document/test this - env_ctx = record_conf.get("env_ctx", {}) - if env_ctx: - collection = collection.with_context(**env_ctx) - exchange_record = exchange_record.with_context(**env_ctx) + env_ctx = self._get_component_env_ctx(record_conf, key) + collection = collection.with_context(**env_ctx) + exchange_record = exchange_record.with_context(**env_ctx) work_ctx = {"exchange_record": exchange_record} # Inject work context from advanced settings work_ctx.update(record_conf.get("work_ctx", {})) @@ -76,6 +75,12 @@ def _get_component(self, exchange_record, key): **match_attrs, ) + def _get_component_env_ctx(self, record_conf, key): + env_ctx = record_conf.get("env_ctx", {}) + # You can use `edi_session` down in the stack to control logics. + env_ctx.update(dict(edi_framework_action=key)) + return env_ctx + def _component_match_attrs(self, exchange_record, key): """Attributes that will be used to lookup components. From ebe0754dd124f790207ece1da5ba69ec5fe68dbc Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 19 May 2023 09:39:23 +0200 Subject: [PATCH 37/70] edi: exc.record._set_related_record use sudo Motivation: you end up using sudo on every call because you don't know if the current user will have perm. Yet, we don't care for such operatation. --- edi_oca/models/edi_exchange_record.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edi_oca/models/edi_exchange_record.py b/edi_oca/models/edi_exchange_record.py index 2ea8656234..40ec49d09b 100644 --- a/edi_oca/models/edi_exchange_record.py +++ b/edi_oca/models/edi_exchange_record.py @@ -342,7 +342,7 @@ def action_open_related_record(self): return self.record.get_formview_action() def _set_related_record(self, odoo_record): - self.update({"model": odoo_record._name, "res_id": odoo_record.id}) + self.sudo().update({"model": odoo_record._name, "res_id": odoo_record.id}) def action_open_related_exchanges(self): self.ensure_one() From 6fd0452736c9c3398ddb51acf1a449830098c583 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 19 May 2023 08:45:27 +0200 Subject: [PATCH 38/70] edi_exchange_template: partial fix for ctx propagation --- edi_exchange_template_oca/models/edi_backend.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/edi_exchange_template_oca/models/edi_backend.py b/edi_exchange_template_oca/models/edi_backend.py index 393eb8bd3d..ad3fe5e2a4 100644 --- a/edi_exchange_template_oca/models/edi_backend.py +++ b/edi_exchange_template_oca/models/edi_backend.py @@ -12,6 +12,12 @@ def _exchange_generate(self, exchange_record, **kw): # Template take precedence over component lookup tmpl = self._get_output_template(exchange_record) if tmpl: + # FIXME: env_ctx is not propagated here because we bypass components completly. + # It would be better to move this machinery inside a `generate` component. + exchange_record = exchange_record.with_context( + edi_framework_action="generate" + ) + tmpl = tmpl.with_context(edi_framework_action="generate") return tmpl.exchange_generate(exchange_record, **kw) return super()._exchange_generate(exchange_record, **kw) From 64472f83e81d49ba760c341d55105e6dbd1aa0e7 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 22 May 2023 14:36:18 +0200 Subject: [PATCH 39/70] edi: speed up exchange record views * filter by default on the last 15 days w/ 'Recent' menu item * filter by default on today on all menu items * allow to browse them all w/ 'All' at the bottom --- edi_oca/views/edi_exchange_record_views.xml | 30 ++++++++++++++++++--- edi_oca/views/menuitems.xml | 13 ++++++--- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/edi_oca/views/edi_exchange_record_views.xml b/edi_oca/views/edi_exchange_record_views.xml index 890a8db8eb..1ed3fbab60 100644 --- a/edi_oca/views/edi_exchange_record_views.xml +++ b/edi_oca/views/edi_exchange_record_views.xml @@ -266,6 +266,20 @@
+ + + Recent exchanges + ir.actions.act_window + edi.exchange.record + tree,form + + + {'search_default_filter_created_today': 1} + + Exchanges ir.actions.act_window @@ -286,7 +300,9 @@ tree,form [] - {'search_default_filter_pending': 1} + {'search_default_filter_pending': 1, 'search_default_filter_created_today': 1} @@ -296,7 +312,9 @@ tree,form [] - {'search_default_filter_failed': 1} + {'search_default_filter_failed': 1, 'search_default_filter_created_today': 1} tree,form
[] - {'search_default_filter_inbound': 1} + {'search_default_filter_inbound': 1, 'search_default_filter_created_today': 1} tree,form [] - {'search_default_filter_outbound': 1} + {'search_default_filter_outbound': 1, 'search_default_filter_created_today': 1} + Date: Wed, 24 May 2023 13:42:31 +0200 Subject: [PATCH 40/70] edi: add 'active' field to exchange type --- edi_oca/models/edi_exchange_type.py | 1 + edi_oca/views/edi_exchange_type_views.xml | 31 ++++++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/edi_oca/models/edi_exchange_type.py b/edi_oca/models/edi_exchange_type.py index 861670068f..7319f3a1f2 100644 --- a/edi_oca/models/edi_exchange_type.py +++ b/edi_oca/models/edi_exchange_type.py @@ -30,6 +30,7 @@ class EDIExchangeType(models.Model): _name = "edi.exchange.type" _description = "EDI Exchange Type" + active = fields.Boolean(default=True) backend_id = fields.Many2one( string="Backend", comodel_name="edi.backend", diff --git a/edi_oca/views/edi_exchange_type_views.xml b/edi_oca/views/edi_exchange_type_views.xml index 3e8c882cda..cec441f475 100644 --- a/edi_oca/views/edi_exchange_type_views.xml +++ b/edi_oca/views/edi_exchange_type_views.xml @@ -3,13 +3,14 @@ edi.exchange.type - + + @@ -18,6 +19,13 @@
+ +