There is no solution with using the email template it was never fixed.
Do this instead.
Create a print format document as an email template instead in custom html.
In client script create this and attach to document you want the custom function to be on like invoice sales order etc,
/*
* ==============================================================================
* SCRIPT DOCUMENTATION: SALES ORDER EMAIL BUTTON
* ==============================================================================
* Purpose:
* Adds "Send Official Email" button specifically for SALES ORDER.
* Uses the updated dynamic Server Script.
* ==============================================================================
*/
frappe.ui.form.on('Sales Invoice', {
refresh: function(frm) {
if (!frm.is_new()) {
frm.add_custom_button(__('Send Official Email'), function() {
// 1. IDENTIFY PARTY (For Sales Order, it is always 'Customer')
let link_doctype = 'Customer';
let link_name = frm.doc.customer;
// 2. FETCH CONTACTS
frappe.call({
method: 'send_html_email',
args: {
action: 'fetch_contacts',
link_doctype: link_doctype,
link_name: link_name
},
callback: function(r_contacts) {
let contacts = r_contacts.message || [];
// Prepare Dropdown
let contact_options = contacts.map(c => ({
label: `${c.first_name || ''} ${c.last_name || ''} (${c.email_id})`,
value: c.email_id
}));
contact_options.unshift({label: 'Select to add...', value: ''});
// 3. FETCH PRINT FORMATS
frappe.call({
method: 'send_html_email',
args: {
action: 'fetch_formats',
doctype: frm.doc.doctype // Sends 'Sales Order'
},
callback: function(r_formats) {
let records = r_formats.message || [];
let format_options = records.map(r => r.name);
let default_format = format_options.length > 0 ? format_options[0] : "";
let default_email = frm.doc.contact_email || frm.doc.email_id || "";
if (format_options.length === 0) {
frappe.msgprint("No Jinja Print Formats found.");
return;
}
// 4. SHOW DIALOG
let d = new frappe.ui.Dialog({
title: `Send ${frm.doc.doctype}`,
width: 800,
fields: [
{
label: 'Select Print Format',
fieldname: 'print_format',
fieldtype: 'Select',
options: format_options,
default: default_format,
reqd: 1,
onchange: function() {
update_preview(d, frm, d.get_value('print_format'));
}
},
{ fieldtype: 'Section Break', label: 'Recipients' },
{
label: 'Quick Select Recipient',
fieldname: 'contact_picker',
fieldtype: 'Select',
options: contact_options,
onchange: function() {
let picked = d.get_value('contact_picker');
if(picked) d.set_value('recipient', picked);
}
},
{
label: 'To (Recipient)',
fieldname: 'recipient',
fieldtype: 'Data',
reqd: 1,
default: default_email
},
{
label: 'Quick Add to CC',
fieldname: 'cc_picker',
fieldtype: 'Select',
options: contact_options,
onchange: function() {
let picked = d.get_value('cc_picker');
if(picked) {
let current_cc = d.get_value('cc') || "";
if(current_cc) {
if(!current_cc.includes(picked)) d.set_value('cc', current_cc + ", " + picked);
} else {
d.set_value('cc', picked);
}
d.set_value('cc_picker', '');
}
}
},
{
label: 'CC',
fieldname: 'cc',
fieldtype: 'Data'
},
{
label: 'Subject',
fieldname: 'subject',
fieldtype: 'Data',
reqd: 1,
default: `${frm.doc.doctype} #${frm.doc.name}`
},
{ fieldtype: 'Section Break', label: 'Message Content' },
{
label: 'Add a Personal Note',
fieldname: 'message_note',
fieldtype: 'Small Text'
},
{
label: 'Preview',
fieldname: 'preview_html',
fieldtype: 'HTML'
}
],
primary_action_label: 'Send Email',
primary_action: function(values) {
frappe.call({
method: 'send_html_email',
args: {
action: 'send_email',
doctype: frm.doc.doctype,
docname: frm.doc.name,
print_format: values.print_format,
recipient: values.recipient,
cc: values.cc,
subject: values.subject,
message_note: values.message_note
},
freeze: true,
freeze_message: "Sending Email...",
callback: function(r) {
if (!r.exc) {
frappe.msgprint('Email Sent Successfully!');
d.hide();
}
}
});
}
});
let update_preview = function(dialog, frm, print_format) {
if (!print_format) return;
dialog.set_value('preview_html', '<div style="padding:20px; color:#777;">Loading Preview...</div>');
frappe.call({
method: 'frappe.www.printview.get_html_and_style',
args: {
doc: frm.doc,
print_format: print_format,
no_letterhead: 0
},
callback: function(r) {
if (r.message) {
dialog.set_value('preview_html', r.message.html);
}
}
});
};
if (default_format) {
update_preview(d, frm, default_format);
}
d.show();
}
});
}
});
});
}
}
});
Now create a server script
# ==============================================================================
# SCRIPT DOCUMENTATION: UNIVERSAL EMAIL SENDER (BACKEND API)
# ==============================================================================
# Purpose:
# Backend API for the "Send Official Email" button across MULTIPLE DocTypes
# (Sales Order, Quotation, Invoice, etc.).
#
# Updates in this version:
# - Made 'fetch_formats' dynamic: Uses the 'doctype' passed from the button
# instead of hardcoding "Sales Order".
# - Made 'fetch_contacts' dynamic: Accepts 'link_doctype' and 'link_name'
# so it works for Leads (Quotations) as well as Customers.
# - Kept the "expose_recipients" fix to ensure CCs are visible.
# ==============================================================================
# We access 'frappe' directly (it is globally available in Server Scripts)
action = frappe.form_dict.get('action')
# ------------------------------------------------------------------------------
# JOB A: FETCH PRINT FORMATS (Dynamic)
# ------------------------------------------------------------------------------
if action == 'fetch_formats':
# We grab the doctype from the request (e.g., "Quotation", "Sales Invoice")
target_doctype = frappe.form_dict.get('doctype')
formats = frappe.get_all('Print Format',
filters={
'doc_type': target_doctype, # Filters by the specific document type
'disabled': 0,
'print_format_type': 'Jinja'
},
fields=['name']
)
frappe.response['message'] = formats
# ------------------------------------------------------------------------------
# JOB B: FETCH PARTY CONTACTS (Dynamic)
# ------------------------------------------------------------------------------
elif action == 'fetch_contacts':
# Accepts generic link fields (e.g., link_doctype="Lead", link_name="John Doe")
link_doctype = frappe.form_dict.get('link_doctype')
link_name = frappe.form_dict.get('link_name')
contacts = frappe.get_all('Contact',
filters={
'link_doctype': link_doctype,
'link_name': link_name
},
fields=['first_name', 'last_name', 'email_id']
)
valid_contacts = [c for c in contacts if c.email_id]
frappe.response['message'] = valid_contacts
# ------------------------------------------------------------------------------
# JOB C: SEND EMAIL
# ------------------------------------------------------------------------------
else:
# 1. FETCH INPUTS
doctype = frappe.form_dict.get('doctype')
docname = frappe.form_dict.get('docname')
print_format = frappe.form_dict.get('print_format')
recipient = frappe.form_dict.get('recipient')
cc = frappe.form_dict.get('cc')
subject = frappe.form_dict.get('subject')
message_note = frappe.form_dict.get('message_note')
# 2. CLEAN & PARSE EMAIL LISTS
recipients_list = []
if recipient:
recipients_list = [r.strip() for r in recipient.split(',') if r.strip()]
cc_list = []
if cc:
cc_list = [c.strip() for c in cc.split(',') if c.strip()]
# 3. GET RAW HTML
raw_html = frappe.db.get_value("Print Format", print_format, "html")
if not raw_html:
final_html = "<p>Error: This Print Format does not contain raw HTML.</p>"
else:
# 4. RENDER JINJA
doc = frappe.get_doc(doctype, docname)
final_html = frappe.render_template(raw_html, {"doc": doc})
# 5. INJECT PERSONAL NOTE
if message_note:
note_html = f"""
<div style="font-family: Arial, sans-serif; padding: 15px; background-color: #fff3cd; border: 1px solid #ffeeba; margin-bottom: 20px; color: #856404; border-radius: 4px;">
<strong>Note:</strong> {message_note}
</div>
"""
final_html = note_html + final_html
# 6. SEND EMAIL
frappe.sendmail(
recipients=recipients_list,
cc=cc_list,
subject=subject,
message=final_html,
reference_doctype=doctype,
reference_name=docname,
expose_recipients="header" # Keeps CC visible
)
This will work you can search cc and emails related to the document inside the popup and send a proper HTML email.
You need to adjust the client script code for each document you attach it to. Paste the code into gemini it will explain where you need to adjust. The server script is universal there is no need to change that per document.
Now you can use printformat doc list as a “Email template instead” the reason i did this is because when the email template although in proper html is pasted inside the text editor for sending emails, the text editor destroys the html inside it making it look like crap. Been a whole day trying to figure it out and there is no fix and it seems this has been like this for years.