import base64 import contextlib import textwrap import uuid from unittest.mock import patch from odoo import Command from odoo.exceptions import ValidationError from odoo.tests import tagged, RecordCapturer from odoo.tools import file_open from odoo.tools.misc import mute_logger from odoo.addons.account.tests.common import AccountTestInvoicingCommon from odoo.addons.mail.tests.common import MailCommon from odoo.addons.test_mimetypes.tests.test_guess_mimetypes import contents class TestAccountInvoiceImportMixin: """ Helpers for uploading attachments on invoices by various means and asserting how they are decoded. """ @classmethod def _get_dummy_pdf_vals(cls): rawpdf_base64 = b'JVBERi0xLjYNJeLjz9MNCjI0IDAgb2JqDTw8L0ZpbHRlci9GbGF0ZURlY29kZS9GaXJzdCA0L0xlbmd0aCAyMTYvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjePI9RS8MwFIX/yn1bi9jepCQ6GYNpFBTEMsW97CVLbjWYNpImmz/fVsXXcw/f/c4SEFarepPTe4iFok8dU09DgtDBQx6TMwT74vaLTE7uSPDUdXM0Xe/73r1FnVwYYEtHR6d9WdY3kX4ipRMV6oojSmxQMoGyac5RLBAXf63p38aGA7XPorLewyvFcYaJile8rB+D/YcwiRdMMGScszO8/IW0MdhsaKKYGA46gXKTr/cUQVY4We/cYMNpnLVeXPJUXHs9fECr7kAFk+eZ5Xr9LcAAfKpQrA0KZW5kc3RyZWFtDWVuZG9iag0yNSAwIG9iag08PC9GaWx0ZXIvRmxhdGVEZWNvZGUvRmlyc3QgNC9MZW5ndGggNDkvTiAxL1R5cGUvT2JqU3RtPj5zdHJlYW0NCmjeslAwULCx0XfOL80rUTDU985MKY42NAIKBsXqh1QWpOoHJKanFtvZAQQYAN/6C60NCmVuZHN0cmVhbQ1lbmRvYmoNMjYgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDkvTGVuZ3RoIDQyL04gMi9UeXBlL09ialN0bT4+c3RyZWFtDQpo3jJTMFAwVzC0ULCx0fcrzS2OBnENFIJi7eyAIsH6LnZ2AAEGAI2FCDcNCmVuZHN0cmVhbQ1lbmRvYmoNMjcgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0ZpcnN0IDUvTGVuZ3RoIDEyMC9OIDEvVHlwZS9PYmpTdG0+PnN0cmVhbQ0KaN4yNFIwULCx0XfOzytJzSspVjAyBgoE6TsX5Rc45VdEGwB5ZoZGCuaWRrH6vqkpmYkYogGJRUCdChZgfUGpxfmlRcmpxUAzA4ryk4NTS6L1A1zc9ENSK0pi7ez0g/JLEktSFQz0QyoLUoF601Pt7AACDADYoCeWDQplbmRzdHJlYW0NZW5kb2JqDTIgMCBvYmoNPDwvTGVuZ3RoIDM1MjUvU3VidHlwZS9YTUwvVHlwZS9NZXRhZGF0YT4+c3RyZWFtDQo8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjQtYzAwNSA3OC4xNDczMjYsIDIwMTIvMDgvMjMtMTM6MDM6MDMgICAgICAgICI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnBkZj0iaHR0cDovL25zLmFkb2JlLmNvbS9wZGYvMS4zLyIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIj4KICAgICAgICAgPHBkZjpQcm9kdWNlcj5BY3JvYmF0IERpc3RpbGxlciA2LjAgKFdpbmRvd3MpPC9wZGY6UHJvZHVjZXI+CiAgICAgICAgIDx4bXA6Q3JlYXRlRGF0ZT4yMDA2LTAzLTA2VDE1OjA2OjMzLTA1OjAwPC94bXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZVBTNS5kbGwgVmVyc2lvbiA1LjIuMjwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxNi0wNy0xNVQxMDoxMjoyMSswODowMDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6TWV0YWRhdGFEYXRlPjIwMTYtMDctMTVUMTA6MTI6MjErMDg6MDA8L3htcDpNZXRhZGF0YURhdGU+CiAgICAgICAgIDx4bXBNTTpEb2N1bWVudElEPnV1aWQ6ZmYzZGNmZDEtMjNmYS00NzZmLTgzOWEtM2U1Y2FlMmRhMmViPC94bXBNTTpEb2N1bWVudElEPgogICAgICAgICA8eG1wTU06SW5zdGFuY2VJRD51dWlkOjM1OTM1MGIzLWFmNDAtNGQ4YS05ZDZjLTAzMTg2YjRmZmIzNjwveG1wTU06SW5zdGFuY2VJRD4KICAgICAgICAgPGRjOmZvcm1hdD5hcHBsaWNhdGlvbi9wZGY8L2RjOmZvcm1hdD4KICAgICAgICAgPGRjOnRpdGxlPgogICAgICAgICAgICA8cmRmOkFsdD4KICAgICAgICAgICAgICAgPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5CbGFuayBQREYgRG9jdW1lbnQ8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6QWx0PgogICAgICAgICA8L2RjOnRpdGxlPgogICAgICAgICA8ZGM6Y3JlYXRvcj4KICAgICAgICAgICAgPHJkZjpTZXE+CiAgICAgICAgICAgICAgIDxyZGY6bGk+RGVwYXJ0bWVudCBvZiBKdXN0aWNlIChFeGVjdXRpdmUgT2ZmaWNlIG9mIEltbWlncmF0aW9uIFJldmlldyk8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6U2VxPgogICAgICAgICA8L2RjOmNyZWF0b3I+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgog' + 682 * b'ICAg' + b'Cjw/eHBhY2tldCBlbmQ9InciPz4NCmVuZHN0cmVhbQ1lbmRvYmoNMTEgMCBvYmoNPDwvTWV0YWRhdGEgMiAwIFIvUGFnZUxhYmVscyA2IDAgUi9QYWdlcyA4IDAgUi9UeXBlL0NhdGFsb2c+Pg1lbmRvYmoNMjMgMCBvYmoNPDwvRmlsdGVyL0ZsYXRlRGVjb2RlL0xlbmd0aCAxMD4+c3RyZWFtDQpIiQIIMAAAAAABDQplbmRzdHJlYW0NZW5kb2JqDTI4IDAgb2JqDTw8L0RlY29kZVBhcm1zPDwvQ29sdW1ucyA0L1ByZWRpY3RvciAxMj4+L0ZpbHRlci9GbGF0ZURlY29kZS9JRFs8REI3Nzc1Q0NFMjI3RjZCMzBDNDQwREY0MjIxREMzOTA+PEJGQ0NDRjNGNTdGNjEzNEFCRDNDMDRBOUU0Q0ExMDZFPl0vSW5mbyA5IDAgUi9MZW5ndGggODAvUm9vdCAxMSAwIFIvU2l6ZSAyOS9UeXBlL1hSZWYvV1sxIDIgMV0+PnN0cmVhbQ0KaN5iYgACJjDByGzIwPT/73koF0wwMUiBWYxA4v9/EMHA9I/hBVCxoDOQeH8DxH2KrIMIglFwIpD1vh5IMJqBxPpArHYgwd/KABBgAP8bEC0NCmVuZHN0cmVhbQ1lbmRvYmoNc3RhcnR4cmVmDQo0NTc2DQolJUVPRg0K' return { 'raw': base64.b64decode(rawpdf_base64), 'type': 'binary', 'mimetype': 'application/pdf', } @classmethod def _get_dummy_pdf_with_embedded_file_vals(cls): """ This PDF has an embedded file with filename 'embedded.xml' and the following content partner_a """ rawpdf_base64 = b'JVBERi0xLjYKJeLjz9MKMyAwIG9iaiAKPDwKL1R5cGUgL0VtYmVkZGVkRmlsZQovRmlsdGVyIC9GbGF0ZURlY29kZQovUGFyYW1zIDEgMCBSCi9MZW5ndGggMiAwIFIKPj4Kc3RyZWFtCnics7GvyM1RKEstKs7Mz7NVMtQzUFJIzUvOT8nMS7dVCg1x07VQsrfjsglJLS5xy8xJdcsvyk0sseNSAAKbgMSikrzUIr/E3FS7Agg7PtFGH1mYy0YfXSsAc6MmMgplbmRzdHJlYW0gCmVuZG9iaiAKMiAwIG9iaiA5MwplbmRvYmogCjEgMCBvYmogCjw8Ci9TaXplIDExNQo+PgplbmRvYmogCjQgMCBvYmogCjw8Ci9UeXBlIC9GCi9GIChlbWJlZGRlZC54bWwpCi9FRiAKPDwKL0YgMyAwIFIKPj4KL1VGICj+/wBlAG0AYgBlAGQAZABlAGQALgB4AG0AbCkKPj4KZW5kb2JqIAo1IDAgb2JqIAo8PAovTmFtZXMgWyj+/wBlAG0AYgBlAGQAZABlAGQALgB4AG0AbCkgNCAwIFJdCj4+CmVuZG9iaiAKNiAwIG9iaiAKPDwKL0VtYmVkZGVkRmlsZXMgNSAwIFIKPj4KZW5kb2JqIAo3IDAgb2JqIAo8PAovU3VidHlwZSAvWE1MCi9UeXBlIC9NZXRhZGF0YQovTGVuZ3RoIDM1MjUKPj4Kc3RyZWFtCjw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNC1jMDA1IDc4LjE0NzMyNiwgMjAxMi8wOC8yMy0xMzowMzowMyAgICAgICAgIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6cGRmPSJodHRwOi8vbnMuYWRvYmUuY29tL3BkZi8xLjMvIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgICAgICAgICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iPgogICAgICAgICA8cGRmOlByb2R1Y2VyPkFjcm9iYXQgRGlzdGlsbGVyIDYuMCAoV2luZG93cyk8L3BkZjpQcm9kdWNlcj4KICAgICAgICAgPHhtcDpDcmVhdGVEYXRlPjIwMDYtMDMtMDZUMTU6MDY6MzMtMDU6MDA8L3htcDpDcmVhdGVEYXRlPgogICAgICAgICA8eG1wOkNyZWF0b3JUb29sPkFkb2JlUFM1LmRsbCBWZXJzaW9uIDUuMi4yPC94bXA6Q3JlYXRvclRvb2w+CiAgICAgICAgIDx4bXA6TW9kaWZ5RGF0ZT4yMDE2LTA3LTE1VDEwOjEyOjIxKzA4OjAwPC94bXA6TW9kaWZ5RGF0ZT4KICAgICAgICAgPHhtcDpNZXRhZGF0YURhdGU+MjAxNi0wNy0xNVQxMDoxMjoyMSswODowMDwveG1wOk1ldGFkYXRhRGF0ZT4KICAgICAgICAgPHhtcE1NOkRvY3VtZW50SUQ+dXVpZDpmZjNkY2ZkMS0yM2ZhLTQ3NmYtODM5YS0zZTVjYWUyZGEyZWI8L3htcE1NOkRvY3VtZW50SUQ+CiAgICAgICAgIDx4bXBNTTpJbnN0YW5jZUlEPnV1aWQ6MzU5MzUwYjMtYWY0MC00ZDhhLTlkNmMtMDMxODZiNGZmYjM2PC94bXBNTTpJbnN0YW5jZUlEPgogICAgICAgICA8ZGM6Zm9ybWF0PmFwcGxpY2F0aW9uL3BkZjwvZGM6Zm9ybWF0PgogICAgICAgICA8ZGM6dGl0bGU+CiAgICAgICAgICAgIDxyZGY6QWx0PgogICAgICAgICAgICAgICA8cmRmOmxpIHhtbDpsYW5nPSJ4LWRlZmF1bHQiPkJsYW5rIFBERiBEb2N1bWVudDwvcmRmOmxpPgogICAgICAgICAgICA8L3JkZjpBbHQ+CiAgICAgICAgIDwvZGM6dGl0bGU+CiAgICAgICAgIDxkYzpjcmVhdG9yPgogICAgICAgICAgICA8cmRmOlNlcT4KICAgICAgICAgICAgICAgPHJkZjpsaT5EZXBhcnRtZW50IG9mIEp1c3RpY2UgKEV4ZWN1dGl2ZSBPZmZpY2Ugb2YgSW1taWdyYXRpb24gUmV2aWV3KTwvcmRmOmxpPgogICAgICAgICAgICA8L3JkZjpTZXE+CiAgICAgICAgIDwvZGM6Y3JlYXRvcj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAg' + 681 * b'ICAg' + b'ICAKPD94cGFja2V0IGVuZD0idyI/PgplbmRzdHJlYW0gCmVuZG9iaiAKOCAwIG9iaiAKPDwKL051bXMgWzAgOSAwIFJdCj4+CmVuZG9iaiAKOSAwIG9iaiAKPDwKL1MgL0QKPj4KZW5kb2JqIAoxMCAwIG9iaiAKPDwKL0tpZHMgWzExIDAgUl0KL1R5cGUgL1BhZ2VzCi9Db3VudCAxCj4+CmVuZG9iaiAKMTIgMCBvYmogCjw8Ci9NZXRhZGF0YSA3IDAgUgovUGFnZUxhYmVscyA4IDAgUgovTmFtZXMgNiAwIFIKL1R5cGUgL0NhdGFsb2cKL1BhZ2VzIDEwIDAgUgo+PgplbmRvYmogCjExIDAgb2JqIAo8PAovUm90YXRlIDAKL1Jlc291cmNlcyAKPDwKL1Byb2NTZXQgWy9QREYgL1RleHRdCj4+Ci9UeXBlIC9QYWdlCi9QYXJlbnQgMTAgMCBSCi9Db250ZW50cyAxMyAwIFIKL01lZGlhQm94IFswIDAgNjEyIDc5Ml0KL0Nyb3BCb3ggWzAgMCA2MTIgNzkyXQo+PgplbmRvYmogCjEzIDAgb2JqIAo8PAovRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDEwCj4+CnN0cmVhbQpIiQIIMAAAAAABCmVuZHN0cmVhbSAKZW5kb2JqIAoxNCAwIG9iaiAKPDwKL01vZERhdGUgKEQ6MjAxNjA3MTUxMDEyMjErMDgnMDAnKQovQ3JlYXRpb25EYXRlIChEOjIwMDYwMzA2MTUwNjMzLTA1JzAwJykKL0F1dGhvciAoRGVwYXJ0bWVudCBvZiBKdXN0aWNlIFwoRXhlY3V0aXZlIE9mZmljZSBvZiBJbW1pZ3JhdGlvbiBSZXZpZXdcKSkKL1RpdGxlIChCbGFuayBQREYgRG9jdW1lbnQpCi9DcmVhdG9yIChBZG9iZVBTNS5kbGwgVmVyc2lvbiA1LjIuMikKL1Byb2R1Y2VyIChBY3JvYmF0IERpc3RpbGxlciA2LjAgXChXaW5kb3dzXCkpCj4+CmVuZG9iaiB4cmVmCjAgMTUKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMjM4IDAwMDAwIG4gCjAwMDAwMDAyMTkgMDAwMDAgbiAKMDAwMDAwMDAxNSAwMDAwMCBuIAowMDAwMDAwMjcxIDAwMDAwIG4gCjAwMDAwMDAzNzQgMDAwMDAgbiAKMDAwMDAwMDQ0MSAwMDAwMCBuIAowMDAwMDAwNDg1IDAwMDAwIG4gCjAwMDAwMDQwOTUgMDAwMDAgbiAKMDAwMDAwNDEzNCAwMDAwMCBuIAowMDAwMDA0MTYzIDAwMDAwIG4gCjAwMDAwMDQzMjQgMDAwMDAgbiAKMDAwMDAwNDIyNCAwMDAwMCBuIAowMDAwMDA0NDg5IDAwMDAwIG4gCjAwMDAwMDQ1NzQgMDAwMDAgbiAKdHJhaWxlcgoKPDwKL0luZm8gMTQgMCBSCi9JRCBbPGRiNzc3NWNjZTIyN2Y2YjMwYzQ0MGRmNDIyMWRjMzkwPiA8YmZjY2NmM2Y1N2Y2MTM0YWJkM2MwNGE5ZTRjYTEwNmU+XQovUm9vdCAxMiAwIFIKL1NpemUgMTUKPj4Kc3RhcnR4cmVmCjQ4NTkKJSVFT0YK' return { 'raw': base64.b64decode(rawpdf_base64), 'type': 'binary', 'mimetype': 'application/pdf', } @classmethod def _get_dummy_xml_vals(cls): return { 'raw': b""" partner_a """, 'mimetype': 'application/xml', } @classmethod def _get_dummy_gif_vals(cls): return { 'raw': base64.b64decode("R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs="), 'mimetype': 'image/gif', } @classmethod def _get_dummy_xlsx_vals(cls): return { 'raw': contents('xlsx'), 'mimetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', } @classmethod def _get_dummy_docx_vals(cls): return { 'raw': contents('docx'), 'mimetype': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', } def assert_attachment_import(self, origin, attachments_vals, expected_invoices): """ Simulate the upload and import of one or more attachments and assert that the created attachments were linked to the expected messages and invoices. :param origin: The source from which the attachments came (see `_upload_and_import_attachments`). :param attachments_vals: A list of values representing the attachments to be uploaded to Odoo :param expected_invoices: a dict { invoice_index (int): { filename: { 'on_invoice': (bool) whether it should be attached on the invoice, 'on_message': (bool) whether it should be attached to a message in the chatter, 'is_decoded': (bool) whether it should have been decoded on the invoice, 'is_new': (bool) whether the call to the decoder should have `new=True` } } } which for each newly-created invoice indicates: (1) which of the files it should be linked to, and, for each file (a) whether it should be attached to the invoice or merely to a message on the invoice. (b) whether it should have been decoded on the invoice """ # Because no decoders are defined in `account` itself, if we want to test the decoder flow we need # to define a fictional format that will be decoded, and patch the `_get_import_file_type` # and `_get_edi_decoder` methods to accept it. with self._patch_import_methods() as decoder_calls: created_attachments, created_messages, created_invoices = self._upload_and_import_attachments(origin, attachments_vals) # Check that no two attachments were created with the same filename (needed for the rest of the test to work properly) self.assertEqual(len(created_attachments), len(created_attachments.grouped('name'))) # Construct a dict representing the way the attachments were linked to new invoices and messages. actual_invoices = {} for message in created_messages.filtered(lambda m: m.model == 'account.move'): for attachment in message.attachment_ids: actual_invoices.setdefault(message.res_id, {}).setdefault(attachment.name, {})['on_message'] = True for attachment in created_attachments: if attachment.res_model == 'account.move': actual_invoices.setdefault(attachment.res_id, {}).setdefault(attachment.name, {})['on_invoice'] = True for decoder_call in decoder_calls: invoice = decoder_call[0] filename = decoder_call[1]['name'] actual_invoices.setdefault(invoice.id, {}).setdefault(filename, {})['is_decoded'] = True if decoder_call[2]: actual_invoices[invoice.id][filename]['is_new'] = True # Map the invoice IDs to the invoice indexes of the expected_invoices. index_by_invoice_id = { invoice_id: index for index, invoice_id in enumerate(created_invoices.mapped('id'), start=1) } actual_invoices = { index_by_invoice_id[invoice_id]: attachment_info for invoice_id, attachment_info in actual_invoices.items() } self.assertDictEqual(actual_invoices, expected_invoices) @contextlib.contextmanager def _patch_import_methods(self): """ Patch the `_get_import_file_type` and `_get_edi_decoder` methods to accept the 'test_xml' format. """ original_get_import_file_type = self.env.registry['account.move']._get_import_file_type def patched_get_import_file_type(self, file_data): """ Patch _get_import_file_type in order to recognize the 'test_xml' format which is an XML whose root tag is 'TestFileFormat'. """ if file_data['xml_tree'] is not None and file_data['xml_tree'].tag == 'TestFileFormat': return 'test_xml' return original_get_import_file_type(self, file_data) decoder_calls = [] original_get_edi_decoder = self.env.registry['account.move']._get_edi_decoder def patched_get_edi_decoder(self, file_data, new): if file_data['import_file_type'] == 'test_xml': def decoder(invoice, file_data, new): if invoice.invoice_line_ids: return invoice._reason_cannot_decode_has_invoice_lines() decoder_calls.append((invoice, file_data, new)) partner_name = file_data['xml_tree'].findtext('.//PartnerName') if partner_name and (partner := self.env['res.partner'].search([('name', '=', partner_name)], limit=1)): invoice.partner_id = partner.id else: raise ValidationError('Could not identify partner!') return { 'decoder': decoder, 'priority': 20, } elif file_data['import_file_type'] == 'pdf': def decoder(invoice, file_data, new): if invoice.invoice_line_ids: return invoice._reason_cannot_decode_has_invoice_lines() decoder_calls.append((invoice, file_data, new)) return { 'decoder': decoder, 'priority': 10, } else: original_decoder_info = original_get_edi_decoder(self, file_data, new) if original_decoder_info is None: return None def decoder(invoice, file_data, new): decoder_calls.append((invoice, file_data, new)) return original_decoder_info['decoder'](invoice, file_data, new) return { **original_decoder_info, 'decoder': decoder, } with ( patch.object(self.env.registry['account.move'], '_get_import_file_type', patched_get_import_file_type), patch.object(self.env.registry['account.move'], '_get_edi_decoder', patched_get_edi_decoder), ): yield decoder_calls def _upload_and_import_attachments(self, origin, attachments_vals): """ Simulate the upload of one or more attachments and their processing by the import framework. Keeps track of the created attachments, messages and invoices, and returns them. :param origin: The source from which the attachments should be introduced into Odoo. Possible values: - 'chatter_message': Simulates a message posted on the chatter of an existing vendor bill. - 'chatter_upload': Simulates attachments uploaded on the chatter of an existing vendor bill. - 'chatter_email': Simulates an incoming e-mail on the chatter of an existing vendor bill. - 'mail_alias': Simulates an incoming e-mail on a purchase journal mail alias. - 'journal': Simulates attachments uploaded on a purchase journal in the dashboard. :param attachments_vals: A list of values representing attachments to upload into Odoo. :return: a dict { 'ir.attachment': created_attachments, 'mail.message': created_messages, 'account.move': created_invoices, } """ with ( RecordCapturer(self.env['ir.attachment'].sudo()) as attachment_capturer, RecordCapturer(self.env['mail.message'].sudo()) as message_capturer, RecordCapturer(self.env['account.move']) as move_capturer, ): journal = self.company_data['default_journal_purchase'] init_vals = {'move_type': 'in_invoice', 'journal_id': journal.id} if origin not in {'chatter_email', 'mail_alias'}: attachments = self.env['ir.attachment'].create(attachments_vals) if origin in {'chatter_upload', 'chatter_message', 'chatter_email'}: move = self.env['account.move'].create(init_vals) match origin: case 'chatter_message': move.message_post(message_type='comment', attachment_ids=attachments.ids) case 'chatter_upload': attachments.write({'res_model': 'account.move', 'res_id': move.id}) attachments._post_add_create() case 'chatter_email': email_raw = self._get_raw_mail_message_str(attachments_vals, email_to='someone@example.com') self.env['mail.thread'].message_process('account.move', email_raw, custom_values=init_vals, thread_id=move.id) case 'mail_alias': email_raw = self._get_raw_mail_message_str(attachments_vals, email_to=journal.alias_id.display_name) self.env['mail.thread'].message_process('account.move', email_raw, custom_values=init_vals) case 'journal': journal.create_document_from_attachment(attachments.ids) case _: raise ValueError(f"Unknown origin: {origin}") return attachment_capturer.records, message_capturer.records, move_capturer.records def _get_raw_mail_message_str(self, attachments_vals, email_to, message_id=None): """ :param attachments_vals: list of attachment values. :param email_to: string that will fill email_to field in the email, probably you'll want to use some journal alias here. :param message_id: Optional. Custom message ID for the email. If not provided, a UUID will be generated. Returns: Formatted email string. """ if not message_id: message_id = str(uuid.uuid4()) attachment_parts = [] for attachment in attachments_vals: encoded_attachment = base64.b64encode(attachment['raw']).decode() attachment_part = textwrap.dedent(f"""\ --000000000000a47519057e029630 Content-Type: {attachment['mimetype']} Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="{attachment['name']}" {encoded_attachment} """) attachment_parts.append(attachment_part) email_raw = textwrap.dedent(f"""\ MIME-Version: 1.0 Date: Fri, 26 Nov 2021 16:27:45 +0100 Message-ID: {message_id} Subject: Incoming bill From: Someone To: {email_to} Content-Type: multipart/alternative; boundary="000000000000a47519057e029630" --000000000000a47519057e029630 Content-Type: text/plain; charset="UTF-8" Here is your requested document(s). """) email_raw += "\n".join(attachment_parts) email_raw += "\n--000000000000a47519057e029630--" return email_raw @tagged('post_install', '-at_install', 'mail_gateway') class TestAccountIncomingSupplierInvoice(AccountTestInvoicingCommon, TestAccountInvoiceImportMixin, MailCommon): @classmethod def setUpClass(cls): super().setUpClass() # Disable OCR company = cls.company_data['company'] if 'extract_in_invoice_digitalization_mode' in company._fields: company.extract_in_invoice_digitalization_mode = 'no_send' company.extract_out_invoice_digitalization_mode = 'no_send' cls.internal_user = cls._create_new_internal_user(login='internal.user@test.odoo.com') cls.supplier_partner = cls.env['res.partner'].create({ 'name': 'Your Supplier', 'email': 'supplier@other.company.com', 'supplier_rank': 10, }) cls.journal = cls.company_data['default_journal_purchase'] cls.pdf1_vals = {'name': 'invoice1.pdf', **cls._get_dummy_pdf_vals()} cls.pdf2_vals = {'name': 'invoice2.pdf', **cls._get_dummy_pdf_vals()} cls.pdf3_vals = {'name': 'invoice3.pdf', **cls._get_dummy_pdf_with_embedded_file_vals()} cls.gif1_vals = {'name': 'gif1.gif', **cls._get_dummy_gif_vals()} cls.gif2_vals = {'name': 'gif2.gif', **cls._get_dummy_gif_vals()} cls.xml1_vals = {'name': 'invoice1.xml', **cls._get_dummy_xml_vals()} cls.xml2_vals = {'name': 'invoice2.xml', **cls._get_dummy_xml_vals()} # These have deliberately similar names to `invoice2.pdf` to test grouping by name similarity # when coming from the mail alias. cls.docx_vals = {'name': 'invoice2.docx', **cls._get_dummy_docx_vals()} cls.xlsx_vals = {'name': 'invoice2.xlsx', **cls._get_dummy_xlsx_vals()} cls.all_attachment_vals = [cls.pdf1_vals, cls.pdf2_vals, cls.pdf3_vals, cls.gif1_vals, cls.gif2_vals, cls.xml1_vals, cls.xml2_vals, cls.docx_vals, cls.xlsx_vals] @classmethod def default_env_context(cls): # OVERRIDE return {} def test_supplier_invoice_mailed_from_supplier(self): message_parsed = { 'message_id': 'message-id-dead-beef', 'message_type': 'email', 'subject': 'Incoming bill', 'from': '%s <%s>' % (self.supplier_partner.name, self.supplier_partner.email), 'to': '%s@%s' % (self.journal.alias_id.alias_name, self.journal.alias_id.alias_domain), 'body': "You know, that thing that you bought.", 'attachments': [b'Hello, invoice'], } invoice = self.env['account.move'].message_new(message_parsed, {'move_type': 'in_invoice', 'journal_id': self.journal.id}) message_ids = invoice.message_ids self.assertEqual(len(message_ids), 1, 'Only one message should be posted in the chatter') self.assertEqual(message_ids.body, '

Vendor Bill Created

', 'Only the invoice creation should be posted') self.assertRegex(invoice.name_placeholder, r'BILL/\d{4}/\d{2}/0001') def test_supplier_invoice_forwarded_by_internal_user_without_supplier(self): """ In this test, the bill was forwarded by an employee, but no partner email address is found in the body.""" message_parsed = { 'message_id': 'message-id-dead-beef', 'message_type': 'email', 'subject': 'Incoming bill', 'from': '%s <%s>' % (self.internal_user.name, self.internal_user.email), 'to': '%s@%s' % (self.journal.alias_id.alias_name, self.journal.alias_id.alias_domain), 'body': "You know, that thing that you bought.", 'attachments': [b'Hello, invoice'], } invoice = self.env['account.move'].message_new(message_parsed, {'move_type': 'in_invoice', 'journal_id': self.journal.id}) self.assertFalse(invoice.partner_id) message_ids = invoice.message_ids self.assertEqual(len(message_ids), 1, 'Only one message should be posted in the chatter') self.assertEqual(message_ids.body, '

Vendor Bill Created

', 'Only the invoice creation should be posted') self.assertEqual(invoice.message_partner_ids, self.env.user.partner_id) def test_supplier_invoice_forwarded_by_internal_with_supplier_in_body(self): """ In this test, the bill was forwarded by an employee, and the partner email address is found in the body.""" message_parsed = { 'message_id': 'message-id-dead-beef', 'message_type': 'email', 'subject': 'Incoming bill', 'from': '%s <%s>' % (self.internal_user.name, self.internal_user.email), 'to': '%s@%s' % (self.journal.alias_id.alias_name, self.journal.alias_id.alias_domain), 'body': "Mail sent by %s <%s>:\nYou know, that thing that you bought." % (self.supplier_partner.name, self.supplier_partner.email), 'attachments': [b'Hello, invoice'], } invoice = self.env['account.move'].message_new(message_parsed, {'move_type': 'in_invoice', 'journal_id': self.journal.id}) self.assertEqual(invoice.partner_id, self.supplier_partner) message_ids = invoice.message_ids self.assertEqual(len(message_ids), 1, 'Only one message should be posted in the chatter') self.assertEqual(message_ids.body, '

Vendor Bill Created

', 'Only the invoice creation should be posted') following_partners = invoice.message_follower_ids.mapped('partner_id') self.assertEqual(following_partners, self.env.user.partner_id) def test_supplier_invoice_forwarded_by_internal_with_internal_in_body(self): """ In this test, the bill was forwarded by an employee, and the internal user email address is found in the body.""" message_parsed = { 'message_id': 'message-id-dead-beef', 'message_type': 'email', 'subject': 'Incoming bill', 'from': '%s <%s>' % (self.internal_user.name, self.internal_user.email), 'to': '%s@%s' % (self.journal.alias_id.alias_name, self.journal.alias_id.alias_domain), 'body': "Mail sent by %s <%s>:\nYou know, that thing that you bought." % (self.internal_user.name, self.internal_user.email), 'attachments': [b'Hello, invoice'], } invoice = self.env['account.move'].message_new(message_parsed, {'move_type': 'in_invoice', 'journal_id': self.journal.id}) self.assertFalse(invoice.partner_id) message_ids = invoice.message_ids self.assertEqual(len(message_ids), 1, 'Only one message should be posted in the chatter') self.assertEqual(message_ids.body, '

Vendor Bill Created

', 'Only the invoice creation should be posted') following_partners = invoice.message_follower_ids.mapped('partner_id') self.assertEqual(following_partners, self.env.user.partner_id) def test_supplier_invoice_forwarded_by_internal_with_unknown_supplier_in_body(self): """ In this test, the bill was forwarded by an employee, and an unknown partner email address is found in the body.""" message_parsed = { 'message_id': 'message-id-dead-beef', 'message_type': 'email', 'subject': 'Incoming bill', 'from': '%s <%s>' % (self.internal_user.name, self.internal_user.email), 'to': '%s@%s' % (self.journal.alias_id.alias_name, self.journal.alias_id.alias_domain), 'body': "Mail sent by %s <%s>:\nYou know, that thing that you bought." % ("Test supplier", "unknown_supplier@other.com"), 'attachments': [b'Hello, invoice'], } invoice = self.env['account.move'].message_new(message_parsed, {'move_type': 'in_invoice', 'journal_id': self.journal.id}) self.assertFalse(invoice.partner_id) message_ids = invoice.message_ids self.assertEqual(len(message_ids), 1, 'Only one message should be posted in the chatter') self.assertEqual(message_ids.body, '

Vendor Bill Created

', 'Only the invoice creation should be posted') following_partners = invoice.message_follower_ids.mapped('partner_id') self.assertEqual(following_partners, self.env.user.partner_id) def test_einvoice_notification(self): purchase_journal = self.company_data['default_journal_purchase'] purchase_journal.incoming_einvoice_notification_email = 'oops_another_bill@example.com' with self.mock_mail_gateway(): self.assert_attachment_import( origin='mail_alias', attachments_vals=[self.pdf1_vals], expected_invoices={ 1: { 'invoice1.pdf': {'on_message': True, 'on_invoice': True, 'is_decoded': True, 'is_new': True}, 'MAIL_invoice1.pdf': {'on_message': True}, }, }, ) self.assertSentEmail( self.company_data['company'].email_formatted, ['oops_another_bill@example.com'], subject=f"{self.company_data['company'].name} - New invoice in {purchase_journal.display_name} journal", ) def test_01_decoder_called(self): move = self.env['account.move'].create({'move_type': 'in_invoice'}) attachment = self.env['ir.attachment'].create(self.xml1_vals) with self._patch_import_methods(): move.message_post(message_type='comment', attachment_ids=attachment.ids) self.assertEqual(move.partner_id, self.partner_a) def test_02_decoder_not_called_if_invoice_has_lines(self): move = self.env['account.move'].create({ 'move_type': 'in_invoice', 'invoice_line_ids': [ Command.create({ 'balance': 100, }) ] }) attachment = self.env['ir.attachment'].create(self.xml1_vals) with self._patch_import_methods(): move.message_post(message_type='comment', attachment_ids=attachment.ids) self.assertFalse(move.partner_id) def test_10_chatter_upload_pdfs(self): self.assert_attachment_import( origin='chatter_upload', attachments_vals=[self.pdf1_vals, self.pdf2_vals], expected_invoices={ 1: { 'invoice1.pdf': {'on_invoice': True, 'is_decoded': True}, 'invoice2.pdf': {'on_invoice': True}, } }, ) def test_11_chatter_message_pdfs(self): self.assert_attachment_import( origin='chatter_message', attachments_vals=[self.pdf1_vals, self.pdf2_vals], expected_invoices={ 1: { 'invoice1.pdf': {'on_invoice': True, 'on_message': True, 'is_decoded': True}, 'invoice2.pdf': {'on_invoice': True, 'on_message': True}, } }, ) def test_12_chatter_email_pdfs(self): self.assert_attachment_import( origin='chatter_email', attachments_vals=[self.pdf1_vals, self.pdf2_vals], expected_invoices={ 1: { 'invoice1.pdf': {'on_invoice': True, 'on_message': True}, 'invoice2.pdf': {'on_invoice': True, 'on_message': True}, } }, ) def test_13_journal_upload_pdfs(self): self.assert_attachment_import( origin='journal', attachments_vals=[self.pdf1_vals, self.pdf2_vals], expected_invoices={ 1: {'invoice1.pdf': {'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True}}, 2: {'invoice2.pdf': {'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True}}, }, ) def test_14_mail_alias_pdfs(self): self.assert_attachment_import( origin='mail_alias', attachments_vals=[self.pdf1_vals, self.pdf2_vals], expected_invoices={ 1: {'invoice1.pdf': {'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True}}, 2: {'invoice2.pdf': {'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True}}, }, ) def test_20_chatter_upload_pdfs_and_gifs(self): self.assert_attachment_import( origin='chatter_upload', attachments_vals=[self.pdf1_vals, self.pdf2_vals, self.gif1_vals, self.gif2_vals], expected_invoices={ 1: { 'invoice1.pdf': {'on_invoice': True, 'is_decoded': True}, 'invoice2.pdf': {'on_invoice': True}, 'gif1.gif': {'on_invoice': True}, 'gif2.gif': {'on_invoice': True}, }, }, ) def test_21_chatter_message_pdfs_and_gifs(self): self.assert_attachment_import( origin='chatter_message', attachments_vals=[self.pdf1_vals, self.pdf2_vals, self.gif1_vals, self.gif2_vals], expected_invoices={ 1: { 'invoice1.pdf': {'on_invoice': True, 'on_message': True, 'is_decoded': True}, 'invoice2.pdf': {'on_invoice': True, 'on_message': True}, 'gif1.gif': {'on_message': True}, 'gif2.gif': {'on_message': True}, }, }, ) def test_22_chatter_email_pdfs_and_gifs(self): self.assert_attachment_import( origin='chatter_email', attachments_vals=[self.pdf1_vals, self.pdf2_vals, self.gif1_vals, self.gif2_vals], expected_invoices={ 1: { 'invoice1.pdf': {'on_invoice': True, 'on_message': True}, 'invoice2.pdf': {'on_invoice': True, 'on_message': True}, 'gif1.gif': {'on_message': True}, 'gif2.gif': {'on_message': True}, }, }, ) def test_23_journal_upload_pdfs_and_gifs(self): self.assert_attachment_import( origin='journal', attachments_vals=[self.pdf1_vals, self.pdf2_vals, self.gif1_vals, self.gif2_vals], expected_invoices={ 1: {'invoice1.pdf': {'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True}}, 2: {'invoice2.pdf': {'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True}}, 3: {'gif1.gif': {'on_invoice': True, 'on_message': True}}, 4: {'gif2.gif': {'on_invoice': True, 'on_message': True}}, }, ) def test_24_mail_alias_pdfs_and_gifs(self): self.assert_attachment_import( origin='mail_alias', attachments_vals=[self.pdf1_vals, self.pdf2_vals, self.gif1_vals, self.gif2_vals], expected_invoices={ 1: { 'invoice1.pdf': {'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True}, 'gif1.gif': {'on_message': True}, 'gif2.gif': {'on_message': True}, }, 2: {'invoice2.pdf': {'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True}}, }, ) def test_25_mail_alias_gifs(self): self.assert_attachment_import( origin='mail_alias', attachments_vals=[self.gif1_vals, self.gif2_vals], expected_invoices={ 1: { 'gif1.gif': {'on_message': True}, 'gif2.gif': {'on_message': True}, }, }, ) def test_30_chatter_upload_pdf_and_xml(self): self.assert_attachment_import( origin='chatter_upload', attachments_vals=[self.pdf1_vals, self.xml1_vals], expected_invoices={ 1: { 'invoice1.xml': {'on_invoice': True, 'is_decoded': True}, 'invoice1.pdf': {'on_invoice': True}, }, }, ) def test_31_chatter_message_pdf_and_xml(self): self.assert_attachment_import( origin='chatter_message', attachments_vals=[self.pdf1_vals, self.xml1_vals], expected_invoices={ 1: { 'invoice1.xml': {'on_message': True, 'is_decoded': True}, 'invoice1.pdf': {'on_invoice': True, 'on_message': True}, }, }, ) def test_32_chatter_email_pdf_and_xml(self): self.assert_attachment_import( origin='chatter_email', attachments_vals=[self.pdf1_vals, self.xml1_vals], expected_invoices={ 1: { 'invoice1.xml': {'on_message': True}, 'invoice1.pdf': {'on_invoice': True, 'on_message': True}, }, }, ) def test_33_journal_upload_pdf_and_xml(self): self.assert_attachment_import( origin='journal', attachments_vals=[self.pdf1_vals, self.xml1_vals], expected_invoices={ 1: {'invoice1.pdf': {'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True}}, 2: {'invoice1.xml': {'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True}}, }, ) def test_34_mail_alias_pdf_and_xml(self): self.assert_attachment_import( origin='mail_alias', attachments_vals=[self.pdf1_vals, self.xml1_vals], expected_invoices={ 1: { 'invoice1.xml': {'on_message': True, 'is_decoded': True, 'is_new': True}, 'invoice1.pdf': {'on_invoice': True, 'on_message': True}, }, }, ) def test_40_chatter_upload_xmls(self): self.assert_attachment_import( origin='chatter_upload', attachments_vals=[self.xml1_vals, self.xml2_vals], expected_invoices={ 1: { 'invoice1.xml': {'on_invoice': True, 'is_decoded': True}, 'invoice2.xml': {'on_invoice': True}, }, }, ) def test_41_chatter_message_xmls(self): self.assert_attachment_import( origin='chatter_message', attachments_vals=[self.xml1_vals, self.xml2_vals], expected_invoices={ 1: { 'invoice1.xml': {'on_message': True, 'is_decoded': True}, 'invoice2.xml': {'on_message': True}, }, }, ) def test_42_chatter_email_xmls(self): self.assert_attachment_import( origin='chatter_email', attachments_vals=[self.xml1_vals, self.xml2_vals], expected_invoices={ 1: { 'invoice1.xml': {'on_message': True}, 'invoice2.xml': {'on_message': True}, }, }, ) def test_43_journal_upload_xmls(self): self.assert_attachment_import( origin='journal', attachments_vals=[self.xml1_vals, self.xml2_vals], expected_invoices={ 1: {'invoice1.xml': {'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True}}, 2: {'invoice2.xml': {'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True}}, }, ) def test_44_mail_alias_xmls(self): self.assert_attachment_import( origin='mail_alias', attachments_vals=[self.xml1_vals, self.xml2_vals], expected_invoices={ 1: {'invoice1.xml': {'on_message': True, 'is_decoded': True, 'is_new': True}}, 2: {'invoice2.xml': {'on_message': True, 'is_decoded': True, 'is_new': True}}, }, ) def test_50_chatter_upload_embedded_pdf(self): self.assert_attachment_import( origin='chatter_upload', attachments_vals=[self.pdf3_vals, self.pdf2_vals], expected_invoices={ 1: { 'embedded.xml': {'is_decoded': True}, 'invoice3.pdf': {'on_invoice': True}, 'invoice2.pdf': {'on_invoice': True}, }, }, ) def test_51_chatter_message_embedded_pdf(self): self.assert_attachment_import( origin='chatter_message', attachments_vals=[self.pdf3_vals, self.pdf2_vals], expected_invoices={ 1: { 'embedded.xml': {'is_decoded': True}, 'invoice3.pdf': {'on_invoice': True, 'on_message': True}, 'invoice2.pdf': {'on_invoice': True, 'on_message': True}, }, }, ) def test_52_chatter_email_embedded_pdf(self): self.assert_attachment_import( origin='chatter_email', attachments_vals=[self.pdf3_vals, self.pdf2_vals], expected_invoices={ 1: { 'invoice3.pdf': {'on_invoice': True, 'on_message': True}, 'invoice2.pdf': {'on_invoice': True, 'on_message': True}, }, }, ) def test_53_journal_upload_embedded_pdf(self): self.assert_attachment_import( origin='journal', attachments_vals=[self.pdf3_vals, self.pdf2_vals], expected_invoices={ 1: { 'embedded.xml': {'is_decoded': True, 'is_new': True}, 'invoice3.pdf': {'on_invoice': True, 'on_message': True}, }, 2: { 'invoice2.pdf': {'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True}, }, }, ) def test_54_mail_alias_embedded_pdf(self): self.assert_attachment_import( origin='mail_alias', attachments_vals=[self.pdf3_vals, self.pdf2_vals], expected_invoices={ 1: { 'embedded.xml': {'is_decoded': True, 'is_new': True}, 'invoice3.pdf': {'on_invoice': True, 'on_message': True}, }, 2: { 'invoice2.pdf': {'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True}, }, }, ) def test_60_chatter_upload_embedded_pdf_and_xml(self): self.assert_attachment_import( origin='chatter_upload', attachments_vals=[self.pdf3_vals, self.xml1_vals], expected_invoices={ 1: { 'invoice1.xml': {'on_invoice': True, 'is_decoded': True}, 'invoice3.pdf': {'on_invoice': True}, }, }, ) def test_61_chatter_message_embedded_pdf_and_xml(self): self.assert_attachment_import( origin='chatter_message', attachments_vals=[self.pdf3_vals, self.xml1_vals], expected_invoices={ 1: { 'invoice1.xml': {'on_message': True, 'is_decoded': True}, 'invoice3.pdf': {'on_invoice': True, 'on_message': True}, }, }, ) def test_62_chatter_email_embedded_pdf_and_xml(self): self.assert_attachment_import( origin='chatter_email', attachments_vals=[self.pdf3_vals, self.xml1_vals], expected_invoices={ 1: { 'invoice1.xml': {'on_message': True}, 'invoice3.pdf': {'on_invoice': True, 'on_message': True}, }, }, ) def test_63_journal_upload_embedded_pdf_and_xml(self): self.assert_attachment_import( origin='journal', attachments_vals=[self.pdf3_vals, self.xml1_vals], expected_invoices={ 1: { 'embedded.xml': {'is_decoded': True, 'is_new': True}, 'invoice3.pdf': {'on_invoice': True, 'on_message': True}, }, 2: { 'invoice1.xml': {'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True}, }, }, ) def test_64_mail_alias_embedded_pdf_and_xml(self): self.assert_attachment_import( origin='mail_alias', attachments_vals=[self.pdf3_vals, self.xml1_vals], expected_invoices={ 1: { 'invoice1.xml': {'on_message': True, 'is_decoded': True, 'is_new': True}, 'invoice3.pdf': {'on_invoice': True, 'on_message': True}, }, }, ) def test_70_chatter_upload_all(self): self.assert_attachment_import( origin='chatter_upload', attachments_vals=self.all_attachment_vals, expected_invoices={ 1: { 'invoice2.docx': {'on_invoice': True}, 'gif1.gif': {'on_invoice': True}, 'gif2.gif': {'on_invoice': True}, 'invoice1.pdf': {'on_invoice': True}, 'invoice2.pdf': {'on_invoice': True}, 'invoice3.pdf': {'on_invoice': True}, 'invoice2.xlsx': {'on_invoice': True}, # The code doesn't put a hard constraint on which of the XMLs gets decoded. 'invoice1.xml': {'is_decoded': True, 'on_invoice': True}, 'invoice2.xml': {'on_invoice': True} }, }, ) def test_71_chatter_message_all(self): self.assert_attachment_import( origin='chatter_message', attachments_vals=self.all_attachment_vals, expected_invoices={ 1: { 'invoice2.docx': {'on_invoice': True, 'on_message': True}, 'gif1.gif': {'on_message': True}, 'gif2.gif': {'on_message': True}, 'invoice1.pdf': {'on_invoice': True, 'on_message': True}, 'invoice2.pdf': {'on_invoice': True, 'on_message': True}, 'invoice3.pdf': {'on_invoice': True, 'on_message': True}, 'invoice2.xlsx': {'on_invoice': True, 'on_message': True}, # The code doesn't put a hard constraint on which of the XMLs gets decoded. 'invoice1.xml': {'is_decoded': True, 'on_message': True}, 'invoice2.xml': {'on_message': True}, }, }, ) def test_72_chatter_email_all(self): self.assert_attachment_import( origin='chatter_email', attachments_vals=self.all_attachment_vals, expected_invoices={ 1: { 'invoice2.docx': {'on_invoice': True, 'on_message': True}, 'gif1.gif': {'on_message': True}, 'gif2.gif': {'on_message': True}, 'invoice1.pdf': {'on_invoice': True, 'on_message': True}, 'invoice2.pdf': {'on_invoice': True, 'on_message': True}, 'invoice3.pdf': {'on_invoice': True, 'on_message': True}, 'invoice2.xlsx': {'on_invoice': True, 'on_message': True}, 'invoice1.xml': {'on_message': True}, 'invoice2.xml': {'on_message': True}, }, }, ) def test_73_journal_upload_all(self): self.assert_attachment_import( origin='journal', attachments_vals=self.all_attachment_vals, expected_invoices={ 1: {'invoice1.pdf': {'is_decoded': True, 'is_new': True, 'on_invoice': True, 'on_message': True}}, 2: {'invoice2.pdf': {'is_decoded': True, 'is_new': True, 'on_invoice': True, 'on_message': True}}, 3: { 'embedded.xml': {'is_decoded': True, 'is_new': True}, 'invoice3.pdf': {'on_invoice': True, 'on_message': True}, }, 4: {'gif1.gif': {'on_invoice': True, 'on_message': True}}, 5: {'gif2.gif': {'on_invoice': True, 'on_message': True}}, 6: {'invoice1.xml': {'is_decoded': True, 'is_new': True, 'on_invoice': True, 'on_message': True}}, 7: {'invoice2.xml': {'is_decoded': True, 'is_new': True, 'on_invoice': True, 'on_message': True}}, 8: {'invoice2.docx': {'on_invoice': True, 'on_message': True}}, 9: {'invoice2.xlsx': {'on_invoice': True, 'on_message': True}}, }, ) def test_74_mail_alias_all(self): self.assert_attachment_import( origin='mail_alias', attachments_vals=self.all_attachment_vals, expected_invoices={ 1: { 'gif1.gif': {'on_message': True}, 'gif2.gif': {'on_message': True}, 'invoice1.pdf': {'on_invoice': True, 'on_message': True}, 'invoice1.xml': {'is_decoded': True, 'is_new': True, 'on_message': True}, }, 2: { 'invoice2.pdf': {'on_invoice': True, 'on_message': True}, 'invoice2.xml': {'is_decoded': True, 'is_new': True, 'on_message': True}, # The XLSX and DOCX are attached to this invoice due to filename similarity 'invoice2.xlsx': {'on_invoice': True, 'on_message': True}, 'invoice2.docx': {'on_invoice': True, 'on_message': True}, }, 3: { 'embedded.xml': {'is_decoded': True, 'is_new': True}, 'invoice3.pdf': {'on_invoice': True, 'on_message': True}, } }, ) def test_import_with_traceback(self): # Verify that even an Exception does not cause the import to fail, and that we log the attachment in the chatter attachment = self.env['ir.attachment'].create(self.xml1_vals) with ( self._patch_import_methods(), patch('odoo.addons.account.models.partner.ResPartner.search', side_effect=ValueError('We want to test an unexpected error')), mute_logger('odoo.addons.account.models.account_document_import_mixin'), ): move_id = self.journal.create_document_from_attachment(attachment.ids).get('res_id') self.assertEqual(self.env['account.move'].browse(move_id).attachment_ids, attachment) def test_import_xml_with_embedded_pdf(self): with file_open("account/tests/test_files/xml_with_embedded_pdf.xml", 'rb') as file: xml_vals = {'name': 'invoice.xml', 'raw': file.read(), 'mimetype': 'application/xml'} self.assert_attachment_import( origin='journal', attachments_vals=[xml_vals], expected_invoices={ 1: { 'invoice.xml': { 'on_invoice': True, 'on_message': True, 'is_decoded': True, 'is_new': True, }, 'test_pdf.pdf': { 'on_invoice': True, 'on_message': True, }, }, }, ) self.assert_attachment_import( origin='mail_alias', attachments_vals=[xml_vals], expected_invoices={ 1: { 'invoice.xml': { 'on_message': True, 'is_decoded': True, 'is_new': True, }, 'test_pdf.pdf': { 'on_invoice': True, 'on_message': True, }, }, }, )