feat: bank details on the Sales Invoice print in the document's language

Bank Account.account_name and Bank.bank_name are translatable (the Link
field Bank Account.bank cannot be; the Bank's name is what prints), and the
Sales Invoice format renders both through _().
This commit is contained in:
kurogeek committed 2026-10-21 03:36:46 +00:00
1 parent 5c15b6637b
commit c8fb983749
4 files changed
+68 -2

No files matched your search

+3 -1
View File
@@ -29,7 +29,9 @@ show the company's and the party's address in the document's print language (the
address is re-rendered at print time; party addresses are re-rendered from the linked address is re-rendered at print time; party addresses are re-rendered from the linked
Address instead of printing the stored `address_display`). The withholding tax certificate Address instead of printing the stored `address_display`). The withholding tax certificate
always takes the Thai translation. Translations are keyed on the exact text, so one always takes the Thai translation. Translations are keyed on the exact text, so one
translation of a city or province serves every address that uses it. translation of a city or province serves every address that uses it. The same goes for
the bank details on the Sales Invoice: *Account Name* on the Bank Account and *Bank Name*
on the Bank are translatable.
### Withholding tax certificate ### Withholding tax certificate
@@ -0,0 +1,32 @@
{
"custom_fields": [],
"custom_perms": [],
"doctype": "Bank",
"links": [],
"property_setters": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"creation": "2026-10-20 12:00:00",
"default_value": null,
"doc_type": "Bank",
"docstatus": 0,
"doctype_or_field": "DocField",
"field_name": "bank_name",
"idx": 0,
"is_system_generated": 0,
"modified": "2026-10-20 12:00:00",
"modified_by": "Administrator",
"module": null,
"name": "Bank-bank_name-translatable",
"owner": "Administrator",
"property": "translatable",
"property_type": "Check",
"row_name": null,
"value": "1"
}
],
"sync_on_migrate": 1
}
@@ -0,0 +1,32 @@
{
"custom_fields": [],
"custom_perms": [],
"doctype": "Bank Account",
"links": [],
"property_setters": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"creation": "2026-10-20 12:00:00",
"default_value": null,
"doc_type": "Bank Account",
"docstatus": 0,
"doctype_or_field": "DocField",
"field_name": "account_name",
"idx": 0,
"is_system_generated": 0,
"modified": "2026-10-20 12:00:00",
"modified_by": "Administrator",
"module": null,
"name": "Bank Account-account_name-translatable",
"owner": "Administrator",
"property": "translatable",
"property_type": "Check",
"row_name": null,
"value": "1"
}
],
"sync_on_migrate": 1
}
@@ -12,7 +12,7 @@
"font": null, "font": null,
"font_size": 14, "font_size": 14,
"format_data": null, "format_data": null,
"html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header, render_field with context -%}\n{%- set bank_account = get_company_bank_account(doc.company) -%}\n{#- ERPNext hides taxes marked \"Is this Tax included in Basic Rate?\" unless Accounts Settings\n enables \"Show Inclusive Tax in Print\"; always show them here so the tax line and\n \"Total (Without Tax)\" appear on invoices with inclusive tax. -#}\n{%- if doc.get(\"taxes\", {\"included_in_print_rate\": 1}) -%}\n\t{%- set ignored = doc.flags.update({\"show_inclusive_tax_in_print\": 1}) -%}\n{%- endif -%}\n{#- App copies of ERPNext's totals includes, printing the posted figures: \"total\" is the net before\n the Additional Discount; \"taxes\" prints the discount's net share, Net Total and each charge after\n discount, so the lines add up to Grand Total whichever total the discount applies on. -#}\n{%- set ignored = doc.print_templates.update({\"total\": \"default_thai_company/templates/print_formats/includes/total.html\"}) -%}\n{%- if doc.print_templates.get(\"taxes\") -%}\n\t{%- set ignored = doc.print_templates.update({\"taxes\": \"default_thai_company/templates/print_formats/includes/taxes.html\"}) -%}\n{%- endif -%}\n{#- Standard layout prints every non-empty field; drop the ones this invoice does not need.\n posting_date / due_date are printed in the heading, customer fields in the Customer section,\n in_words in the left column of the Totals section. -#}\n{%- set hidden_fields = [\n\t\"total_qty\", \"update_billed_amount_in_delivery_note\", \"amount_eligible_for_commission\", \"company_tax_id\",\n\t\"disable_rounded_total\",\n\t\"posting_date\", \"due_date\",\n\t\"customer_name\", \"address_display\", \"contact_display\", \"contact_mobile\", \"contact_email\", \"tax_id\",\n\t\"in_words\",\n] -%}\n{#- rounded_total keeps a value even when rounding is disabled, so the standard layout would print it. -#}\n{%- if doc.is_rounded_total_disabled() -%}{%- set ignored = hidden_fields.append(\"rounded_total\") -%}{%- endif -%}\n{#- add_header renders this string via frappe.render_template with {\"doc\": doc}, so it is kept\n unrendered with {% raw %}. Same markup as the stock heading plus a right-aligned date block. -#}\n{%- set heading_template -%}\n{% raw %}\n<div class=\"print-heading\">\n\t<style>\n\t\t.print-heading .heading-dates { float: right; text-align: right; margin-top: 4px; white-space: nowrap; }\n\t\t.print-heading .heading-dates label { display: inline; font-weight: 700; margin: 0 4px 0 0; }\n\t\t.print-heading .heading-clear { clear: both; }\n\t</style>\n\t<div class=\"heading-dates\">\n\t\t<div><label>{{ _(doc.meta.get_label(\"posting_date\")) }}:</label> {{ doc.get_formatted(\"posting_date\") }}</div>\n\t\t{%- if doc.due_date %}\n\t\t<div><label>{{ _(doc.meta.get_label(\"due_date\")) }}:</label> {{ doc.get_formatted(\"due_date\") }}</div>\n\t\t{%- endif %}\n\t</div>\n\t<h2>\n\t\t<div>{{ _(doc.select_print_heading) or (_(doc.print_heading) if doc.print_heading != None else _(doc.doctype)) }}</div>\n\t\t<small class=\"sub-heading\">{{ _(doc.sub_heading) if doc.sub_heading != None else _(doc.name) }}</small>\n\t</h2>\n\t<div class=\"heading-clear\"></div>\n</div>\n{% endraw %}\n{%- endset -%}\n{#- Standard layout prints every non-empty child column; keep the item table to the essentials -#}\n{%- set item_columns = [\n\t{\"fieldname\": \"item_code\"},\n\t{\"fieldname\": \"description\"},\n\t{\"fieldname\": \"qty\"},\n\t{\"fieldname\": \"rate\"},\n\t{\"fieldname\": \"amount\"},\n] -%}\n\n{%- macro render_fields(fields, no_of_cols) -%}\n\t{%- for df in fields -%}\n\t\t{%- if df.fieldname not in hidden_fields -%}\n\t\t\t{%- if df.fieldname == \"items\" -%}\n\t\t\t\t{%- set df = df.as_dict() -%}\n\t\t\t\t{%- set ignored = df.update({\"visible_columns\": item_columns}) -%}\n\t\t\t{%- endif -%}\n\t\t\t{{ render_field(df, doc, no_of_cols) }}\n\t\t{%- endif -%}\n\t{%- endfor -%}\n{%- endmacro -%}\n\n{% for page in layout %}\n<div class=\"page-break\">\n\t<div {% if print_settings.repeat_header_footer %} id=\"header-html\" class=\"hidden-pdf\" {% endif %}>\n\t\t{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings, heading_template) }}\n\t</div>\n\n\t{% if loop.first %}\n\t<div class=\"row section-break\" data-label=\"{{ _(\"Customer\") }}\">\n\t\t<div class=\"col-xs-12 column-break\">\n\t\t\t<h4>{{ _(\"Customer\") }}</h4>\n\t\t\t<style>\n\t\t\t\t.customer-details { width: 100%; }\n\t\t\t\t.customer-details td { padding: 0 !important; border: 0; vertical-align: top; }\n\t\t\t\t.customer-details td.text-muted { width: 40%; padding-right: 1em !important; white-space: nowrap; }\n\t\t\t</style>\n\t\t</div>\n\t\t<div class=\"col-xs-6 column-break\">\n\t\t\t<table class=\"customer-details\">\n\t\t\t\t<tbody>\n\t\t\t\t\t<tr><td class=\"text-muted\">{{ _(doc.meta.get_label(\"customer_name\")) }}</td><td>{{ doc.customer_name or doc.customer }}</td></tr>\n\t\t\t\t\t{#- Re-rLine truncated "html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header, render_field with context -%}\n{%- set bank_account = get_company_bank_account(doc.company) -%}\n{#- ERPNext hides taxes marked \"Is this Tax included in Basic Rate?\" unless Accounts Settings\n enables \"Show Inclusive Tax in Print\"; always show them here so the tax line and\n \"Total (Without Tax)\" appear on invoices with inclusive tax. -#}\n{%- if doc.get(\"taxes\", {\"included_in_print_rate\": 1}) -%}\n\t{%- set ignored = doc.flags.update({\"show_inclusive_tax_in_print\": 1}) -%}\n{%- endif -%}\n{#- App copies of ERPNext's totals includes, printing the posted figures: \"total\" is the net before\n the Additional Discount; \"taxes\" prints the discount's net share, Net Total and each charge after\n discount, so the lines add up to Grand Total whichever total the discount applies on. -#}\n{%- set ignored = doc.print_templates.update({\"total\": \"default_thai_company/templates/print_formats/includes/total.html\"}) -%}\n{%- if doc.print_templates.get(\"taxes\") -%}\n\t{%- set ignored = doc.print_templates.update({\"taxes\": \"default_thai_company/templates/print_formats/includes/taxes.html\"}) -%}\n{%- endif -%}\n{#- Standard layout prints every non-empty field; drop the ones this invoice does not need.\n posting_date / due_date are printed in the heading, customer fields in the Customer section,\n in_words in the left column of the Totals section. -#}\n{%- set hidden_fields = [\n\t\"total_qty\", \"update_billed_amount_in_delivery_note\", \"amount_eligible_for_commission\", \"company_tax_id\",\n\t\"disable_rounded_total\",\n\t\"posting_date\", \"due_date\",\n\t\"customer_name\", \"address_display\", \"contact_display\", \"contact_mobile\", \"contact_email\", \"tax_id\",\n\t\"in_words\",\n] -%}\n{#- rounded_total keeps a value even when rounding is disabled, so the standard layout would print it. -#}\n{%- if doc.is_rounded_total_disabled() -%}{%- set ignored = hidden_fields.append(\"rounded_total\") -%}{%- endif -%}\n{#- add_header renders this string via frappe.render_template with {\"doc\": doc}, so it is kept\n unrendered with {% raw %}. Same markup as the stock heading plus a right-aligned date block. -#}\n{%- set heading_template -%}\n{% raw %}\n<div class=\"print-heading\">\n\t<style>\n\t\t.print-heading .heading-dates { float: right; text-align: right; margin-top: 4px; white-space: nowrap; }\n\t\t.print-heading .heading-dates label { display: inline; font-weight: 700; margin: 0 4px 0 0; }\n\t\t.print-heading .heading-clear { clear: both; }\n\t</style>\n\t<div class=\"heading-dates\">\n\t\t<div><label>{{ _(doc.meta.get_label(\"posting_date\")) }}:</label> {{ doc.get_formatted(\"posting_date\") }}</div>\n\t\t{%- if doc.due_date %}\n\t\t<div><label>{{ _(doc.meta.get_label(\"due_date\")) }}:</label> {{ doc.get_formatted(\"due_date\") }}</div>\n\t\t{%- endif %}\n\t</div>\n\t<h2>\n\t\t<div>{{ _(doc.select_print_heading) or (_(doc.print_heading) if doc.print_heading != None else _(doc.doctype)) }}</div>\n\t\t<small class=\"sub-heading\">{{ _(doc.sub_heading) if doc.sub_heading != None else _(doc.name) }}</small>\n\t</h2>\n\t<div class=\"heading-clear\"></div>\n</div>\n{% endraw %}\n{%- endset -%}\n{#- Standard layout prints every non-empty child column; keep the item table to the essentials -#}\n{%- set item_columns = [\n\t{\"fieldname\": \"item_code\"},\n\t{\"fieldname\": \"description\"},\n\t{\"fieldname\": \"qty\"},\n\t{\"fieldname\": \"rate\"},\n\t{\"fieldname\": \"amount\"},\n] -%}\n\n{%- macro render_fields(fields, no_of_cols) -%}\n\t{%- for df in fields -%}\n\t\t{%- if df.fieldname not in hidden_fields -%}\n\t\t\t{%- if df.fieldname == \"items\" -%}\n\t\t\t\t{%- set df = df.as_dict() -%}\n\t\t\t\t{%- set ignored = df.update({\"visible_columns\": item_columns}) -%}\n\t\t\t{%- endif -%}\n\t\t\t{{ render_field(df, doc, no_of_cols) }}\n\t\t{%- endif -%}\n\t{%- endfor -%}\n{%- endmacro -%}\n\n{% for page in layout %}\n<div class=\"page-break\">\n\t<div {% if print_settings.repeat_header_footer %} id=\"header-html\" class=\"hidden-pdf\" {% endif %}>\n\t\t{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings, heading_template) }}\n\t</div>\n\n\t{% if loop.first %}\n\t<div class=\"row section-break\" data-label=\"{{ _(\"Customer\") }}\">\n\t\t<div class=\"col-xs-12 column-break\">\n\t\t\t<h4>{{ _(\"Customer\") }}</h4>\n\t\t\t<style>\n\t\t\t\t.customer-details { width: 100%; }\n\t\t\t\t.customer-details td { padding: 0 !important; border: 0; vertical-align: top; }\n\t\t\t\t.customer-details td.text-muted { width: 40%; padding-right: 1em !important; white-space: nowrap; }\n\t\t\t</style>\n\t\t</div>\n\t\t<div class=\"col-xs-6 column-break\">\n\t\t\t<table class=\"customer-details\">\n\t\t\t\t<tbody>\n\t\t\t\t\t<tr><td class=\"text-muted\">{{ _(doc.meta.get_label(\"customer_name\")) }}</td><td>{{ doc.customer_name or doc.customer }}</td></tr>\n\t\t\t\t\t{#- Re-rLine truncated
"line_breaks": 0, "line_breaks": 0,
"margin_bottom": 15.0, "margin_bottom": 15.0,
"margin_left": 15.0, "margin_left": 15.0,