diff --git a/README.md b/README.md
index 109684e..c0511c4 100644
--- a/README.md
+++ b/README.md
@@ -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 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
-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
diff --git a/default_thai_company/default_thai_company/custom/bank.json b/default_thai_company/default_thai_company/custom/bank.json
new file mode 100644
index 0000000..2a4d28d
--- /dev/null
+++ b/default_thai_company/default_thai_company/custom/bank.json
@@ -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
+}
diff --git a/default_thai_company/default_thai_company/custom/bank_account.json b/default_thai_company/default_thai_company/custom/bank_account.json
new file mode 100644
index 0000000..5d817ad
--- /dev/null
+++ b/default_thai_company/default_thai_company/custom/bank_account.json
@@ -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
+}
diff --git a/default_thai_company/fixtures/print_format.json b/default_thai_company/fixtures/print_format.json
index 66856fe..cdd56a7 100644
--- a/default_thai_company/fixtures/print_format.json
+++ b/default_thai_company/fixtures/print_format.json
@@ -12,7 +12,7 @@
"font": null,
"font_size": 14,
"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
\n\t\n\t
\n\t\t
{{ doc.get_formatted(\"posting_date\") }}
\n\t\t{%- if doc.due_date %}\n\t\t
{{ doc.get_formatted(\"due_date\") }}
\n\t\t{%- endif %}\n\t
\n\t
\n\t\t
{{ _(doc.select_print_heading) or (_(doc.print_heading) if doc.print_heading != None else _(doc.doctype)) }}
\n\t\t{{ _(doc.sub_heading) if doc.sub_heading != None else _(doc.name) }}\n\t
\n\t\n
\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
\n\t\t\t\t\t{#- Re-rendered from the Address so its translations follow the print language. -#}\n\t\t\t\t\t{%- set address = get_print_address(doc.customer_address) -%}\n\t\t\t\t\t{% if address %}\n\t\t\t\t\t
{{ _(doc.meta.get_label(\"address_display\")) }}
{{ address }}
\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t{% if doc.tax_id %}\n\t\t\t\t\t
{{ _(doc.meta.get_label(\"tax_id\")) }}
{{ doc.tax_id }}
\n\t\t\t\t\t{% endif %}\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t\t
\n\t\t\t{% if doc.contact_person %}\n\t\t\t{#- Sales Invoice carries the contact's mobile and email but not its phone; read that from the Contact. -#}\n\t\t\t{%- set contact_phone = frappe.db.get_value(\"Contact\", doc.contact_person, \"phone\") -%}\n\t\t\t
\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t{% if doc.contact_mobile %}\n\t\t\t\t\t
{{ _(doc.meta.get_label(\"contact_mobile\")) }}
{{ doc.contact_mobile }}
\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t{% if doc.contact_email %}\n\t\t\t\t\t
{{ _(doc.meta.get_label(\"contact_email\")) }}
{{ doc.contact_email }}
\n\t\t\t\t\t{% endif %}\n\t\t\t\t\n\t\t\t
\n\t\t\t{% endif %}\n\t\t
\n\t
\n\t{% endif %}\n\n\t{% for section in page %}\n\t
\n\t\t{%- if doc.print_line_breaks and loop.index != 1 -%}{%- endif -%}\n\t\t{%- if doc.print_section_headings and section.label and section.has_data -%}\n\t\t
{{ _(section.label) }}
\n\t\t{%- endif -%}\n\t\t{%- set no_of_cols = section.columns|len -%}\n\t\t{%- set section_fieldnames = [] -%}\n\t\t{%- for column in section.columns -%}\n\t\t\t{%- for df in column.fields -%}{%- set ignored = section_fieldnames.append(df.fieldname) -%}{%- endfor -%}\n\t\t{%- endfor -%}\n\t\t{%- if \"total\" in section_fieldnames -%}\n\t\t{#- \"total\" sits in a 3-column section, so its label/value grid does not line up with the\n\t\t Taxes and Totals sections below it. Render it in the same right-half 6/6 grid they use. -#}\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t{%- for column in section.columns -%}{{ render_fields(column.fields, 1) }}{%- endfor -%}\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t
\n\t\t{%- else -%}\n\t\t{% for column in section.columns %}\n\t\t\t
\n\t\t\t{%- if loop.first and \"in_words\" in section_fieldnames and doc.in_words -%}\n\t\t\t{#- The Totals section's left column is empty in print; put the amount in words there,\n\t\t\t on the same line as Grand Total, instead of under Rounded Total on the right. -#}\n\t\t\t\t
\n\t{% endfor %}\n\n\t{% if loop.last and bank_account %}\n\t
\n\t\t{%- if doc.print_line_breaks -%}{%- endif -%}\n\t\t
\n\t\t\t
{{ _(\"Bank Account Details\") }}
\n\t\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t
{{ _(\"Bank\") }}
{{ bank_account.bank }}
\n\t\t\t\t\t
{{ _(\"Account Name\") }}
{{ bank_account.account_name }}
\n\t\t\t\t\t{% if bank_account.bank_account_no %}\n\t\t\t\t\t
{{ _(\"Bank Account No\") }}
{{ bank_account.bank_account_no }}
\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t{% if bank_account.iban %}\n\t\t\t\t\t
{{ _(\"IBAN\") }}
{{ bank_account.iban }}
\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t{% if bank_account.branch_code %}\n\t\t\t\t\t
{{ _(\"Branch Code\") }}
{{ bank_account.branch_code }}
\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t{% if bank_account.swift_number %}\n\t\t\t\t\t
{{ _(\"SWIFT number\") }}
{{ bank_account.swift_number }}
\n\t\t\t\t\t{% endif %}\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t
\n\t{% endif %}\n\n\t{% if loop.last %}\n\t
\n\t\t
\n\t\t\t\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
{{ _(\"On behalf of\") }} {{ doc.customer_name or doc.customer }}
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
{{ _(\"Receiver\") }}{{ _(\"Date\") }}
\n\t\t\t\t\t\t\t
{{ _(\"Position\") }}
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
{{ _(\"On behalf of\") }} {{ doc.company }}
\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t
{{ _(\"Issuer\") }}{{ _(\"Date\") }}
\n\t\t\t\t\t\t\t
{{ _(\"Position\") }}
\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t
\n\t{% endif %}\n\n\t\n
\n{% endfor %}\n\n\n\n\n\n\n\n\n",
+ "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
\n\t\n\t
\n\t\t
{{ doc.get_formatted(\"posting_date\") }}
\n\t\t{%- if doc.due_date %}\n\t\t
{{ doc.get_formatted(\"due_date\") }}
\n\t\t{%- endif %}\n\t
\n\t
\n\t\t
{{ _(doc.select_print_heading) or (_(doc.print_heading) if doc.print_heading != None else _(doc.doctype)) }}
\n\t\t{{ _(doc.sub_heading) if doc.sub_heading != None else _(doc.name) }}\n\t
\n\t\n
\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
\n\t\t\t\t\t{#- Re-rendered from the Address so its translations follow the print language. -#}\n\t\t\t\t\t{%- set address = get_print_address(doc.customer_address) -%}\n\t\t\t\t\t{% if address %}\n\t\t\t\t\t
{{ _(doc.meta.get_label(\"address_display\")) }}
{{ address }}
\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t{% if doc.tax_id %}\n\t\t\t\t\t
{{ _(doc.meta.get_label(\"tax_id\")) }}
{{ doc.tax_id }}
\n\t\t\t\t\t{% endif %}\n\t\t\t\t\n\t\t\t
\n\t\t
\n\t\t
\n\t\t\t{% if doc.contact_person %}\n\t\t\t{#- Sales Invoice carries the contact's mobile and email but not its phone; read that from the Contact. -#}\n\t\t\t{%- set contact_phone = frappe.db.get_value(\"Contact\", doc.contact_person, \"phone\") -%}\n\t\t\t
\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t{% if doc.contact_mobile %}\n\t\t\t\t\t
{{ _(doc.meta.get_label(\"contact_mobile\")) }}
{{ doc.contact_mobile }}
\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t{% if doc.contact_email %}\n\t\t\t\t\t
{{ _(doc.meta.get_label(\"contact_email\")) }}
{{ doc.contact_email }}
\n\t\t\t\t\t{% endif %}\n\t\t\t\t\n\t\t\t
\n\t\t\t{% endif %}\n\t\t
\n\t
\n\t{% endif %}\n\n\t{% for section in page %}\n\t
\n\t\t{%- if doc.print_line_breaks and loop.index != 1 -%}{%- endif -%}\n\t\t{%- if doc.print_section_headings and section.label and section.has_data -%}\n\t\t
{{ _(section.label) }}
\n\t\t{%- endif -%}\n\t\t{%- set no_of_cols = section.columns|len -%}\n\t\t{%- set section_fieldnames = [] -%}\n\t\t{%- for column in section.columns -%}\n\t\t\t{%- for df in column.fields -%}{%- set ignored = section_fieldnames.append(df.fieldname) -%}{%- endfor -%}\n\t\t{%- endfor -%}\n\t\t{%- if \"total\" in section_fieldnames -%}\n\t\t{#- \"total\" sits in a 3-column section, so its label/value grid does not line up with the\n\t\t Taxes and Totals sections below it. Render it in the same right-half 6/6 grid they use. -#}\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t{%- for column in section.columns -%}{{ render_fields(column.fields, 1) }}{%- endfor -%}\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t
\n\t\t{%- else -%}\n\t\t{% for column in section.columns %}\n\t\t\t
\n\t\t\t{%- if loop.first and \"in_words\" in section_fieldnames and doc.in_words -%}\n\t\t\t{#- The Totals section's left column is empty in print; put the amount in words there,\n\t\t\t on the same line as Grand Total, instead of under Rounded Total on the right. -#}\n\t\t\t\t