Issue with Email Template Table Styling in ERPNext

Hi Frappe community,

I’m working on an email template in ERPNext, and I’m having trouble with table styling. Specifically, the column headers in my table are not aligned correctly, and the text appears cramped together. Here’s an example of my current HTML structure:

But in email preview, it is not working,


Thanks in advance for your help!

@Mohammadsami_Bakhtya Please write html code properly and if you can style your table then use inline css for table from w3school reference.

Example :

<style>#mytable {border: 1px solid #000; width: 100%; border-collapse: collapse; text-align: center;} #mytable td {border: 1px solid #000;} #mytable th {border: 1px solid #000;} #mytable tr {border: 1px solid #000;} </style>
<table id='mytable'>
<tbody>
<tr>
<th>Company</th>
<th>Contact</th>
<th>Country</th>
</tr>
<tr>
<td>Alfreds Futterkiste</td>
<td>Maria Anders</td>
<td>Germany</td>
</tr>
<tr>
<td>Centro comercial Moctezuma</td>
<td>Francisco Chang</td>
<td>Mexico</td>
</tr>
</tbody>
</table>

I have tried all things.
Have used inline css also, but not working. i hace used simple html to explain the issue.

I have used your html also, here is the preview:

I have used below one also, using inline css, but not working.

{% set cmp = frappe.get_doc("Company", company) %}
<div style="width: 80%; margin: auto;">
    <div style="text-align: center; padding: 20px;">
        <img src="{{ company.logo }}" alt="{{ company }} Logo" style="max-width: 200px;">
        <h1>Sales Invoice</h1>
    </div>
    <div style="margin: 20px 0;">
        <p style="margin: 10px 0;">Dear {{ customer }},</p>
        <p style="margin: 10px 0;">Thank you for your purchase. Please find your invoice details below:</p>
        
        <table style="border-collapse: collapse; width: 100%;">
            <tr>
                <th style="border: 1px solid #ddd; padding: 8px; background-color: #f2f2f2; text-align: left;">Invoice Number</th>
                <td style="border: 1px solid #ddd; padding: 8px; text-align: left;">{{ name }}</td>
            </tr>
            <tr>
                <th style="border: 1px solid #ddd; padding: 8px; background-color: #f2f2f2; text-align: left;">Invoice Date</th>
                <td style="border: 1px solid #ddd; padding: 8px; text-align: left;">{{ posting_date }}</td>
            </tr>
            <tr>
                <th style="border: 1px solid #ddd; padding: 8px; background-color: #f2f2f2; text-align: left;">Due Date</th>
                <td style="border: 1px solid #ddd; padding: 8px; text-align: left;">{{ due_date }}</td>
            </tr>
        </table>

        <h2 style="margin-top: 20px;">Invoice Items:</h2>
        <table style="border-collapse: collapse; width: 100%;">
            <thead>
                <tr>
                    <th style="border: 1px solid #ddd; padding: 8px; background-color: #f2f2f2; text-align: left;">Description</th>
                    <th style="border: 1px solid #ddd; padding: 8px; background-color: #f2f2f2; text-align: left;">Quantity</th>
                    <th style="border: 1px solid #ddd; padding: 8px; background-color: #f2f2f2; text-align: left;">Unit Price</th>
                    <th style="border: 1px solid #ddd; padding: 8px; background-color: #f2f2f2; text-align: left;">Total</th>
                </tr>
            </thead>
            <tbody>
                {% for item in items %}
                <tr>
                    <td style="border: 1px solid #ddd; padding: 8px; text-align: left;">{{ item.item_code }}</td>
                    <td style="border: 1px solid #ddd; padding: 8px; text-align: left;">{{ item.qty }}</td>
                    <td style="border: 1px solid #ddd; padding: 8px; text-align: left;">{{ item.rate }}</td>
                    <td style="border: 1px solid #ddd; padding: 8px; text-align: left;">{{ item.amount }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>

        <p style="margin-top: 20px;"><strong>Total Amount:</strong> {{ grand_total }}</p>
    </div>
    <div style="margin-top: 20px; text-align: center;">
        
        <p>If you have any questions, please contact us at {{ cmp.phone_no or "" }} or {{ cmp.email or ""}}.</p>
        <p>Thank you for your business!</p>
        <p>{{ company }}</p>
    </div>
</div>

@Mohammadsami_Bakhtya If you enable the “use HTML” option, the email template will display HTML code correctly. However, if you disable it, the text editor’s preview will no longer render the HTML and will instead show the raw HTML code. Please take note of this behavior.

Already i have enabled he “Use Html”.

I am experiencing the same.

Is there a solution to this please ?

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.