From 217b64d01646479813c1256d7899b465703704b2 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 19 Nov 2020 18:35:05 +0100 Subject: [PATCH 001/453] Add edi module --- edi_core_oca/README.rst | 91 ++++ edi_core_oca/__init__.py | 1 + edi_core_oca/__manifest__.py | 20 + edi_core_oca/demo/edi_backend_demo.xml | 11 + edi_core_oca/models/__init__.py | 2 + edi_core_oca/models/edi_backend.py | 39 ++ edi_core_oca/models/edi_backend_type.py | 32 ++ edi_core_oca/readme/CONTRIBUTORS.rst | 1 + edi_core_oca/readme/DESCRIPTION.rst | 6 + edi_core_oca/security/ir_model_access.xml | 21 + edi_core_oca/static/description/icon.png | Bin 0 -> 9455 bytes edi_core_oca/static/description/index.html | 432 ++++++++++++++++++ edi_core_oca/tests/__init__.py | 1 + edi_core_oca/tests/common.py | 31 ++ edi_core_oca/tests/test_edi_backend.py | 25 + edi_core_oca/utils.py | 10 + edi_core_oca/views/edi_backend_type_views.xml | 67 +++ edi_core_oca/views/edi_backend_views.xml | 76 +++ 18 files changed, 866 insertions(+) create mode 100644 edi_core_oca/README.rst create mode 100644 edi_core_oca/__init__.py create mode 100644 edi_core_oca/__manifest__.py create mode 100644 edi_core_oca/demo/edi_backend_demo.xml create mode 100644 edi_core_oca/models/__init__.py create mode 100644 edi_core_oca/models/edi_backend.py create mode 100644 edi_core_oca/models/edi_backend_type.py create mode 100644 edi_core_oca/readme/CONTRIBUTORS.rst create mode 100644 edi_core_oca/readme/DESCRIPTION.rst create mode 100644 edi_core_oca/security/ir_model_access.xml create mode 100644 edi_core_oca/static/description/icon.png create mode 100644 edi_core_oca/static/description/index.html create mode 100644 edi_core_oca/tests/__init__.py create mode 100644 edi_core_oca/tests/common.py create mode 100644 edi_core_oca/tests/test_edi_backend.py create mode 100644 edi_core_oca/utils.py create mode 100644 edi_core_oca/views/edi_backend_type_views.xml create mode 100644 edi_core_oca/views/edi_backend_views.xml diff --git a/edi_core_oca/README.rst b/edi_core_oca/README.rst new file mode 100644 index 000000000..1f346ac67 --- /dev/null +++ b/edi_core_oca/README.rst @@ -0,0 +1,91 @@ +================ +Base EDI Backend +================ + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi-lightgray.png?logo=github + :target: https://github.com/OCA/edi/tree/13.0/edi + :alt: OCA/edi +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/edi-13-0/edi-13-0-edi + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/226/13.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Base EDI backend. + +Provides following models: + +1. EDI Backend, to centralize configuration +2. EDI Backend Type, to classify EDI backends (eg: UBL, GS1, pick-yours) + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE + +Contributors +~~~~~~~~~~~~ + +* Simone Orsi + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +.. |maintainer-simahawk| image:: https://github.com/simahawk.png?size=40px + :target: https://github.com/simahawk + :alt: simahawk + +Current `maintainer `__: + +|maintainer-simahawk| + +This module is part of the `OCA/edi `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/edi_core_oca/__init__.py b/edi_core_oca/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/edi_core_oca/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py new file mode 100644 index 000000000..5bd5e3540 --- /dev/null +++ b/edi_core_oca/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Base EDI Backend", + "summary": """Base module to define EDI backends""", + "version": "13.0.1.0.0", + "development_status": "Alpha", + "license": "AGPL-3", + "author": "ACSONE,Odoo Community Association (OCA)", + "maintainers": ["simahawk"], + "depends": ["base_edi", "component"], + "data": [ + "security/ir_model_access.xml", + "views/edi_backend_views.xml", + "views/edi_backend_type_views.xml", + ], + "demo": ["demo/edi_backend_demo.xml"], +} diff --git a/edi_core_oca/demo/edi_backend_demo.xml b/edi_core_oca/demo/edi_backend_demo.xml new file mode 100644 index 000000000..4e69bf811 --- /dev/null +++ b/edi_core_oca/demo/edi_backend_demo.xml @@ -0,0 +1,11 @@ + + + + Demo EDI backend type + demo_backend + + + Demo EDI backend + + + diff --git a/edi_core_oca/models/__init__.py b/edi_core_oca/models/__init__.py new file mode 100644 index 000000000..9fc4c1a76 --- /dev/null +++ b/edi_core_oca/models/__init__.py @@ -0,0 +1,2 @@ +from . import edi_backend +from . import edi_backend_type diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py new file mode 100644 index 000000000..ca583f299 --- /dev/null +++ b/edi_core_oca/models/edi_backend.py @@ -0,0 +1,39 @@ +# Copyright 2020 ACSONE SA +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class EDIBackend(models.Model): + """Generic backend to control EDI exchanges. + + Backends can be organized with types. + """ + + _name = "edi.backend" + _description = "EDI Backend" + _inherit = ["collection.base"] + + name = fields.Char(required=True) + backend_type_id = fields.Many2one( + string="EDI Backend type", + comodel_name="edi.backend.type", + required=True, + ondelete="restrict", + ) + + def _get_component(self, safe=False, work_ctx=None, **kw): + """Retrieve components for current backend. + + :param safe: boolean, if true does not break if component is not found + :param work_ctx: dictionary with work context params + :param kw: keyword args to lookup for components (eg: usage) + """ + work_ctx = work_ctx or {} + with self.work_on(self._name, **work_ctx) as work: + if safe: + component = work.many_components(**kw) + return component[0] if component else None + return work.component(**kw) diff --git a/edi_core_oca/models/edi_backend_type.py b/edi_core_oca/models/edi_backend_type.py new file mode 100644 index 000000000..3363f83b2 --- /dev/null +++ b/edi_core_oca/models/edi_backend_type.py @@ -0,0 +1,32 @@ +# Copyright 2020 ACSONE SA +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + +from ..utils import normalize_string + + +class EDIBackendType(models.Model): + """Define a kind of backend. + """ + + _name = "edi.backend.type" + _description = "EDI Backend Type" + + name = fields.Char(required=True) + code = fields.Char(required=True, inverse="_inverse_code",) + + _sql_constraints = [ + ("uniq_code", "unique(code)", "Backend type code must be unique!") + ] + + @api.onchange("name", "code") + def _onchange_code(self): + for rec in self: + rec.code = rec.code or rec.name + + def _inverse_code(self): + for rec in self: + # Make sure it's always normalized + rec.code = normalize_string(rec.code) diff --git a/edi_core_oca/readme/CONTRIBUTORS.rst b/edi_core_oca/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..f583948be --- /dev/null +++ b/edi_core_oca/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Simone Orsi diff --git a/edi_core_oca/readme/DESCRIPTION.rst b/edi_core_oca/readme/DESCRIPTION.rst new file mode 100644 index 000000000..ea48fe64c --- /dev/null +++ b/edi_core_oca/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +Base EDI backend. + +Provides following models: + +1. EDI Backend, to centralize configuration +2. EDI Backend Type, to classify EDI backends (eg: UBL, GS1, pick-yours) diff --git a/edi_core_oca/security/ir_model_access.xml b/edi_core_oca/security/ir_model_access.xml new file mode 100644 index 000000000..90582598b --- /dev/null +++ b/edi_core_oca/security/ir_model_access.xml @@ -0,0 +1,21 @@ + + + + access_edi_backend_type manager + + + + + + + + + access_edi_backend manager + + + + + + + + diff --git a/edi_core_oca/static/description/icon.png b/edi_core_oca/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/edi_core_oca/static/description/index.html b/edi_core_oca/static/description/index.html new file mode 100644 index 000000000..e18661bba --- /dev/null +++ b/edi_core_oca/static/description/index.html @@ -0,0 +1,432 @@ + + + + + + +Base EDI Backend + + + +
+

Base EDI Backend

+ + +

Alpha License: AGPL-3 OCA/edi Translate me on Weblate Try me on Runbot

+

Base EDI backend.

+

Provides following models:

+
    +
  1. EDI Backend, to centralize configuration
  2. +
  3. EDI Backend Type, to classify EDI backends (eg: UBL, GS1, pick-yours)
  4. +
+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+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.

+

Current maintainer:

+

simahawk

+

This module is part of the OCA/edi project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/edi_core_oca/tests/__init__.py b/edi_core_oca/tests/__init__.py new file mode 100644 index 000000000..14b2d8538 --- /dev/null +++ b/edi_core_oca/tests/__init__.py @@ -0,0 +1 @@ +from . import test_edi_backend diff --git a/edi_core_oca/tests/common.py b/edi_core_oca/tests/common.py new file mode 100644 index 000000000..0d6f1c3c1 --- /dev/null +++ b/edi_core_oca/tests/common.py @@ -0,0 +1,31 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import os + +from odoo.tests.common import SavepointCase, tagged + + +@tagged("-at_install", "post_install") +class EDIBackendCommonTestCase(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env( + context=dict( + cls.env.context, tracking_disable=True, test_queue_job_no_delay=True + ) + ) + cls.backend = cls._get_backend() + cls.backend_model = cls.env["edi.backend"] + cls.backend_type_model = cls.env["edi.backend.type"] + + def read_test_file(self, filename): + path = os.path.join(os.path.dirname(__file__), "examples", filename) + with open(path, "r") as thefile: + return thefile.read() + + @classmethod + def _get_backend(cls): + return cls.env.ref("edi.demo_edi_backend") diff --git a/edi_core_oca/tests/test_edi_backend.py b/edi_core_oca/tests/test_edi_backend.py new file mode 100644 index 000000000..9e1b77f8d --- /dev/null +++ b/edi_core_oca/tests/test_edi_backend.py @@ -0,0 +1,25 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import psycopg2 + +from odoo.tools import mute_logger + +from .common import EDIBackendCommonTestCase + + +class EDIBackendTestCase(EDIBackendCommonTestCase): + def test_type_code(self): + btype = self.backend_type_model.create( + {"name": "Test new type", "code": "Test new type"} + ) + self.assertEqual(btype.code, "test_new_type") + + @mute_logger("odoo.sql_db") + def test_type_code_uniq(self): + existing_code = self.backend.backend_type_id.code + with self.assertRaises(psycopg2.IntegrityError): + self.backend_type_model.create( + {"name": "Test new type", "code": existing_code} + ) diff --git a/edi_core_oca/utils.py b/edi_core_oca/utils.py new file mode 100644 index 000000000..f15f4321e --- /dev/null +++ b/edi_core_oca/utils.py @@ -0,0 +1,10 @@ +# Copyright 2020 ACSONE SA +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.http_routing.models.ir_http import slugify + + +def normalize_string(a_string, sep="_"): + """Normalize given string, replace dashes with given separator.""" + return slugify(a_string).replace("-", sep) diff --git a/edi_core_oca/views/edi_backend_type_views.xml b/edi_core_oca/views/edi_backend_type_views.xml new file mode 100644 index 000000000..1e3c0ec4e --- /dev/null +++ b/edi_core_oca/views/edi_backend_type_views.xml @@ -0,0 +1,67 @@ + + + + edi.backend.type + + + + + + + + + edi.backend.type + +
+ + + + + + +
+
+
+ + edi.backend.type + + + + + + + + + EDI Backend Type + ir.actions.act_window + edi.backend.type + tree,form + + [] + {} + + + + + form + + + + + + tree + + + + +
diff --git a/edi_core_oca/views/edi_backend_views.xml b/edi_core_oca/views/edi_backend_views.xml new file mode 100644 index 000000000..7a86ed9e5 --- /dev/null +++ b/edi_core_oca/views/edi_backend_views.xml @@ -0,0 +1,76 @@ + + + + edi.backend + + + + + + + + edi.backend + +
+ +
+
+ + + +
+
+
+
+ + edi.backend + + + + + + + + + + EDI Backend + ir.actions.act_window + edi.backend + tree,form + + [] + {} + + + + + form + + + + + + tree + + + + +
From 10013df20f2d048e46620eb9770822acc929c59e Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sat, 21 Nov 2020 12:40:36 +0100 Subject: [PATCH 002/453] [IMP] edi: define type, record, record create helper Co-Authored-By: Simone Orsi --- edi_core_oca/__manifest__.py | 5 +- edi_core_oca/models/__init__.py | 2 + edi_core_oca/models/edi_backend.py | 20 +++ edi_core_oca/models/edi_exchange_record.py | 117 +++++++++++++++ edi_core_oca/models/edi_exchange_type.py | 68 +++++++++ edi_core_oca/readme/CONTRIBUTORS.rst | 1 + edi_core_oca/readme/DESCRIPTION.rst | 4 +- edi_core_oca/security/ir_model_access.xml | 18 +++ edi_core_oca/static/description/icon.png | Bin 9455 -> 5055 bytes edi_core_oca/static/description/icon.svg | 142 ++++++++++++++++++ .../templates/exchange_chatter_msg.xml | 31 ++++ edi_core_oca/tests/common.py | 62 ++++++++ edi_core_oca/tests/test_edi_backend.py | 37 ++++- .../views/edi_exchange_record_views.xml | 99 ++++++++++++ .../views/edi_exchange_type_views.xml | 96 ++++++++++++ 15 files changed, 695 insertions(+), 7 deletions(-) create mode 100644 edi_core_oca/models/edi_exchange_record.py create mode 100644 edi_core_oca/models/edi_exchange_type.py create mode 100644 edi_core_oca/static/description/icon.svg create mode 100644 edi_core_oca/templates/exchange_chatter_msg.xml create mode 100644 edi_core_oca/views/edi_exchange_record_views.xml create mode 100644 edi_core_oca/views/edi_exchange_type_views.xml diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py index 5bd5e3540..99e623375 100644 --- a/edi_core_oca/__manifest__.py +++ b/edi_core_oca/__manifest__.py @@ -10,11 +10,14 @@ "license": "AGPL-3", "author": "ACSONE,Odoo Community Association (OCA)", "maintainers": ["simahawk"], - "depends": ["base_edi", "component"], + "depends": ["base_edi", "component", "mail"], "data": [ "security/ir_model_access.xml", "views/edi_backend_views.xml", "views/edi_backend_type_views.xml", + "views/edi_exchange_record_views.xml", + "views/edi_exchange_type_views.xml", + "templates/exchange_chatter_msg.xml", ], "demo": ["demo/edi_backend_demo.xml"], } diff --git a/edi_core_oca/models/__init__.py b/edi_core_oca/models/__init__.py index 9fc4c1a76..29a82090c 100644 --- a/edi_core_oca/models/__init__.py +++ b/edi_core_oca/models/__init__.py @@ -1,2 +1,4 @@ from . import edi_backend from . import edi_backend_type +from . import edi_exchange_record +from . import edi_exchange_type diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index ca583f299..f422bc01a 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -1,10 +1,15 @@ # Copyright 2020 ACSONE SA +# Copyright 2020 Creu Blanca # @author Simone Orsi # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + from odoo import fields, models +_logger = logging.getLogger(__name__) + class EDIBackend(models.Model): """Generic backend to control EDI exchanges. @@ -37,3 +42,18 @@ def _get_component(self, safe=False, work_ctx=None, **kw): component = work.many_components(**kw) return component[0] if component else None return work.component(**kw) + + def create_record(self, type_code, values): + """Create an exchange record for current backend. + + :param type_code: edi.exchange.type code + :param values: edi.exchange.record values + :return: edi.exchange.record record + """ + self.ensure_one() + export_type = self.env["edi.exchange.type"].search( + [("code", "=", type_code), ("backend_id", "=", self.id)], limit=1 + ) + export_type.ensure_one() + values["type_id"] = export_type.id + return self.env["edi.exchange.record"].create(values) diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py new file mode 100644 index 000000000..22d3ba8d7 --- /dev/null +++ b/edi_core_oca/models/edi_exchange_record.py @@ -0,0 +1,117 @@ +# Copyright 2020 ACSONE SA +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, exceptions, fields, models + + +class EDIExchangeRecord(models.Model): + """ + Define an exchange record. + """ + + _name = "edi.exchange.record" + _inherit = "mail.thread" + _description = "EDI exchange Record" + + name = fields.Char(compute="_compute_name") + type_id = fields.Many2one( + string="EDI Exchange type", + comodel_name="edi.exchange.type", + required=True, + ondelete="cascade", + auto_join=True, + ) + direction = fields.Selection(related="type_id.direction",) + backend_id = fields.Many2one( + comodel_name="edi.backend", related="type_id.backend_id", + ) + model = fields.Char(index=True, required=False, readonly=True) + res_id = fields.Many2oneReference( + string="Record ID", + index=True, + required=False, + readonly=True, + model_field="model", + ) + exchange_file = fields.Binary(attachment=True) + exchange_filename = fields.Char( + compute="_compute_exchange_filename", readonly=False, store=True + ) + exchanged_on = fields.Datetime( + string="Exchanged on", + help="Sent or received on this date.", + compute="_compute_exchanged_on", + store=True, + ) + exchange_identification_code = fields.Char( + track_visibility="onchange", + help="Identification of the EDI, useful to search and join other documents", + ) + ack_file = fields.Binary(attachment=True) + ack_filename = fields.Char( + compute="_compute_exchange_filename", readonly=False, store=True + ) + ack_received_on = fields.Datetime( + string="ACK received on", + readonly=True, + compute="_compute_ack_received_on", + store=True, + ) + edi_exchange_state = fields.Selection( + string="Exchange state", + readonly=True, + default="new", + selection=[ + ("new", "New"), + # output exchange states + ("output_pending", "Waiting to be sent"), + ("output_error_on_send", "error on send"), + ("output_sent", "Sent"), + ("output_sent_and_processed", "Sent and processed"), + ("output_sent_and_error", "Sent and error"), + # input exchange states + ("input_pending", "Waiting to be received"), + ("input_received", "Received"), + ("input_processed", "Processed"), + ("input_processed_error", "Error on process"), + ], + ) + exchange_error = fields.Text(string="Exchange error", readonly=True) + + @api.depends("type_id.code", "model", "res_id") + def _compute_name(self): + for rec in self: + rec.name = "{} - {}".format( + rec.type_id.name, rec.record.name if rec.model else "Unrelated" + ) + + @api.depends("model", "type_id", "type_id.ack_needed") + def _compute_exchange_filename(self): + for rec in self: + if not rec.exchange_filename: + rec.exchange_filename = rec.type_id._make_exchange_filename(rec) + if rec.type_id.ack_needed and not rec.ack_filename: + rec.ack_filename = rec.type_id._make_exchange_filename(rec, ack=True) + + @api.constrains("edi_exchange_state") + def _constrain_edi_exchange_state(self): + for rec in self: + if rec.edi_exchange_state == "new": + continue + if not rec.edi_exchange_state.startswith(rec.direction): + raise exceptions.ValidationError( + _("Exchange state must respect direction!") + ) + + @property + def record(self): + return self.env[self.model].browse(self.res_id) + + def name_get(self): + result = [] + for rec in self: + dt = fields.Datetime.to_string(rec.exchanged_on) if rec.exchanged_on else "" + name = "[{}] {} {}".format(rec.type_id.name, rec.record.name, dt) + result.append((rec.id, name)) + return result diff --git a/edi_core_oca/models/edi_exchange_type.py b/edi_core_oca/models/edi_exchange_type.py new file mode 100644 index 000000000..8f67bab2d --- /dev/null +++ b/edi_core_oca/models/edi_exchange_type.py @@ -0,0 +1,68 @@ +# Copyright 2020 ACSONE SA +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + +from odoo.addons.http_routing.models.ir_http import slugify + + +class EDIExchangeType(models.Model): + """ + Define a kind of exchange. + """ + + _name = "edi.exchange.type" + _description = "EDI Exchange Type" + + backend_id = fields.Many2one( + string="EDI backend", + comodel_name="edi.backend", + required=True, + ondelete="cascade", + ) + name = fields.Char(required=True) + code = fields.Char(required=True) + direction = fields.Selection( + selection=[("input", "Input"), ("output", "Output")], required=True + ) + exchange_filename_pattern = fields.Char(default="{record_name}-{type.code}-{dt}") + exchange_file_ext = fields.Char(required=True) + + ack_needed = fields.Boolean() + ack_name = fields.Char() + ack_code = fields.Char() + ack_filename_pattern = fields.Char(default="{type.exchange_filename_pattern}.ack") + ack_file_ext = fields.Char(default="") + + def _make_exchange_filename(self, record, ack=False): + """Generate filename.""" + pattern = self.exchange_filename_pattern + ext = self.exchange_file_ext + if ack: + pattern = self.ack_filename_pattern + ext = self.ack_file_ext + pattern = pattern + ".{ext}" + dt = slugify(fields.Datetime.to_string(fields.Datetime.now())) + record_name = self._get_record_name(record) + return pattern.format( + exchange_record=record, + record=record.record, + record_name=record_name, + type=self, + dt=dt, + ext=ext, + ) + + def _get_record_name(self, record): + if hasattr(record.record, "_get_edi_exchange_record_name"): + return record.record._get_edi_exchange_record_name(record, self) + return slugify(record.record.name) + + _sql_constraints = [ + ( + "code_uniq", + "unique(code, backend_id)", + "The code must be unique per backend", + ) + ] diff --git a/edi_core_oca/readme/CONTRIBUTORS.rst b/edi_core_oca/readme/CONTRIBUTORS.rst index f583948be..4945a3dc4 100644 --- a/edi_core_oca/readme/CONTRIBUTORS.rst +++ b/edi_core_oca/readme/CONTRIBUTORS.rst @@ -1 +1,2 @@ * Simone Orsi +* Enric Tobella diff --git a/edi_core_oca/readme/DESCRIPTION.rst b/edi_core_oca/readme/DESCRIPTION.rst index ea48fe64c..da17e1a49 100644 --- a/edi_core_oca/readme/DESCRIPTION.rst +++ b/edi_core_oca/readme/DESCRIPTION.rst @@ -3,4 +3,6 @@ Base EDI backend. Provides following models: 1. EDI Backend, to centralize configuration -2. EDI Backend Type, to classify EDI backends (eg: UBL, GS1, pick-yours) +2. EDI Backend Type, to classify EDI backends (eg: UBL, GS1, e-invoice, pick-yours) +3. EDI Exchange Type, to define file types of exchange +4. EDI Exchange Record, to define a record exchanged between systems diff --git a/edi_core_oca/security/ir_model_access.xml b/edi_core_oca/security/ir_model_access.xml index 90582598b..38e555905 100644 --- a/edi_core_oca/security/ir_model_access.xml +++ b/edi_core_oca/security/ir_model_access.xml @@ -18,4 +18,22 @@ + + access_edi_exchange_type manager + + + + + + + + + access_edi_exchange_record manager + + + + + + + diff --git a/edi_core_oca/static/description/icon.png b/edi_core_oca/static/description/icon.png index 3a0328b516c4980e8e44cdb63fd945757ddd132d..a79752645ce327baadb6aba260751a044a5e4a8a 100644 GIT binary patch literal 5055 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 literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I diff --git a/edi_core_oca/static/description/icon.svg b/edi_core_oca/static/description/icon.svg new file mode 100644 index 000000000..3060d8aa5 --- /dev/null +++ b/edi_core_oca/static/description/icon.svg @@ -0,0 +1,142 @@ + + + + + + image/svg+xml + + icon + + + + + + + + + + + + + + + icon + + + + + + + + + diff --git a/edi_core_oca/templates/exchange_chatter_msg.xml b/edi_core_oca/templates/exchange_chatter_msg.xml new file mode 100644 index 000000000..f48dadcae --- /dev/null +++ b/edi_core_oca/templates/exchange_chatter_msg.xml @@ -0,0 +1,31 @@ + + + + diff --git a/edi_core_oca/tests/common.py b/edi_core_oca/tests/common.py index 0d6f1c3c1..d7f955153 100644 --- a/edi_core_oca/tests/common.py +++ b/edi_core_oca/tests/common.py @@ -1,9 +1,15 @@ # Copyright 2020 ACSONE +# Copyright 2020 Creu Blanca # @author: Simone Orsi # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import datetime import os +from contextlib import contextmanager +from mock import patch + +from odoo import fields from odoo.tests.common import SavepointCase, tagged @@ -20,6 +26,51 @@ def setUpClass(cls): cls.backend = cls._get_backend() cls.backend_model = cls.env["edi.backend"] cls.backend_type_model = cls.env["edi.backend.type"] + cls.exchange_type_in = cls._create_exchange_type( + name="Test CSV input", + code="test_csv_input", + direction="input", + exchange_file_ext="csv", + exchange_filename_pattern="{record.ref}-{type.code}-{dt}", + ) + cls.exchange_type_out = cls._create_exchange_type( + name="Test CSV output", + code="test_csv_output", + direction="output", + exchange_file_ext="csv", + exchange_filename_pattern="{record.ref}-{type.code}-{dt}", + ) + cls.partner = cls.env.ref("base.res_partner_1") + cls.partner.ref = "EDI_EXC_TEST" + + @contextmanager + def mocked_today(self, forced_today): + """ Helper to make easily a python "with statement" mocking the "today" date. + :param forced_today: The expected "today" date as a str or Date object. + :return: An object to be used like 'with self.mocked_today():'. + """ + + if isinstance(forced_today, str): + forced_today_date = fields.Date.from_string(forced_today) + forced_today_datetime = fields.Datetime.from_string(forced_today) + elif isinstance(forced_today, datetime.datetime): + forced_today_datetime = forced_today + forced_today_date = forced_today_datetime.date() + else: + forced_today_date = forced_today + forced_today_datetime = datetime.datetime.combine( + forced_today_date, datetime.time() + ) + + def today(*args, **kwargs): + return forced_today_date + + with patch.object(fields.Date, "today", today): + with patch.object(fields.Date, "context_today", today): + with patch.object( + fields.Datetime, "now", return_value=forced_today_datetime + ): + yield def read_test_file(self, filename): path = os.path.join(os.path.dirname(__file__), "examples", filename) @@ -29,3 +80,14 @@ def read_test_file(self, filename): @classmethod def _get_backend(cls): return cls.env.ref("edi.demo_edi_backend") + + @classmethod + def _create_exchange_type(cls, **kw): + model = cls.env["edi.exchange.type"] + vals = { + "name": "Test CSV exchange", + "backend_id": cls.backend.id, + "backend_type_id": cls.backend.backend_type_id.id, + } + vals.update(kw) + return model.create(vals) diff --git a/edi_core_oca/tests/test_edi_backend.py b/edi_core_oca/tests/test_edi_backend.py index 9e1b77f8d..9292be138 100644 --- a/edi_core_oca/tests/test_edi_backend.py +++ b/edi_core_oca/tests/test_edi_backend.py @@ -4,6 +4,7 @@ import psycopg2 +from odoo.exceptions import ValidationError from odoo.tools import mute_logger from .common import EDIBackendCommonTestCase @@ -16,10 +17,36 @@ def test_type_code(self): ) self.assertEqual(btype.code, "test_new_type") - @mute_logger("odoo.sql_db") def test_type_code_uniq(self): existing_code = self.backend.backend_type_id.code - with self.assertRaises(psycopg2.IntegrityError): - self.backend_type_model.create( - {"name": "Test new type", "code": existing_code} - ) + with mute_logger("odoo.sql_db"): + with self.assertRaises(psycopg2.IntegrityError): + self.backend_type_model.create( + {"name": "Test new type", "code": existing_code} + ) + + def test_record_state(self): + with self.assertRaises(ValidationError): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + "edi_exchange_state": "output_pending", + } + self.backend.create_record("test_csv_input", vals) + + def test_create_record(self): + with self.mocked_today("2020-10-21 10:00:00"): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + } + record = self.backend.create_record("test_csv_input", vals) + expected = { + "type_id": self.exchange_type_in.id, + "edi_exchange_state": "new", + "exchange_filename": "EDI_EXC_TEST-test_csv_" + "input-2020-10-21-10-00-00.csv", + } + self.assertRecordValues(record, [expected]) + self.assertEqual(record.record, self.partner) + self.assertEqual(record.edi_exchange_state, "new") diff --git a/edi_core_oca/views/edi_exchange_record_views.xml b/edi_core_oca/views/edi_exchange_record_views.xml new file mode 100644 index 000000000..cdec20fdb --- /dev/null +++ b/edi_core_oca/views/edi_exchange_record_views.xml @@ -0,0 +1,99 @@ + + + + edi.exchange.record + + + + + + + + + + + + + + edi.exchange.record + +
+ +
+
+ + + + + + + + + + +
+
+
+
+ + edi.exchange.record + + + + + + + + + + + + + + + + EDI Exchange Record + ir.actions.act_window + edi.exchange.record + tree,form + + [] + {} + + + + + form + + + + + + tree + + + +
diff --git a/edi_core_oca/views/edi_exchange_type_views.xml b/edi_core_oca/views/edi_exchange_type_views.xml new file mode 100644 index 000000000..b3a5a0f94 --- /dev/null +++ b/edi_core_oca/views/edi_exchange_type_views.xml @@ -0,0 +1,96 @@ + + + + edi.exchange.type + + + + + + + + + + + edi.exchange.type + +
+ +
+
+ + + + + + + + + + + + + + + + +
+
+
+
+ + edi.exchange.type + + + + + + + + + + + + + + EDI Exchange Type + ir.actions.act_window + edi.exchange.type + tree,form + + [] + {} + + + + + form + + + + + + tree + + + +
From 286da40cc7adba0a0a75ca1fcec0092fecec3395 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 23 Nov 2020 07:52:46 +0100 Subject: [PATCH 003/453] edi: small fix/imp on models --- edi_core_oca/models/edi_backend.py | 74 +++++++++++++++++++++- edi_core_oca/models/edi_exchange_record.py | 6 +- edi_core_oca/models/edi_exchange_type.py | 38 +++++++---- 3 files changed, 100 insertions(+), 18 deletions(-) diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index f422bc01a..b1148e4a4 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -51,9 +51,77 @@ def create_record(self, type_code, values): :return: edi.exchange.record record """ self.ensure_one() + _values = self._create_record_prepare_values(type_code, values) + return self.env["edi.exchange.record"].create(_values) + + def _create_record_prepare_values(self, type_code, values): + res = values.copy() # do not pollute original dict export_type = self.env["edi.exchange.type"].search( - [("code", "=", type_code), ("backend_id", "=", self.id)], limit=1 + self._get_exchange_type_domain(type_code), limit=1 ) export_type.ensure_one() - values["type_id"] = export_type.id - return self.env["edi.exchange.record"].create(values) + res["type_id"] = export_type.id + res["backend_id"] = self.id + return res +<<<<<<< HEAD +======= + + def _get_exchange_type_domain(self, code): + return [ + ("code", "=", code), + "|", + ("backend_type_id", "=", self.backend_type_id.id), + ("backend_id", "=", self.id), + ] + + def generate_output(self, exchange_record, store=True, force=False, **kw): + """Generate output content for given exchange record. + + :param exchange_record: edi.exchange.record recordset + :param store: store output on the record itself + :param force: allow to re-genetate the content + :param kw: keyword args to be propagated to output generate handler + """ + self.ensure_one() + self._validate_generate_output(exchange_record, force=force) + output = self._generate_output(exchange_record, **kw) + if output and store: + if not isinstance(output, bytes): + output = output.encode() + exchange_record.update( + { + "exchange_file": base64.b64encode(output), + "edi_exchange_state": "output_pending", + } + ) + return tools.pycompat.to_text(output) + + def _validate_generate_output(self, exchange_record, force=False): + exchange_record.ensure_one() + if ( + exchange_record.edi_exchange_state != "new" + and exchange_record.exchange_file + and not force + ): + raise exceptions.UserError( + _( + "Exchange record ID=%d is not in draft state " + "and has already an output value." + ) + % exchange_record.id + ) + if not exchange_record.direction != "outbound": + raise exceptions.UserError( + _("Exchange record ID=%d is not file is not meant to b generated") + % exchange_record.id + ) + if exchange_record.exchange_file: + raise exceptions.UserError( + _("Exchabge record ID=%d already has a file to process!") + % exchange_record.id + ) + + def _generate_output(self, exchange_record, **kw): + """To be implemented""" + raise NotImplementedError() +>>>>>>> 6df23cc... fixup! edi: small fix/imp on models diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index 22d3ba8d7..b87d6e477 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -13,6 +13,7 @@ class EDIExchangeRecord(models.Model): _name = "edi.exchange.record" _inherit = "mail.thread" _description = "EDI exchange Record" + _order = "exchanged_on desc" name = fields.Char(compute="_compute_name") type_id = fields.Many2one( @@ -22,9 +23,9 @@ class EDIExchangeRecord(models.Model): ondelete="cascade", auto_join=True, ) - direction = fields.Selection(related="type_id.direction",) + direction = fields.Selection(related="type_id.direction") backend_id = fields.Many2one( - comodel_name="edi.backend", related="type_id.backend_id", + comodel_name="edi.backend", related="type_id.backend_id" ) model = fields.Char(index=True, required=False, readonly=True) res_id = fields.Many2oneReference( @@ -44,6 +45,7 @@ class EDIExchangeRecord(models.Model): compute="_compute_exchanged_on", store=True, ) + # TODO: use sequence and make it unique exchange_identification_code = fields.Char( track_visibility="onchange", help="Identification of the EDI, useful to search and join other documents", diff --git a/edi_core_oca/models/edi_exchange_type.py b/edi_core_oca/models/edi_exchange_type.py index 8f67bab2d..bee580dea 100644 --- a/edi_core_oca/models/edi_exchange_type.py +++ b/edi_core_oca/models/edi_exchange_type.py @@ -2,7 +2,7 @@ # @author Simone Orsi # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import fields, models +from odoo import _, api, exceptions, fields, models from odoo.addons.http_routing.models.ir_http import slugify @@ -16,10 +16,13 @@ class EDIExchangeType(models.Model): _description = "EDI Exchange Type" backend_id = fields.Many2one( - string="EDI backend", - comodel_name="edi.backend", + string="EDI backend", comodel_name="edi.backend", ondelete="restrict", + ) + backend_type_id = fields.Many2one( + string="EDI Backend type", + comodel_name="edi.backend.type", required=True, - ondelete="cascade", + ondelete="restrict", ) name = fields.Char(required=True) code = fields.Char(required=True) @@ -27,7 +30,8 @@ class EDIExchangeType(models.Model): selection=[("input", "Input"), ("output", "Output")], required=True ) exchange_filename_pattern = fields.Char(default="{record_name}-{type.code}-{dt}") - exchange_file_ext = fields.Char(required=True) + # TODO make required if exchange_filename_pattern is + exchange_file_ext = fields.Char() ack_needed = fields.Boolean() ack_name = fields.Char() @@ -35,6 +39,22 @@ class EDIExchangeType(models.Model): ack_filename_pattern = fields.Char(default="{type.exchange_filename_pattern}.ack") ack_file_ext = fields.Char(default="") + _sql_constraints = [ + ( + "code_uniq", + "unique(code, backend_id)", + "The code must be unique per backend", + ) + ] + + @api.constrains("backend_id", "backend_type_id") + def _check_backend(self): + for rec in self: + if not rec.backend_id: + continue + if rec.backend_id.backend_type_id != rec.backend_type_id: + raise exceptions.UserError(_("Backend should respect backend type!")) + def _make_exchange_filename(self, record, ack=False): """Generate filename.""" pattern = self.exchange_filename_pattern @@ -58,11 +78,3 @@ def _get_record_name(self, record): if hasattr(record.record, "_get_edi_exchange_record_name"): return record.record._get_edi_exchange_record_name(record, self) return slugify(record.record.name) - - _sql_constraints = [ - ( - "code_uniq", - "unique(code, backend_id)", - "The code must be unique per backend", - ) - ] From 33dbced28b9965726da06a57886501f0a4d8c9a3 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 23 Nov 2020 07:55:12 +0100 Subject: [PATCH 004/453] edi: use freezegun for tests --- edi_core_oca/tests/common.py | 34 -------------------------- edi_core_oca/tests/test_edi_backend.py | 31 +++++++++++------------ 2 files changed, 16 insertions(+), 49 deletions(-) diff --git a/edi_core_oca/tests/common.py b/edi_core_oca/tests/common.py index d7f955153..6e1b5f5cd 100644 --- a/edi_core_oca/tests/common.py +++ b/edi_core_oca/tests/common.py @@ -3,13 +3,8 @@ # @author: Simone Orsi # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import datetime import os -from contextlib import contextmanager -from mock import patch - -from odoo import fields from odoo.tests.common import SavepointCase, tagged @@ -43,35 +38,6 @@ def setUpClass(cls): cls.partner = cls.env.ref("base.res_partner_1") cls.partner.ref = "EDI_EXC_TEST" - @contextmanager - def mocked_today(self, forced_today): - """ Helper to make easily a python "with statement" mocking the "today" date. - :param forced_today: The expected "today" date as a str or Date object. - :return: An object to be used like 'with self.mocked_today():'. - """ - - if isinstance(forced_today, str): - forced_today_date = fields.Date.from_string(forced_today) - forced_today_datetime = fields.Datetime.from_string(forced_today) - elif isinstance(forced_today, datetime.datetime): - forced_today_datetime = forced_today - forced_today_date = forced_today_datetime.date() - else: - forced_today_date = forced_today - forced_today_datetime = datetime.datetime.combine( - forced_today_date, datetime.time() - ) - - def today(*args, **kwargs): - return forced_today_date - - with patch.object(fields.Date, "today", today): - with patch.object(fields.Date, "context_today", today): - with patch.object( - fields.Datetime, "now", return_value=forced_today_datetime - ): - yield - def read_test_file(self, filename): path = os.path.join(os.path.dirname(__file__), "examples", filename) with open(path, "r") as thefile: diff --git a/edi_core_oca/tests/test_edi_backend.py b/edi_core_oca/tests/test_edi_backend.py index 9292be138..264209429 100644 --- a/edi_core_oca/tests/test_edi_backend.py +++ b/edi_core_oca/tests/test_edi_backend.py @@ -3,6 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import psycopg2 +from freezegun import freeze_time from odoo.exceptions import ValidationError from odoo.tools import mute_logger @@ -34,19 +35,19 @@ def test_record_state(self): } self.backend.create_record("test_csv_input", vals) + @freeze_time("2020-10-21 10:00:00") def test_create_record(self): - with self.mocked_today("2020-10-21 10:00:00"): - vals = { - "model": self.partner._name, - "res_id": self.partner.id, - } - record = self.backend.create_record("test_csv_input", vals) - expected = { - "type_id": self.exchange_type_in.id, - "edi_exchange_state": "new", - "exchange_filename": "EDI_EXC_TEST-test_csv_" - "input-2020-10-21-10-00-00.csv", - } - self.assertRecordValues(record, [expected]) - self.assertEqual(record.record, self.partner) - self.assertEqual(record.edi_exchange_state, "new") + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + } + record = self.backend.create_record("test_csv_input", vals) + expected = { + "type_id": self.exchange_type_in.id, + "edi_exchange_state": "new", + "exchange_filename": "EDI_EXC_TEST-test_csv_" + "input-2020-10-21-10-00-00.csv", + } + self.assertRecordValues(record, [expected]) + self.assertEqual(record.record, self.partner) + self.assertEqual(record.edi_exchange_state, "new") From ffcf7cd1b773d6d35a3d7a7881daede58ea324fd Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sat, 21 Nov 2020 12:52:03 +0100 Subject: [PATCH 005/453] [IMP] edi: backend add generate_output Co-Authored-By: Simone Orsi --- edi_core_oca/models/edi_backend.py | 10 ++++++---- edi_core_oca/tests/test_edi_backend.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index b1148e4a4..2c9266990 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -4,9 +4,10 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import base64 import logging -from odoo import fields, models +from odoo import _, exceptions, fields, models, tools _logger = logging.getLogger(__name__) @@ -15,6 +16,10 @@ class EDIBackend(models.Model): """Generic backend to control EDI exchanges. Backends can be organized with types. + + The backend should be responsible for generating export records. + For each record it can generate or parse their values + depending on their direction (incoming, outgoing). """ _name = "edi.backend" @@ -63,8 +68,6 @@ def _create_record_prepare_values(self, type_code, values): res["type_id"] = export_type.id res["backend_id"] = self.id return res -<<<<<<< HEAD -======= def _get_exchange_type_domain(self, code): return [ @@ -124,4 +127,3 @@ def _validate_generate_output(self, exchange_record, force=False): def _generate_output(self, exchange_record, **kw): """To be implemented""" raise NotImplementedError() ->>>>>>> 6df23cc... fixup! edi: small fix/imp on models diff --git a/edi_core_oca/tests/test_edi_backend.py b/edi_core_oca/tests/test_edi_backend.py index 264209429..67287b7f8 100644 --- a/edi_core_oca/tests/test_edi_backend.py +++ b/edi_core_oca/tests/test_edi_backend.py @@ -2,6 +2,7 @@ # @author: Simone Orsi # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import mock import psycopg2 from freezegun import freeze_time @@ -51,3 +52,14 @@ def test_create_record(self): self.assertRecordValues(record, [expected]) self.assertEqual(record.record, self.partner) self.assertEqual(record.edi_exchange_state, "new") + + def test_generate_record_output(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + } + record = self.backend.create_record("test_csv_output", vals) + with mock.patch.object(type(self.backend), "_generate_output") as mocked: + mocked.return_value = "Any string" + self.backend.generate_output(record) + mocked.assert_called_with(record) From 176641d4afe8543cc607629232ad75345bae60bd Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 23 Nov 2020 08:28:33 +0100 Subject: [PATCH 006/453] edi: exchange type validate backend vs type --- edi_core_oca/models/edi_exchange_record.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index b87d6e477..22a8afd63 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -117,3 +117,17 @@ def name_get(self): name = "[{}] {} {}".format(rec.type_id.name, rec.record.name, dt) result.append((rec.id, name)) return result + + @api.constrains("backend_id", "type_id") + def _constrain_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!") + ) From bf52b4536230715899f4012a9c8d72073a26ab92 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 23 Nov 2020 11:39:38 +0100 Subject: [PATCH 007/453] edi bump 13.0.1.1.0 --- edi_core_oca/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py index 99e623375..1061f5ac8 100644 --- a/edi_core_oca/__manifest__.py +++ b/edi_core_oca/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base EDI Backend", "summary": """Base module to define EDI backends""", - "version": "13.0.1.0.0", + "version": "13.0.1.1.0", "development_status": "Alpha", "license": "AGPL-3", "author": "ACSONE,Odoo Community Association (OCA)", From d58591c6cdac253a65009c8d0cf94113118326e4 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 23 Nov 2020 10:40:32 +0000 Subject: [PATCH 008/453] [UPD] README.rst --- edi_core_oca/README.rst | 5 ++++- edi_core_oca/static/description/index.html | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/edi_core_oca/README.rst b/edi_core_oca/README.rst index 1f346ac67..01c1161f0 100644 --- a/edi_core_oca/README.rst +++ b/edi_core_oca/README.rst @@ -30,7 +30,9 @@ Base EDI backend. Provides following models: 1. EDI Backend, to centralize configuration -2. EDI Backend Type, to classify EDI backends (eg: UBL, GS1, pick-yours) +2. EDI Backend Type, to classify EDI backends (eg: UBL, GS1, e-invoice, pick-yours) +3. EDI Exchange Type, to define file types of exchange +4. EDI Exchange Record, to define a record exchanged between systems .. IMPORTANT:: This is an alpha version, the data model and design can change at any time without warning. @@ -64,6 +66,7 @@ Contributors ~~~~~~~~~~~~ * Simone Orsi +* Enric Tobella Maintainers ~~~~~~~~~~~ diff --git a/edi_core_oca/static/description/index.html b/edi_core_oca/static/description/index.html index e18661bba..4fcd09cd5 100644 --- a/edi_core_oca/static/description/index.html +++ b/edi_core_oca/static/description/index.html @@ -372,7 +372,9 @@

Base EDI Backend

Provides following models:

  1. EDI Backend, to centralize configuration
  2. -
  3. EDI Backend Type, to classify EDI backends (eg: UBL, GS1, pick-yours)
  4. +
  5. EDI Backend Type, to classify EDI backends (eg: UBL, GS1, e-invoice, pick-yours)
  6. +
  7. EDI Exchange Type, to define file types of exchange
  8. +
  9. EDI Exchange Record, to define a record exchanged between systems

Important

@@ -412,6 +414,7 @@

Authors

Contributors

From 06e727958b19b9b2ec8e5ee814ca3460c47d6c42 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sat, 21 Nov 2020 13:05:20 +0100 Subject: [PATCH 009/453] [IMP] edi: Set Send function Co-Authored-By: Simone Orsi --- edi_core_oca/models/edi_backend.py | 65 ++++++++++++++++++++++ edi_core_oca/models/edi_exchange_record.py | 18 ++++++ edi_core_oca/tests/test_edi_backend.py | 57 ++++++++++++++++++- 3 files changed, 139 insertions(+), 1 deletion(-) diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index 2c9266990..894378c7a 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -127,3 +127,68 @@ def _validate_generate_output(self, exchange_record, force=False): def _generate_output(self, exchange_record, **kw): """To be implemented""" raise NotImplementedError() + + def exchange_send(self, exchange_record): + """Send exchange file.""" + self.ensure_one() + exchange_record.ensure_one() + # In case already sent: skip sending and check the state + check = self._exchange_output_check(exchange_record) + if not check: + return False + try: + self._exchange_send(exchange_record) + except Exception as err: + if self.env.context.get("_edi_send_break_on_error"): + raise + error = str(err) + state = "output_error_on_send" + message = exchange_record._exchange_send_error_msg() + res = False + else: + message = exchange_record._exchange_sent_msg() + error = None + state = "output_sent" + res = True + finally: + exchange_record.edi_exchange_state = state + exchange_record.exchange_error = error + if message: + self._exchange_notify_record(exchange_record, message) + return res + + def _exchange_output_check(self, exchange_record): + if exchange_record.direction != "output": + raise exceptions.UserError( + _("Record ID=%d is not meant to be sent!") % exchange_record.id + ) + if not exchange_record.exchange_file: + raise exceptions.UserError( + _("Record ID=%d has no file to send!") % exchange_record.id + ) + return exchange_record.edi_exchange_state in [ + "output_pending", + "output_error_on_send", + ] + + def _exchange_send(self, exchange_record): + # TODO: maybe lookup for an `exchange_record.model` specific component 1st + component = self._get_component(usage="edi.send.%s" % self.backend_type_id.code) + if component: + return component.send(exchange_record) + raise NotImplementedError() + + def _exchange_notify_record(self, record, message, level="info"): + """Attach notification of exchange state to the original record.""" + if not hasattr(record.record, "message_post_with_view"): + return + record.record.message_post_with_view( + "edi.message_edi_exchange_link", + values={ + "backend": self, + "exchange_record": record, + "message": message, + "level": level, + }, + subtype_id=self.env.ref("mail.mt_note").id, + ) diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index 22a8afd63..7c02ce928 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -96,6 +96,12 @@ def _compute_exchange_filename(self): if rec.type_id.ack_needed and not rec.ack_filename: rec.ack_filename = rec.type_id._make_exchange_filename(rec, ack=True) + @api.depends("edi_exchange_state") + def _compute_exchanged_on(self): + for rec in self: + if rec.edi_exchange_state in ["output_sent"]: + rec.exchanged_on = fields.Datetime.now() + @api.constrains("edi_exchange_state") def _constrain_edi_exchange_state(self): for rec in self: @@ -131,3 +137,15 @@ def _constrain_backend(self): raise exceptions.ValidationError( _("Backend type must match with exchange type's backend type!") ) + + def _exchange_sent_msg(self): + return _("File %s sent") % self.exchange_filename + + def _exchange_send_error_msg(self): + return _("An error happened while sending. Please check exchange record info.") + + def action_exchange_send(self): + self.ensure_one() + if not self.direction == "output": + raise exceptions.UserError(_("An output record is required for sending!")) + return self.backend_id.exchange_send(self) diff --git a/edi_core_oca/tests/test_edi_backend.py b/edi_core_oca/tests/test_edi_backend.py index 67287b7f8..a8c49aa2b 100644 --- a/edi_core_oca/tests/test_edi_backend.py +++ b/edi_core_oca/tests/test_edi_backend.py @@ -2,16 +2,20 @@ # @author: Simone Orsi # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import base64 + import mock import psycopg2 from freezegun import freeze_time -from odoo.exceptions import ValidationError +from odoo.exceptions import UserError, ValidationError +from odoo.tests.common import tagged from odoo.tools import mute_logger from .common import EDIBackendCommonTestCase +@tagged("-at_install", "post_install") class EDIBackendTestCase(EDIBackendCommonTestCase): def test_type_code(self): btype = self.backend_type_model.create( @@ -63,3 +67,54 @@ def test_generate_record_output(self): mocked.return_value = "Any string" self.backend.generate_output(record) mocked.assert_called_with(record) + + def test_send_record(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + "edi_exchange_state": "output_pending", + "exchange_file": base64.b64encode(b"1234"), + } + record = self.backend.create_record("test_csv_output", vals) + self.assertFalse(record.exchanged_on) + with mock.patch.object(type(self.backend), "_exchange_send") as patch: + patch.return_value = "AAA" + record.action_exchange_send() + patch.assert_called() + self.assertEqual(record.edi_exchange_state, "output_sent") + self.assertTrue(record.exchanged_on) + + def test_send_record_with_error(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + "edi_exchange_state": "output_pending", + "exchange_file": base64.b64encode(b"1234"), + } + record = self.backend.create_record("test_csv_output", vals) + record.action_exchange_send() + self.assertEqual(record.edi_exchange_state, "output_error_on_send") + + def test_send_inbound_error(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + } + record = self.backend.create_record("test_csv_input", vals) + with mock.patch.object(type(self.backend), "_exchange_send") as patch: + patch.return_value = "AAA" + with self.assertRaises(UserError): + record.action_exchange_send() + patch.assert_not_called() + + def test_send_not_generated_record(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + } + record = self.backend.create_record("test_csv_output", vals) + with mock.patch.object(type(self.backend), "_exchange_send") as patch: + patch.return_value = "AAA" + with self.assertRaises(UserError): + record.action_exchange_send() + patch.assert_not_called() From e6dceb2937fb42e958f7ad8b43f6aa00b603b8f2 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 23 Nov 2020 19:26:56 +0100 Subject: [PATCH 010/453] edi bump 13.0.1.2.0 --- edi_core_oca/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py index 1061f5ac8..b9bdea371 100644 --- a/edi_core_oca/__manifest__.py +++ b/edi_core_oca/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base EDI Backend", "summary": """Base module to define EDI backends""", - "version": "13.0.1.1.0", + "version": "13.0.1.2.0", "development_status": "Alpha", "license": "AGPL-3", "author": "ACSONE,Odoo Community Association (OCA)", From 1ce9aa811ce972c45417c1e48d837d0dbbe0abb7 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Wed, 25 Nov 2020 11:17:33 +0000 Subject: [PATCH 011/453] [UPD] Update edi.pot --- edi_core_oca/i18n/edi.pot | 544 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 544 insertions(+) create mode 100644 edi_core_oca/i18n/edi.pot diff --git a/edi_core_oca/i18n/edi.pot b/edi_core_oca/i18n/edi.pot new file mode 100644 index 000000000..de20a7b9a --- /dev/null +++ b/edi_core_oca/i18n/edi.pot @@ -0,0 +1,544 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * edi +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: edi +#: model_terms:ir.ui.view,arch_db:edi.message_edi_exchange_link +msgid "EDI exchange:" +msgstr "" + +#. module: edi +#: model_terms:ir.ui.view,arch_db:edi.message_edi_exchange_link +msgid "Message:" +msgstr "" + +#. module: edi +#: model_terms:ir.ui.view,arch_db:edi.message_edi_exchange_link +msgid "State:" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__ack_received_on +msgid "ACK received on" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__ack_code +msgid "Ack Code" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__ack_file +msgid "Ack File" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__ack_file_ext +msgid "Ack File Ext" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__ack_filename +msgid "Ack Filename" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__ack_filename_pattern +msgid "Ack Filename Pattern" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__ack_name +msgid "Ack Name" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__ack_needed +msgid "Ack Needed" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi_exchange_record.py:0 +#, python-format +msgid "An error happened while sending. Please check exchange record info." +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi_exchange_record.py:0 +#, python-format +msgid "An output record is required for sending!" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: edi +#: model_terms:ir.ui.view,arch_db:edi.edi_exchange_type_view_search +msgid "Backend" +msgstr "" + +#. module: edi +#: model_terms:ir.ui.view,arch_db:edi.edi_backend_view_search +msgid "Backend Type" +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi_exchange_record.py:0 +#, python-format +msgid "Backend must match with exchange type's backend!" +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi_exchange_type.py:0 +#, python-format +msgid "Backend should respect backend type!" +msgstr "" + +#. module: edi +#: model:ir.model.constraint,message:edi.constraint_edi_backend_type_uniq_code +msgid "Backend type code must be unique!" +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi_exchange_record.py:0 +#, python-format +msgid "Backend type must match with exchange type's backend type!" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_backend_type__code +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__code +msgid "Code" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_backend__create_uid +#: model:ir.model.fields,field_description:edi.field_edi_backend_type__create_uid +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__create_uid +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__create_uid +msgid "Created by" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_backend__create_date +#: model:ir.model.fields,field_description:edi.field_edi_backend_type__create_date +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__create_date +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__create_date +msgid "Created on" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__direction +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__direction +msgid "Direction" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_backend__display_name +#: model:ir.model.fields,field_description:edi.field_edi_backend_type__display_name +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__display_name +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__display_name +msgid "Display Name" +msgstr "" + +#. module: edi +#: model:ir.actions.act_window,name:edi.act_open_edi_backend_view +#: model:ir.model,name:edi.model_edi_backend +#: model:ir.ui.menu,name:edi.menu_edi_backend +#: model:ir.ui.menu,name:edi.menu_edi_backend_root +#: model_terms:ir.ui.view,arch_db:edi.edi_backend_view_form +#: model_terms:ir.ui.view,arch_db:edi.edi_backend_view_search +#: model_terms:ir.ui.view,arch_db:edi.edi_backend_view_tree +msgid "EDI Backend" +msgstr "" + +#. module: edi +#: model:ir.actions.act_window,name:edi.act_open_edi_backend_type_view +#: model:ir.model,name:edi.model_edi_backend_type +#: model:ir.ui.menu,name:edi.menu_edi_backend_type +#: model:ir.ui.menu,name:edi.menu_edi_exchange +#: model_terms:ir.ui.view,arch_db:edi.edi_backend_type_view_form +#: model_terms:ir.ui.view,arch_db:edi.edi_backend_type_view_search +#: model_terms:ir.ui.view,arch_db:edi.edi_backend_type_view_tree +msgid "EDI Backend Type" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_backend__backend_type_id +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__backend_type_id +msgid "EDI Backend type" +msgstr "" + +#. module: edi +#: model:ir.actions.act_window,name:edi.act_open_edi_exchange_record_view +#: model:ir.ui.menu,name:edi.menu_edi_exchange_record +#: model_terms:ir.ui.view,arch_db:edi.edi_exchange_record_view_form +#: model_terms:ir.ui.view,arch_db:edi.edi_exchange_record_view_search +#: model_terms:ir.ui.view,arch_db:edi.edi_exchange_record_view_tree +msgid "EDI Exchange Record" +msgstr "" + +#. module: edi +#: model:ir.actions.act_window,name:edi.act_open_edi_exchange_type_view +#: model:ir.model,name:edi.model_edi_exchange_type +#: model:ir.ui.menu,name:edi.menu_edi_exchange_type +#: model_terms:ir.ui.view,arch_db:edi.edi_exchange_type_view_form +#: model_terms:ir.ui.view,arch_db:edi.edi_exchange_type_view_search +#: model_terms:ir.ui.view,arch_db:edi.edi_exchange_type_view_tree +msgid "EDI Exchange Type" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__type_id +msgid "EDI Exchange type" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__backend_id +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__backend_id +msgid "EDI backend" +msgstr "" + +#. module: edi +#: model:ir.model,name:edi.model_edi_exchange_record +msgid "EDI exchange Record" +msgstr "" + +#. module: edi +#: model:ir.model.fields.selection,name:edi.selection__edi_exchange_record__edi_exchange_state__input_processed_error +msgid "Error on process" +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi_backend.py:0 +#, python-format +msgid "Exchabge record ID=%d already has a file to process!" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__exchange_file +msgid "Exchange File" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__exchange_file_ext +msgid "Exchange File Ext" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__exchange_filename +msgid "Exchange Filename" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__exchange_filename_pattern +msgid "Exchange Filename Pattern" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__exchange_identification_code +msgid "Exchange Identification Code" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__exchange_error +msgid "Exchange error" +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi_backend.py:0 +#, python-format +msgid "Exchange record ID=%d is not file is not meant to b generated" +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi_backend.py:0 +#, python-format +msgid "" +"Exchange record ID=%d is not in draft state and has already an output value." +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__edi_exchange_state +msgid "Exchange state" +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi_exchange_record.py:0 +#, python-format +msgid "Exchange state must respect direction!" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__exchanged_on +msgid "Exchanged on" +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi_exchange_record.py:0 +#, python-format +msgid "File %s sent" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__message_channel_ids +msgid "Followers (Channels)" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: edi +#: model_terms:ir.ui.view,arch_db:edi.edi_exchange_record_view_search +#: model_terms:ir.ui.view,arch_db:edi.edi_exchange_type_view_search +msgid "Group By" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_backend__id +#: model:ir.model.fields,field_description:edi.field_edi_backend_type__id +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__id +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__id +msgid "ID" +msgstr "" + +#. module: edi +#: model:ir.model.fields,help:edi.field_edi_exchange_record__exchange_identification_code +msgid "Identification of the EDI, useful to search and join other documents" +msgstr "" + +#. module: edi +#: model:ir.model.fields,help:edi.field_edi_exchange_record__message_needaction +#: model:ir.model.fields,help:edi.field_edi_exchange_record__message_unread +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: edi +#: model:ir.model.fields,help:edi.field_edi_exchange_record__message_has_error +#: model:ir.model.fields,help:edi.field_edi_exchange_record__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: edi +#: model:ir.model.fields.selection,name:edi.selection__edi_exchange_type__direction__input +msgid "Input" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_backend____last_update +#: model:ir.model.fields,field_description:edi.field_edi_backend_type____last_update +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record____last_update +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type____last_update +msgid "Last Modified on" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_backend__write_uid +#: model:ir.model.fields,field_description:edi.field_edi_backend_type__write_uid +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__write_uid +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_backend__write_date +#: model:ir.model.fields,field_description:edi.field_edi_backend_type__write_date +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__write_date +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__write_date +msgid "Last Updated on" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__message_main_attachment_id +msgid "Main Attachment" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__message_ids +msgid "Messages" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__model +msgid "Model" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_backend__name +#: model:ir.model.fields,field_description:edi.field_edi_backend_type__name +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__name +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__name +msgid "Name" +msgstr "" + +#. module: edi +#: model:ir.model.fields.selection,name:edi.selection__edi_exchange_record__edi_exchange_state__new +msgid "New" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: edi +#: model:ir.model.fields,help:edi.field_edi_exchange_record__message_needaction_counter +msgid "Number of messages which requires an action" +msgstr "" + +#. module: edi +#: model:ir.model.fields,help:edi.field_edi_exchange_record__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: edi +#: model:ir.model.fields,help:edi.field_edi_exchange_record__message_unread_counter +msgid "Number of unread messages" +msgstr "" + +#. module: edi +#: model:ir.model.fields.selection,name:edi.selection__edi_exchange_type__direction__output +msgid "Output" +msgstr "" + +#. module: edi +#: model:ir.model.fields.selection,name:edi.selection__edi_exchange_record__edi_exchange_state__input_processed +msgid "Processed" +msgstr "" + +#. module: edi +#: model:ir.model.fields.selection,name:edi.selection__edi_exchange_record__edi_exchange_state__input_received +msgid "Received" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__res_id +msgid "Record ID" +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi_backend.py:0 +#, python-format +msgid "Record ID=%d has no file to send!" +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi_backend.py:0 +#, python-format +msgid "Record ID=%d is not meant to be sent!" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__message_has_sms_error +msgid "SMS Delivery error" +msgstr "" + +#. module: edi +#: model:ir.model.fields.selection,name:edi.selection__edi_exchange_record__edi_exchange_state__output_sent +msgid "Sent" +msgstr "" + +#. module: edi +#: model:ir.model.fields.selection,name:edi.selection__edi_exchange_record__edi_exchange_state__output_sent_and_error +msgid "Sent and error" +msgstr "" + +#. module: edi +#: model:ir.model.fields.selection,name:edi.selection__edi_exchange_record__edi_exchange_state__output_sent_and_processed +msgid "Sent and processed" +msgstr "" + +#. module: edi +#: model:ir.model.fields,help:edi.field_edi_exchange_record__exchanged_on +msgid "Sent or received on this date." +msgstr "" + +#. module: edi +#: model_terms:ir.ui.view,arch_db:edi.edi_exchange_record_view_search +msgid "State" +msgstr "" + +#. module: edi +#: model:ir.model.constraint,message:edi.constraint_edi_exchange_type_code_uniq +msgid "The code must be unique per backend" +msgstr "" + +#. module: edi +#: model_terms:ir.ui.view,arch_db:edi.edi_exchange_record_view_search +msgid "Type" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__message_unread +msgid "Unread Messages" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__message_unread_counter +msgid "Unread Messages Counter" +msgstr "" + +#. module: edi +#: model:ir.model.fields.selection,name:edi.selection__edi_exchange_record__edi_exchange_state__input_pending +msgid "Waiting to be received" +msgstr "" + +#. module: edi +#: model:ir.model.fields.selection,name:edi.selection__edi_exchange_record__edi_exchange_state__output_pending +msgid "Waiting to be sent" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: edi +#: model:ir.model.fields,help:edi.field_edi_exchange_record__website_message_ids +msgid "Website communication history" +msgstr "" + +#. module: edi +#: model:ir.model.fields.selection,name:edi.selection__edi_exchange_record__edi_exchange_state__output_error_on_send +msgid "error on send" +msgstr "" From 1aca79d7fca1e7330572a91809895ee03c8b5431 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 24 Nov 2020 18:03:04 +0100 Subject: [PATCH 012/453] edi: lookup components by convention --- edi_core_oca/models/edi_backend.py | 51 ++++++++++++++++++++++---- edi_core_oca/tests/test_edi_backend.py | 4 ++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index 894378c7a..3372da0f6 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -9,6 +9,8 @@ from odoo import _, exceptions, fields, models, tools +from odoo.addons.component.exception import NoComponentError + _logger = logging.getLogger(__name__) @@ -34,19 +36,27 @@ class EDIBackend(models.Model): ondelete="restrict", ) - def _get_component(self, safe=False, work_ctx=None, **kw): + def _get_component(self, usage_candidates, safe=True, work_ctx=None, **kw): """Retrieve components for current backend. + :param usage_candidates: + list of usage to try by priority. 1st found, 1st returned :param safe: boolean, if true does not break if component is not found :param work_ctx: dictionary with work context params :param kw: keyword args to lookup for components (eg: usage) """ + component = None work_ctx = work_ctx or {} with self.work_on(self._name, **work_ctx) as work: - if safe: - component = work.many_components(**kw) - return component[0] if component else None - return work.component(**kw) + for usage in usage_candidates: + component = work.many_components(usage=usage, **kw) + if component: + return component[0] + if not component and not safe: + raise NoComponentError( + "No componend found matching any of: {}".format(usage_candidates) + ) + return component def create_record(self, type_code, values): """Create an exchange record for current backend. @@ -125,9 +135,24 @@ def _validate_generate_output(self, exchange_record, force=False): ) def _generate_output(self, exchange_record, **kw): - """To be implemented""" + # TODO: maybe lookup for an `exchange_record.model` specific component 1st + candidates = self._generate_output_component_usage_candidates(exchange_record) + component = self._get_component(candidates) + if component: + return component.generate(exchange_record) raise NotImplementedError() + def _generate_output_component_usage_candidates(self, exchange_record): + """Retrieve candidates for exchange send components.""" + base_usage = "edi.output.{}".format(self.backend_type_id.code) + type_code = exchange_record.type_id.code + return [ + # specific for backend type and exchange type + base_usage + "." + type_code, + # specific for backend type + base_usage, + ] + def exchange_send(self, exchange_record): """Send exchange file.""" self.ensure_one() @@ -173,11 +198,23 @@ def _exchange_output_check(self, exchange_record): def _exchange_send(self, exchange_record): # TODO: maybe lookup for an `exchange_record.model` specific component 1st - component = self._get_component(usage="edi.send.%s" % self.backend_type_id.code) + candidates = self._exchange_send_component_usage_candidates(exchange_record) + component = self._get_component(candidates) if component: return component.send(exchange_record) raise NotImplementedError() + def _exchange_send_component_usage_candidates(self, exchange_record): + """Retrieve candidates for exchange send components.""" + base_usage = "edi.send.{}".format(self.backend_type_id.code) + type_code = exchange_record.exchange_type_id.code + return [ + # specific for backend type and exchange type + base_usage + "." + type_code, + # specific for backend type + base_usage, + ] + def _exchange_notify_record(self, record, message, level="info"): """Attach notification of exchange state to the original record.""" if not hasattr(record.record, "message_post_with_view"): diff --git a/edi_core_oca/tests/test_edi_backend.py b/edi_core_oca/tests/test_edi_backend.py index a8c49aa2b..a0cbc5940 100644 --- a/edi_core_oca/tests/test_edi_backend.py +++ b/edi_core_oca/tests/test_edi_backend.py @@ -118,3 +118,7 @@ def test_send_not_generated_record(self): with self.assertRaises(UserError): record.action_exchange_send() patch.assert_not_called() + + # TODO: + # 1. split output from incoming + # 2. test components lookup a ComponentRegistryCase From 54fba67922cc83708bd04769954472ea69912a97 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 24 Nov 2020 18:04:05 +0100 Subject: [PATCH 013/453] edi: exchange record backend mandatory --- edi_core_oca/models/edi_exchange_record.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index 7c02ce928..41c83770b 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -24,9 +24,7 @@ class EDIExchangeRecord(models.Model): auto_join=True, ) direction = fields.Selection(related="type_id.direction") - backend_id = fields.Many2one( - comodel_name="edi.backend", related="type_id.backend_id" - ) + backend_id = fields.Many2one(comodel_name="edi.backend", required=True) model = fields.Char(index=True, required=False, readonly=True) res_id = fields.Many2oneReference( string="Record ID", From eb544c929305825e6e6fee06589c83a8ced2c8f2 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Tue, 24 Nov 2020 18:04:29 +0100 Subject: [PATCH 014/453] edi: exchange type backend ondelete=set null --- edi_core_oca/models/edi_exchange_type.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edi_core_oca/models/edi_exchange_type.py b/edi_core_oca/models/edi_exchange_type.py index bee580dea..a7743c708 100644 --- a/edi_core_oca/models/edi_exchange_type.py +++ b/edi_core_oca/models/edi_exchange_type.py @@ -16,7 +16,7 @@ class EDIExchangeType(models.Model): _description = "EDI Exchange Type" backend_id = fields.Many2one( - string="EDI backend", comodel_name="edi.backend", ondelete="restrict", + string="EDI backend", comodel_name="edi.backend", ondelete="set null", ) backend_type_id = fields.Many2one( string="EDI Backend type", From 3219612fbac023e9e7a5525137672bcca172c305 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Thu, 26 Nov 2020 11:53:16 +0000 Subject: [PATCH 015/453] [UPD] Update edi.pot --- edi_core_oca/i18n/edi.pot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edi_core_oca/i18n/edi.pot b/edi_core_oca/i18n/edi.pot index de20a7b9a..3aa137491 100644 --- a/edi_core_oca/i18n/edi.pot +++ b/edi_core_oca/i18n/edi.pot @@ -91,6 +91,7 @@ msgid "Attachment Count" msgstr "" #. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__backend_id #: model_terms:ir.ui.view,arch_db:edi.edi_exchange_type_view_search msgid "Backend" msgstr "" @@ -212,7 +213,6 @@ msgid "EDI Exchange type" msgstr "" #. module: edi -#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__backend_id #: model:ir.model.fields,field_description:edi.field_edi_exchange_type__backend_id msgid "EDI backend" msgstr "" From 861b5d0febf11a3984ec874a30a7db95049b80e6 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 26 Nov 2020 12:03:27 +0000 Subject: [PATCH 016/453] edi 13.0.1.3.0 --- edi_core_oca/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py index b9bdea371..062eec1c3 100644 --- a/edi_core_oca/__manifest__.py +++ b/edi_core_oca/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base EDI Backend", "summary": """Base module to define EDI backends""", - "version": "13.0.1.2.0", + "version": "13.0.1.3.0", "development_status": "Alpha", "license": "AGPL-3", "author": "ACSONE,Odoo Community Association (OCA)", From 6c34c1b5b40498b7eab0dbff569d84aff7bd052a Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Wed, 25 Nov 2020 00:31:41 +0100 Subject: [PATCH 017/453] [IMP] edi: Add mixins --- edi_core_oca/models/__init__.py | 1 + .../models/edi_exchange_consumer_mixin.py | 47 +++++++++++++++++ edi_core_oca/models/edi_exchange_type.py | 2 +- edi_core_oca/tests/__init__.py | 1 + edi_core_oca/tests/models.py | 14 ++++++ edi_core_oca/tests/test_edi_mixin.py | 50 +++++++++++++++++++ 6 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 edi_core_oca/models/edi_exchange_consumer_mixin.py create mode 100644 edi_core_oca/tests/models.py create mode 100644 edi_core_oca/tests/test_edi_mixin.py diff --git a/edi_core_oca/models/__init__.py b/edi_core_oca/models/__init__.py index 29a82090c..9e918b7e4 100644 --- a/edi_core_oca/models/__init__.py +++ b/edi_core_oca/models/__init__.py @@ -1,4 +1,5 @@ from . import edi_backend from . import edi_backend_type from . import edi_exchange_record +from . import edi_exchange_consumer_mixin from . import edi_exchange_type diff --git a/edi_core_oca/models/edi_exchange_consumer_mixin.py b/edi_core_oca/models/edi_exchange_consumer_mixin.py new file mode 100644 index 000000000..ac9b291dc --- /dev/null +++ b/edi_core_oca/models/edi_exchange_consumer_mixin.py @@ -0,0 +1,47 @@ +# Copyright 2020 ACSONE SA +# Copyright 2020 Creu Blanca +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class EDIExchangeConsumerMixin(models.AbstractModel): + """Record that might have related EDI Exchange records + """ + + _name = "edi.exchange.consumer.mixin" + _description = "Abstract record where exchange records can be assigned" + + exchange_record_ids = fields.One2many( + "edi.exchange.record", + inverse_name="res_id", + domain=lambda r: [("model", "=", r._name)], + ) + exchange_record_count = fields.Integer(compute="_compute_exchange_record_count") + + def _has_exchange_record(self, exchange_type, backend, extra_domain=False): + """This function is useful when generating the configuration""" + domain = [ + ("model", "=", self._name), + ("res_id", "=", self.id), + ("backend_id", "=", backend.id), + ("type_id.code", "=", exchange_type), + ] + if extra_domain: + domain += extra_domain + return bool(self.env["edi.exchange.record"].search_count(domain)) + + @api.depends("exchange_record_ids") + def _compute_exchange_record_count(self): + for record in self: + record.exchange_record_count = len(record.exchange_record_ids) + + def action_view_edi_records(self): + self.ensure_one() + action = self.env.ref("edi.act_open_edi_exchange_record_view").read()[0] + action["domain"] = [("model", "=", self._name), ("res_id", "=", self.id)] + return action + + def _edi_generate_records(self): + raise NotImplementedError() diff --git a/edi_core_oca/models/edi_exchange_type.py b/edi_core_oca/models/edi_exchange_type.py index a7743c708..8d3d562e8 100644 --- a/edi_core_oca/models/edi_exchange_type.py +++ b/edi_core_oca/models/edi_exchange_type.py @@ -77,4 +77,4 @@ def _make_exchange_filename(self, record, ack=False): def _get_record_name(self, record): if hasattr(record.record, "_get_edi_exchange_record_name"): return record.record._get_edi_exchange_record_name(record, self) - return slugify(record.record.name) + return slugify(record.record.display_name) diff --git a/edi_core_oca/tests/__init__.py b/edi_core_oca/tests/__init__.py index 14b2d8538..01d545e46 100644 --- a/edi_core_oca/tests/__init__.py +++ b/edi_core_oca/tests/__init__.py @@ -1 +1,2 @@ from . import test_edi_backend +from . import test_edi_mixin diff --git a/edi_core_oca/tests/models.py b/edi_core_oca/tests/models.py new file mode 100644 index 000000000..13967335b --- /dev/null +++ b/edi_core_oca/tests/models.py @@ -0,0 +1,14 @@ +# Copyright 2020 Creu Blanca +# @author: Enric Tobella +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class EdiExchangeConsumerTest(models.Model): + _name = "edi.exchange.consumer.test" + _inherit = ["edi.exchange.consumer.mixin"] + _description = "Model used only for test" + + def _get_edi_exchange_record_name(self, exchange_record, exchange_type): + return self.id diff --git a/edi_core_oca/tests/test_edi_mixin.py b/edi_core_oca/tests/test_edi_mixin.py new file mode 100644 index 000000000..593f96073 --- /dev/null +++ b/edi_core_oca/tests/test_edi_mixin.py @@ -0,0 +1,50 @@ +# Copyright 2020 Creu Blanca +# @author: Enric Tobella +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo_test_helper import FakeModelLoader + +from .common import EDIBackendCommonTestCase + + +class EDIBackendTestCase(EDIBackendCommonTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Load fake models ->/ + cls.loader = FakeModelLoader(cls.env, cls.__module__) + cls.loader.backup_registry() + from .models import EdiExchangeConsumerTest + + cls.loader.update_registry((EdiExchangeConsumerTest,)) + cls.new_record = ( + cls.env["edi.exchange.consumer.test"] + .with_context(test_backend_id=cls.backend.id) + .create({}) + ) + cls.exchange_type_out.exchange_filename_pattern = "{record.id}" + + @classmethod + def tearDownClass(cls): + cls.loader.restore_registry() + super().tearDownClass() + + def setUp(self): + super().setUp() + + def test_mixin(self): + self.assertEqual(0, self.new_record.exchange_record_count) + vals = { + "model": self.new_record._name, + "res_id": self.new_record.id, + } + exchange_type = "test_csv_output" + exchange_record = self.backend.create_record(exchange_type, vals) + self.new_record.refresh() + self.assertEqual(1, self.new_record.exchange_record_count) + action = self.new_record.action_view_edi_records() + self.new_record.refresh() + self.assertEqual( + exchange_record, self.env["edi.exchange.record"].search(action["domain"]) + ) + self.new_record._has_exchange_record(exchange_type, self.backend) From 5ec41e2fc068f790eedd691c9a08f2a6815e62ca Mon Sep 17 00:00:00 2001 From: oca-travis Date: Thu, 26 Nov 2020 13:59:30 +0000 Subject: [PATCH 018/453] [UPD] Update edi.pot --- edi_core_oca/i18n/edi.pot | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/edi_core_oca/i18n/edi.pot b/edi_core_oca/i18n/edi.pot index 3aa137491..1b9142273 100644 --- a/edi_core_oca/i18n/edi.pot +++ b/edi_core_oca/i18n/edi.pot @@ -33,6 +33,11 @@ msgstr "" msgid "ACK received on" msgstr "" +#. module: edi +#: model:ir.model,name:edi.model_edi_exchange_consumer_mixin +msgid "Abstract record where exchange records can be assigned" +msgstr "" + #. module: edi #: model:ir.model.fields,field_description:edi.field_edi_exchange_type__ack_code msgid "Ack Code" @@ -155,6 +160,7 @@ msgstr "" #. module: edi #: model:ir.model.fields,field_description:edi.field_edi_backend__display_name #: model:ir.model.fields,field_description:edi.field_edi_backend_type__display_name +#: model:ir.model.fields,field_description:edi.field_edi_exchange_consumer_mixin__display_name #: model:ir.model.fields,field_description:edi.field_edi_exchange_record__display_name #: model:ir.model.fields,field_description:edi.field_edi_exchange_type__display_name msgid "Display Name" @@ -258,6 +264,16 @@ msgstr "" msgid "Exchange Identification Code" msgstr "" +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_consumer_mixin__exchange_record_ids +msgid "Exchange Record" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_consumer_mixin__exchange_record_count +msgid "Exchange Record Count" +msgstr "" + #. module: edi #: model:ir.model.fields,field_description:edi.field_edi_exchange_record__exchange_error msgid "Exchange error" @@ -322,6 +338,7 @@ msgstr "" #. module: edi #: model:ir.model.fields,field_description:edi.field_edi_backend__id #: model:ir.model.fields,field_description:edi.field_edi_backend_type__id +#: model:ir.model.fields,field_description:edi.field_edi_exchange_consumer_mixin__id #: model:ir.model.fields,field_description:edi.field_edi_exchange_record__id #: model:ir.model.fields,field_description:edi.field_edi_exchange_type__id msgid "ID" @@ -357,6 +374,7 @@ msgstr "" #. module: edi #: model:ir.model.fields,field_description:edi.field_edi_backend____last_update #: model:ir.model.fields,field_description:edi.field_edi_backend_type____last_update +#: model:ir.model.fields,field_description:edi.field_edi_exchange_consumer_mixin____last_update #: model:ir.model.fields,field_description:edi.field_edi_exchange_record____last_update #: model:ir.model.fields,field_description:edi.field_edi_exchange_type____last_update msgid "Last Modified on" From 0cee9432d70d47db54c5e25f2e9a385739afc55b Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 26 Nov 2020 14:10:02 +0000 Subject: [PATCH 019/453] edi 13.0.1.4.0 --- edi_core_oca/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py index 062eec1c3..a9f3b61b5 100644 --- a/edi_core_oca/__manifest__.py +++ b/edi_core_oca/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base EDI Backend", "summary": """Base module to define EDI backends""", - "version": "13.0.1.3.0", + "version": "13.0.1.4.0", "development_status": "Alpha", "license": "AGPL-3", "author": "ACSONE,Odoo Community Association (OCA)", From 9e4de7f0a58497694d4f32680e199f6359d94e59 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 26 Nov 2020 16:59:50 +0100 Subject: [PATCH 020/453] edi.backend: unify component usage get --- edi_core_oca/models/edi_backend.py | 39 +++++++++++++----------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index 3372da0f6..31af3b97c 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -136,15 +136,19 @@ def _validate_generate_output(self, exchange_record, force=False): def _generate_output(self, exchange_record, **kw): # TODO: maybe lookup for an `exchange_record.model` specific component 1st - candidates = self._generate_output_component_usage_candidates(exchange_record) - component = self._get_component(candidates) + candidates = self._get_component_usage_candidates(exchange_record, "output") + component = self._get_component( + candidates, work_ctx={"exchange_record": exchange_record} + ) if component: - return component.generate(exchange_record) - raise NotImplementedError() + return component.generate() + raise NotImplementedError("No handler for `_generate_output`") - def _generate_output_component_usage_candidates(self, exchange_record): - """Retrieve candidates for exchange send components.""" - base_usage = "edi.output.{}".format(self.backend_type_id.code) + def _get_component_usage_candidates(self, exchange_record, key): + """Retrieve usage candidates for components.""" + base_usage = "edi.{key}.{backend.backend_type_id.code}".format( + backend=self, key=key + ) type_code = exchange_record.type_id.code return [ # specific for backend type and exchange type @@ -198,22 +202,13 @@ def _exchange_output_check(self, exchange_record): def _exchange_send(self, exchange_record): # TODO: maybe lookup for an `exchange_record.model` specific component 1st - candidates = self._exchange_send_component_usage_candidates(exchange_record) - component = self._get_component(candidates) + candidates = self._get_component_usage_candidates(exchange_record, "send") + component = self._get_component( + candidates, work_ctx={"exchange_record": exchange_record} + ) if component: - return component.send(exchange_record) - raise NotImplementedError() - - def _exchange_send_component_usage_candidates(self, exchange_record): - """Retrieve candidates for exchange send components.""" - base_usage = "edi.send.{}".format(self.backend_type_id.code) - type_code = exchange_record.exchange_type_id.code - return [ - # specific for backend type and exchange type - base_usage + "." + type_code, - # specific for backend type - base_usage, - ] + return component.send() + raise NotImplementedError("No handler for `_exchange_send`") def _exchange_notify_record(self, record, message, level="info"): """Attach notification of exchange state to the original record.""" From d7a6a0dbdc4cc5b78f4ee5532744d0a5270df639 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 26 Nov 2020 17:02:16 +0100 Subject: [PATCH 021/453] edi.exchange.record: add handy methods to set/get file --- edi_core_oca/models/edi_exchange_record.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index 41c83770b..eacd22601 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -2,6 +2,8 @@ # @author Simone Orsi # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import base64 + from odoo import _, api, exceptions, fields, models @@ -114,6 +116,20 @@ def _constrain_edi_exchange_state(self): def record(self): return self.env[self.model].browse(self.res_id) + def _set_output(self, output_string, encoding="utf-8"): + """Handy method to no have to convert b64 back and forth.""" + self.ensure_one() + if not isinstance(output_string, bytes): + output_string = bytes(output_string, encoding) + self.exchange_file = base64.b64encode(output_string) + + def _get_output(self): + """Handy method to no have to convert b64 back and forth.""" + self.ensure_one() + if not self.exchange_file: + return "" + return base64.b64decode(self.exchange_file).decode() + def name_get(self): result = [] for rec in self: From 6d4035e45746c59fc29656368456824eb2f3fe49 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 26 Nov 2020 17:04:26 +0100 Subject: [PATCH 022/453] edi.backend: add cron to handle record state updates --- edi_core_oca/__init__.py | 1 + edi_core_oca/__manifest__.py | 1 + edi_core_oca/components/__init__.py | 1 + edi_core_oca/components/base.py | 42 ++++++++++ edi_core_oca/data/cron.xml | 35 ++++++++ edi_core_oca/models/edi_backend.py | 83 +++++++++++++++++-- edi_core_oca/models/edi_exchange_type.py | 5 +- edi_core_oca/tests/common.py | 39 ++++++++- edi_core_oca/tests/fake_components.py | 45 ++++++++++ edi_core_oca/tests/test_edi_backend_cron.py | 73 ++++++++++++++++ .../views/edi_exchange_type_views.xml | 1 + 11 files changed, 313 insertions(+), 13 deletions(-) create mode 100644 edi_core_oca/components/__init__.py create mode 100644 edi_core_oca/components/base.py create mode 100644 edi_core_oca/data/cron.xml create mode 100644 edi_core_oca/tests/fake_components.py create mode 100644 edi_core_oca/tests/test_edi_backend_cron.py diff --git a/edi_core_oca/__init__.py b/edi_core_oca/__init__.py index 0650744f6..f24d3e242 100644 --- a/edi_core_oca/__init__.py +++ b/edi_core_oca/__init__.py @@ -1 +1,2 @@ +from . import components from . import models diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py index a9f3b61b5..1d5d2cbb0 100644 --- a/edi_core_oca/__manifest__.py +++ b/edi_core_oca/__manifest__.py @@ -12,6 +12,7 @@ "maintainers": ["simahawk"], "depends": ["base_edi", "component", "mail"], "data": [ + "data/cron.xml", "security/ir_model_access.xml", "views/edi_backend_views.xml", "views/edi_backend_type_views.xml", diff --git a/edi_core_oca/components/__init__.py b/edi_core_oca/components/__init__.py new file mode 100644 index 000000000..0e4444933 --- /dev/null +++ b/edi_core_oca/components/__init__.py @@ -0,0 +1 @@ +from . import base diff --git a/edi_core_oca/components/base.py b/edi_core_oca/components/base.py new file mode 100644 index 000000000..7356a05b4 --- /dev/null +++ b/edi_core_oca/components/base.py @@ -0,0 +1,42 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.component.core import AbstractComponent + + +class EDIBackendComponentMixin(AbstractComponent): + + _name = "edi.component.mixin" + _collection = "edi.backend" + + @property + def exchange_record(self): + return self.work.exchange_record + + +class EDIBackendOutputComponentMixin(AbstractComponent): + + _name = "edi.component.output.mixin" + _inherit = "edi.component.mixin" + + def generate(self): + raise NotImplementedError() + + +class EDIBackendSendComponentMixin(AbstractComponent): + + _name = "edi.component.send.mixin" + _inherit = "edi.component.mixin" + + def send(self): + raise NotImplementedError() + + +class EDIBackendCheckComponentMixin(AbstractComponent): + + _name = "edi.component.check.mixin" + _inherit = "edi.component.mixin" + + def check(self): + raise NotImplementedError() diff --git a/edi_core_oca/data/cron.xml b/edi_core_oca/data/cron.xml new file mode 100644 index 000000000..ba5e17486 --- /dev/null +++ b/edi_core_oca/data/cron.xml @@ -0,0 +1,35 @@ + + + + EDI exchange check status + + + 1 + hours + -1 + + + code + model.search([])._cron_check_output_exchange_sync() + + + EDI exchange input status + + + 1 + hours + -1 + + + code + model.search([])._cron_check_input_exchange_sync() + + diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index 31af3b97c..8edb36c9f 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -58,6 +58,10 @@ def _get_component(self, usage_candidates, safe=True, work_ctx=None, **kw): ) return component + @property + def exchange_record_model(self): + return self.env["edi.exchange.record"] + def create_record(self, type_code, values): """Create an exchange record for current backend. @@ -67,7 +71,7 @@ def create_record(self, type_code, values): """ self.ensure_one() _values = self._create_record_prepare_values(type_code, values) - return self.env["edi.exchange.record"].create(_values) + return self.exchange_record_model.create(_values) def _create_record_prepare_values(self, type_code, values): res = values.copy() # do not pollute original dict @@ -162,7 +166,7 @@ def exchange_send(self, exchange_record): self.ensure_one() exchange_record.ensure_one() # In case already sent: skip sending and check the state - check = self._exchange_output_check(exchange_record) + check = self._output_check_send(exchange_record) if not check: return False try: @@ -170,7 +174,7 @@ def exchange_send(self, exchange_record): except Exception as err: if self.env.context.get("_edi_send_break_on_error"): raise - error = str(err) + error = repr(err) state = "output_error_on_send" message = exchange_record._exchange_send_error_msg() res = False @@ -186,7 +190,7 @@ def exchange_send(self, exchange_record): self._exchange_notify_record(exchange_record, message) return res - def _exchange_output_check(self, exchange_record): + def _output_check_send(self, exchange_record): if exchange_record.direction != "output": raise exceptions.UserError( _("Record ID=%d is not meant to be sent!") % exchange_record.id @@ -210,17 +214,80 @@ def _exchange_send(self, exchange_record): return component.send() raise NotImplementedError("No handler for `_exchange_send`") - def _exchange_notify_record(self, record, message, level="info"): + def _exchange_notify_record(self, exchange_record, message, level="info"): """Attach notification of exchange state to the original record.""" - if not hasattr(record.record, "message_post_with_view"): + if not hasattr(exchange_record.record, "message_post_with_view"): return - record.record.message_post_with_view( + exchange_record.record.message_post_with_view( "edi.message_edi_exchange_link", values={ "backend": self, - "exchange_record": record, + "exchange_record": exchange_record, "message": message, "level": level, }, subtype_id=self.env.ref("mail.mt_note").id, ) + + def _cron_check_output_exchange_sync(self, **kw): + for backend in self: + backend._check_output_exchange_sync(**kw) + + def _check_output_exchange_sync(self, skip_send=False): + """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. + """ + # Generate output files + new_records = self.exchange_record_model.search( + self._output_new_records_domain() + ) + _logger.info( + "EDI Exchange output sync: " "found %d new records to process.", + len(new_records), + ) + for rec in new_records: + self.generate_output(rec) + + if skip_send: + return + pending_records = self.exchange_record_model.search( + self._output_pending_records_domain() + ) + _logger.info( + "EDI Exchange output sync: " "found %d pending records to process.", + len(pending_records), + ) + for rec in pending_records: + if rec.edi_exchange_state == "output_pending": + self.exchange_send(rec) + else: + self._exchange_output_check_state(rec) + + def _output_new_records_domain(self): + return [ + ("type_id.exchange_file_auto_generate", "=", True), + ("type_id.direction", "=", "output"), + ("edi_exchange_state", "=", "new"), + ("exchange_file", "=", False), + ] + + def _output_pending_records_domain(self): + states = ("output_pending", "output_sent", "output_sent_and_error") + return [ + ("type_id.direction", "=", "output"), + ("edi_exchange_state", "in", states), + ] + + def _exchange_output_check_state(self, exchange_record): + # TODO: maybe lookup for an `exchange_record.model` specific component 1st + candidates = self._get_component_usage_candidates(exchange_record, "check") + component = self._get_component( + candidates, work_ctx={"exchange_record": exchange_record} + ) + if component: + return component.check() + raise NotImplementedError("No handler for `_exchange_output_check_state`") diff --git a/edi_core_oca/models/edi_exchange_type.py b/edi_core_oca/models/edi_exchange_type.py index 8d3d562e8..d51e91983 100644 --- a/edi_core_oca/models/edi_exchange_type.py +++ b/edi_core_oca/models/edi_exchange_type.py @@ -32,7 +32,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() - + 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. " + ) ack_needed = fields.Boolean() ack_name = fields.Char() ack_code = fields.Char() diff --git a/edi_core_oca/tests/common.py b/edi_core_oca/tests/common.py index 6e1b5f5cd..988ec53c5 100644 --- a/edi_core_oca/tests/common.py +++ b/edi_core_oca/tests/common.py @@ -7,18 +7,22 @@ from odoo.tests.common import SavepointCase, tagged +from odoo.addons.component.tests.common import ( + SavepointComponentCase, + SavepointComponentRegistryCase, +) -@tagged("-at_install", "post_install") -class EDIBackendCommonTestCase(SavepointCase): + +class EDIBackendTestMixin(object): @classmethod - def setUpClass(cls): - super().setUpClass() + def _setup_records(cls): cls.env = cls.env( context=dict( cls.env.context, tracking_disable=True, test_queue_job_no_delay=True ) ) cls.backend = cls._get_backend() + cls.backend_type_code = cls.backend.backend_type_id.code cls.backend_model = cls.env["edi.backend"] cls.backend_type_model = cls.env["edi.backend.type"] cls.exchange_type_in = cls._create_exchange_type( @@ -57,3 +61,30 @@ def _create_exchange_type(cls, **kw): } vals.update(kw) return model.create(vals) + + +@tagged("-at_install", "post_install") +class EDIBackendCommonTestCase(SavepointCase, EDIBackendTestMixin): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._setup_records() + + +@tagged("-at_install", "post_install") +class EDIBackendCommonComponentTestCase(SavepointComponentCase, EDIBackendTestMixin): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._setup_records() + + +@tagged("-at_install", "post_install") +class EDIBackendCommonComponentRegistryTestCase( + SavepointComponentRegistryCase, EDIBackendTestMixin +): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._setup_records() + cls._load_module_components(cls, "edi") diff --git a/edi_core_oca/tests/fake_components.py b/edi_core_oca/tests/fake_components.py new file mode 100644 index 000000000..459adbf6f --- /dev/null +++ b/edi_core_oca/tests/fake_components.py @@ -0,0 +1,45 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.component.core import Component + + +class FakeOutputGenerator(Component): + _name = "fake.output.generator" + _inherit = "edi.component.output.mixin" + _usage = "edi.output.demo_backend.test_csv_output" + + def generate(self): + if self.env.context.get("test_break_it"): + raise ValueError(self.env.context.get("test_break_it", "YOU BROKE IT!")) + return self.env.context.get( + "fake_output", "FAKE_OUTPUT: %d" % self.exchange_record.id + ) + + +class FakeOutputSender(Component): + _name = "fake.output.sender" + _inherit = "edi.component.send.mixin" + _usage = "edi.send.demo_backend.test_csv_output" + + def send(self): + if self.env.context.get("test_break_it"): + raise ValueError(self.env.context.get("test_break_it", "YOU BROKE IT!")) + return self.env.context.get( + "fake_output", "FAKE_OUTPUT: %d" % self.exchange_record.id + ) + + +class FakeOutputChecker(Component): + _name = "fake.output.checker" + _inherit = "edi.component.check.mixin" + _usage = "edi.check.demo_backend.test_csv_output" + + def check(self): + if self.env.context.get("test_break_it"): + raise ValueError(self.env.context.get("test_break_it", "YOU BROKE IT!")) + update_values = self.env.context.get("test_output_check_update_values") + if update_values: + self.exchange_record.write(update_values) + return True diff --git a/edi_core_oca/tests/test_edi_backend_cron.py b/edi_core_oca/tests/test_edi_backend_cron.py new file mode 100644 index 000000000..e6a0abf0f --- /dev/null +++ b/edi_core_oca/tests/test_edi_backend_cron.py @@ -0,0 +1,73 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import tagged + +from .common import EDIBackendCommonComponentRegistryTestCase +from .fake_components import FakeOutputChecker, FakeOutputGenerator, FakeOutputSender + + +@tagged("-at_install", "post_install") +class EDIBackendTestCase(EDIBackendCommonComponentRegistryTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._build_components( + cls, FakeOutputGenerator, FakeOutputSender, FakeOutputChecker + ) + cls.partner2 = cls.env.ref("base.res_partner_10") + cls.partner3 = cls.env.ref("base.res_partner_12") + cls.record1 = cls.backend.create_record( + "test_csv_output", {"model": cls.partner._name, "res_id": cls.partner.id} + ) + cls.record2 = cls.backend.create_record( + "test_csv_output", {"model": cls.partner._name, "res_id": cls.partner2.id} + ) + cls.record3 = cls.backend.create_record( + "test_csv_output", {"model": cls.partner._name, "res_id": cls.partner3.id} + ) + cls.records = cls.record1 + cls.record1 + cls.record3 + + def test_generate_output_new_no_auto(self): + # No content ready to be sent, no auto-generate, nothing happens + for rec in self.records: + self.assertEqual(rec.edi_exchange_state, "new") + self.backend._cron_check_output_exchange_sync() + for rec in self.records: + self.assertEqual(rec.edi_exchange_state, "new") + + def test_generate_output_new_auto_skip_send(self): + self.exchange_type_out.exchange_file_auto_generate = True + # No content ready to be sent, will get the content but not send it + for rec in self.records: + self.assertEqual(rec.edi_exchange_state, "new") + self.backend._cron_check_output_exchange_sync(skip_send=True) + for rec in self.records: + self.assertEqual(rec.edi_exchange_state, "output_pending") + self.assertEqual(rec._get_output(), "FAKE_OUTPUT: %s" % rec.id) + + def test_generate_output_new_auto_send(self): + self.exchange_type_out.exchange_file_auto_generate = True + # No content ready to be sent, will get the content and send it + for rec in self.records: + self.assertEqual(rec.edi_exchange_state, "new") + self.backend._cron_check_output_exchange_sync() + for rec in self.records: + self.assertEqual(rec.edi_exchange_state, "output_sent") + self.assertEqual(rec._get_output(), "FAKE_OUTPUT: %s" % rec.id) + + def test_generate_output_output_ready_auto_send(self): + # No content ready to be sent, will get the content and send it + for rec in self.records: + self.assertEqual(rec.edi_exchange_state, "new") + self.record1._set_output("READY") + self.record1.edi_exchange_state = "output_sent" + self.backend.with_context( + test_output_check_update_values={ + "edi_exchange_state": "output_sent_and_processed" + } + )._cron_check_output_exchange_sync() + 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_core_oca/views/edi_exchange_type_views.xml b/edi_core_oca/views/edi_exchange_type_views.xml index b3a5a0f94..1c55630eb 100644 --- a/edi_core_oca/views/edi_exchange_type_views.xml +++ b/edi_core_oca/views/edi_exchange_type_views.xml @@ -31,6 +31,7 @@ + From 8e9acfa99bea8dcca57be4fb1e267994f6cd9337 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Fri, 27 Nov 2020 11:52:58 +0000 Subject: [PATCH 023/453] [UPD] Update edi.pot --- edi_core_oca/i18n/edi.pot | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/edi_core_oca/i18n/edi.pot b/edi_core_oca/i18n/edi.pot index 1b9142273..a5c76699d 100644 --- a/edi_core_oca/i18n/edi.pot +++ b/edi_core_oca/i18n/edi.pot @@ -95,6 +95,13 @@ msgstr "" msgid "Attachment Count" msgstr "" +#. module: edi +#: model:ir.model.fields,help:edi.field_edi_exchange_type__exchange_file_auto_generate +msgid "" +"Auto generate output for records missing their payload. If active, a cron " +"will take care of generating the output when not set yet. " +msgstr "" + #. module: edi #: model:ir.model.fields,field_description:edi.field_edi_exchange_record__backend_id #: model_terms:ir.ui.view,arch_db:edi.edi_exchange_type_view_search @@ -228,6 +235,20 @@ msgstr "" msgid "EDI exchange Record" msgstr "" +#. module: edi +#: model:ir.actions.server,name:edi.cron_edi_backend_check_output_exchange_ir_actions_server +#: model:ir.cron,cron_name:edi.cron_edi_backend_check_output_exchange +#: model:ir.cron,name:edi.cron_edi_backend_check_output_exchange +msgid "EDI exchange check status" +msgstr "" + +#. module: edi +#: model:ir.actions.server,name:edi.cron_edi_backend_check_input_exchange_ir_actions_server +#: model:ir.cron,cron_name:edi.cron_edi_backend_check_input_exchange +#: model:ir.cron,name:edi.cron_edi_backend_check_input_exchange +msgid "EDI exchange input status" +msgstr "" + #. module: edi #: model:ir.model.fields.selection,name:edi.selection__edi_exchange_record__edi_exchange_state__input_processed_error msgid "Error on process" @@ -244,6 +265,11 @@ msgstr "" msgid "Exchange File" msgstr "" +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_type__exchange_file_auto_generate +msgid "Exchange File Auto Generate" +msgstr "" + #. module: edi #: model:ir.model.fields,field_description:edi.field_edi_exchange_type__exchange_file_ext msgid "Exchange File Ext" From 36c6695b0ade35953bd67e973a0dfd99c9755696 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 27 Nov 2020 12:03:23 +0000 Subject: [PATCH 024/453] edi 13.0.1.5.0 --- edi_core_oca/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py index 1d5d2cbb0..dc3025e2f 100644 --- a/edi_core_oca/__manifest__.py +++ b/edi_core_oca/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base EDI Backend", "summary": """Base module to define EDI backends""", - "version": "13.0.1.4.0", + "version": "13.0.1.5.0", "development_status": "Alpha", "license": "AGPL-3", "author": "ACSONE,Odoo Community Association (OCA)", From 145c907738166b6d4ea98cdd23a512f089f1ea97 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 27 Nov 2020 10:36:55 +0100 Subject: [PATCH 025/453] edi: add specific events, improve record msg lookup --- edi_core_oca/__manifest__.py | 2 +- edi_core_oca/models/edi_backend.py | 45 +++++++++++++++++++++- edi_core_oca/models/edi_exchange_record.py | 21 +++++++--- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py index dc3025e2f..a9e160293 100644 --- a/edi_core_oca/__manifest__.py +++ b/edi_core_oca/__manifest__.py @@ -10,7 +10,7 @@ "license": "AGPL-3", "author": "ACSONE,Odoo Community Association (OCA)", "maintainers": ["simahawk"], - "depends": ["base_edi", "component", "mail"], + "depends": ["base_edi", "component", "component_event", "mail"], "data": [ "data/cron.xml", "security/ir_model_access.xml", diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index 8edb36c9f..59e270be2 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -176,10 +176,11 @@ def exchange_send(self, exchange_record): raise error = repr(err) state = "output_error_on_send" - message = exchange_record._exchange_send_error_msg() + message = exchange_record._exchange_status_message("send_ko") res = False else: - message = exchange_record._exchange_sent_msg() + # TODO: maybe the send handler should return desired message and state + message = exchange_record._exchange_status_message("send_ok") error = None state = "output_sent" res = True @@ -291,3 +292,43 @@ def _exchange_output_check_state(self, exchange_record): if component: return component.check() raise NotImplementedError("No handler for `_exchange_output_check_state`") + + def _trigger_edi_event_make_name(self, exchange_record, suffix=None): + return "on_edi_{type.code}_{exchange_record.edi_exchange_state}{suffix}".format( + exchange_record=exchange_record, + type=exchange_record.type_id, + suffix=("_" + suffix) if suffix else "", + ) + + def _trigger_edi_event(self, exchange_record, name=None, suffix=None): + """Trigger a component event linked to this backend and edi exchange.""" + name = name or self._trigger_edi_event_make_name(exchange_record, suffix=suffix) + self._event(name).notify(exchange_record) + + def _notify_done(self, exchange_record): + self._exchange_notify_record( + exchange_record, exchange_record._exchange_status_message("process_ok") + ) + self._trigger_edi_event(exchange_record) + + def _notify_error(self, exchange_record, message_key): + self._exchange_notify_record( + exchange_record, + exchange_record._exchange_status_message(message_key), + level="error", + ) + self._trigger_edi_event(exchange_record) + + def _notify_ack_received(self, exchange_record): + self._exchange_notify_record( + exchange_record, exchange_record._exchange_status_message("ack_received") + ) + self._trigger_edi_event(exchange_record, suffix="ack_received") + + def _notify_ack_missing(self, exchange_record): + self._exchange_notify_record( + exchange_record, + exchange_record._exchange_status_message("ack_missing"), + level="warning", + ) + self._trigger_edi_event(exchange_record, suffix="ack_missing") diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index eacd22601..8c66fdd71 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -152,11 +152,22 @@ def _constrain_backend(self): _("Backend type must match with exchange type's backend type!") ) - def _exchange_sent_msg(self): - return _("File %s sent") % self.exchange_filename - - def _exchange_send_error_msg(self): - return _("An error happened while sending. Please check exchange record info.") + @property + def _exchange_status_messages(self): + return { + # status: message + "send_ok": _("File %s sent") % self.exchange_filename, + "send_ko": _( + "An error happened while sending. Please check exchange record info." + ), + "process_ok": _("File %s processed successfully ") % self.exchange_filename, + "process_ko": _("File %s processed with errors") % self.exchange_filename, + "ack_received": _("ACK file received."), + "ack_missing": _("ACK file is required for this exchange but not found."), + } + + def _exchange_status_message(self, key): + return self._exchange_status_messages[key] def action_exchange_send(self): self.ensure_one() From 340126798c96eff36a511bc38a8dccaf22bc9de0 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 27 Nov 2020 10:37:51 +0100 Subject: [PATCH 026/453] edi: pass backend to component ctx --- edi_core_oca/components/base.py | 4 ++++ edi_core_oca/models/edi_backend.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/edi_core_oca/components/base.py b/edi_core_oca/components/base.py index 7356a05b4..8546574c8 100644 --- a/edi_core_oca/components/base.py +++ b/edi_core_oca/components/base.py @@ -14,6 +14,10 @@ class EDIBackendComponentMixin(AbstractComponent): def exchange_record(self): return self.work.exchange_record + @property + def backend(self): + return self.work.backend + class EDIBackendOutputComponentMixin(AbstractComponent): diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index 59e270be2..b811609b7 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -47,6 +47,8 @@ def _get_component(self, usage_candidates, safe=True, work_ctx=None, **kw): """ component = None work_ctx = work_ctx or {} + if "backend" not in work_ctx: + work_ctx["backend"] = self with self.work_on(self._name, **work_ctx) as work: for usage in usage_candidates: component = work.many_components(usage=usage, **kw) From 113fa2a9c46b9c7575cc1c6c2f691f3b18f3c284 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 27 Nov 2020 10:39:57 +0100 Subject: [PATCH 027/453] edi.exchange.record: improve set/get file --- edi_core_oca/models/edi_exchange_record.py | 12 +++++++----- edi_core_oca/tests/test_edi_backend_cron.py | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index 8c66fdd71..c0c022120 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -116,19 +116,21 @@ def _constrain_edi_exchange_state(self): def record(self): return self.env[self.model].browse(self.res_id) - def _set_output(self, output_string, encoding="utf-8"): + def _set_file_content( + self, output_string, encoding="utf-8", field_name="exchange_file" + ): """Handy method to no have to convert b64 back and forth.""" self.ensure_one() if not isinstance(output_string, bytes): output_string = bytes(output_string, encoding) - self.exchange_file = base64.b64encode(output_string) + self[field_name] = base64.b64encode(output_string) - def _get_output(self): + def _get_file_content(self, field_name="exchange_file"): """Handy method to no have to convert b64 back and forth.""" self.ensure_one() - if not self.exchange_file: + if not self[field_name]: return "" - return base64.b64decode(self.exchange_file).decode() + return base64.b64decode(self[field_name]).decode() def name_get(self): result = [] diff --git a/edi_core_oca/tests/test_edi_backend_cron.py b/edi_core_oca/tests/test_edi_backend_cron.py index e6a0abf0f..2736239b9 100644 --- a/edi_core_oca/tests/test_edi_backend_cron.py +++ b/edi_core_oca/tests/test_edi_backend_cron.py @@ -45,7 +45,7 @@ def test_generate_output_new_auto_skip_send(self): self.backend._cron_check_output_exchange_sync(skip_send=True) for rec in self.records: self.assertEqual(rec.edi_exchange_state, "output_pending") - self.assertEqual(rec._get_output(), "FAKE_OUTPUT: %s" % rec.id) + self.assertEqual(rec._get_file_content(), "FAKE_OUTPUT: %s" % rec.id) def test_generate_output_new_auto_send(self): self.exchange_type_out.exchange_file_auto_generate = True @@ -55,13 +55,13 @@ def test_generate_output_new_auto_send(self): self.backend._cron_check_output_exchange_sync() for rec in self.records: self.assertEqual(rec.edi_exchange_state, "output_sent") - self.assertEqual(rec._get_output(), "FAKE_OUTPUT: %s" % rec.id) + self.assertEqual(rec._get_file_content(), "FAKE_OUTPUT: %s" % rec.id) def test_generate_output_output_ready_auto_send(self): # No content ready to be sent, will get the content and send it for rec in self.records: self.assertEqual(rec.edi_exchange_state, "new") - self.record1._set_output("READY") + self.record1._set_file_content("READY") self.record1.edi_exchange_state = "output_sent" self.backend.with_context( test_output_check_update_values={ From 0fad5fcc31f9ecb2e2cbca706bee2cbf7e75e87b Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 27 Nov 2020 10:41:55 +0100 Subject: [PATCH 028/453] edi.backend: improve exception handling --- edi_core_oca/models/edi_backend.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index b811609b7..e8ecaa502 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -173,7 +173,7 @@ def exchange_send(self, exchange_record): return False try: self._exchange_send(exchange_record) - except Exception as err: + except self._swallable_exceptions() as err: if self.env.context.get("_edi_send_break_on_error"): raise error = repr(err) @@ -193,6 +193,15 @@ def exchange_send(self, exchange_record): self._exchange_notify_record(exchange_record, message) return res + def _swallable_exceptions(self): + # TODO: improve this list + return ( + ValueError, + FileNotFoundError, + exceptions.UserError, + exceptions.ValidationError, + ) + def _output_check_send(self, exchange_record): if exchange_record.direction != "output": raise exceptions.UserError( From a6abee7a0df1f4162ff42534a6390891226c37d9 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 27 Nov 2020 10:42:34 +0100 Subject: [PATCH 029/453] edi.backend: add docstrings and TODO --- edi_core_oca/components/base.py | 4 ++++ edi_core_oca/models/edi_backend.py | 1 + edi_core_oca/models/edi_exchange_record.py | 1 + 3 files changed, 6 insertions(+) diff --git a/edi_core_oca/components/base.py b/edi_core_oca/components/base.py index 8546574c8..0b7208379 100644 --- a/edi_core_oca/components/base.py +++ b/edi_core_oca/components/base.py @@ -20,6 +20,8 @@ def backend(self): class EDIBackendOutputComponentMixin(AbstractComponent): + """Generate output content. + """ _name = "edi.component.output.mixin" _inherit = "edi.component.mixin" @@ -29,6 +31,8 @@ def generate(self): class EDIBackendSendComponentMixin(AbstractComponent): + """Send output records. + """ _name = "edi.component.send.mixin" _inherit = "edi.component.mixin" diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index e8ecaa502..ca136ccb9 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -163,6 +163,7 @@ def _get_component_usage_candidates(self, exchange_record, key): base_usage, ] + # TODO: add job config for these methods def exchange_send(self, exchange_record): """Send exchange file.""" self.ensure_one() diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index c0c022120..510c87bf8 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -17,6 +17,7 @@ class EDIExchangeRecord(models.Model): _description = "EDI exchange Record" _order = "exchanged_on desc" + # TODO: add unique identifier using a sequence name = fields.Char(compute="_compute_name") type_id = fields.Many2one( string="EDI Exchange type", From 896017fe0300e1f4a61398b564ca9780a323be60 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Sat, 28 Nov 2020 18:28:57 +0100 Subject: [PATCH 030/453] edi: improve and fix tests --- edi_core_oca/models/edi_backend.py | 14 ++- edi_core_oca/models/edi_exchange_record.py | 3 +- edi_core_oca/tests/__init__.py | 6 +- edi_core_oca/tests/common.py | 18 ++- edi_core_oca/tests/fake_components.py | 3 + edi_core_oca/tests/test_backend_base.py | 26 ++++ edi_core_oca/tests/test_backend_output.py | 102 ++++++++++++++++ edi_core_oca/tests/test_edi_backend.py | 124 -------------------- edi_core_oca/tests/test_edi_backend_cron.py | 3 - edi_core_oca/tests/test_record.py | 38 ++++++ edi_core_oca/tests/test_type.py | 25 ++++ 11 files changed, 224 insertions(+), 138 deletions(-) create mode 100644 edi_core_oca/tests/test_backend_base.py create mode 100644 edi_core_oca/tests/test_backend_output.py delete mode 100644 edi_core_oca/tests/test_edi_backend.py create mode 100644 edi_core_oca/tests/test_record.py create mode 100644 edi_core_oca/tests/test_type.py diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index ca136ccb9..314f2a9c9 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -172,6 +172,9 @@ def exchange_send(self, exchange_record): check = self._output_check_send(exchange_record) if not check: return False + state = exchange_record.edi_exchange_state + error = False + message = None try: self._exchange_send(exchange_record) except self._swallable_exceptions() as err: @@ -188,8 +191,15 @@ def exchange_send(self, exchange_record): state = "output_sent" res = True finally: - exchange_record.edi_exchange_state = state - exchange_record.exchange_error = error + exchange_record.write( + { + "edi_exchange_state": state, + "exchange_error": error, + # FIXME: this should come from _compute_exchanged_on + # but somehow it's failing in send tests (in record tests it works). + "exchanged_on": fields.Datetime.now(), + } + ) if message: self._exchange_notify_record(exchange_record, message) return res diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index 510c87bf8..90e7b91f7 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -45,6 +45,7 @@ class EDIExchangeRecord(models.Model): help="Sent or received on this date.", compute="_compute_exchanged_on", store=True, + readonly=False, ) # TODO: use sequence and make it unique exchange_identification_code = fields.Char( @@ -174,6 +175,4 @@ def _exchange_status_message(self, key): def action_exchange_send(self): self.ensure_one() - if not self.direction == "output": - raise exceptions.UserError(_("An output record is required for sending!")) return self.backend_id.exchange_send(self) diff --git a/edi_core_oca/tests/__init__.py b/edi_core_oca/tests/__init__.py index 01d545e46..4fea13d41 100644 --- a/edi_core_oca/tests/__init__.py +++ b/edi_core_oca/tests/__init__.py @@ -1,2 +1,4 @@ -from . import test_edi_backend -from . import test_edi_mixin +from . import test_type +from . import test_record +from . import test_backend_base +from . import test_backend_output diff --git a/edi_core_oca/tests/common.py b/edi_core_oca/tests/common.py index 988ec53c5..062c08ee3 100644 --- a/edi_core_oca/tests/common.py +++ b/edi_core_oca/tests/common.py @@ -15,12 +15,17 @@ class EDIBackendTestMixin(object): @classmethod - def _setup_records(cls): - cls.env = cls.env( - context=dict( - cls.env.context, tracking_disable=True, test_queue_job_no_delay=True - ) + def _setup_context(cls): + return dict( + cls.env.context, tracking_disable=True, test_queue_job_no_delay=True ) + + @classmethod + def _setup_env(cls): + cls.env = cls.env(context=cls._setup_context()) + + @classmethod + def _setup_records(cls): cls.backend = cls._get_backend() cls.backend_type_code = cls.backend.backend_type_id.code cls.backend_model = cls.env["edi.backend"] @@ -68,6 +73,7 @@ class EDIBackendCommonTestCase(SavepointCase, EDIBackendTestMixin): @classmethod def setUpClass(cls): super().setUpClass() + cls._setup_env() cls._setup_records() @@ -76,6 +82,7 @@ class EDIBackendCommonComponentTestCase(SavepointComponentCase, EDIBackendTestMi @classmethod def setUpClass(cls): super().setUpClass() + cls._setup_env() cls._setup_records() @@ -86,5 +93,6 @@ class EDIBackendCommonComponentRegistryTestCase( @classmethod def setUpClass(cls): super().setUpClass() + cls._setup_env() cls._setup_records() cls._load_module_components(cls, "edi") diff --git a/edi_core_oca/tests/fake_components.py b/edi_core_oca/tests/fake_components.py index 459adbf6f..ef9069c3c 100644 --- a/edi_core_oca/tests/fake_components.py +++ b/edi_core_oca/tests/fake_components.py @@ -23,9 +23,12 @@ class FakeOutputSender(Component): _inherit = "edi.component.send.mixin" _usage = "edi.send.demo_backend.test_csv_output" + FAKE_SEND_COLLECTOR = [] + def send(self): if self.env.context.get("test_break_it"): raise ValueError(self.env.context.get("test_break_it", "YOU BROKE IT!")) + self.FAKE_SEND_COLLECTOR.append(self.exchange_record._get_file_content()) return self.env.context.get( "fake_output", "FAKE_OUTPUT: %d" % self.exchange_record.id ) diff --git a/edi_core_oca/tests/test_backend_base.py b/edi_core_oca/tests/test_backend_base.py new file mode 100644 index 000000000..e7847d160 --- /dev/null +++ b/edi_core_oca/tests/test_backend_base.py @@ -0,0 +1,26 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from freezegun import freeze_time + +from .common import EDIBackendCommonTestCase + + +class EDIBackendTestCase(EDIBackendCommonTestCase): + @freeze_time("2020-10-21 10:00:00") + def test_create_record(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + } + record = self.backend.create_record("test_csv_input", vals) + expected = { + "type_id": self.exchange_type_in.id, + "edi_exchange_state": "new", + "exchange_filename": "EDI_EXC_TEST-test_csv_" + "input-2020-10-21-10-00-00.csv", + } + self.assertRecordValues(record, [expected]) + self.assertEqual(record.record, self.partner) + self.assertEqual(record.edi_exchange_state, "new") diff --git a/edi_core_oca/tests/test_backend_output.py b/edi_core_oca/tests/test_backend_output.py new file mode 100644 index 000000000..2d36ac3b1 --- /dev/null +++ b/edi_core_oca/tests/test_backend_output.py @@ -0,0 +1,102 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import mock +from freezegun import freeze_time + +from odoo import fields +from odoo.exceptions import UserError + +from .common import EDIBackendCommonComponentRegistryTestCase +from .fake_components import FakeOutputChecker, FakeOutputGenerator, FakeOutputSender + + +class EDIBackendTestCase(EDIBackendCommonComponentRegistryTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._build_components( + # TODO: test all components lookup + cls, + FakeOutputGenerator, + FakeOutputSender, + FakeOutputChecker, + ) + 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() + FakeOutputSender.FAKE_SEND_COLLECTOR = [] + + def test_generate_record_output(self): + self.backend.with_context(fake_output="yeah!").generate_output(self.record) + self.assertEqual(self.record._get_file_content(), "yeah!") + + def test_send_record(self): + self.record.write({"edi_exchange_state": "output_pending"}) + self.record._set_file_content("TEST %d" % self.record.id) + self.assertFalse(self.record.exchanged_on) + with freeze_time("2020-10-21 10:00:00"): + self.record.action_exchange_send() + self.assertIn( + self.record._get_file_content(), FakeOutputSender.FAKE_SEND_COLLECTOR + ) + self.assertRecordValues(self.record, [{"edi_exchange_state": "output_sent"}]) + self.assertEqual( + fields.Datetime.to_string(self.record.exchanged_on), "2020-10-21 10:00:00" + ) + + def test_send_record_with_error(self): + self.record.write({"edi_exchange_state": "output_pending"}) + self.record._set_file_content("TEST %d" % self.record.id) + self.assertFalse(self.record.exchanged_on) + self.record.with_context( + test_break_it="OOPS! Something went wrong :(" + ).action_exchange_send() + self.assertNotIn( + self.record._get_file_content(), FakeOutputSender.FAKE_SEND_COLLECTOR + ) + self.assertRecordValues( + self.record, + [ + { + "edi_exchange_state": "output_error_on_send", + "exchange_error": "ValueError('OOPS! Something went wrong :(',)", + } + ], + ) + + def test_send_invalid_direction(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + } + record = self.backend.create_record("test_csv_input", vals) + with mock.patch.object(type(self.backend), "_exchange_send") as mocked: + mocked.return_value = "AAA" + with self.assertRaises(UserError) as err: + record.action_exchange_send() + self.assertEqual( + err.exception.name, "Record ID=%d is not meant to be sent!" % record.id + ) + mocked.assert_not_called() + + def test_send_not_generated_record(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + } + record = self.backend.create_record("test_csv_output", vals) + with mock.patch.object(type(self.backend), "_exchange_send") as mocked: + mocked.return_value = "AAA" + with self.assertRaises(UserError) as err: + record.action_exchange_send() + self.assertEqual( + err.exception.name, "Record ID=%d has no file to send!" % record.id + ) + mocked.assert_not_called() diff --git a/edi_core_oca/tests/test_edi_backend.py b/edi_core_oca/tests/test_edi_backend.py deleted file mode 100644 index a0cbc5940..000000000 --- a/edi_core_oca/tests/test_edi_backend.py +++ /dev/null @@ -1,124 +0,0 @@ -# Copyright 2020 ACSONE -# @author: Simone Orsi -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -import base64 - -import mock -import psycopg2 -from freezegun import freeze_time - -from odoo.exceptions import UserError, ValidationError -from odoo.tests.common import tagged -from odoo.tools import mute_logger - -from .common import EDIBackendCommonTestCase - - -@tagged("-at_install", "post_install") -class EDIBackendTestCase(EDIBackendCommonTestCase): - def test_type_code(self): - btype = self.backend_type_model.create( - {"name": "Test new type", "code": "Test new type"} - ) - self.assertEqual(btype.code, "test_new_type") - - def test_type_code_uniq(self): - existing_code = self.backend.backend_type_id.code - with mute_logger("odoo.sql_db"): - with self.assertRaises(psycopg2.IntegrityError): - self.backend_type_model.create( - {"name": "Test new type", "code": existing_code} - ) - - def test_record_state(self): - with self.assertRaises(ValidationError): - vals = { - "model": self.partner._name, - "res_id": self.partner.id, - "edi_exchange_state": "output_pending", - } - self.backend.create_record("test_csv_input", vals) - - @freeze_time("2020-10-21 10:00:00") - def test_create_record(self): - vals = { - "model": self.partner._name, - "res_id": self.partner.id, - } - record = self.backend.create_record("test_csv_input", vals) - expected = { - "type_id": self.exchange_type_in.id, - "edi_exchange_state": "new", - "exchange_filename": "EDI_EXC_TEST-test_csv_" - "input-2020-10-21-10-00-00.csv", - } - self.assertRecordValues(record, [expected]) - self.assertEqual(record.record, self.partner) - self.assertEqual(record.edi_exchange_state, "new") - - def test_generate_record_output(self): - vals = { - "model": self.partner._name, - "res_id": self.partner.id, - } - record = self.backend.create_record("test_csv_output", vals) - with mock.patch.object(type(self.backend), "_generate_output") as mocked: - mocked.return_value = "Any string" - self.backend.generate_output(record) - mocked.assert_called_with(record) - - def test_send_record(self): - vals = { - "model": self.partner._name, - "res_id": self.partner.id, - "edi_exchange_state": "output_pending", - "exchange_file": base64.b64encode(b"1234"), - } - record = self.backend.create_record("test_csv_output", vals) - self.assertFalse(record.exchanged_on) - with mock.patch.object(type(self.backend), "_exchange_send") as patch: - patch.return_value = "AAA" - record.action_exchange_send() - patch.assert_called() - self.assertEqual(record.edi_exchange_state, "output_sent") - self.assertTrue(record.exchanged_on) - - def test_send_record_with_error(self): - vals = { - "model": self.partner._name, - "res_id": self.partner.id, - "edi_exchange_state": "output_pending", - "exchange_file": base64.b64encode(b"1234"), - } - record = self.backend.create_record("test_csv_output", vals) - record.action_exchange_send() - self.assertEqual(record.edi_exchange_state, "output_error_on_send") - - def test_send_inbound_error(self): - vals = { - "model": self.partner._name, - "res_id": self.partner.id, - } - record = self.backend.create_record("test_csv_input", vals) - with mock.patch.object(type(self.backend), "_exchange_send") as patch: - patch.return_value = "AAA" - with self.assertRaises(UserError): - record.action_exchange_send() - patch.assert_not_called() - - def test_send_not_generated_record(self): - vals = { - "model": self.partner._name, - "res_id": self.partner.id, - } - record = self.backend.create_record("test_csv_output", vals) - with mock.patch.object(type(self.backend), "_exchange_send") as patch: - patch.return_value = "AAA" - with self.assertRaises(UserError): - record.action_exchange_send() - patch.assert_not_called() - - # TODO: - # 1. split output from incoming - # 2. test components lookup a ComponentRegistryCase diff --git a/edi_core_oca/tests/test_edi_backend_cron.py b/edi_core_oca/tests/test_edi_backend_cron.py index 2736239b9..919b29e08 100644 --- a/edi_core_oca/tests/test_edi_backend_cron.py +++ b/edi_core_oca/tests/test_edi_backend_cron.py @@ -2,13 +2,10 @@ # @author: Simone Orsi # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo.tests.common import tagged - from .common import EDIBackendCommonComponentRegistryTestCase from .fake_components import FakeOutputChecker, FakeOutputGenerator, FakeOutputSender -@tagged("-at_install", "post_install") class EDIBackendTestCase(EDIBackendCommonComponentRegistryTestCase): @classmethod def setUpClass(cls): diff --git a/edi_core_oca/tests/test_record.py b/edi_core_oca/tests/test_record.py new file mode 100644 index 000000000..188eebddb --- /dev/null +++ b/edi_core_oca/tests/test_record.py @@ -0,0 +1,38 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from freezegun import freeze_time + +from odoo import exceptions, fields + +from .common import EDIBackendCommonTestCase + + +class EDIBackendTestCase(EDIBackendCommonTestCase): + # TODO: test more + def test_record_validate_state(self): + with self.assertRaises(exceptions.ValidationError) as 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_exchange_date(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + "edi_exchange_state": "output_pending", + } + record = self.backend.create_record("test_csv_output", vals) + self.assertFalse(record.exchanged_on) + with freeze_time("2020-10-21 10:00:00"): + record.edi_exchange_state = "output_sent" + self.assertEqual( + fields.Datetime.to_string(record.exchanged_on), "2020-10-21 10:00:00" + ) diff --git a/edi_core_oca/tests/test_type.py b/edi_core_oca/tests/test_type.py new file mode 100644 index 000000000..ec8f34c6b --- /dev/null +++ b/edi_core_oca/tests/test_type.py @@ -0,0 +1,25 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import psycopg2 + +from odoo.tools import mute_logger + +from .common import EDIBackendCommonTestCase + + +class EDIBackendTestCase(EDIBackendCommonTestCase): + def test_type_code(self): + btype = self.backend_type_model.create( + {"name": "Test new type", "code": "Test new type"} + ) + self.assertEqual(btype.code, "test_new_type") + + def test_type_code_uniq(self): + existing_code = self.backend.backend_type_id.code + with mute_logger("odoo.sql_db"): + with self.assertRaises(psycopg2.IntegrityError): + self.backend_type_model.create( + {"name": "Test new type", "code": existing_code} + ) From 761cf61ccb6b931f51f336a2b0736210c908a55a Mon Sep 17 00:00:00 2001 From: oca-travis Date: Sun, 29 Nov 2020 09:39:20 +0000 Subject: [PATCH 031/453] [UPD] Update edi.pot --- edi_core_oca/i18n/edi.pot | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/edi_core_oca/i18n/edi.pot b/edi_core_oca/i18n/edi.pot index a5c76699d..e127c7933 100644 --- a/edi_core_oca/i18n/edi.pot +++ b/edi_core_oca/i18n/edi.pot @@ -28,6 +28,18 @@ msgstr "" msgid "State:" msgstr "" +#. module: edi +#: code:addons/edi/models/edi_exchange_record.py:0 +#, python-format +msgid "ACK file is required for this exchange but not found." +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi_exchange_record.py:0 +#, python-format +msgid "ACK file received." +msgstr "" + #. module: edi #: model:ir.model.fields,field_description:edi.field_edi_exchange_record__ack_received_on msgid "ACK received on" @@ -84,12 +96,6 @@ msgstr "" msgid "An error happened while sending. Please check exchange record info." msgstr "" -#. module: edi -#: code:addons/edi/models/edi_exchange_record.py:0 -#, python-format -msgid "An output record is required for sending!" -msgstr "" - #. module: edi #: model:ir.model.fields,field_description:edi.field_edi_exchange_record__message_attachment_count msgid "Attachment Count" @@ -334,6 +340,18 @@ msgstr "" msgid "Exchanged on" msgstr "" +#. module: edi +#: code:addons/edi/models/edi_exchange_record.py:0 +#, python-format +msgid "File %s processed successfully " +msgstr "" + +#. module: edi +#: code:addons/edi/models/edi_exchange_record.py:0 +#, python-format +msgid "File %s processed with errors" +msgstr "" + #. module: edi #: code:addons/edi/models/edi_exchange_record.py:0 #, python-format From 9515337d11652b0998f2fddf4dced4403441536e Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Sun, 29 Nov 2020 09:49:59 +0000 Subject: [PATCH 032/453] edi 13.0.1.6.0 --- edi_core_oca/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py index a9e160293..0ada671b7 100644 --- a/edi_core_oca/__manifest__.py +++ b/edi_core_oca/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base EDI Backend", "summary": """Base module to define EDI backends""", - "version": "13.0.1.5.0", + "version": "13.0.1.6.0", "development_status": "Alpha", "license": "AGPL-3", "author": "ACSONE,Odoo Community Association (OCA)", From df8b3cf7a40ff50e836f944edc660bec8dcfcc76 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sat, 21 Nov 2020 14:40:02 +0100 Subject: [PATCH 033/453] [IMP] edi: Define a processing function for incoming documents Co-Authored-By: Simone Orsi --- edi_core_oca/components/base.py | 9 ++++ edi_core_oca/models/edi_backend.py | 57 ++++++++++++++++++++++ edi_core_oca/models/edi_exchange_record.py | 11 ++++- 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/edi_core_oca/components/base.py b/edi_core_oca/components/base.py index 0b7208379..03e636f14 100644 --- a/edi_core_oca/components/base.py +++ b/edi_core_oca/components/base.py @@ -48,3 +48,12 @@ class EDIBackendCheckComponentMixin(AbstractComponent): def check(self): raise NotImplementedError() + + +class EDIBackendCheckComponentProcess(AbstractComponent): + + _name = "edi.component.process.mixin" + _inherit = "edi.component.mixin" + + def process(self): + raise NotImplementedError() diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index 314f2a9c9..eb59c835e 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -354,3 +354,60 @@ def _notify_ack_missing(self, exchange_record): level="warning", ) self._trigger_edi_event(exchange_record, suffix="ack_missing") + + def _exchange_input_check(self, exchange_record): + if not exchange_record.direction != "inbound": + raise exceptions.UserError( + _("Record ID=%d is not meant to be processed") % exchange_record.id + ) + if not exchange_record.exchange_file: + raise exceptions.UserError( + _("Record ID=%d has no file to process!") % exchange_record.id + ) + return exchange_record.edi_exchange_state in [ + "input_received", + "input_processed_error", + ] + + def exchange_process(self, exchange_record): + """ Process an incoming document + + This function should be called when an exchange record has been received + it could integrate check where to relate or modificate the data + """ + self.ensure_one() + exchange_record.ensure_one() + # In case already processed: skip processing and check the state + check = self._exchange_input_check(exchange_record) + if not check: + return False + try: + self._exchange_process(exchange_record) + except self._swallable_exceptions() as err: + if self.env.context.get("_edi_receive_break_on_error"): + raise + error = repr(err) + state = "input_processed_error" + message = exchange_record._exchange_processed_ko_msg() + res = False + else: + message = exchange_record._exchange_processed_ok_msg() + error = None + state = "input_processed" + res = True + finally: + exchange_record.edi_exchange_state = state + exchange_record.exchange_error = error + if message: + self._exchange_notify_record(exchange_record, message) + return res + + def _exchange_process(self, exchange_record): + # TODO: maybe lookup for an `exchange_record.model` specific component 1st + candidates = self._get_component_usage_candidates(exchange_record, "input") + component = self._get_component( + candidates, work_ctx={"exchange_record": exchange_record} + ) + if component: + return component.process() + raise NotImplementedError() diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index 90e7b91f7..8ea919fe5 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -101,7 +101,7 @@ def _compute_exchange_filename(self): @api.depends("edi_exchange_state") def _compute_exchanged_on(self): for rec in self: - if rec.edi_exchange_state in ["output_sent"]: + if rec.edi_exchange_state in ("input_received", "output_sent"): rec.exchanged_on = fields.Datetime.now() @api.constrains("edi_exchange_state") @@ -176,3 +176,12 @@ def _exchange_status_message(self, key): def action_exchange_send(self): self.ensure_one() return self.backend_id.exchange_send(self) + + def _exchange_processed_ok_msg(self): + return _("File %s processed successfully ") % self.exchange_filename + + def _exchange_processed_ko_msg(self): + return _("File %s processed with errors") % self.exchange_filename + + def _exchange_processed_ack_needed_missing_msg(self): + return _("ACK file is required for this exchange but not found.") From d71aa59ee87258bc4a80aeed023474811c6def6a Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Sun, 29 Nov 2020 11:04:13 +0100 Subject: [PATCH 034/453] edi: sort out components mixins, add input mixins --- edi_core_oca/components/__init__.py | 2 ++ edi_core_oca/components/base.py | 40 -------------------------- edi_core_oca/components/base_input.py | 25 ++++++++++++++++ edi_core_oca/components/base_output.py | 39 +++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 40 deletions(-) create mode 100644 edi_core_oca/components/base_input.py create mode 100644 edi_core_oca/components/base_output.py diff --git a/edi_core_oca/components/__init__.py b/edi_core_oca/components/__init__.py index 0e4444933..2b7005753 100644 --- a/edi_core_oca/components/__init__.py +++ b/edi_core_oca/components/__init__.py @@ -1 +1,3 @@ from . import base +from . import base_output +from . import base_input diff --git a/edi_core_oca/components/base.py b/edi_core_oca/components/base.py index 03e636f14..05a694b36 100644 --- a/edi_core_oca/components/base.py +++ b/edi_core_oca/components/base.py @@ -17,43 +17,3 @@ def exchange_record(self): @property def backend(self): return self.work.backend - - -class EDIBackendOutputComponentMixin(AbstractComponent): - """Generate output content. - """ - - _name = "edi.component.output.mixin" - _inherit = "edi.component.mixin" - - def generate(self): - raise NotImplementedError() - - -class EDIBackendSendComponentMixin(AbstractComponent): - """Send output records. - """ - - _name = "edi.component.send.mixin" - _inherit = "edi.component.mixin" - - def send(self): - raise NotImplementedError() - - -class EDIBackendCheckComponentMixin(AbstractComponent): - - _name = "edi.component.check.mixin" - _inherit = "edi.component.mixin" - - def check(self): - raise NotImplementedError() - - -class EDIBackendCheckComponentProcess(AbstractComponent): - - _name = "edi.component.process.mixin" - _inherit = "edi.component.mixin" - - def process(self): - raise NotImplementedError() diff --git a/edi_core_oca/components/base_input.py b/edi_core_oca/components/base_input.py new file mode 100644 index 000000000..bb175d90c --- /dev/null +++ b/edi_core_oca/components/base_input.py @@ -0,0 +1,25 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.component.core import AbstractComponent + + +class EDIBackendInputComponentMixin(AbstractComponent): + """Generate input content. + """ + + _name = "edi.component.input.mixin" + _inherit = "edi.component.mixin" + + def process(self): + raise NotImplementedError() + + +# class EDIBackendCheckComponentMixin(AbstractComponent): + +# _name = "edi.component.check.mixin" +# _inherit = "edi.component.mixin" + +# def check(self): +# raise NotImplementedError() diff --git a/edi_core_oca/components/base_output.py b/edi_core_oca/components/base_output.py new file mode 100644 index 000000000..f83ac047b --- /dev/null +++ b/edi_core_oca/components/base_output.py @@ -0,0 +1,39 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.component.core import AbstractComponent + + +class EDIBackendOutputComponentMixin(AbstractComponent): + """Generate output content. + """ + + _name = "edi.component.output.mixin" + _inherit = "edi.component.mixin" + _usage = "edi.output.generate.*" + + def generate(self): + raise NotImplementedError() + + +class EDIBackendSendComponentMixin(AbstractComponent): + """Send output records. + """ + + _name = "edi.component.send.mixin" + _inherit = "edi.component.mixin" + _usage = "edi.output.send.*" + + def send(self): + raise NotImplementedError() + + +class EDIBackendCheckComponentMixin(AbstractComponent): + + _name = "edi.component.check.mixin" + _inherit = "edi.component.mixin" + _usage = "edi.output.check.*" + + def check(self): + raise NotImplementedError() From 8e1862d95dc51abfffd639c027760ecb70d2230f Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Sun, 29 Nov 2020 11:05:42 +0100 Subject: [PATCH 035/453] edi.backend: lookup component by direction too --- edi_core_oca/models/edi_backend.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index eb59c835e..ca808e364 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -142,7 +142,7 @@ def _validate_generate_output(self, exchange_record, force=False): def _generate_output(self, exchange_record, **kw): # TODO: maybe lookup for an `exchange_record.model` specific component 1st - candidates = self._get_component_usage_candidates(exchange_record, "output") + candidates = self._get_component_usage_candidates(exchange_record, "generate") component = self._get_component( candidates, work_ctx={"exchange_record": exchange_record} ) @@ -152,9 +152,14 @@ def _generate_output(self, exchange_record, **kw): def _get_component_usage_candidates(self, exchange_record, key): """Retrieve usage candidates for components.""" - base_usage = "edi.{key}.{backend.backend_type_id.code}".format( - backend=self, key=key - ) + # fmt:off + base_usage = ".".join([ + "edi", + exchange_record.direction, + key, + self.backend_type_id.code, + ]) + # fmt:on type_code = exchange_record.type_id.code return [ # specific for backend type and exchange type From 5513ecc45b71a1671667988437dc6375dec42dbf Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Sun, 29 Nov 2020 11:06:21 +0100 Subject: [PATCH 036/453] edi: test improve fake components, add input base --- edi_core_oca/tests/fake_components.py | 73 ++++++++++++++------- edi_core_oca/tests/test_backend_output.py | 12 ++-- edi_core_oca/tests/test_edi_backend_cron.py | 24 +++++-- 3 files changed, 72 insertions(+), 37 deletions(-) diff --git a/edi_core_oca/tests/fake_components.py b/edi_core_oca/tests/fake_components.py index ef9069c3c..2c2adcf4d 100644 --- a/edi_core_oca/tests/fake_components.py +++ b/edi_core_oca/tests/fake_components.py @@ -5,44 +5,67 @@ from odoo.addons.component.core import Component -class FakeOutputGenerator(Component): +class FakeComponentMixin(Component): + + FAKED_COLLECTOR = [] + + def _fake_it(self): + if self.env.context.get("test_break_it"): + raise ValueError(self.env.context.get("test_break_it", "YOU BROKE IT!")) + update_values = self.env.context.get("fake_update_values") + if update_values: + self.exchange_record.write(update_values) + self.FAKED_COLLECTOR.append(self._call_key(self.exchange_record)) + return self.env.context.get("fake_output", self._call_key(self.exchange_record)) + + @classmethod + def _call_key(cls, rec): + return "{}: {}".format(cls._name, rec.id) + + @classmethod + def reset_faked(cls): + cls.FAKED_COLLECTOR = [] + + @classmethod + def check_called_for(cls, rec): + return cls._call_key(rec) in cls.FAKED_COLLECTOR + + @classmethod + def check_not_called_for(cls, rec): + return cls._call_key(rec) not in cls.FAKED_COLLECTOR + + +class FakeOutputGenerator(FakeComponentMixin): _name = "fake.output.generator" _inherit = "edi.component.output.mixin" - _usage = "edi.output.demo_backend.test_csv_output" + _usage = "edi.output.generate.demo_backend.test_csv_output" def generate(self): - if self.env.context.get("test_break_it"): - raise ValueError(self.env.context.get("test_break_it", "YOU BROKE IT!")) - return self.env.context.get( - "fake_output", "FAKE_OUTPUT: %d" % self.exchange_record.id - ) + return self._fake_it() -class FakeOutputSender(Component): +class FakeOutputSender(FakeComponentMixin): _name = "fake.output.sender" _inherit = "edi.component.send.mixin" - _usage = "edi.send.demo_backend.test_csv_output" - - FAKE_SEND_COLLECTOR = [] + _usage = "edi.output.send.demo_backend.test_csv_output" def send(self): - if self.env.context.get("test_break_it"): - raise ValueError(self.env.context.get("test_break_it", "YOU BROKE IT!")) - self.FAKE_SEND_COLLECTOR.append(self.exchange_record._get_file_content()) - return self.env.context.get( - "fake_output", "FAKE_OUTPUT: %d" % self.exchange_record.id - ) + return self._fake_it() -class FakeOutputChecker(Component): +class FakeOutputChecker(FakeComponentMixin): _name = "fake.output.checker" _inherit = "edi.component.check.mixin" - _usage = "edi.check.demo_backend.test_csv_output" + _usage = "edi.output.check.demo_backend.test_csv_output" def check(self): - if self.env.context.get("test_break_it"): - raise ValueError(self.env.context.get("test_break_it", "YOU BROKE IT!")) - update_values = self.env.context.get("test_output_check_update_values") - if update_values: - self.exchange_record.write(update_values) - return True + return self._fake_it() + + +class FakeInputProcess(FakeComponentMixin): + _name = "fake.input.process" + _inherit = "edi.component.input.mixin" + _usage = "edi.input.process.demo_backend.test_csv_input" + + def process(self): + return self._fake_it() diff --git a/edi_core_oca/tests/test_backend_output.py b/edi_core_oca/tests/test_backend_output.py index 2d36ac3b1..18ce6e0bd 100644 --- a/edi_core_oca/tests/test_backend_output.py +++ b/edi_core_oca/tests/test_backend_output.py @@ -31,7 +31,9 @@ def setUpClass(cls): def setUp(self): super().setUp() - FakeOutputSender.FAKE_SEND_COLLECTOR = [] + FakeOutputGenerator.reset_faked() + FakeOutputSender.reset_faked() + FakeOutputChecker.reset_faked() def test_generate_record_output(self): self.backend.with_context(fake_output="yeah!").generate_output(self.record) @@ -43,9 +45,7 @@ def test_send_record(self): self.assertFalse(self.record.exchanged_on) with freeze_time("2020-10-21 10:00:00"): self.record.action_exchange_send() - self.assertIn( - self.record._get_file_content(), FakeOutputSender.FAKE_SEND_COLLECTOR - ) + self.assertTrue(FakeOutputSender.check_called_for(self.record)) self.assertRecordValues(self.record, [{"edi_exchange_state": "output_sent"}]) self.assertEqual( fields.Datetime.to_string(self.record.exchanged_on), "2020-10-21 10:00:00" @@ -58,9 +58,7 @@ def test_send_record_with_error(self): self.record.with_context( test_break_it="OOPS! Something went wrong :(" ).action_exchange_send() - self.assertNotIn( - self.record._get_file_content(), FakeOutputSender.FAKE_SEND_COLLECTOR - ) + self.assertTrue(FakeOutputSender.check_not_called_for(self.record)) self.assertRecordValues( self.record, [ diff --git a/edi_core_oca/tests/test_edi_backend_cron.py b/edi_core_oca/tests/test_edi_backend_cron.py index 919b29e08..60d7e1604 100644 --- a/edi_core_oca/tests/test_edi_backend_cron.py +++ b/edi_core_oca/tests/test_edi_backend_cron.py @@ -26,6 +26,12 @@ def setUpClass(cls): ) cls.records = cls.record1 + cls.record1 + cls.record3 + def setUp(self): + super().setUp() + FakeOutputGenerator.reset_faked() + FakeOutputSender.reset_faked() + FakeOutputChecker.reset_faked() + def test_generate_output_new_no_auto(self): # No content ready to be sent, no auto-generate, nothing happens for rec in self.records: @@ -42,7 +48,10 @@ def test_generate_output_new_auto_skip_send(self): self.backend._cron_check_output_exchange_sync(skip_send=True) for rec in self.records: self.assertEqual(rec.edi_exchange_state, "output_pending") - self.assertEqual(rec._get_file_content(), "FAKE_OUTPUT: %s" % rec.id) + self.assertTrue(FakeOutputGenerator.check_called_for(rec)) + self.assertEqual( + rec._get_file_content(), FakeOutputGenerator._call_key(rec) + ) def test_generate_output_new_auto_send(self): self.exchange_type_out.exchange_file_auto_generate = True @@ -52,7 +61,11 @@ def test_generate_output_new_auto_send(self): self.backend._cron_check_output_exchange_sync() for rec in self.records: self.assertEqual(rec.edi_exchange_state, "output_sent") - self.assertEqual(rec._get_file_content(), "FAKE_OUTPUT: %s" % rec.id) + self.assertTrue(FakeOutputGenerator.check_called_for(rec)) + self.assertEqual( + rec._get_file_content(), FakeOutputGenerator._call_key(rec) + ) + self.assertTrue(FakeOutputSender.check_called_for(rec)) def test_generate_output_output_ready_auto_send(self): # No content ready to be sent, will get the content and send it @@ -61,10 +74,11 @@ def test_generate_output_output_ready_auto_send(self): self.record1._set_file_content("READY") self.record1.edi_exchange_state = "output_sent" self.backend.with_context( - test_output_check_update_values={ - "edi_exchange_state": "output_sent_and_processed" - } + fake_update_values={"edi_exchange_state": "output_sent_and_processed"} )._cron_check_output_exchange_sync() 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") + 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)) From a07fe4db58fcf1f939250dd437c35e2c6ea8837a Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Sun, 29 Nov 2020 11:48:04 +0100 Subject: [PATCH 037/453] edi: test process w/ fake components --- edi_core_oca/models/edi_backend.py | 19 +++-- edi_core_oca/models/edi_exchange_record.py | 11 +-- edi_core_oca/tests/test_backend_input.py | 85 ++++++++++++++++++++++ 3 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 edi_core_oca/tests/test_backend_input.py diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index ca808e364..83d5f8a4a 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -375,7 +375,7 @@ def _exchange_input_check(self, exchange_record): ] def exchange_process(self, exchange_record): - """ Process an incoming document + """Process an incoming document. This function should be called when an exchange record has been received it could integrate check where to relate or modificate the data @@ -393,23 +393,30 @@ def exchange_process(self, exchange_record): raise error = repr(err) state = "input_processed_error" - message = exchange_record._exchange_processed_ko_msg() + message = exchange_record._exchange_status_message("process_ko") res = False else: - message = exchange_record._exchange_processed_ok_msg() + message = exchange_record._exchange_status_message("process_ok") error = None state = "input_processed" res = True finally: - exchange_record.edi_exchange_state = state - exchange_record.exchange_error = error + exchange_record.write( + { + "edi_exchange_state": state, + "exchange_error": error, + # FIXME: this should come from _compute_exchanged_on + # but somehow it's failing in send tests (in record tests it works). + "exchanged_on": fields.Datetime.now(), + } + ) if message: self._exchange_notify_record(exchange_record, message) return res def _exchange_process(self, exchange_record): # TODO: maybe lookup for an `exchange_record.model` specific component 1st - candidates = self._get_component_usage_candidates(exchange_record, "input") + candidates = self._get_component_usage_candidates(exchange_record, "process") component = self._get_component( candidates, work_ctx={"exchange_record": exchange_record} ) diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index 8ea919fe5..a0e5b4513 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -177,11 +177,6 @@ def action_exchange_send(self): self.ensure_one() return self.backend_id.exchange_send(self) - def _exchange_processed_ok_msg(self): - return _("File %s processed successfully ") % self.exchange_filename - - def _exchange_processed_ko_msg(self): - return _("File %s processed with errors") % self.exchange_filename - - def _exchange_processed_ack_needed_missing_msg(self): - return _("ACK file is required for this exchange but not found.") + def action_exchange_process(self): + self.ensure_one() + return self.backend_id.exchange_process(self) diff --git a/edi_core_oca/tests/test_backend_input.py b/edi_core_oca/tests/test_backend_input.py new file mode 100644 index 000000000..c2b5849df --- /dev/null +++ b/edi_core_oca/tests/test_backend_input.py @@ -0,0 +1,85 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import base64 + +import mock +from freezegun import freeze_time + +from odoo import fields +from odoo.exceptions import UserError + +from .common import EDIBackendCommonComponentRegistryTestCase +from .fake_components import FakeInputProcess + +# from odoo.tools import mute_logger + + +class EDIBackendTestCase(EDIBackendCommonComponentRegistryTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._build_components( + # TODO: test all components lookup + cls, + FakeInputProcess, + ) + vals = { + "model": cls.partner._name, + "res_id": cls.partner.id, + "exchange_file": base64.b64encode(b"1234"), + } + cls.record = cls.backend.create_record("test_csv_input", vals) + + def setUp(self): + super().setUp() + FakeInputProcess.reset_faked() + + def test_process_record(self): + self.record.write({"edi_exchange_state": "input_received"}) + with freeze_time("2020-10-22 10:00:00"): + self.record.action_exchange_process() + self.assertTrue(FakeInputProcess.check_called_for(self.record)) + self.assertRecordValues( + self.record, [{"edi_exchange_state": "input_processed"}] + ) + self.assertEqual( + fields.Datetime.to_string(self.record.exchanged_on), "2020-10-22 10:00:00" + ) + + def test_process_record_with_error(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + "edi_exchange_state": "input_received", + "exchange_file": base64.b64encode(b"1234"), + } + record = self.backend.create_record("test_csv_input", vals) + self.backend.exchange_process(record) + self.assertEqual(record.edi_exchange_state, "input_processed_error") + + def test_process_no_file_record(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + "edi_exchange_state": "input_received", + } + record = self.backend.create_record("test_csv_input", vals) + with mock.patch.object(type(self.backend), "_exchange_process") as patch: + patch.return_value = "AAA" + with self.assertRaises(UserError): + self.backend.exchange_process(record) + patch.assert_not_called() + + def test_process_outbound_record(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + } + record = self.backend.create_record("test_csv_output", vals) + with mock.patch.object(type(self.backend), "_exchange_process") as patch: + patch.return_value = "AAA" + with self.assertRaises(UserError): + self.backend.exchange_process(record) + patch.assert_not_called() From f57f16643b8b6793e2b1d1af0ce1f6879c056cde Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sun, 29 Nov 2020 12:43:24 +0100 Subject: [PATCH 038/453] [IMP] edi: input processing, finishing tests --- edi_core_oca/models/edi_backend.py | 2 +- edi_core_oca/tests/__init__.py | 1 + edi_core_oca/tests/test_backend_input.py | 50 +++++++++++------------- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index 83d5f8a4a..75a30d6ef 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -361,7 +361,7 @@ def _notify_ack_missing(self, exchange_record): self._trigger_edi_event(exchange_record, suffix="ack_missing") def _exchange_input_check(self, exchange_record): - if not exchange_record.direction != "inbound": + if not exchange_record.direction == "input": raise exceptions.UserError( _("Record ID=%d is not meant to be processed") % exchange_record.id ) diff --git a/edi_core_oca/tests/__init__.py b/edi_core_oca/tests/__init__.py index 4fea13d41..3d0944a04 100644 --- a/edi_core_oca/tests/__init__.py +++ b/edi_core_oca/tests/__init__.py @@ -2,3 +2,4 @@ from . import test_record from . import test_backend_base from . import test_backend_output +from . import test_backend_input diff --git a/edi_core_oca/tests/test_backend_input.py b/edi_core_oca/tests/test_backend_input.py index c2b5849df..5a3ea8da9 100644 --- a/edi_core_oca/tests/test_backend_input.py +++ b/edi_core_oca/tests/test_backend_input.py @@ -4,7 +4,6 @@ import base64 -import mock from freezegun import freeze_time from odoo import fields @@ -13,8 +12,6 @@ from .common import EDIBackendCommonComponentRegistryTestCase from .fake_components import FakeInputProcess -# from odoo.tools import mute_logger - class EDIBackendTestCase(EDIBackendCommonComponentRegistryTestCase): @classmethod @@ -49,28 +46,27 @@ def test_process_record(self): ) def test_process_record_with_error(self): - vals = { - "model": self.partner._name, - "res_id": self.partner.id, - "edi_exchange_state": "input_received", - "exchange_file": base64.b64encode(b"1234"), - } - record = self.backend.create_record("test_csv_input", vals) - self.backend.exchange_process(record) - self.assertEqual(record.edi_exchange_state, "input_processed_error") + self.record.write({"edi_exchange_state": "input_received"}) + self.record._set_file_content("TEST %d" % self.record.id) + self.record.with_context( + test_break_it="OOPS! Something went wrong :(" + ).action_exchange_process() + self.assertTrue(FakeInputProcess.check_not_called_for(self.record)) + self.assertRecordValues( + self.record, + [ + { + "edi_exchange_state": "input_processed_error", + "exchange_error": "ValueError('OOPS! Something went wrong :(',)", + } + ], + ) def test_process_no_file_record(self): - vals = { - "model": self.partner._name, - "res_id": self.partner.id, - "edi_exchange_state": "input_received", - } - record = self.backend.create_record("test_csv_input", vals) - with mock.patch.object(type(self.backend), "_exchange_process") as patch: - patch.return_value = "AAA" - with self.assertRaises(UserError): - self.backend.exchange_process(record) - patch.assert_not_called() + self.record.write({"edi_exchange_state": "input_received"}) + self.record.exchange_file = False + with self.assertRaises(UserError): + self.record.action_exchange_process() def test_process_outbound_record(self): vals = { @@ -78,8 +74,6 @@ def test_process_outbound_record(self): "res_id": self.partner.id, } record = self.backend.create_record("test_csv_output", vals) - with mock.patch.object(type(self.backend), "_exchange_process") as patch: - patch.return_value = "AAA" - with self.assertRaises(UserError): - self.backend.exchange_process(record) - patch.assert_not_called() + record._set_file_content("TEST %d" % record.id) + with self.assertRaises(UserError): + record.action_exchange_process() From af68c3cd82609228d053225e042f527624fcf93e Mon Sep 17 00:00:00 2001 From: oca-travis Date: Mon, 30 Nov 2020 09:47:29 +0000 Subject: [PATCH 039/453] [UPD] Update edi.pot --- edi_core_oca/i18n/edi.pot | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/edi_core_oca/i18n/edi.pot b/edi_core_oca/i18n/edi.pot index e127c7933..210f4792c 100644 --- a/edi_core_oca/i18n/edi.pot +++ b/edi_core_oca/i18n/edi.pot @@ -518,12 +518,24 @@ msgstr "" msgid "Record ID" msgstr "" +#. module: edi +#: code:addons/edi/models/edi_backend.py:0 +#, python-format +msgid "Record ID=%d has no file to process!" +msgstr "" + #. module: edi #: code:addons/edi/models/edi_backend.py:0 #, python-format msgid "Record ID=%d has no file to send!" msgstr "" +#. module: edi +#: code:addons/edi/models/edi_backend.py:0 +#, python-format +msgid "Record ID=%d is not meant to be processed" +msgstr "" + #. module: edi #: code:addons/edi/models/edi_backend.py:0 #, python-format From 23b3e8c34e2234f3508571e2931aa47578947991 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 30 Nov 2020 09:58:15 +0000 Subject: [PATCH 040/453] edi 13.0.1.7.0 --- edi_core_oca/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py index 0ada671b7..931e90a6b 100644 --- a/edi_core_oca/__manifest__.py +++ b/edi_core_oca/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base EDI Backend", "summary": """Base module to define EDI backends""", - "version": "13.0.1.6.0", + "version": "13.0.1.7.0", "development_status": "Alpha", "license": "AGPL-3", "author": "ACSONE,Odoo Community Association (OCA)", From 8c4f681b31e8e5c2c59751d017d9708d69b9b4b1 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Fri, 27 Nov 2020 10:34:24 +0100 Subject: [PATCH 041/453] Add edi_storage --- edi_core_oca/models/edi_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py index 75a30d6ef..befd41460 100644 --- a/edi_core_oca/models/edi_backend.py +++ b/edi_core_oca/models/edi_backend.py @@ -274,7 +274,7 @@ def _check_output_exchange_sync(self, skip_send=False): self._output_new_records_domain() ) _logger.info( - "EDI Exchange output sync: " "found %d new records to process.", + "EDI Exchange output sync: found %d new records to process.", len(new_records), ) for rec in new_records: @@ -286,7 +286,7 @@ def _check_output_exchange_sync(self, skip_send=False): self._output_pending_records_domain() ) _logger.info( - "EDI Exchange output sync: " "found %d pending records to process.", + "EDI Exchange output sync: found %d pending records to process.", len(pending_records), ) for rec in pending_records: From ecad87a935a523b3611b99ccb2548830242d8cb7 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Sun, 29 Nov 2020 18:14:57 +0100 Subject: [PATCH 042/453] edi: fix consumer mixin tests --- edi_core_oca/tests/__init__.py | 1 + .../tests/{models.py => fake_models.py} | 4 +- edi_core_oca/tests/test_consumer_mixin.py | 51 +++++++++++++++++++ edi_core_oca/tests/test_edi_mixin.py | 50 ------------------ 4 files changed, 55 insertions(+), 51 deletions(-) rename edi_core_oca/tests/{models.py => fake_models.py} (87%) create mode 100644 edi_core_oca/tests/test_consumer_mixin.py delete mode 100644 edi_core_oca/tests/test_edi_mixin.py diff --git a/edi_core_oca/tests/__init__.py b/edi_core_oca/tests/__init__.py index 3d0944a04..a2baa897d 100644 --- a/edi_core_oca/tests/__init__.py +++ b/edi_core_oca/tests/__init__.py @@ -3,3 +3,4 @@ from . import test_backend_base from . import test_backend_output from . import test_backend_input +from . import test_consumer_mixin diff --git a/edi_core_oca/tests/models.py b/edi_core_oca/tests/fake_models.py similarity index 87% rename from edi_core_oca/tests/models.py rename to edi_core_oca/tests/fake_models.py index 13967335b..86ce7a01e 100644 --- a/edi_core_oca/tests/models.py +++ b/edi_core_oca/tests/fake_models.py @@ -2,7 +2,7 @@ # @author: Enric Tobella # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import models +from odoo import fields, models class EdiExchangeConsumerTest(models.Model): @@ -10,5 +10,7 @@ class EdiExchangeConsumerTest(models.Model): _inherit = ["edi.exchange.consumer.mixin"] _description = "Model used only for test" + name = fields.Char() + def _get_edi_exchange_record_name(self, exchange_record, exchange_type): return self.id diff --git a/edi_core_oca/tests/test_consumer_mixin.py b/edi_core_oca/tests/test_consumer_mixin.py new file mode 100644 index 000000000..f253df53b --- /dev/null +++ b/edi_core_oca/tests/test_consumer_mixin.py @@ -0,0 +1,51 @@ +# Copyright 2020 Creu Blanca +# @author: Enric Tobella +# 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 EDIBackendCommonTestCase + + +# 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 TestConsumerMixinCase(EDIBackendCommonTestCase): + @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.consumer_record = cls.env["edi.exchange.consumer.test"].create( + {"name": "Test Consumer"} + ) + cls.exchange_type_out.exchange_filename_pattern = "{record.id}" + + @classmethod + def tearDownClass(cls): + cls.loader.restore_registry() + super().tearDownClass() + + def test_mixin(self): + self.assertEqual(0, self.consumer_record.exchange_record_count) + vals = { + "model": self.consumer_record._name, + "res_id": self.consumer_record.id, + } + exchange_type = "test_csv_output" + exchange_record = self.backend.create_record(exchange_type, vals) + self.consumer_record.refresh() + self.assertEqual(1, self.consumer_record.exchange_record_count) + 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_type, self.backend) diff --git a/edi_core_oca/tests/test_edi_mixin.py b/edi_core_oca/tests/test_edi_mixin.py deleted file mode 100644 index 593f96073..000000000 --- a/edi_core_oca/tests/test_edi_mixin.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2020 Creu Blanca -# @author: Enric Tobella -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo_test_helper import FakeModelLoader - -from .common import EDIBackendCommonTestCase - - -class EDIBackendTestCase(EDIBackendCommonTestCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - # Load fake models ->/ - cls.loader = FakeModelLoader(cls.env, cls.__module__) - cls.loader.backup_registry() - from .models import EdiExchangeConsumerTest - - cls.loader.update_registry((EdiExchangeConsumerTest,)) - cls.new_record = ( - cls.env["edi.exchange.consumer.test"] - .with_context(test_backend_id=cls.backend.id) - .create({}) - ) - cls.exchange_type_out.exchange_filename_pattern = "{record.id}" - - @classmethod - def tearDownClass(cls): - cls.loader.restore_registry() - super().tearDownClass() - - def setUp(self): - super().setUp() - - def test_mixin(self): - self.assertEqual(0, self.new_record.exchange_record_count) - vals = { - "model": self.new_record._name, - "res_id": self.new_record.id, - } - exchange_type = "test_csv_output" - exchange_record = self.backend.create_record(exchange_type, vals) - self.new_record.refresh() - self.assertEqual(1, self.new_record.exchange_record_count) - action = self.new_record.action_view_edi_records() - self.new_record.refresh() - self.assertEqual( - exchange_record, self.env["edi.exchange.record"].search(action["domain"]) - ) - self.new_record._has_exchange_record(exchange_type, self.backend) From 920480e23af553aa2b70cc2bf7fe662b4c4099a2 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Sun, 29 Nov 2020 18:48:45 +0100 Subject: [PATCH 043/453] edi: add unique identifier --- edi_core_oca/__manifest__.py | 1 + edi_core_oca/data/sequence.xml | 11 ++++++++++ edi_core_oca/models/edi_exchange_record.py | 19 ++++++++++++------ edi_core_oca/tests/test_record.py | 20 ++++++++++++++++++- .../views/edi_exchange_record_views.xml | 5 +++++ 5 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 edi_core_oca/data/sequence.xml diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py index 931e90a6b..6026d6f67 100644 --- a/edi_core_oca/__manifest__.py +++ b/edi_core_oca/__manifest__.py @@ -13,6 +13,7 @@ "depends": ["base_edi", "component", "component_event", "mail"], "data": [ "data/cron.xml", + "data/sequence.xml", "security/ir_model_access.xml", "views/edi_backend_views.xml", "views/edi_backend_type_views.xml", diff --git a/edi_core_oca/data/sequence.xml b/edi_core_oca/data/sequence.xml new file mode 100644 index 000000000..37eb19893 --- /dev/null +++ b/edi_core_oca/data/sequence.xml @@ -0,0 +1,11 @@ + + + + + EDI Exchange Record + edi.exchange + EDI/%(year)s/ + 10 + + + diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index a0e5b4513..6ac33835a 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -17,8 +17,8 @@ class EDIExchangeRecord(models.Model): _description = "EDI exchange Record" _order = "exchanged_on desc" - # TODO: add unique identifier using a sequence name = fields.Char(compute="_compute_name") + identifier = fields.Char(required=True, index=True, readonly=True) type_id = fields.Many2one( string="EDI Exchange type", comodel_name="edi.exchange.type", @@ -47,11 +47,6 @@ class EDIExchangeRecord(models.Model): store=True, readonly=False, ) - # TODO: use sequence and make it unique - exchange_identification_code = fields.Char( - track_visibility="onchange", - help="Identification of the EDI, useful to search and join other documents", - ) ack_file = fields.Binary(attachment=True) ack_filename = fields.Char( compute="_compute_exchange_filename", readonly=False, store=True @@ -83,6 +78,10 @@ class EDIExchangeRecord(models.Model): ) exchange_error = fields.Text(string="Exchange error", readonly=True) + _sql_constraints = [ + ("identifier_uniq", "unique(identifier)", "The identifier must be unique.",) + ] + @api.depends("type_id.code", "model", "res_id") def _compute_name(self): for rec in self: @@ -142,6 +141,14 @@ def name_get(self): result.append((rec.id, name)) return result + @api.model + def create(self, vals): + vals["identifier"] = self._get_identifier() + return super().create(vals) + + def _get_identifier(self): + return self.env["ir.sequence"].next_by_code("edi.exchange") + @api.constrains("backend_id", "type_id") def _constrain_backend(self): for rec in self: diff --git a/edi_core_oca/tests/test_record.py b/edi_core_oca/tests/test_record.py index 188eebddb..7a65c0f84 100644 --- a/edi_core_oca/tests/test_record.py +++ b/edi_core_oca/tests/test_record.py @@ -9,8 +9,26 @@ from .common import EDIBackendCommonTestCase -class EDIBackendTestCase(EDIBackendCommonTestCase): +class EDIRecordTestCase(EDIBackendCommonTestCase): # TODO: test more + + def test_record_identifier(self): + vals = { + "model": self.partner._name, + "res_id": self.partner.id, + } + record = self.backend.create_record("test_csv_output", vals) + self.assertTrue( + record.identifier.startswith("EDI/{}".format(fields.Date.today().year)) + ) + new_record = self.backend.create_record( + "test_csv_output", dict(vals, identifier=record.identifier) + ) + self.assertTrue( + new_record.identifier.startswith("EDI/{}".format(fields.Date.today().year)) + ) + self.assertNotEqual(new_record.identifier, record.identifier) + def test_record_validate_state(self): with self.assertRaises(exceptions.ValidationError) as err: vals = { diff --git a/edi_core_oca/views/edi_exchange_record_views.xml b/edi_core_oca/views/edi_exchange_record_views.xml index cdec20fdb..918923f1b 100644 --- a/edi_core_oca/views/edi_exchange_record_views.xml +++ b/edi_core_oca/views/edi_exchange_record_views.xml @@ -5,6 +5,7 @@ + @@ -24,6 +25,9 @@

+

+ +

@@ -48,6 +52,7 @@ + Date: Sun, 29 Nov 2020 22:19:13 +0100 Subject: [PATCH 044/453] edi: add external identifier --- edi_core_oca/models/edi_exchange_record.py | 8 +++++++- edi_core_oca/views/edi_exchange_record_views.xml | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index 6ac33835a..d1fbdd4bf 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -19,6 +19,7 @@ class EDIExchangeRecord(models.Model): name = fields.Char(compute="_compute_name") identifier = fields.Char(required=True, index=True, readonly=True) + external_identifier = fields.Char(index=True, readonly=True) type_id = fields.Many2one( string="EDI Exchange type", comodel_name="edi.exchange.type", @@ -79,7 +80,12 @@ class EDIExchangeRecord(models.Model): exchange_error = fields.Text(string="Exchange error", readonly=True) _sql_constraints = [ - ("identifier_uniq", "unique(identifier)", "The identifier must be unique.",) + ("identifier_uniq", "unique(identifier)", "The identifier must be unique."), + ( + "external_identifier_uniq", + "unique(external_identifier)", + "The external_identifier must be unique.", + ), ] @api.depends("type_id.code", "model", "res_id") diff --git a/edi_core_oca/views/edi_exchange_record_views.xml b/edi_core_oca/views/edi_exchange_record_views.xml index 918923f1b..daa09f0f1 100644 --- a/edi_core_oca/views/edi_exchange_record_views.xml +++ b/edi_core_oca/views/edi_exchange_record_views.xml @@ -6,6 +6,7 @@ + @@ -26,8 +27,13 @@

+

+

+

@@ -53,6 +59,7 @@ + Date: Sun, 29 Nov 2020 22:19:50 +0100 Subject: [PATCH 045/453] edi: fix and organize menu items --- edi_core_oca/__manifest__.py | 1 + edi_core_oca/views/edi_backend_type_views.xml | 12 ------- edi_core_oca/views/edi_backend_views.xml | 12 ------- .../views/edi_exchange_record_views.xml | 6 ---- .../views/edi_exchange_type_views.xml | 6 ---- edi_core_oca/views/menuitems.xml | 35 +++++++++++++++++++ 6 files changed, 36 insertions(+), 36 deletions(-) create mode 100644 edi_core_oca/views/menuitems.xml diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py index 6026d6f67..46df6c5af 100644 --- a/edi_core_oca/__manifest__.py +++ b/edi_core_oca/__manifest__.py @@ -19,6 +19,7 @@ "views/edi_backend_type_views.xml", "views/edi_exchange_record_views.xml", "views/edi_exchange_type_views.xml", + "views/menuitems.xml", "templates/exchange_chatter_msg.xml", ], "demo": ["demo/edi_backend_demo.xml"], diff --git a/edi_core_oca/views/edi_backend_type_views.xml b/edi_core_oca/views/edi_backend_type_views.xml index 1e3c0ec4e..9e85359de 100644 --- a/edi_core_oca/views/edi_backend_type_views.xml +++ b/edi_core_oca/views/edi_backend_type_views.xml @@ -52,16 +52,4 @@ tree - - diff --git a/edi_core_oca/views/edi_backend_views.xml b/edi_core_oca/views/edi_backend_views.xml index 7a86ed9e5..bb5b4ed9f 100644 --- a/edi_core_oca/views/edi_backend_views.xml +++ b/edi_core_oca/views/edi_backend_views.xml @@ -61,16 +61,4 @@ tree - - diff --git a/edi_core_oca/views/edi_exchange_record_views.xml b/edi_core_oca/views/edi_exchange_record_views.xml index daa09f0f1..6df241059 100644 --- a/edi_core_oca/views/edi_exchange_record_views.xml +++ b/edi_core_oca/views/edi_exchange_record_views.xml @@ -102,10 +102,4 @@ tree - diff --git a/edi_core_oca/views/edi_exchange_type_views.xml b/edi_core_oca/views/edi_exchange_type_views.xml index 1c55630eb..65038b083 100644 --- a/edi_core_oca/views/edi_exchange_type_views.xml +++ b/edi_core_oca/views/edi_exchange_type_views.xml @@ -88,10 +88,4 @@ tree - diff --git a/edi_core_oca/views/menuitems.xml b/edi_core_oca/views/menuitems.xml new file mode 100644 index 000000000..8e9c1df1c --- /dev/null +++ b/edi_core_oca/views/menuitems.xml @@ -0,0 +1,35 @@ + + + + + + + + From 3b12b4d9983082653d03b838e556c08c69b95118 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Sun, 29 Nov 2020 22:20:16 +0100 Subject: [PATCH 046/453] edi: disable not ready cron --- edi_core_oca/data/cron.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/edi_core_oca/data/cron.xml b/edi_core_oca/data/cron.xml index ba5e17486..399ee36a4 100644 --- a/edi_core_oca/data/cron.xml +++ b/edi_core_oca/data/cron.xml @@ -16,7 +16,8 @@ code model.search([])._cron_check_output_exchange_sync() - + From 9c363f5e3f4dfc3994160663217c40d94b85b6ea Mon Sep 17 00:00:00 2001 From: oca-travis Date: Mon, 30 Nov 2020 11:50:09 +0000 Subject: [PATCH 047/453] [UPD] Update edi.pot --- edi_core_oca/i18n/edi.pot | 50 +++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/edi_core_oca/i18n/edi.pot b/edi_core_oca/i18n/edi.pot index 210f4792c..e4e855f33 100644 --- a/edi_core_oca/i18n/edi.pot +++ b/edi_core_oca/i18n/edi.pot @@ -142,12 +142,22 @@ msgstr "" msgid "Backend type must match with exchange type's backend type!" msgstr "" +#. module: edi +#: model:ir.ui.menu,name:edi.menu_edi_backend +msgid "Backends" +msgstr "" + #. module: edi #: model:ir.model.fields,field_description:edi.field_edi_backend_type__code #: model:ir.model.fields,field_description:edi.field_edi_exchange_type__code msgid "Code" msgstr "" +#. module: edi +#: model:ir.ui.menu,name:edi.menu_edi_config +msgid "Config" +msgstr "" + #. module: edi #: model:ir.model.fields,field_description:edi.field_edi_backend__create_uid #: model:ir.model.fields,field_description:edi.field_edi_backend_type__create_uid @@ -182,8 +192,6 @@ msgstr "" #. module: edi #: model:ir.actions.act_window,name:edi.act_open_edi_backend_view #: model:ir.model,name:edi.model_edi_backend -#: model:ir.ui.menu,name:edi.menu_edi_backend -#: model:ir.ui.menu,name:edi.menu_edi_backend_root #: model_terms:ir.ui.view,arch_db:edi.edi_backend_view_form #: model_terms:ir.ui.view,arch_db:edi.edi_backend_view_search #: model_terms:ir.ui.view,arch_db:edi.edi_backend_view_tree @@ -194,7 +202,6 @@ msgstr "" #: model:ir.actions.act_window,name:edi.act_open_edi_backend_type_view #: model:ir.model,name:edi.model_edi_backend_type #: model:ir.ui.menu,name:edi.menu_edi_backend_type -#: model:ir.ui.menu,name:edi.menu_edi_exchange #: model_terms:ir.ui.view,arch_db:edi.edi_backend_type_view_form #: model_terms:ir.ui.view,arch_db:edi.edi_backend_type_view_search #: model_terms:ir.ui.view,arch_db:edi.edi_backend_type_view_tree @@ -209,7 +216,6 @@ msgstr "" #. module: edi #: model:ir.actions.act_window,name:edi.act_open_edi_exchange_record_view -#: model:ir.ui.menu,name:edi.menu_edi_exchange_record #: model_terms:ir.ui.view,arch_db:edi.edi_exchange_record_view_form #: model_terms:ir.ui.view,arch_db:edi.edi_exchange_record_view_search #: model_terms:ir.ui.view,arch_db:edi.edi_exchange_record_view_tree @@ -248,13 +254,6 @@ msgstr "" msgid "EDI exchange check status" msgstr "" -#. module: edi -#: model:ir.actions.server,name:edi.cron_edi_backend_check_input_exchange_ir_actions_server -#: model:ir.cron,cron_name:edi.cron_edi_backend_check_input_exchange -#: model:ir.cron,name:edi.cron_edi_backend_check_input_exchange -msgid "EDI exchange input status" -msgstr "" - #. module: edi #: model:ir.model.fields.selection,name:edi.selection__edi_exchange_record__edi_exchange_state__input_processed_error msgid "Error on process" @@ -291,11 +290,6 @@ msgstr "" msgid "Exchange Filename Pattern" msgstr "" -#. module: edi -#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__exchange_identification_code -msgid "Exchange Identification Code" -msgstr "" - #. module: edi #: model:ir.model.fields,field_description:edi.field_edi_exchange_consumer_mixin__exchange_record_ids msgid "Exchange Record" @@ -340,6 +334,16 @@ msgstr "" msgid "Exchanged on" msgstr "" +#. module: edi +#: model:ir.ui.menu,name:edi.menu_edi_exchange_record +msgid "Exchanges" +msgstr "" + +#. module: edi +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__external_identifier +msgid "External Identifier" +msgstr "" + #. module: edi #: code:addons/edi/models/edi_exchange_record.py:0 #, python-format @@ -389,8 +393,8 @@ msgid "ID" msgstr "" #. module: edi -#: model:ir.model.fields,help:edi.field_edi_exchange_record__exchange_identification_code -msgid "Identification of the EDI, useful to search and join other documents" +#: model:ir.model.fields,field_description:edi.field_edi_exchange_record__identifier +msgid "Identifier" msgstr "" #. module: edi @@ -577,6 +581,16 @@ msgstr "" msgid "The code must be unique per backend" msgstr "" +#. module: edi +#: model:ir.model.constraint,message:edi.constraint_edi_exchange_record_external_identifier_uniq +msgid "The external_identifier must be unique." +msgstr "" + +#. module: edi +#: model:ir.model.constraint,message:edi.constraint_edi_exchange_record_identifier_uniq +msgid "The identifier must be unique." +msgstr "" + #. module: edi #: model_terms:ir.ui.view,arch_db:edi.edi_exchange_record_view_search msgid "Type" From 5336cfc561ebef23e72a3a4a118d2ba1bdc314d3 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 30 Nov 2020 12:00:56 +0000 Subject: [PATCH 048/453] edi 13.0.1.8.0 --- edi_core_oca/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py index 46df6c5af..1b5f6ea6c 100644 --- a/edi_core_oca/__manifest__.py +++ b/edi_core_oca/__manifest__.py @@ -5,7 +5,7 @@ { "name": "Base EDI Backend", "summary": """Base module to define EDI backends""", - "version": "13.0.1.7.0", + "version": "13.0.1.8.0", "development_status": "Alpha", "license": "AGPL-3", "author": "ACSONE,Odoo Community Association (OCA)", From 35f7a1bc81a325e016d99fcedd09f837510a8ed7 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Sun, 29 Nov 2020 12:09:59 +0100 Subject: [PATCH 049/453] [IMP] edi: Refactoring views --- edi_core_oca/models/edi_exchange_record.py | 14 +++++- edi_core_oca/models/edi_exchange_type.py | 13 ++++-- .../views/edi_exchange_record_views.xml | 45 ++++++++++++++++--- .../views/edi_exchange_type_views.xml | 1 + 4 files changed, 63 insertions(+), 10 deletions(-) diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index d1fbdd4bf..f8ce18797 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -78,6 +78,7 @@ class EDIExchangeRecord(models.Model): ], ) exchange_error = fields.Text(string="Exchange error", readonly=True) + ack_needed = fields.Boolean(related="type_id.ack_needed") _sql_constraints = [ ("identifier_uniq", "unique(identifier)", "The identifier must be unique."), @@ -98,6 +99,8 @@ def _compute_name(self): @api.depends("model", "type_id", "type_id.ack_needed") def _compute_exchange_filename(self): for rec in self: + if not rec.type_id: + continue if not rec.exchange_filename: rec.exchange_filename = rec.type_id._make_exchange_filename(rec) if rec.type_id.ack_needed and not rec.ack_filename: @@ -143,7 +146,10 @@ def name_get(self): result = [] for rec in self: dt = fields.Datetime.to_string(rec.exchanged_on) if rec.exchanged_on else "" - name = "[{}] {} {}".format(rec.type_id.name, rec.record.name, dt) + rec_name = rec.identifier + if rec.res_id and rec.model: + rec_name = rec.record.display_name + name = "[{}] {} {}".format(rec.type_id.name, rec_name, dt) result.append((rec.id, name)) return result @@ -193,3 +199,9 @@ def action_exchange_send(self): def action_exchange_process(self): self.ensure_one() return self.backend_id.exchange_process(self) + + def action_open_related_record(self): + self.ensure_one() + if not self.model or not self.res_id: + return {} + return self.record.get_formview_action() diff --git a/edi_core_oca/models/edi_exchange_type.py b/edi_core_oca/models/edi_exchange_type.py index d51e91983..d142ee505 100644 --- a/edi_core_oca/models/edi_exchange_type.py +++ b/edi_core_oca/models/edi_exchange_type.py @@ -58,7 +58,7 @@ def _check_backend(self): if rec.backend_id.backend_type_id != rec.backend_type_id: raise exceptions.UserError(_("Backend should respect backend type!")) - def _make_exchange_filename(self, record, ack=False): + def _make_exchange_filename(self, exchange_record, ack=False): """Generate filename.""" pattern = self.exchange_filename_pattern ext = self.exchange_file_ext @@ -67,10 +67,13 @@ def _make_exchange_filename(self, record, ack=False): ext = self.ack_file_ext pattern = pattern + ".{ext}" dt = slugify(fields.Datetime.to_string(fields.Datetime.now())) - record_name = self._get_record_name(record) + record_name = self._get_record_name(exchange_record) + record = exchange_record + if exchange_record.model and exchange_record.res_id: + record = exchange_record.record return pattern.format( - exchange_record=record, - record=record.record, + exchange_record=exchange_record, + record=record, record_name=record_name, type=self, dt=dt, @@ -78,6 +81,8 @@ def _make_exchange_filename(self, record, ack=False): ) def _get_record_name(self, record): + if not record.res_id or not record.model: + return slugify(record.display_name) if hasattr(record.record, "_get_edi_exchange_record_name"): return record.record._get_edi_exchange_record_name(record, self) return slugify(record.record.display_name) diff --git a/edi_core_oca/views/edi_exchange_record_views.xml b/edi_core_oca/views/edi_exchange_record_views.xml index 6df241059..f78dadba4 100644 --- a/edi_core_oca/views/edi_exchange_record_views.xml +++ b/edi_core_oca/views/edi_exchange_record_views.xml @@ -8,8 +8,9 @@ - - + + + @@ -20,6 +21,13 @@ edi.exchange.record
+
+