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',
|
||||
'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",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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