From b55c8a5bcaf9f93407d1e11e8768ac10b35d5167 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Wed, 28 Jan 2026 15:54:32 +0100 Subject: [PATCH 1/5] [ADD] edi_mail_import_oca --- edi_mail_import_oca/README.rst | 88 ++++ edi_mail_import_oca/__init__.py | 1 + edi_mail_import_oca/__manifest__.py | 16 + edi_mail_import_oca/models/__init__.py | 2 + .../models/edi_exchange_record.py | 65 +++ .../models/edi_exchange_type.py | 27 ++ edi_mail_import_oca/pyproject.toml | 3 + edi_mail_import_oca/readme/CONTEXT.md | 3 + edi_mail_import_oca/readme/CONTRIBUTORS.md | 2 + edi_mail_import_oca/readme/DESCRIPTION.md | 1 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 435 ++++++++++++++++++ edi_mail_import_oca/tests/__init__.py | 1 + .../tests/test_mail_import_oca.py | 124 +++++ .../views/edi_exchange_type.xml | 54 +++ 15 files changed, 822 insertions(+) create mode 100644 edi_mail_import_oca/README.rst create mode 100644 edi_mail_import_oca/__init__.py create mode 100644 edi_mail_import_oca/__manifest__.py create mode 100644 edi_mail_import_oca/models/__init__.py create mode 100644 edi_mail_import_oca/models/edi_exchange_record.py create mode 100644 edi_mail_import_oca/models/edi_exchange_type.py create mode 100644 edi_mail_import_oca/pyproject.toml create mode 100644 edi_mail_import_oca/readme/CONTEXT.md create mode 100644 edi_mail_import_oca/readme/CONTRIBUTORS.md create mode 100644 edi_mail_import_oca/readme/DESCRIPTION.md create mode 100644 edi_mail_import_oca/static/description/icon.png create mode 100644 edi_mail_import_oca/static/description/index.html create mode 100644 edi_mail_import_oca/tests/__init__.py create mode 100644 edi_mail_import_oca/tests/test_mail_import_oca.py create mode 100644 edi_mail_import_oca/views/edi_exchange_type.xml diff --git a/edi_mail_import_oca/README.rst b/edi_mail_import_oca/README.rst new file mode 100644 index 000000000..a2c65cb57 --- /dev/null +++ b/edi_mail_import_oca/README.rst @@ -0,0 +1,88 @@ +=================== +Edi Mail Import Oca +=================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:aff4c256557f2507c286234eccc96a32869385fb0ab0fab7f5509a292ae01a43 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |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--framework-lightgray.png?logo=github + :target: https://github.com/OCA/edi-framework/tree/17.0/edi_mail_import_oca + :alt: OCA/edi-framework +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/edi-framework-17-0/edi-framework-17-0-edi_mail_import_oca + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/edi-framework&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to process mails received as edi.exchange.records to +process them. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +Usually, when we think about EDI entrances, we think about SFTP or API, +however, there is an standard way to receive files that is emails. + +This module tries to offer a way to reuse EDI and mail interface in +order to handle everything. + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Dixmit + +Contributors +------------ + +- `Dixmit `__ + + - Enric Tobella + +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. + +This module is part of the `OCA/edi-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/edi_mail_import_oca/__init__.py b/edi_mail_import_oca/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/edi_mail_import_oca/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/edi_mail_import_oca/__manifest__.py b/edi_mail_import_oca/__manifest__.py new file mode 100644 index 000000000..5d35edc94 --- /dev/null +++ b/edi_mail_import_oca/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Edi Mail Import Oca", + "summary": """Process emails as EDI exchange recordsç""", + "version": "17.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/edi-framework", + "depends": ["edi_oca", "mail"], + "data": [ + "views/edi_exchange_type.xml", + ], + "demo": [], +} diff --git a/edi_mail_import_oca/models/__init__.py b/edi_mail_import_oca/models/__init__.py new file mode 100644 index 000000000..6128f744d --- /dev/null +++ b/edi_mail_import_oca/models/__init__.py @@ -0,0 +1,2 @@ +from . import edi_exchange_type +from . import edi_exchange_record diff --git a/edi_mail_import_oca/models/edi_exchange_record.py b/edi_mail_import_oca/models/edi_exchange_record.py new file mode 100644 index 000000000..0fc39c7da --- /dev/null +++ b/edi_mail_import_oca/models/edi_exchange_record.py @@ -0,0 +1,65 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import base64 +import email +import json +import logging +import re + +from odoo import _, api, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class EdiExchangeRecord(models.Model): + _inherit = "edi.exchange.record" + + @api.model + def message_new(self, msg_dict, custom_values=None): + record = super().message_new( + msg_dict, + custom_values=custom_values, + ) + if record.type_id.direction != "input": + raise UserError(_("Received email for non-incoming exchange type.")) + if record.type_id.mail_as_attachment: + new_message_dict = msg_dict.copy() + attachments = new_message_dict.pop("attachments", []) + new_message_dict["attachments"] = [] + for attachment in attachments: + new_message_dict["attachments"].append( + { + "info": attachment.info, + "data": self._process_email_attachment(attachment), + "fname": attachment.fname, + } + ) + record._set_file_content(json.dumps(new_message_dict)) + record.edi_exchange_state = "input_received" + else: + content = False + filename = False + for attachment in msg_dict.get("attachments", []): + if re.match( + record.type_id.exchange_filename_pattern or ".*", + attachment.fname, + re.IGNORECASE, + ): + content = self._process_email_attachment(attachment) + filename = attachment.fname + break + if content: + record._set_file_content(content) + record.exchange_filename = filename + record.edi_exchange_state = "input_received" + return record + + def _process_email_attachment(self, attachment): + """Process email attachment to be stored as file content.""" + data = attachment[1] + if isinstance(data, email.message.EmailMessage): + data = data.as_bytes() + if not isinstance(data, bytes): + data = str(data).encode("utf-8") + return base64.b64encode(data).decode("utf-8") diff --git a/edi_mail_import_oca/models/edi_exchange_type.py b/edi_mail_import_oca/models/edi_exchange_type.py new file mode 100644 index 000000000..2856f8262 --- /dev/null +++ b/edi_mail_import_oca/models/edi_exchange_type.py @@ -0,0 +1,27 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import ast + +from odoo import fields, models + + +class EdiExchangeType(models.Model): + _name = "edi.exchange.type" + _inherit = ["edi.exchange.type", "mail.alias.mixin"] + + mail_as_attachment = fields.Boolean( + string="Import Email as an Attachment", + ) + + def _alias_get_creation_values(self): + values = super()._alias_get_creation_values() + values["alias_model_id"] = ( + self.env["ir.model"].sudo()._get("edi.exchange.record").id + ) + if self.id: + values["alias_defaults"] = defaults = ast.literal_eval( + self.alias_defaults or "{}" + ) + defaults["backend_id"] = self.backend_id.id + defaults["type_id"] = self.id + return values diff --git a/edi_mail_import_oca/pyproject.toml b/edi_mail_import_oca/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/edi_mail_import_oca/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/edi_mail_import_oca/readme/CONTEXT.md b/edi_mail_import_oca/readme/CONTEXT.md new file mode 100644 index 000000000..75b5eb86e --- /dev/null +++ b/edi_mail_import_oca/readme/CONTEXT.md @@ -0,0 +1,3 @@ +Usually, when we think about EDI entrances, we think about SFTP or API, however, there is an standard way to receive files that is emails. + +This module tries to offer a way to reuse EDI and mail interface in order to handle everything. \ No newline at end of file diff --git a/edi_mail_import_oca/readme/CONTRIBUTORS.md b/edi_mail_import_oca/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..2c066ba7f --- /dev/null +++ b/edi_mail_import_oca/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Dixmit](https://www.dixmit.com) + - Enric Tobella diff --git a/edi_mail_import_oca/readme/DESCRIPTION.md b/edi_mail_import_oca/readme/DESCRIPTION.md new file mode 100644 index 000000000..0c3f0ceba --- /dev/null +++ b/edi_mail_import_oca/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module allows to process mails received as edi.exchange.records to process them. diff --git a/edi_mail_import_oca/static/description/icon.png b/edi_mail_import_oca/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z z?n?eq1XP>p-IM$Z^C;2L3itnbJZAip*Zo0aw2bs8@(s^~*8T9go!%dHcAz2lM;`yp zD=7&xjFV$S&5uDaiScyD?B-i1ze`+CoRtz`Wn+Zl&#s4&}MO{@N!ufrzjG$B79)Y2d3tBk&)TxUTw@QS0TEL_?njX|@vq?Uz(nBFK5Pq7*xj#u*R&i|?7+6# z+|r_n#SW&LXhtheZdah{ZVoqwyT{D>MC3nkFF#N)xLi{p7J1jXlmVeb;cP5?e(=f# zuT7fvjSbjS781v?7{)-X3*?>tq?)Yd)~|1{BDS(pqC zC}~H#WXlkUW*H5CDOo<)#x7%RY)A;ShGhI5s*#cRDA8YgqG(HeKDx+#(ZQ?386dv! zlXCO)w91~Vw4AmOcATuV653fa9R$fyK8ul%rG z-wfS zihugoZyr38Im?Zuh6@RcF~t1anQu7>#lPpb#}4cOA!EM11`%f*07RqOVkmX{p~KJ9 z^zP;K#|)$`^Rb{rnHGH{~>1(fawV0*Z#)}M`m8-?ZJV<+e}s9wE# z)l&az?w^5{)`S(%MRzxdNqrs1n*-=jS^_jqE*5XDrA0+VE`5^*p3CuM<&dZEeCjoz zR;uu_H9ZPZV|fQq`Cyw4nscrVwi!fE6ciMmX$!_hN7uF;jjKG)d2@aC4ropY)8etW=xJvni)8eHi`H$%#zn^WJ5NLc-rqk|u&&4Z6fD_m&JfSI1Bvb?b<*n&sfl0^t z=HnmRl`XrFvMKB%9}>PaA`m-fK6a0(8=qPkWS5bb4=v?XcWi&hRY?O5HdulRi4?fN zlsJ*N-0Qw+Yic@s0(2uy%F@ib;GjXt01Fmx5XbRo6+n|pP(&nodMoap^z{~q ziEeaUT@Mxe3vJSfI6?uLND(CNr=#^W<1b}jzW58bIfyWTDle$mmS(|x-0|2UlX+9k zQ^EX7Nw}?EzVoBfT(-LT|=9N@^hcn-_p&sqG z&*oVs2JSU+N4ZD`FhCAWaS;>|wH2G*Id|?pa#@>tyxX`+4HyIArWDvVrX)2WAOQff z0qyHu&-S@i^MS-+j--!pr4fPBj~_8({~e1bfcl0wI1kaoN>mJL6KUPQm5N7lB(ui1 zE-o%kq)&djzWJ}ob<-GfDlkB;F31j-VHKvQUGQ3sp`CwyGJk_i!y^sD0fqC@$9|jO zOqN!r!8-p==F@ZVP=U$qSpY(gQ0)59P1&t@y?5rvg<}E+GB}26NYPp4f2YFQrQtot5mn3wu_qprZ=>Ig-$ zbW26Ws~IgY>}^5w`vTB(G`PTZaDiGBo5o(tp)qli|NeV( z@H_=R8V39rt5J5YB2Ky?4eJJ#b`_iBe2ot~6%7mLt5t8Vwi^Jy7|jWXqa3amOIoRb zOr}WVFP--DsS`1WpN%~)t3R!arKF^Q$e12KEqU36AWwnCBICpH4XCsfnyrHr>$I$4 z!DpKX$OKLWarN7nv@!uIA+~RNO)l$$w}p(;b>mx8pwYvu;dD_unryX_NhT8*Tj>BTrTTL&!?O+%Rv;b?B??gSzdp?6Uug9{ zd@V08Z$BdI?fpoCS$)t4mg4rT8Q_I}h`0d-vYZ^|dOB*Q^S|xqTV*vIg?@fVFSmMpaw0qtTRbx} z({Pg?#{2`sc9)M5N$*N|4;^t$+QP?#mov zGVC@I*lBVrOU-%2y!7%)fAKjpEFsgQc4{amtiHb95KQEwvf<(3T<9-Zm$xIew#P22 zc2Ix|App^>v6(3L_MCU0d3W##AB0M~3D00EWoKZqsJYT(#@w$Y_H7G22M~ApVFTRHMI_3be)Lkn#0F*V8Pq zc}`Cjy$bE;FJ6H7p=0y#R>`}-m4(0F>%@P|?7fx{=R^uFdISRnZ2W_xQhD{YuR3t< z{6yxu=4~JkeA;|(J6_nv#>Nvs&FuLA&PW^he@t(UwFFE8)|a!R{`E`K`i^ZnyE4$k z;(749Ix|oi$c3QbEJ3b~D_kQsPz~fIUKym($a_7dJ?o+40*OLl^{=&oq$<#Q(yyrp z{J-FAniyAw9tPbe&IhQ|a`DqFTVQGQ&Gq3!C2==4x{6EJwiPZ8zub-iXoUtkJiG{} zPaR&}_fn8_z~(=;5lD-aPWD3z8PZS@AaUiomF!G8I}Mf>e~0g#BelA-5#`cj;O5>N Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/edi_mail_import_oca/static/description/index.html b/edi_mail_import_oca/static/description/index.html new file mode 100644 index 000000000..e987802ad --- /dev/null +++ b/edi_mail_import_oca/static/description/index.html @@ -0,0 +1,435 @@ + + + + + +Edi Mail Import Oca + + + +
+

Edi Mail Import Oca

+ + +

Beta License: AGPL-3 OCA/edi-framework Translate me on Weblate Try me on Runboat

+

This module allows to process mails received as edi.exchange.records to +process them.

+

Table of contents

+ +
+

Use Cases / Context

+

Usually, when we think about EDI entrances, we think about SFTP or API, +however, there is an standard way to receive files that is emails.

+

This module tries to offer a way to reuse EDI and mail interface in +order to handle everything.

+
+
+

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 to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Dixmit
  • +
+
+
+

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.

+

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

+

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

+
+
+
+ + diff --git a/edi_mail_import_oca/tests/__init__.py b/edi_mail_import_oca/tests/__init__.py new file mode 100644 index 000000000..51632faa6 --- /dev/null +++ b/edi_mail_import_oca/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mail_import_oca diff --git a/edi_mail_import_oca/tests/test_mail_import_oca.py b/edi_mail_import_oca/tests/test_mail_import_oca.py new file mode 100644 index 000000000..75522b6d8 --- /dev/null +++ b/edi_mail_import_oca/tests/test_mail_import_oca.py @@ -0,0 +1,124 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import json + +from odoo.tests import tagged + +from odoo.addons.mail.tests.common import MailCommon +from odoo.addons.test_mail.data.test_mail_data import MAIL_EML_ATTACHMENT + + +@tagged("mail_gateway") +class TestEmailParsing(MailCommon): + """Test email parsing and import via mail gateway""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.backend_type = cls.env["edi.backend.type"].create( + { + "name": "Mail Import OCA Test Backend Type", + "code": "mail_import_oca_test_backend_type", + } + ) + cls.backend = cls.env["edi.backend"].create( + { + "name": "Mail Import OCA Test Backend", + "backend_type_id": cls.backend_type.id, + } + ) + cls.exchange_type = cls.env["edi.exchange.type"].create( + { + "name": "Test Exchange Type", + "code": "test_exchange_type", + "direction": "input", + "alias_name": "edi-input", + "backend_type_id": cls.backend_type.id, + "backend_id": cls.backend.id, + } + ) + + def test_import_full(self): + self.assertTrue(self.exchange_type.alias_email) + self.exchange_type.mail_as_attachment = True + self.assertFalse( + self.env["edi.exchange.record"].search( + [ + ("type_id", "=", self.exchange_type.id), + ] + ) + ) + mail = self.format( + MAIL_EML_ATTACHMENT, + to=self.exchange_type.alias_email, + subject="purchase test mail", + target_model="account.move", + msg_id="", + ) + self.env["mail.thread"].message_process("mail.thread", mail) + record = self.env["edi.exchange.record"].search( + [ + ("type_id", "=", self.exchange_type.id), + ] + ) + self.assertTrue(record) + self.assertEqual(record.edi_exchange_state, "input_received") + file_content = record._get_file_content() + self.assertTrue(file_content) + data = json.loads(file_content) + self.assertIn("body", data) + + def test_import_specific_file(self): + self.assertTrue(self.exchange_type.alias_email) + self.exchange_type.mail_as_attachment = False + self.exchange_type.exchange_filename_pattern = ".*eml" + self.assertFalse( + self.env["edi.exchange.record"].search( + [ + ("type_id", "=", self.exchange_type.id), + ] + ) + ) + mail = self.format( + MAIL_EML_ATTACHMENT, + to=self.exchange_type.alias_email, + subject="purchase test mail", + target_model="account.move", + msg_id="", + ) + self.env["mail.thread"].message_process("mail.thread", mail) + record = self.env["edi.exchange.record"].search( + [ + ("type_id", "=", self.exchange_type.id), + ] + ) + self.assertTrue(record) + self.assertEqual(record.edi_exchange_state, "input_received") + self.assertEqual(record.exchange_filename, "original_msg.eml") + + def test_import_no_file_found(self): + self.assertTrue(self.exchange_type.alias_email) + self.exchange_type.mail_as_attachment = False + self.exchange_type.exchange_filename_pattern = ".*xml" + self.assertFalse( + self.env["edi.exchange.record"].search( + [ + ("type_id", "=", self.exchange_type.id), + ] + ) + ) + mail = self.format( + MAIL_EML_ATTACHMENT, + to=self.exchange_type.alias_email, + subject="purchase test mail", + target_model="account.move", + msg_id="", + ) + self.env["mail.thread"].message_process("mail.thread", mail) + record = self.env["edi.exchange.record"].search( + [ + ("type_id", "=", self.exchange_type.id), + ] + ) + self.assertTrue(record) + self.assertEqual(record.edi_exchange_state, "new") diff --git a/edi_mail_import_oca/views/edi_exchange_type.xml b/edi_mail_import_oca/views/edi_exchange_type.xml new file mode 100644 index 000000000..217a80b4c --- /dev/null +++ b/edi_mail_import_oca/views/edi_exchange_type.xml @@ -0,0 +1,54 @@ + + + + + + edi.exchange.type + + + + +
+ + + +
+ + + +
+
+
+
+ + + +
From b64d22a951e2dda1f179b8441c660af399defce7 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 2 Feb 2026 15:18:49 +0100 Subject: [PATCH 2/5] [IMP] edi_mail_import_oca: Black, isort, prettier --- edi_mail_import_oca/README.rst | 16 ++++++---- .../static/description/index.html | 30 +++++++++++-------- .../views/edi_exchange_type.xml | 14 ++++----- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/edi_mail_import_oca/README.rst b/edi_mail_import_oca/README.rst index a2c65cb57..53c282af7 100644 --- a/edi_mail_import_oca/README.rst +++ b/edi_mail_import_oca/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + =================== Edi Mail Import Oca =================== @@ -13,17 +17,17 @@ Edi Mail Import Oca .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-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--framework-lightgray.png?logo=github - :target: https://github.com/OCA/edi-framework/tree/17.0/edi_mail_import_oca + :target: https://github.com/OCA/edi-framework/tree/19.0/edi_mail_import_oca :alt: OCA/edi-framework .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/edi-framework-17-0/edi-framework-17-0-edi_mail_import_oca + :target: https://translation.odoo-community.org/projects/edi-framework-19-0/edi-framework-19-0-edi_mail_import_oca :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/edi-framework&target_branch=17.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/edi-framework&target_branch=19.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -51,7 +55,7 @@ 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 to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -83,6 +87,6 @@ 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. -This module is part of the `OCA/edi-framework `_ project on GitHub. +This module is part of the `OCA/edi-framework `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/edi_mail_import_oca/static/description/index.html b/edi_mail_import_oca/static/description/index.html index e987802ad..ad1fbae95 100644 --- a/edi_mail_import_oca/static/description/index.html +++ b/edi_mail_import_oca/static/description/index.html @@ -3,7 +3,7 @@ -Edi Mail Import Oca +README.rst -
-

Edi Mail Import Oca

+
+ + +Odoo Community Association + +
+

Edi Mail Import Oca

-

Beta License: AGPL-3 OCA/edi-framework Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/edi-framework Translate me on Weblate Try me on Runboat

This module allows to process mails received as edi.exchange.records to process them.

Table of contents

@@ -386,30 +391,30 @@

Edi Mail Import Oca

-

Use Cases / Context

+

Use Cases / Context

Usually, when we think about EDI entrances, we think about SFTP or API, however, there is an standard way to receive files that is emails.

This module tries to offer a way to reuse EDI and mail interface in order to handle everything.

-

Bug Tracker

+

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 to smash it by providing a detailed and welcomed -feedback.

+feedback.

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Dixmit
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -426,10 +431,11 @@

Maintainers

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.

-

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

+

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

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

+
diff --git a/edi_mail_import_oca/views/edi_exchange_type.xml b/edi_mail_import_oca/views/edi_exchange_type.xml index 217a80b4c..4f8987969 100644 --- a/edi_mail_import_oca/views/edi_exchange_type.xml +++ b/edi_mail_import_oca/views/edi_exchange_type.xml @@ -2,7 +2,6 @@ - edi.exchange.type @@ -28,11 +27,11 @@ @ + name="alias_domain_id" + class="oe_inline" + placeholder="e.g. domain.com" + options="{'no_create': True, 'no_open': True}" + /> @@ -48,7 +47,4 @@ - - - From a88067bb0e3ea0c7f064af0cc944ff05808fd477 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 2 Feb 2026 15:19:38 +0100 Subject: [PATCH 3/5] [MIG] edi_mail_import_oca: Migration to 19.0 --- edi_mail_import_oca/__manifest__.py | 4 ++-- edi_mail_import_oca/models/edi_exchange_record.py | 6 ++++-- edi_mail_import_oca/views/edi_exchange_type.xml | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/edi_mail_import_oca/__manifest__.py b/edi_mail_import_oca/__manifest__.py index 5d35edc94..a5c735fd1 100644 --- a/edi_mail_import_oca/__manifest__.py +++ b/edi_mail_import_oca/__manifest__.py @@ -4,11 +4,11 @@ { "name": "Edi Mail Import Oca", "summary": """Process emails as EDI exchange recordsç""", - "version": "17.0.1.0.0", + "version": "19.0.1.0.0", "license": "AGPL-3", "author": "Dixmit,Odoo Community Association (OCA)", "website": "https://github.com/OCA/edi-framework", - "depends": ["edi_oca", "mail"], + "depends": ["edi_core_oca", "mail"], "data": [ "views/edi_exchange_type.xml", ], diff --git a/edi_mail_import_oca/models/edi_exchange_record.py b/edi_mail_import_oca/models/edi_exchange_record.py index 0fc39c7da..4a5498699 100644 --- a/edi_mail_import_oca/models/edi_exchange_record.py +++ b/edi_mail_import_oca/models/edi_exchange_record.py @@ -6,7 +6,7 @@ import logging import re -from odoo import _, api, models +from odoo import api, models from odoo.exceptions import UserError _logger = logging.getLogger(__name__) @@ -22,7 +22,9 @@ def message_new(self, msg_dict, custom_values=None): custom_values=custom_values, ) if record.type_id.direction != "input": - raise UserError(_("Received email for non-incoming exchange type.")) + raise UserError( + self.env._("Received email for non-incoming exchange type.") + ) if record.type_id.mail_as_attachment: new_message_dict = msg_dict.copy() attachments = new_message_dict.pop("attachments", []) diff --git a/edi_mail_import_oca/views/edi_exchange_type.xml b/edi_mail_import_oca/views/edi_exchange_type.xml index 4f8987969..e4590efce 100644 --- a/edi_mail_import_oca/views/edi_exchange_type.xml +++ b/edi_mail_import_oca/views/edi_exchange_type.xml @@ -4,7 +4,7 @@ edi.exchange.type - + From 12991dd579081b741957ad9c279212c9b9e614b2 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Wed, 4 Feb 2026 06:26:47 +0100 Subject: [PATCH 4/5] [IMP] edi_mail_import_oca: Pass verification logic to constrain. Remove unnecessary sudo --- .../models/edi_exchange_record.py | 5 ----- edi_mail_import_oca/models/edi_exchange_type.py | 17 +++++++++++++---- .../tests/test_mail_import_oca.py | 10 ++++++++++ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/edi_mail_import_oca/models/edi_exchange_record.py b/edi_mail_import_oca/models/edi_exchange_record.py index 4a5498699..30ce58378 100644 --- a/edi_mail_import_oca/models/edi_exchange_record.py +++ b/edi_mail_import_oca/models/edi_exchange_record.py @@ -7,7 +7,6 @@ import re from odoo import api, models -from odoo.exceptions import UserError _logger = logging.getLogger(__name__) @@ -21,10 +20,6 @@ def message_new(self, msg_dict, custom_values=None): msg_dict, custom_values=custom_values, ) - if record.type_id.direction != "input": - raise UserError( - self.env._("Received email for non-incoming exchange type.") - ) if record.type_id.mail_as_attachment: new_message_dict = msg_dict.copy() attachments = new_message_dict.pop("attachments", []) diff --git a/edi_mail_import_oca/models/edi_exchange_type.py b/edi_mail_import_oca/models/edi_exchange_type.py index 2856f8262..48360e9bb 100644 --- a/edi_mail_import_oca/models/edi_exchange_type.py +++ b/edi_mail_import_oca/models/edi_exchange_type.py @@ -2,7 +2,8 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import ast -from odoo import fields, models +from odoo import api, fields, models +from odoo.exceptions import ValidationError class EdiExchangeType(models.Model): @@ -13,11 +14,19 @@ class EdiExchangeType(models.Model): string="Import Email as an Attachment", ) + @api.constrains("direction", "alias_domain_id", "alias_name") + def _check_mail_configuration(self): + for record in self: + if record.direction != "input" and record.alias_email: + raise ValidationError( + self.env._( + "You cannot have a receiving email for a non-incoming type." + ) + ) + def _alias_get_creation_values(self): values = super()._alias_get_creation_values() - values["alias_model_id"] = ( - self.env["ir.model"].sudo()._get("edi.exchange.record").id - ) + values["alias_model_id"] = self.env["ir.model"]._get("edi.exchange.record").id if self.id: values["alias_defaults"] = defaults = ast.literal_eval( self.alias_defaults or "{}" diff --git a/edi_mail_import_oca/tests/test_mail_import_oca.py b/edi_mail_import_oca/tests/test_mail_import_oca.py index 75522b6d8..f78036605 100644 --- a/edi_mail_import_oca/tests/test_mail_import_oca.py +++ b/edi_mail_import_oca/tests/test_mail_import_oca.py @@ -2,6 +2,7 @@ import json +from odoo.exceptions import ValidationError from odoo.tests import tagged from odoo.addons.mail.tests.common import MailCommon @@ -38,6 +39,15 @@ def setUpClass(cls): } ) + def test_constraint(self): + with self.assertRaises(ValidationError): + self.exchange_type.direction = "output" + + def test_constraint_no_error_on_no_alias(self): + self.exchange_type.alias_name = False + self.exchange_type.direction = "output" + self.assertFalse(self.exchange_type.alias_email) + def test_import_full(self): self.assertTrue(self.exchange_type.alias_email) self.exchange_type.mail_as_attachment = True From 422b3b0d38a809c46bc940d35dbcda055ce96d6b Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Wed, 4 Feb 2026 07:23:24 +0100 Subject: [PATCH 5/5] [IMP] edi_mail_import_oca: Move logic to backend --- edi_mail_import_oca/README.rst | 23 ++++ edi_mail_import_oca/__manifest__.py | 1 + edi_mail_import_oca/models/__init__.py | 3 +- edi_mail_import_oca/models/edi_backend.py | 31 +++++ .../models/edi_exchange_record.py | 91 +++++++++----- .../models/edi_exchange_type.py | 35 ++---- edi_mail_import_oca/readme/CONFIGURE.md | 13 ++ .../static/description/index.html | 41 ++++-- edi_mail_import_oca/tests/__init__.py | 1 + .../tests/test_mail_import_oca.py | 117 ++++++++---------- .../tests/test_mail_import_oca_no_file.py | 65 ++++++++++ edi_mail_import_oca/views/edi_backend.xml | 63 ++++++++++ .../views/edi_exchange_type.xml | 42 +------ 13 files changed, 356 insertions(+), 170 deletions(-) create mode 100644 edi_mail_import_oca/models/edi_backend.py create mode 100644 edi_mail_import_oca/readme/CONFIGURE.md create mode 100644 edi_mail_import_oca/tests/test_mail_import_oca_no_file.py create mode 100644 edi_mail_import_oca/views/edi_backend.xml diff --git a/edi_mail_import_oca/README.rst b/edi_mail_import_oca/README.rst index 53c282af7..bf0c71b24 100644 --- a/edi_mail_import_oca/README.rst +++ b/edi_mail_import_oca/README.rst @@ -49,6 +49,29 @@ however, there is an standard way to receive files that is emails. This module tries to offer a way to reuse EDI and mail interface in order to handle everything. +Configuration +============= + +For handling this incoming data, you just need to create a Backend and +define the alias. + +The alias will be used to detect input mails. If the system detects an +email, it will take all the exchange types related to the backend and +review by exchange_filename if the pattern is filled. It will create a +new exchange record for each exchange type that fulfills: + +1. Has the same backend type id +2. Has no backend or the backend is the current one +3. Has mail ``Mail Record Policy`` is ``full`` (it will generate a JSON + file with the full email) or the policy is ``pattern`` and there is a + file that follows the pattern (it will be used as the file for the + exchange record) + +If not exchange type can be found, a UserError and a bounce email are +sent. + +Processing will be handled in the standard way. + Bug Tracker =========== diff --git a/edi_mail_import_oca/__manifest__.py b/edi_mail_import_oca/__manifest__.py index a5c735fd1..2039da3b7 100644 --- a/edi_mail_import_oca/__manifest__.py +++ b/edi_mail_import_oca/__manifest__.py @@ -10,6 +10,7 @@ "website": "https://github.com/OCA/edi-framework", "depends": ["edi_core_oca", "mail"], "data": [ + "views/edi_backend.xml", "views/edi_exchange_type.xml", ], "demo": [], diff --git a/edi_mail_import_oca/models/__init__.py b/edi_mail_import_oca/models/__init__.py index 6128f744d..f076fb171 100644 --- a/edi_mail_import_oca/models/__init__.py +++ b/edi_mail_import_oca/models/__init__.py @@ -1,2 +1,3 @@ -from . import edi_exchange_type +from . import edi_backend from . import edi_exchange_record +from . import edi_exchange_type diff --git a/edi_mail_import_oca/models/edi_backend.py b/edi_mail_import_oca/models/edi_backend.py new file mode 100644 index 000000000..616127469 --- /dev/null +++ b/edi_mail_import_oca/models/edi_backend.py @@ -0,0 +1,31 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import ast + +from odoo import models + + +class EdiBackend(models.Model): + _name = "edi.backend" + _inherit = ["edi.backend", "mail.alias.mixin"] + + def _alias_get_creation_values(self): + values = super()._alias_get_creation_values() + values["alias_model_id"] = self.env["ir.model"]._get("edi.exchange.record").id + if self.id: + values["alias_defaults"] = defaults = ast.literal_eval( + self.alias_defaults or "{}" + ) + defaults["backend_id"] = self.id + return values + + def _mail_exchange_type_pending_input_domain(self): + """Domain for retrieving input exchange types for emails.""" + return [ + ("backend_type_id", "=", self.backend_type_id.id), + ("direction", "=", "input"), + "|", + ("backend_id", "=", False), + ("backend_id", "=", self.id), + ("mail_record_policy", "!=", False), + ] diff --git a/edi_mail_import_oca/models/edi_exchange_record.py b/edi_mail_import_oca/models/edi_exchange_record.py index 30ce58378..821c97a27 100644 --- a/edi_mail_import_oca/models/edi_exchange_record.py +++ b/edi_mail_import_oca/models/edi_exchange_record.py @@ -5,8 +5,10 @@ import json import logging import re +from collections import defaultdict from odoo import api, models +from odoo.exceptions import UserError _logger = logging.getLogger(__name__) @@ -16,42 +18,75 @@ class EdiExchangeRecord(models.Model): @api.model def message_new(self, msg_dict, custom_values=None): + backend_id = custom_values.get("backend_id") if custom_values else None + data = defaultdict(list) + if backend_id and not custom_values.get("type_id"): + backend = self.env["edi.backend"].browse(backend_id) + types = self.env["edi.exchange.type"].search( + backend._mail_exchange_type_pending_input_domain() + ) + for exchange_type in types: + if exchange_type.mail_record_policy == "pattern": + for attachment in msg_dict.get("attachments", []): + if re.match( + exchange_type.exchange_filename_pattern, + attachment.fname, + re.IGNORECASE, + ): + data[exchange_type.id].append( + [ + attachment.fname, + self._process_email_attachment(attachment), + ] + ) + else: + new_message_dict = msg_dict.copy() + attachments = new_message_dict.pop("attachments", []) + new_message_dict["attachments"] = [] + for attachment in attachments: + new_message_dict["attachments"].append( + { + "info": attachment.info, + "data": self._process_email_attachment(attachment), + "fname": attachment.fname, + } + ) + data[exchange_type.id].append( + ["email_message.json", json.dumps(new_message_dict)] + ) + if (not custom_values or "type_id" not in custom_values) and not data: + raise UserError( + self.env._( + "No exchange type found for incoming email with subject '%s'", + msg_dict.get("subject"), + ) + ) + if data: + for exchange_type_id in data: + filename, content = data[exchange_type_id].pop() + custom_values["type_id"] = exchange_type_id + break record = super().message_new( msg_dict, custom_values=custom_values, ) - if record.type_id.mail_as_attachment: - new_message_dict = msg_dict.copy() - attachments = new_message_dict.pop("attachments", []) - new_message_dict["attachments"] = [] - for attachment in attachments: - new_message_dict["attachments"].append( - { - "info": attachment.info, - "data": self._process_email_attachment(attachment), - "fname": attachment.fname, + if data: + record._set_file_content(content) + record.exchange_filename = filename + record.edi_exchange_state = "input_received" + for exchange_type_id in data: + for filename, content in data[exchange_type_id]: + new_record = record.copy( + default={ + "type_id": exchange_type_id, } ) - record._set_file_content(json.dumps(new_message_dict)) - record.edi_exchange_state = "input_received" - else: - content = False - filename = False - for attachment in msg_dict.get("attachments", []): - if re.match( - record.type_id.exchange_filename_pattern or ".*", - attachment.fname, - re.IGNORECASE, - ): - content = self._process_email_attachment(attachment) - filename = attachment.fname - break - if content: - record._set_file_content(content) - record.exchange_filename = filename - record.edi_exchange_state = "input_received" + new_record._set_file_content(content) + new_record.exchange_filename = filename + new_record.edi_exchange_state = "input_received" return record + @api.model def _process_email_attachment(self, attachment): """Process email attachment to be stored as file content.""" data = attachment[1] diff --git a/edi_mail_import_oca/models/edi_exchange_type.py b/edi_mail_import_oca/models/edi_exchange_type.py index 48360e9bb..2481b2c4d 100644 --- a/edi_mail_import_oca/models/edi_exchange_type.py +++ b/edi_mail_import_oca/models/edi_exchange_type.py @@ -1,36 +1,15 @@ # Copyright 2026 Dixmit # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -import ast -from odoo import api, fields, models -from odoo.exceptions import ValidationError +from odoo import fields, models class EdiExchangeType(models.Model): - _name = "edi.exchange.type" - _inherit = ["edi.exchange.type", "mail.alias.mixin"] + _inherit = "edi.exchange.type" - mail_as_attachment = fields.Boolean( - string="Import Email as an Attachment", + mail_record_policy = fields.Selection( + [ + ("full", "Only archive the full mail message"), + ("pattern", "Match single filename pattern"), + ] ) - - @api.constrains("direction", "alias_domain_id", "alias_name") - def _check_mail_configuration(self): - for record in self: - if record.direction != "input" and record.alias_email: - raise ValidationError( - self.env._( - "You cannot have a receiving email for a non-incoming type." - ) - ) - - def _alias_get_creation_values(self): - values = super()._alias_get_creation_values() - values["alias_model_id"] = self.env["ir.model"]._get("edi.exchange.record").id - if self.id: - values["alias_defaults"] = defaults = ast.literal_eval( - self.alias_defaults or "{}" - ) - defaults["backend_id"] = self.backend_id.id - defaults["type_id"] = self.id - return values diff --git a/edi_mail_import_oca/readme/CONFIGURE.md b/edi_mail_import_oca/readme/CONFIGURE.md new file mode 100644 index 000000000..4a3ea4364 --- /dev/null +++ b/edi_mail_import_oca/readme/CONFIGURE.md @@ -0,0 +1,13 @@ +For handling this incoming data, you just need to create a Backend and define the alias. + +The alias will be used to detect input mails. +If the system detects an email, it will take all the exchange types related to the backend and review by exchange_filename if the pattern is filled. +It will create a new exchange record for each exchange type that fulfills: + +1. Has the same backend type id +2. Has no backend or the backend is the current one +3. Has mail `Mail Record Policy` is `full` (it will generate a JSON file with the full email) or the policy is `pattern` and there is a file that follows the pattern (it will be used as the file for the exchange record) + +If not exchange type can be found, a UserError and a bounce email are sent. + +Processing will be handled in the standard way. diff --git a/edi_mail_import_oca/static/description/index.html b/edi_mail_import_oca/static/description/index.html index ad1fbae95..72a804140 100644 --- a/edi_mail_import_oca/static/description/index.html +++ b/edi_mail_import_oca/static/description/index.html @@ -381,11 +381,12 @@

Edi Mail Import Oca

+
+

Configuration

+

For handling this incoming data, you just need to create a Backend and +define the alias.

+

The alias will be used to detect input mails. If the system detects an +email, it will take all the exchange types related to the backend and +review by exchange_filename if the pattern is filled. It will create a +new exchange record for each exchange type that fulfills:

+
    +
  1. Has the same backend type id
  2. +
  3. Has no backend or the backend is the current one
  4. +
  5. Has mail Mail Record Policy is full (it will generate a JSON +file with the full email) or the policy is pattern and there is a +file that follows the pattern (it will be used as the file for the +exchange record)
  6. +
+

If not exchange type can be found, a UserError and a bounce email are +sent.

+

Processing will be handled in the standard way.

+
-

Bug Tracker

+

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 to smash it by providing a detailed and welcomed @@ -406,15 +427,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Dixmit
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association diff --git a/edi_mail_import_oca/tests/__init__.py b/edi_mail_import_oca/tests/__init__.py index 51632faa6..720a332c1 100644 --- a/edi_mail_import_oca/tests/__init__.py +++ b/edi_mail_import_oca/tests/__init__.py @@ -1 +1,2 @@ from . import test_mail_import_oca +from . import test_mail_import_oca_no_file diff --git a/edi_mail_import_oca/tests/test_mail_import_oca.py b/edi_mail_import_oca/tests/test_mail_import_oca.py index f78036605..b5ed1c255 100644 --- a/edi_mail_import_oca/tests/test_mail_import_oca.py +++ b/edi_mail_import_oca/tests/test_mail_import_oca.py @@ -1,15 +1,11 @@ -# Part of Odoo. See LICENSE file for full copyright and licensing details. - +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import json -from odoo.exceptions import ValidationError -from odoo.tests import tagged - from odoo.addons.mail.tests.common import MailCommon from odoo.addons.test_mail.data.test_mail_data import MAIL_EML_ATTACHMENT -@tagged("mail_gateway") class TestEmailParsing(MailCommon): """Test email parsing and import via mail gateway""" @@ -26,41 +22,67 @@ def setUpClass(cls): { "name": "Mail Import OCA Test Backend", "backend_type_id": cls.backend_type.id, + "alias_name": "edi-input", } ) - cls.exchange_type = cls.env["edi.exchange.type"].create( + cls.exchange_type_01 = cls.env["edi.exchange.type"].create( { "name": "Test Exchange Type", - "code": "test_exchange_type", + "code": "test_exchange_type_01", + "mail_record_policy": "full", "direction": "input", - "alias_name": "edi-input", "backend_type_id": cls.backend_type.id, "backend_id": cls.backend.id, } ) - - def test_constraint(self): - with self.assertRaises(ValidationError): - self.exchange_type.direction = "output" - - def test_constraint_no_error_on_no_alias(self): - self.exchange_type.alias_name = False - self.exchange_type.direction = "output" - self.assertFalse(self.exchange_type.alias_email) + cls.exchange_type_02 = cls.env["edi.exchange.type"].create( + { + "name": "Test Exchange Type", + "code": "test_exchange_type_02", + "mail_record_policy": "pattern", + "exchange_filename_pattern": ".*eml", + "direction": "input", + "backend_type_id": cls.backend_type.id, + "backend_id": cls.backend.id, + } + ) + cls.exchange_type_03 = cls.env["edi.exchange.type"].create( + { + "name": "Test Exchange Type", + "code": "test_exchange_type_03", + "mail_record_policy": "pattern", + "exchange_filename_pattern": ".*jpg", + "direction": "input", + "backend_type_id": cls.backend_type.id, + "backend_id": cls.backend.id, + } + ) + cls.exchange_type_04 = cls.env["edi.exchange.type"].create( + { + "name": "Test Exchange Type", + "code": "test_exchange_type_04", + "exchange_filename_pattern": ".*eml", + "direction": "input", + "backend_type_id": cls.backend_type.id, + "backend_id": cls.backend.id, + } + ) + cls.exchange_types = ( + cls.exchange_type_01 | cls.exchange_type_02 | cls.exchange_type_03 + ) def test_import_full(self): - self.assertTrue(self.exchange_type.alias_email) - self.exchange_type.mail_as_attachment = True + self.assertTrue(self.backend.alias_email) self.assertFalse( self.env["edi.exchange.record"].search( [ - ("type_id", "=", self.exchange_type.id), + ("type_id", "in", self.exchange_types.ids), ] ) ) mail = self.format( MAIL_EML_ATTACHMENT, - to=self.exchange_type.alias_email, + to=self.backend.alias_email, subject="purchase test mail", target_model="account.move", msg_id="", @@ -68,7 +90,7 @@ def test_import_full(self): self.env["mail.thread"].message_process("mail.thread", mail) record = self.env["edi.exchange.record"].search( [ - ("type_id", "=", self.exchange_type.id), + ("type_id", "=", self.exchange_type_01.id), ] ) self.assertTrue(record) @@ -77,58 +99,25 @@ def test_import_full(self): self.assertTrue(file_content) data = json.loads(file_content) self.assertIn("body", data) - - def test_import_specific_file(self): - self.assertTrue(self.exchange_type.alias_email) - self.exchange_type.mail_as_attachment = False - self.exchange_type.exchange_filename_pattern = ".*eml" - self.assertFalse( - self.env["edi.exchange.record"].search( - [ - ("type_id", "=", self.exchange_type.id), - ] - ) - ) - mail = self.format( - MAIL_EML_ATTACHMENT, - to=self.exchange_type.alias_email, - subject="purchase test mail", - target_model="account.move", - msg_id="", - ) - self.env["mail.thread"].message_process("mail.thread", mail) record = self.env["edi.exchange.record"].search( [ - ("type_id", "=", self.exchange_type.id), + ("type_id", "=", self.exchange_type_02.id), ] ) self.assertTrue(record) self.assertEqual(record.edi_exchange_state, "input_received") self.assertEqual(record.exchange_filename, "original_msg.eml") - - def test_import_no_file_found(self): - self.assertTrue(self.exchange_type.alias_email) - self.exchange_type.mail_as_attachment = False - self.exchange_type.exchange_filename_pattern = ".*xml" self.assertFalse( self.env["edi.exchange.record"].search( [ - ("type_id", "=", self.exchange_type.id), + ("type_id", "=", self.exchange_type_03.id), ] ) ) - mail = self.format( - MAIL_EML_ATTACHMENT, - to=self.exchange_type.alias_email, - subject="purchase test mail", - target_model="account.move", - msg_id="", - ) - self.env["mail.thread"].message_process("mail.thread", mail) - record = self.env["edi.exchange.record"].search( - [ - ("type_id", "=", self.exchange_type.id), - ] + self.assertFalse( + self.env["edi.exchange.record"].search( + [ + ("type_id", "=", self.exchange_type_04.id), + ] + ) ) - self.assertTrue(record) - self.assertEqual(record.edi_exchange_state, "new") diff --git a/edi_mail_import_oca/tests/test_mail_import_oca_no_file.py b/edi_mail_import_oca/tests/test_mail_import_oca_no_file.py new file mode 100644 index 000000000..2192bee22 --- /dev/null +++ b/edi_mail_import_oca/tests/test_mail_import_oca_no_file.py @@ -0,0 +1,65 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api +from odoo.exceptions import UserError +from odoo.tools import mute_logger + +from odoo.addons.mail.tests.common import MailCommon +from odoo.addons.test_mail.data.test_mail_data import MAIL_EML_ATTACHMENT + + +class TestEmailParsing(MailCommon): + """Test email parsing and import via mail gateway""" + + def setUp(self): + super().setUp() + self.registry_enter_test_mode() + # now we make a test cursor for self.cr + self.cr = self.registry.cursor() + self.addCleanup(self.cr.close) + self.env = api.Environment(self.cr, api.SUPERUSER_ID, {}) + self.backend_type = self.env["edi.backend.type"].create( + { + "name": "Mail Import OCA Test Backend Type", + "code": "mail_import_oca_test_backend_type", + } + ) + self.backend = self.env["edi.backend"].create( + { + "name": "Mail Import OCA Test Backend", + "backend_type_id": self.backend_type.id, + "alias_name": "edi-input", + } + ) + self.exchange_type = self.env["edi.exchange.type"].create( + { + "name": "Test Exchange Type", + "code": "test_exchange_type", + "direction": "input", + "mail_record_policy": "pattern", + "exchange_filename_pattern": ".*xml", + "backend_type_id": self.backend_type.id, + "backend_id": self.backend.id, + } + ) + + @mute_logger("odoo.addons.edi_mail_import_oca.models.edi_exchange_record") + def test_import_no_file_found(self): + self.assertTrue(self.backend.alias_email) + self.assertFalse( + self.env["edi.exchange.record"].search( + [ + ("type_id", "=", self.exchange_type.id), + ] + ) + ) + mail = self.format( + MAIL_EML_ATTACHMENT, + to=self.backend.alias_email, + subject="purchase test mail", + target_model="account.move", + msg_id="", + ) + with self.assertRaises(UserError): + self.env["mail.thread"].message_process("mail.thread", mail) diff --git a/edi_mail_import_oca/views/edi_backend.xml b/edi_mail_import_oca/views/edi_backend.xml new file mode 100644 index 000000000..07f505ebd --- /dev/null +++ b/edi_mail_import_oca/views/edi_backend.xml @@ -0,0 +1,63 @@ + + + + + edi.backend + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+
+
+
diff --git a/edi_mail_import_oca/views/edi_exchange_type.xml b/edi_mail_import_oca/views/edi_exchange_type.xml index e4590efce..8459c0ea0 100644 --- a/edi_mail_import_oca/views/edi_exchange_type.xml +++ b/edi_mail_import_oca/views/edi_exchange_type.xml @@ -6,45 +6,9 @@ edi.exchange.type - - -
- - - -
- - - -
-
+ + +