updates to account_email_to_pdf - total rewrite
This commit is contained in:
parent
cc9a5e172d
commit
f3259fd6a1
4 changed files with 216 additions and 36 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
'name': 'Account Email to PDF',
|
"name": "Account Email to PDF",
|
||||||
'version': '18.0.1.0.0',
|
"version": "18.0.1.0.0",
|
||||||
'category': 'Accounting',
|
"category": "Accounting",
|
||||||
'summary': 'Convert email messages to PDF attachments for vendor bills',
|
"summary": "Convert email messages to PDF attachments for vendor bills",
|
||||||
'description': """
|
"description": """
|
||||||
Account Email to PDF
|
Account Email to PDF
|
||||||
===================
|
====================
|
||||||
This module converts email messages without attachments into PDF attachments
|
This module converts email messages without attachments into PDF attachments
|
||||||
when processing incoming emails for vendor bills.
|
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
|
from the email content and attach it to the message, allowing the vendor bill
|
||||||
creation process to continue.
|
creation process to continue.
|
||||||
""",
|
""",
|
||||||
'author': 'Bemade',
|
"author": "Bemade",
|
||||||
'website': 'https://bemade.org',
|
"website": "https://bemade.org",
|
||||||
'depends': ['account'],
|
"depends": ["account"],
|
||||||
'data': [],
|
"data": [],
|
||||||
'installable': True,
|
"installable": True,
|
||||||
'auto_install': False,
|
"auto_install": False,
|
||||||
'license': 'LGPL-3',
|
"license": "LGPL-3",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,28 @@
|
||||||
import base64
|
import base64
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from contextlib import closing
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from email.utils import formatdate
|
from email.utils import formatdate
|
||||||
from html import escape
|
from html import escape
|
||||||
|
|
||||||
from odoo import models, fields
|
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__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AccountMove(models.Model):
|
class AccountMove(models.Model):
|
||||||
_inherit = 'account.move'
|
_inherit = "account.move"
|
||||||
|
|
||||||
def _check_and_decode_attachment(self, attachments):
|
def _check_and_decode_attachment(self, attachments):
|
||||||
"""Override to convert email message to PDF if no attachments are present."""
|
"""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
|
# Original code would return False here, causing email rejection
|
||||||
# Instead, we'll create a PDF from the email message
|
# 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:
|
if message_dict:
|
||||||
try:
|
try:
|
||||||
# Create a PDF from the email content
|
# Create a PDF from the email content
|
||||||
|
|
@ -28,25 +32,95 @@ class AccountMove(models.Model):
|
||||||
return super()._check_and_decode_attachment([pdf_attachment])
|
return super()._check_and_decode_attachment([pdf_attachment])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.exception("Error creating PDF from email: %s", e)
|
_logger.exception("Error creating PDF from email: %s", e)
|
||||||
|
|
||||||
# If we couldn't create a PDF, fall back to original behavior
|
# Proceed with the original method
|
||||||
return False
|
# If we were unable to generate a PDF, the original method will bounce the email
|
||||||
|
|
||||||
# If there are attachments, proceed with the original method
|
|
||||||
return super()._check_and_decode_attachment(attachments)
|
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):
|
def _create_pdf_from_email(self, message_dict):
|
||||||
"""Create a PDF attachment from an email message."""
|
"""Create a PDF attachment from an email message."""
|
||||||
# Extract email details
|
# Extract email details
|
||||||
email_from = message_dict.get('email_from', 'Unknown Sender')
|
email_from = message_dict.get("email_from", "Unknown Sender")
|
||||||
email_date = message_dict.get('date', datetime.now())
|
email_date = message_dict.get("date", datetime.now())
|
||||||
subject = message_dict.get('subject', 'No Subject')
|
subject = message_dict.get("subject", "No Subject")
|
||||||
body = message_dict.get('body', '')
|
body = message_dict.get("body", "")
|
||||||
|
|
||||||
# Format the date if it's a datetime object
|
# Format the date if it's a datetime object
|
||||||
if isinstance(email_date, datetime):
|
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
|
# Create HTML content for the PDF
|
||||||
html_content = f"""
|
html_content = f"""
|
||||||
<html>
|
<html>
|
||||||
|
|
@ -71,17 +145,19 @@ class AccountMove(models.Model):
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Convert HTML to PDF
|
# 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)
|
# Create attachment tuple (filename, content, mime_type)
|
||||||
filename = f"Email_{subject.replace(' ', '_')[:30]}.pdf"
|
filename = f"Email_{subject.replace(' ', '_')[:30]}.pdf"
|
||||||
attachment = (
|
attachment = (
|
||||||
filename,
|
filename,
|
||||||
base64.b64encode(pdf_content).decode('utf-8'),
|
base64.b64encode(pdf_content).decode("utf-8"),
|
||||||
'application/pdf'
|
"application/pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
_logger.info("Created PDF attachment from email: %s", filename)
|
_logger.info("Created PDF attachment from email: %s", filename)
|
||||||
return attachment
|
return attachment
|
||||||
|
|
|
||||||
1
account_email_to_pdf/tests/__init__.py
Normal file
1
account_email_to_pdf/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import test_email_to_pdf
|
||||||
103
account_email_to_pdf/tests/test_email_to_pdf.py
Normal file
103
account_email_to_pdf/tests/test_email_to_pdf.py
Normal 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")
|
||||||
Loading…
Reference in a new issue