updates to account_email_to_pdf - total rewrite

This commit is contained in:
Marc Durepos 2025-03-27 10:49:45 -04:00
parent cc9a5e172d
commit f3259fd6a1
4 changed files with 216 additions and 36 deletions

View file

@ -1,11 +1,11 @@
{
'name': 'Account Email to PDF',
'version': '18.0.1.0.0',
'category': 'Accounting',
'summary': 'Convert email messages to PDF attachments for vendor bills',
'description': """
"name": "Account Email to PDF",
"version": "18.0.1.0.0",
"category": "Accounting",
"summary": "Convert email messages to PDF attachments for vendor bills",
"description": """
Account Email to PDF
===================
====================
This module converts email messages without attachments into PDF attachments
when processing incoming emails for vendor bills.
@ -13,11 +13,11 @@ Instead of rejecting emails without attachments, the system will create a PDF
from the email content and attach it to the message, allowing the vendor bill
creation process to continue.
""",
'author': 'Bemade',
'website': 'https://bemade.org',
'depends': ['account'],
'data': [],
'installable': True,
'auto_install': False,
'license': 'LGPL-3',
"author": "Bemade",
"website": "https://bemade.org",
"depends": ["account"],
"data": [],
"installable": True,
"auto_install": False,
"license": "LGPL-3",
}

View file

@ -1,24 +1,28 @@
import base64
import logging
import os
import subprocess
import tempfile
from contextlib import closing
from datetime import datetime
from email.utils import formatdate
from html import escape
from odoo import models, fields
from odoo.tools.pdf import html_to_pdf
from odoo.tools.misc import find_in_path
_logger = logging.getLogger(__name__)
class AccountMove(models.Model):
_inherit = 'account.move'
_inherit = "account.move"
def _check_and_decode_attachment(self, attachments):
"""Override to convert email message to PDF if no attachments are present."""
if not attachments or self.env.context.get('no_new_invoice'):
if not attachments or self.env.context.get("no_new_invoice"):
# Original code would return False here, causing email rejection
# Instead, we'll create a PDF from the email message
message_dict = self.env.context.get('message_dict', {})
message_dict = self.env.context.get("message_dict", {})
if message_dict:
try:
# Create a PDF from the email content
@ -28,25 +32,95 @@ class AccountMove(models.Model):
return super()._check_and_decode_attachment([pdf_attachment])
except Exception as e:
_logger.exception("Error creating PDF from email: %s", e)
# If we couldn't create a PDF, fall back to original behavior
return False
# If there are attachments, proceed with the original method
# Proceed with the original method
# If we were unable to generate a PDF, the original method will bounce the email
return super()._check_and_decode_attachment(attachments)
@classmethod
def _html_to_pdf(cls, html_content):
"""Convert HTML content to PDF using wkhtmltopdf.
Args:
html_content (str): HTML content to convert to PDF
Returns:
bytes: PDF content as bytes or False if conversion failed
"""
wkhtmltopdf_bin = find_in_path("wkhtmltopdf")
if not wkhtmltopdf_bin:
_logger.error("Cannot find wkhtmltopdf executable in system path")
return False
# Create temporary files for the HTML input and PDF output
html_file_fd, html_file_path = tempfile.mkstemp(
suffix=".html", prefix="email_to_pdf."
)
pdf_file_fd, pdf_file_path = tempfile.mkstemp(
suffix=".pdf", prefix="email_to_pdf."
)
try:
# Write the HTML content to the temporary file
with closing(os.fdopen(html_file_fd, "wb")) as html_file:
html_file.write(html_content.encode("utf-8"))
# Close the PDF file descriptor as wkhtmltopdf will write to it
os.close(pdf_file_fd)
# Basic wkhtmltopdf command arguments
command = [wkhtmltopdf_bin]
command.extend(["--encoding", "utf-8"])
command.extend(["--page-size", "A4"])
command.extend(["--margin-top", "10mm"])
command.extend(["--margin-bottom", "10mm"])
command.extend(["--margin-left", "10mm"])
command.extend(["--margin-right", "10mm"])
command.append(html_file_path)
command.append(pdf_file_path)
# Execute wkhtmltopdf
process = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
out, err = process.communicate()
if process.returncode not in [0, 1]:
_logger.error(
"wkhtmltopdf failed with error code %s: %s", process.returncode, err
)
return False
# Read the generated PDF
with open(pdf_file_path, "rb") as pdf_file:
pdf_content = pdf_file.read()
return pdf_content
except Exception as e:
_logger.exception("Error during PDF generation: %s", e)
return False
finally:
# Clean up temporary files
try:
os.unlink(html_file_path)
os.unlink(pdf_file_path)
except (OSError, IOError):
_logger.error("Failed to remove temporary files")
def _create_pdf_from_email(self, message_dict):
"""Create a PDF attachment from an email message."""
# Extract email details
email_from = message_dict.get('email_from', 'Unknown Sender')
email_date = message_dict.get('date', datetime.now())
subject = message_dict.get('subject', 'No Subject')
body = message_dict.get('body', '')
email_from = message_dict.get("email_from", "Unknown Sender")
email_date = message_dict.get("date", datetime.now())
subject = message_dict.get("subject", "No Subject")
body = message_dict.get("body", "")
# Format the date if it's a datetime object
if isinstance(email_date, datetime):
email_date = email_date.strftime('%Y-%m-%d %H:%M:%S')
email_date = email_date.strftime("%Y-%m-%d %H:%M:%S")
# Create HTML content for the PDF
html_content = f"""
<html>
@ -71,17 +145,19 @@ class AccountMove(models.Model):
</body>
</html>
"""
# Convert HTML to PDF
pdf_content = html_to_pdf(html_content)
pdf_content = self._html_to_pdf(html_content)
if not pdf_content:
return False
# Create attachment tuple (filename, content, mime_type)
filename = f"Email_{subject.replace(' ', '_')[:30]}.pdf"
attachment = (
filename,
base64.b64encode(pdf_content).decode('utf-8'),
'application/pdf'
base64.b64encode(pdf_content).decode("utf-8"),
"application/pdf",
)
_logger.info("Created PDF attachment from email: %s", filename)
return attachment

View file

@ -0,0 +1 @@
from . import test_email_to_pdf

View file

@ -0,0 +1,103 @@
import base64
from odoo.tests.common import TransactionCase
from odoo.tools.misc import find_in_path
class TestHtmlToPdf(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# 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 = """
<html>
<head>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>Test HTML Document</h1>
<p>This is a test paragraph with <b>bold text</b> and <i>italic text</i>.</p>
<ul>
<li>List item 1</li>
<li>List item 2</li>
<li>List item 3</li>
</ul>
</body>
</html>
"""
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_html_to_pdf_with_complex_content(self):
"""Test HTML to PDF conversion with more complex content."""
if not self.wkhtmltopdf_available:
self.skipTest("wkhtmltopdf not available")
# More complex HTML with tables and images
complex_html = """
<html>
<head>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>Complex HTML Test</h1>
<table>
<tr>
<th>Header 1</th>
<th>Header 2</th>
<th>Header 3</th>
</tr>
<tr>
<td>Row 1, Cell 1</td>
<td>Row 1, Cell 2</td>
<td>Row 1, Cell 3</td>
</tr>
<tr>
<td>Row 2, Cell 1</td>
<td>Row 2, Cell 2</td>
<td>Row 2, Cell 3</td>
</tr>
</table>
</body>
</html>
"""
# Convert complex HTML to PDF
pdf_content = self.account_move._html_to_pdf(complex_html)
# Verify the PDF was created
self.assertTrue(
pdf_content, "PDF content should be generated from complex HTML"
)
self.assertTrue(
pdf_content.startswith(b"%PDF-"), "Content should be a valid PDF"
)
self.assertTrue(len(pdf_content) > 100, "PDF should have reasonable size")