oca-ocb-accounting/odoo-bringout-oca-ocb-account/account/tests/test_account_incoming_supplier_invoice.py
Ernad Husremovic 768b70e05e 19.0 vanilla
2026-03-09 09:30:07 +01:00

1034 lines
53 KiB
Python

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
<?xml version="1.0" encoding="UTF-8"?>
<TestFileFormat>
<PartnerName>partner_a</PartnerName>
</TestFileFormat>
"""
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"""<?xml version="1.0" encoding="UTF-8"?>
<TestFileFormat>
<PartnerName>partner_a</PartnerName>
</TestFileFormat>
""",
'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 <someone@some.company.com>
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, '<p>Vendor Bill Created</p>', '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, '<p>Vendor Bill Created</p>', '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, '<p>Vendor Bill Created</p>', '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, '<p>Vendor Bill Created</p>', '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, '<p>Vendor Bill Created</p>', '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,
},
},
},
)