from odoo.tests.common import TransactionCase from odoo.tools.misc import find_in_path from datetime import datetime import base64 class TestHtmlToPdf(TransactionCase): """ Test the functionality of converting an email to a PDF when it's received for a supplier invoice. This test simulates the full flow of an email being received through the mail alias and verifies that an account move is created with a PDF attachment generated from the email content. It's important to also run the tests in account, which can be done using the test tag: `/account:TestAccountIncomingSupplierInvoice.test_extend_with_attachments_document_formats` This specific test ensures we are not creating more attachments than necessary. """ @classmethod def setUpClass(cls): super().setUpClass() # Set up sender and alias for testing cls.sender_email = "sender@example.com" cls.alias_email = "test-invoices@example.com" # Check if wkhtmltopdf is available cls.wkhtmltopdf_available = bool(find_in_path("wkhtmltopdf")) # Get the model class to access the classmethod cls.account_move = cls.env["account.move"] # Simple test HTML content cls.test_html = """

Test HTML Document

This is a test paragraph with bold text and italic text.

""" # Set up a supplier for testing cls.supplier = cls.env["res.partner"].create( { "name": "Test Supplier", "email": cls.sender_email, } ) # Set up a journal for incoming invoices cls.journal = cls.env["account.journal"].search( [("type", "=", "purchase")], limit=1 ) # Set up the mail alias domain cls.alias_domain = cls.env["mail.alias.domain"].create({"name": "example.com"}) # Set up a mail alias for the journal cls.alias = cls.env["mail.alias"].create( { "alias_name": cls.alias_email, "alias_model_id": cls.env["ir.model"] .search([("model", "=", "account.move")], limit=1) .id, "alias_domain_id": cls.alias_domain.id, "alias_defaults": f'{{"move_type": "in_invoice", "journal_id": {cls.journal.id}}}', } ) cls.journal.alias_id = cls.alias def test_html_to_pdf_conversion(self): """Test the direct HTML to PDF conversion.""" if not self.wkhtmltopdf_available: self.skipTest("wkhtmltopdf not available") # Call the method to convert HTML to PDF pdf_content = self.account_move._html_to_pdf(self.test_html) # Verify the PDF was created self.assertTrue(pdf_content, "PDF content should be generated") # Verify it's a valid PDF self.assertTrue( pdf_content.startswith(b"%PDF-"), "Content should be a valid PDF" ) self.assertTrue(len(pdf_content) > 100, "PDF should have reasonable size") def test_plain_text_to_pdf_conversion(self): """Test the conversion of plain text email to PDF.""" if not self.wkhtmltopdf_available: self.skipTest("wkhtmltopdf not available") # Simple plain text content plain_text = "This is a plain text email.\nIt has no HTML formatting.\nJust plain text content.\n\nRegards,\nTest Sender" # Call the method to convert plain text to PDF pdf_content = self.account_move._html_to_pdf(plain_text) # Verify the PDF was created self.assertTrue( pdf_content, "PDF content should be generated even for plain text" ) # Verify it's a valid PDF is_pdf = pdf_content and pdf_content.startswith(b"%PDF-") self.assertTrue(is_pdf, "Content should be a valid PDF") self.assertTrue(len(pdf_content) > 100, "PDF should have reasonable size") # Integration test def test_account_email_to_pdf_full_flow(self): """Test that an email without attachments is correctly converted to PDF and an account move is created. This test simulates the full flow of an email being received through the mail alias and verifies that an account move is created with a PDF attachment generated from the email content. """ # Get the current timestamp timestamp = datetime.now().strftime("%Y%m%d%H%M%S") message_id = f"" subject = f"Invoice from Test Supplier {timestamp}" date = datetime.now().strftime("%a, %d %b %Y %H:%M:%S +0000") # HTML body of the email html_body = "

Invoice Test

This is a test invoice from Test Supplier.

Amount: $100.00

Date: 2025-03-27

" # Construct the raw email with proper headers # Make sure the From header is correctly formatted for Odoo to parse raw_email = f"""Return-Path: <{self.sender_email}> X-Original-To: {self.alias_email} Delivered-To: {self.alias_email} Received: from mail.example.com (mail.example.com [192.168.1.1]) From: {self.sender_email} To: {self.alias_email} Subject: {subject} Date: {date} Message-ID: {message_id} MIME-Version: 1.0 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: 7bit {html_body}""" # Process the email through the mail gateway # This simulates what happens when fetchmail receives an email invoice_count_before = self.env["account.move"].search_count( [("move_type", "=", "in_invoice")] ) # Process the email through the mail gateway # We need to specify the model explicitly since we're in a test environment # In production, the alias would determine this automatically self.env["mail.thread"].with_context(fetchmail_server_id=1).message_process( model=None, message=raw_email, save_original=True, strip_attachments=False, ) # Count account moves after the test # Note: move_type is the correct field name in Odoo, even if the linter doesn't recognize it move_count_after = self.env["account.move"].search_count( [("move_type", "=", "in_invoice")] ) self.assertEqual( move_count_after, invoice_count_before + 1, "A new account move should be created", ) # Find the newly created invoice invoice = self.env["account.move"].search( [("move_type", "=", "in_invoice")], order="id desc", limit=1 ) self.assertTrue(invoice, "An account move should be created") messages = invoice.message_ids self.assertEqual(len(messages), 1, "The account move should have one message") attachment = messages.attachment_ids self.assertTrue(attachment, "The message should have an attachment") attachment = attachment.filtered( lambda a: a.mimetype == "application/pdf" or ".pdf" in a.name ) self.assertTrue(attachment, "The message should have a PDF attachment") # Verify the invoice is linked to the correct supplier self.assertEqual( invoice.partner_id, self.supplier, "The invoice should be linked to the correct supplier", ) # Verify the invoice type self.assertEqual( invoice.move_type, "in_invoice", "The invoice should be an incoming invoice" ) def test_plain_text_email_to_pdf_full_flow(self): """Test that a plain text email without attachments is correctly converted to PDF and an account move is created. This test simulates the full flow of a plain text email being received through the mail alias and verifies that an account move is created with a PDF attachment generated from the email content. """ # Get the current timestamp timestamp = datetime.now().strftime("%Y%m%d%H%M%S") message_id = f"" subject = f"Plain Text Invoice from Test Supplier {timestamp}" date = datetime.now().strftime("%a, %d %b %Y %H:%M:%S +0000") # Plain text body of the email plain_text_body = """Invoice Test This is a test invoice from Test Supplier. Amount: $100.00 Date: 2025-03-27 Thank you for your business. """ # Construct the raw email with proper headers # Make sure the From header is correctly formatted for Odoo to parse raw_email = f"""Return-Path: <{self.sender_email}> X-Original-To: {self.alias_email} Delivered-To: {self.alias_email} Received: from mail.example.com (mail.example.com [192.168.1.1]) From: {self.sender_email} To: {self.alias_email} Subject: {subject} Date: {date} Message-ID: {message_id} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit {plain_text_body}""" # Process the email through the mail gateway # This simulates what happens when fetchmail receives an email invoice_count_before = self.env["account.move"].search_count( [("move_type", "=", "in_invoice")] ) # Process the email through the mail gateway self.env["mail.thread"].with_context(fetchmail_server_id=1).message_process( model=None, message=raw_email, save_original=True, strip_attachments=False, ) # Count account moves after the test move_count_after = self.env["account.move"].search_count( [("move_type", "=", "in_invoice")] ) self.assertEqual( move_count_after, invoice_count_before + 1, "A new account move should be created from plain text email", ) # Find the newly created invoice invoice = self.env["account.move"].search( [("move_type", "=", "in_invoice")], order="id desc", limit=1 ) self.assertTrue( invoice, "An account move should be created from plain text email" ) messages = invoice.message_ids self.assertEqual(len(messages), 1, "The account move should have one message") attachment = messages.attachment_ids self.assertTrue(attachment, "The message should have an attachment") attachment = attachment.filtered( lambda a: a.mimetype == "application/pdf" or ".pdf" in a.name ) self.assertTrue(attachment, "The message should have a PDF attachment") # Verify PDF content if possible if attachment: pdf_data = base64.b64decode(attachment.datas) if attachment.datas else b"" self.assertTrue( pdf_data.startswith(b"%PDF-"), f"Attachment should be a valid PDF even when source is plain text, starts with {pdf_data[:20]}", ) self.assertTrue( len(pdf_data) > 100, "PDF from plain text should have reasonable size" )