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="company_type" position="after">
|
||||
<br/>
|
||||
<field name="is_odoo_user"/>Odoo User
|
||||
<field name="is_odoo_partner"/>Odoo Partner
|
||||
<field name="is_odoo_user"/>
|
||||
Odoo User
|
||||
<field name="is_odoo_partner"/>
|
||||
Odoo Partner
|
||||
<br/>
|
||||
<div invisible="is_odoo_partner == False">
|
||||
<h3><field name="odoo_partner_type"/> Partner</h3>
|
||||
<h3>
|
||||
<field name="odoo_partner_type"/>
|
||||
Partner</h3>
|
||||
</div>
|
||||
</field>
|
||||
<group name="container_row_2" position="after">
|
||||
<group name="container_row_3">
|
||||
<field name="relation_all_ids">
|
||||
<list>
|
||||
<field name="other_partner_id" required="True" options="{'no_create': True}"
|
||||
string="Customer" domain="[['relation_all_ids.type_selection_id.id', '=', 1]]"/>
|
||||
<field name="other_partner_id" required="True" options="{'no_create': True}" string="Customer" domain="[['relation_all_ids.type_selection_id.id', '=', 1]]"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="active" invisible="1"/>
|
||||
|
|
@ -38,9 +41,6 @@
|
|||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_tree"/>
|
||||
<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">
|
||||
<field name="is_odoo_user"/>
|
||||
<field name="is_odoo_partner"/>
|
||||
|
|
@ -54,20 +54,17 @@
|
|||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.res_partner_kanban_view"/>
|
||||
<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">
|
||||
<field name="odoo_partner_type"/>
|
||||
</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>
|
||||
</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">
|
||||
<field name="odoo_partner_type"/>
|
||||
</strong>
|
||||
</xpath>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue