Merge branch '18.0' of git.bemade.org:bemade/bemade-addons into HEAD
This commit is contained in:
commit
85964620cf
10 changed files with 412 additions and 102 deletions
1
account_email_to_pdf/__init__.py
Normal file
1
account_email_to_pdf/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
23
account_email_to_pdf/__manifest__.py
Normal file
23
account_email_to_pdf/__manifest__.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"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.
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
1
account_email_to_pdf/models/__init__.py
Normal file
1
account_email_to_pdf/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import account_move
|
||||||
184
account_email_to_pdf/models/account_move.py
Normal file
184
account_email_to_pdf/models/account_move.py
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
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.misc import find_in_path
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountMove(models.Model):
|
||||||
|
_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"):
|
||||||
|
# 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", {})
|
||||||
|
if message_dict:
|
||||||
|
try:
|
||||||
|
# Create a PDF from the email content
|
||||||
|
pdf_attachment = self._create_pdf_from_email(message_dict)
|
||||||
|
if pdf_attachment:
|
||||||
|
_logger.info(
|
||||||
|
"Successfully created PDF attachment, proceeding with invoice creation"
|
||||||
|
)
|
||||||
|
# We need to return the result of _extend_with_attachments directly
|
||||||
|
# as that's what the original method would return
|
||||||
|
# Convert the list to a recordset before passing to _extend_with_attachments
|
||||||
|
attachment_recordset = self.env["ir.attachment"].browse(
|
||||||
|
[pdf_attachment.id]
|
||||||
|
)
|
||||||
|
return self._extend_with_attachments(
|
||||||
|
attachment_recordset,
|
||||||
|
new=bool(self._context.get("from_alias")),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.exception("Error creating PDF from email: %s", e)
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message_dict (dict): Email message dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ir.attachment: The created attachment record or False if failed
|
||||||
|
"""
|
||||||
|
# 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", "")
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
# Create HTML content for the PDF
|
||||||
|
html_content = f"""
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: Arial, sans-serif; margin: 20px; }}
|
||||||
|
.email-header {{ border-bottom: 1px solid #ccc; padding-bottom: 10px; margin-bottom: 20px; }}
|
||||||
|
.email-meta {{ color: #666; font-size: 0.9em; margin-bottom: 5px; }}
|
||||||
|
.email-subject {{ font-size: 1.2em; font-weight: bold; margin-bottom: 15px; }}
|
||||||
|
.email-body {{ line-height: 1.5; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-header">
|
||||||
|
<div class="email-meta">From: {escape(email_from)}</div>
|
||||||
|
<div class="email-meta">Date: {escape(email_date)}</div>
|
||||||
|
<div class="email-subject">{escape(subject)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="email-body">
|
||||||
|
{body}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Convert HTML to PDF
|
||||||
|
pdf_content = self._html_to_pdf(html_content)
|
||||||
|
if not pdf_content:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Create a proper ir.attachment record
|
||||||
|
filename = f"Email_{subject.replace(' ', '_')[:30]}.pdf"
|
||||||
|
attachment_vals = {
|
||||||
|
"name": filename,
|
||||||
|
"datas": base64.b64encode(pdf_content),
|
||||||
|
"mimetype": "application/pdf",
|
||||||
|
"res_model": "mail.message",
|
||||||
|
"res_id": message_dict.get("id", 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
attachment = self.env["ir.attachment"].create(attachment_vals)
|
||||||
|
_logger.info("Created PDF attachment from email: %s", filename)
|
||||||
|
return attachment
|
||||||
2
account_email_to_pdf/tests/__init__.py
Normal file
2
account_email_to_pdf/tests/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import test_email_to_pdf
|
||||||
|
from . import test_email_integration
|
||||||
88
account_email_to_pdf/tests/test_email_integration.py
Normal file
88
account_email_to_pdf/tests/test_email_integration.py
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import base64
|
||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.tools.misc import find_in_path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmailProcessing(TransactionCase):
|
||||||
|
"""Integration tests for email processing with PDF generation."""
|
||||||
|
|
||||||
|
@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 methods
|
||||||
|
cls.account_move = cls.env["account.move"]
|
||||||
|
|
||||||
|
def test_create_pdf_from_email(self):
|
||||||
|
"""Test that an email without attachments can be converted to PDF.
|
||||||
|
|
||||||
|
This test directly verifies that the _create_pdf_from_email method
|
||||||
|
correctly generates a PDF attachment from an email message without
|
||||||
|
attachments, allowing the invoice creation process to continue.
|
||||||
|
"""
|
||||||
|
if not self.wkhtmltopdf_available:
|
||||||
|
self.skipTest("wkhtmltopdf not available")
|
||||||
|
|
||||||
|
# Create a sample email message dictionary (similar to what would be parsed from an email)
|
||||||
|
message_dict = {
|
||||||
|
"subject": "Test Invoice",
|
||||||
|
"from": "test@example.com",
|
||||||
|
"to": "invoices@example.com",
|
||||||
|
"body": "<html><body><h1>Invoice Test</h1><p>This is a test invoice.</p></body></html>",
|
||||||
|
"attachments": [], # No attachments
|
||||||
|
"message_id": "<test123@example.com>",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call the method directly to create a PDF from the email
|
||||||
|
attachment = self.account_move._create_pdf_from_email(message_dict)
|
||||||
|
|
||||||
|
# Verify that an attachment was created
|
||||||
|
self.assertTrue(attachment, "An attachment should have been created")
|
||||||
|
# The actual name format is 'Email_' + subject + '.pdf' with spaces replaced by underscores
|
||||||
|
self.assertEqual(
|
||||||
|
attachment.name,
|
||||||
|
"Email_Test_Invoice.pdf",
|
||||||
|
"Attachment name should match expected format",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
attachment.mimetype, "application/pdf", "Attachment should be a PDF"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the content of the PDF attachment
|
||||||
|
pdf_data = base64.b64decode(attachment.datas)
|
||||||
|
self.assertTrue(pdf_data.startswith(b"%PDF-"), "Content should be a valid PDF")
|
||||||
|
self.assertTrue(len(pdf_data) > 100, "PDF should have a reasonable size")
|
||||||
|
|
||||||
|
def test_check_and_decode_attachment_with_empty_attachments(self):
|
||||||
|
"""Test that _check_and_decode_attachment doesn't reject emails with no attachments."""
|
||||||
|
if not self.wkhtmltopdf_available:
|
||||||
|
self.skipTest("wkhtmltopdf not available")
|
||||||
|
|
||||||
|
# Set up a context with a message_dict to simulate email processing
|
||||||
|
message_dict = {
|
||||||
|
"subject": "Test Invoice",
|
||||||
|
"from": "test@example.com",
|
||||||
|
"to": "invoices@example.com",
|
||||||
|
"body": "<html><body><h1>Invoice Test</h1><p>This is a test invoice.</p></body></html>",
|
||||||
|
"attachments": [], # No attachments
|
||||||
|
"message_id": "<test123@example.com>",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call the method with an empty attachments list
|
||||||
|
# We need to pass the message_dict in the context so _create_pdf_from_email can access it
|
||||||
|
result = self.account_move.with_context(
|
||||||
|
message_dict=message_dict
|
||||||
|
)._check_and_decode_attachment([])
|
||||||
|
|
||||||
|
# Verify that the result is not False (which would mean email rejection)
|
||||||
|
self.assertNotEqual(
|
||||||
|
result,
|
||||||
|
False,
|
||||||
|
"Should not reject the email when no attachments are provided",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify that the result contains attachment data
|
||||||
|
self.assertTrue(result, "Should return attachment data")
|
||||||
102
account_email_to_pdf/tests/test_email_to_pdf.py
Normal file
102
account_email_to_pdf/tests/test_email_to_pdf.py
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
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")
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
/** @odoo-module **/
|
|
||||||
|
|
||||||
import ListController from 'web.ListController';
|
|
||||||
import ListView from 'web.ListView';
|
|
||||||
|
|
||||||
import KanbanController from 'web.KanbanController';
|
|
||||||
import KanbanView from 'web.KanbanView';
|
|
||||||
|
|
||||||
const viewRegistry = require('web.view_registry');
|
|
||||||
|
|
||||||
const OdooScrapperListController = ListController.extend({
|
|
||||||
// buttons_template must match the t-name on the template for the button (static xml)
|
|
||||||
buttons_template: 'odoo_scrapper.list_view_buttons',
|
|
||||||
events: _.extend({}, ListController.prototype.events, {
|
|
||||||
'click .o_button_get_partner': '_onGetPartnerClick',
|
|
||||||
}),
|
|
||||||
// This may need to be async function() if there needs to be an await this._rpc({ ... }); call to not reload early
|
|
||||||
_onGetPartnerClick: function () {
|
|
||||||
this._rpc({
|
|
||||||
model: 'res.partner',
|
|
||||||
method: 'get_odoo_partner',
|
|
||||||
args: [],
|
|
||||||
}).then(() => {
|
|
||||||
this.reload();
|
|
||||||
});
|
|
||||||
// Couldn't test this for real, but it runs the action. May need a this.reload()
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const OdooScrapperListView = ListView.extend({
|
|
||||||
config: _.extend({}, ListView.prototype.config, {
|
|
||||||
Controller: OdooScrapperListController,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// key must match with the js_class attribute of the tree view you want to modify
|
|
||||||
viewRegistry.add('res_partner_odoo_scrapper_tree', OdooScrapperListView);
|
|
||||||
|
|
||||||
const OdooScrapperKanbanController = KanbanController.extend({
|
|
||||||
// buttons_template must match the t-name on the template for the button (static xml)
|
|
||||||
buttons_template: 'odoo_scrapper.kanban_view_buttons',
|
|
||||||
events: _.extend({}, KanbanController.prototype.events, {
|
|
||||||
'click .o_button_get_partner': '_onGetPartnerClick',
|
|
||||||
}),
|
|
||||||
// This may need to be async function() if there needs to be an await this._rpc({ ... }); call to not reload early
|
|
||||||
_onGetPartnerClick: function () {
|
|
||||||
this._rpc({
|
|
||||||
model: 'res.partner',
|
|
||||||
method: 'get_odoo_partner',
|
|
||||||
args: [],
|
|
||||||
}).then(() => {
|
|
||||||
this.reload();
|
|
||||||
});
|
|
||||||
// Couldn't test this for real, but it runs the action. May need a this.reload()
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const OdooScrapperKanbanView = KanbanView.extend({
|
|
||||||
config: _.extend({}, KanbanView.prototype.config, {
|
|
||||||
Controller: OdooScrapperKanbanController,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// key must match with the js_class attribute of the tree view you want to modify
|
|
||||||
viewRegistry.add('res_partner_odoo_scrapper_kanban', OdooScrapperKanbanView);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
<templates id="template" xml:space="preserve">
|
|
||||||
|
|
||||||
<t t-extend="ListView.buttons" t-name="odoo_scrapper.list_view_buttons"> <!-- t-name must match with js controller button template -->
|
|
||||||
<t t-jquery="button.o_list_button_add" t-operation="after">
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-primary ml4 o_button_get_partner"
|
|
||||||
title="Get Odoo Partner"> Get Odoo Partner </button>
|
|
||||||
</t>
|
|
||||||
</t>
|
|
||||||
|
|
||||||
<t t-extend="KanbanView.buttons" t-name="odoo_scrapper.kanban_view_buttons">
|
|
||||||
<t t-jquery="button" t-operation="after">
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-primary ml4 o_button_get_partner"
|
|
||||||
title="Get Odoo Partner"> Get Odoo Partner </button>
|
|
||||||
</t>
|
|
||||||
</t>
|
|
||||||
|
|
||||||
</templates>
|
|
||||||
|
|
@ -8,19 +8,22 @@
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<field name="company_type" position="after">
|
<field name="company_type" position="after">
|
||||||
<br/>
|
<br/>
|
||||||
<field name="is_odoo_user"/>Odoo User
|
<field name="is_odoo_user"/>
|
||||||
<field name="is_odoo_partner"/>Odoo Partner
|
Odoo User
|
||||||
|
<field name="is_odoo_partner"/>
|
||||||
|
Odoo Partner
|
||||||
<br/>
|
<br/>
|
||||||
<div invisible="is_odoo_partner == False">
|
<div invisible="is_odoo_partner == False">
|
||||||
<h3><field name="odoo_partner_type"/> Partner</h3>
|
<h3>
|
||||||
|
<field name="odoo_partner_type"/>
|
||||||
|
Partner</h3>
|
||||||
</div>
|
</div>
|
||||||
</field>
|
</field>
|
||||||
<group name="container_row_2" position="after">
|
<group name="container_row_2" position="after">
|
||||||
<group name="container_row_3">
|
<group name="container_row_3">
|
||||||
<field name="relation_all_ids">
|
<field name="relation_all_ids">
|
||||||
<list>
|
<list>
|
||||||
<field name="other_partner_id" required="True" options="{'no_create': True}"
|
<field name="other_partner_id" required="True" options="{'no_create': True}" string="Customer" domain="[['relation_all_ids.type_selection_id.id', '=', 1]]"/>
|
||||||
string="Customer" domain="[['relation_all_ids.type_selection_id.id', '=', 1]]"/>
|
|
||||||
<field name="date_start"/>
|
<field name="date_start"/>
|
||||||
<field name="date_end"/>
|
<field name="date_end"/>
|
||||||
<field name="active" invisible="1"/>
|
<field name="active" invisible="1"/>
|
||||||
|
|
@ -38,9 +41,6 @@
|
||||||
<field name="model">res.partner</field>
|
<field name="model">res.partner</field>
|
||||||
<field name="inherit_id" ref="base.view_partner_tree"/>
|
<field name="inherit_id" ref="base.view_partner_tree"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//list" position="attributes">
|
|
||||||
<attribute name="js_class">res_partner_odoo_scrapper_tree</attribute>
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//list" position="inside">
|
<xpath expr="//list" position="inside">
|
||||||
<field name="is_odoo_user"/>
|
<field name="is_odoo_user"/>
|
||||||
<field name="is_odoo_partner"/>
|
<field name="is_odoo_partner"/>
|
||||||
|
|
@ -54,20 +54,17 @@
|
||||||
<field name="model">res.partner</field>
|
<field name="model">res.partner</field>
|
||||||
<field name="inherit_id" ref="base.res_partner_kanban_view"/>
|
<field name="inherit_id" ref="base.res_partner_kanban_view"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//kanban" position="attributes">
|
|
||||||
<attribute name="js_class">res_partner_odoo_scrapper_kanban</attribute>
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//kanban" position="inside">
|
<xpath expr="//kanban" position="inside">
|
||||||
<field name="odoo_partner_type"/>
|
<field name="odoo_partner_type"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//div[hasclass('oe_kanban_global_click') and hasclass('o_kanban_record_has_image_fill') and hasclass('o_res_partner_kanban')]" position="attributes">
|
<xpath expr="//div[hasclass('o_kanban_image_fill')]" position="attributes">
|
||||||
<attribute name="t-attf-class">oe_kanban_global_click o_kanban_record_has_image_fill o_res_partner_kanban oe_kanban_color_#{record.color}</attribute>
|
<attribute name="t-attf-class">oe_kanban_global_click o_kanban_record_has_image_fill o_res_partner_kanban oe_kanban_color_#{record.color}</attribute>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//strong[hasclass('o_kanban_record_title') and hasclass('oe_partner_heading')]" position="before">
|
<field name="display_name" position="before">
|
||||||
<strong t-if="record.odoo_partner_type.raw_value" class="o_kanban_record_subtitle oe_partner_heading">
|
<strong t-if="record.odoo_partner_type.raw_value" class="o_kanban_record_subtitle oe_partner_heading">
|
||||||
<field name="odoo_partner_type"/>
|
<field name="odoo_partner_type"/>
|
||||||
</strong>
|
</strong>
|
||||||
</xpath>
|
</field>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue