diff --git a/README.md b/README.md
index 6782b2a..86a0bfa 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,14 @@ bench get-app $URL_OF_THIS_REPO --branch develop
bench install-app default_thai_company
```
+### Thai language
+
+Every string the app adds (custom field labels, messages, print format and letter head
+text, Tax Withholding Category names) is translated in `translations/th.csv`; the same
+file overrides the upstream Thai for the standard labels the print formats show. The
+print formats embed Sarabun (`public/fonts`, OFL) so PDFs render Thai on servers
+without a Thai system font. Switch a user or the print language to `th` to use it.
+
### Contributing
This app uses `pre-commit` for code formatting and linting. Please [install pre-commit](https://pre-commit.com/#installation) and enable it for this repository:
diff --git a/default_thai_company/fixtures/letter_head.json b/default_thai_company/fixtures/letter_head.json
index d3e1ee3..a829534 100644
--- a/default_thai_company/fixtures/letter_head.json
+++ b/default_thai_company/fixtures/letter_head.json
@@ -1,7 +1,7 @@
[
{
"align": "Left",
- "content": "\n{% set company = get_letter_head_company(doc) %}\n{% if company %}\n
\n {% if company.company_logo %}\n \n {% endif %}\n
\n
{{ company.company_name }}
\n \n {% if company.tax_id %}
Tax ID: {{ company.tax_id }}
{% endif %}\n {% if company.address %}
{{ company.address }}
{% endif %}\n {% if company.phone_no %}
Phone: {{ company.phone_no }}
{% endif %}\n {% if company.email %}
Email: {{ company.email }}
{% endif %}\n \n
\n
\n{% endif %}\n",
+ "content": "\n{% set company = get_letter_head_company(doc) %}\n{% if company %}\n
\n {% if company.company_logo %}\n \n {% endif %}\n
\n
{{ company.company_name }}
\n \n {% if company.tax_id %}
{{ _(\"Tax ID\") }}: {{ company.tax_id }}
{% endif %}\n {% if company.address %}
{{ company.address }}
{% endif %}\n {% if company.phone_no %}
{{ _(\"Phone\") }}: {{ company.phone_no }}
{% endif %}\n {% if company.email %}
{{ _(\"Email\") }}: {{ company.email }}
{% endif %}\n \n
\n
\n{% endif %}\n",
"disabled": 0,
"docstatus": 0,
"doctype": "Letter Head",
@@ -18,7 +18,7 @@
"image_width": 0.0,
"is_default": 1,
"letter_head_name": "Default Letter Head",
- "modified": "2026-09-10 09:51:23.933752",
+ "modified": "2026-09-15 06:44:37.653888",
"name": "Default Letter Head",
"source": "HTML"
}
diff --git a/default_thai_company/fixtures/print_format.json b/default_thai_company/fixtures/print_format.json
index e48c70b..c98cccd 100644
--- a/default_thai_company/fixtures/print_format.json
+++ b/default_thai_company/fixtures/print_format.json
@@ -2,7 +2,7 @@
{
"absolute_value": 0,
"align_labels_right": 0,
- "css": "/* Print Style \"Redesign\" gives .letter-head a 30px margin; tighten it. */\n.print-format .letter-head {\n\tmargin-bottom: 5px;\n}\n\n/* One vertical rhythm for every label/value line. \"Redesign\" stacks field rows with\n margin-top: 15px (doubled where Total/Taxes nest a layout .row) while the custom\n Customer / Bank tables and the heading dates had none; use line-height 1.8 everywhere\n and no row margins instead. */\n.print-format .row:not(.section-break) {\n\tmargin-top: 0 !important;\n\tline-height: 1.8;\n}\n.print-format .heading-dates,\n.print-format .customer-details td,\n.print-format .bank-account-details td {\n\tline-height: 1.8;\n}\n\n/* standard.css gives .table 20px top/bottom margins; keep the item table close to its neighbours. */\n.print-format [data-fieldname=\"items\"] > .table {\n\tmargin-top: 5px;\n\tmargin-bottom: 5px;\n}",
+ "css": "/* Thai text needs a font with Thai glyphs; wkhtmltopdf only sees the server's fonts and a\n headless server rarely has one, so ship Sarabun (OFL, see public/fonts/OFL.txt) with the app.\n Asset URLs are made absolute for the PDF renderer by frappe.utils.scrub_urls. */\n@font-face {\n\tfont-family: \"Sarabun\";\n\tfont-weight: 400;\n\tsrc: url(\"/assets/default_thai_company/fonts/Sarabun-Regular.ttf\");\n}\n@font-face {\n\tfont-family: \"Sarabun\";\n\tfont-weight: 700;\n\tsrc: url(\"/assets/default_thai_company/fonts/Sarabun-Bold.ttf\");\n}\n.print-format {\n\tfont-family: Sarabun, Inter, \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n}\n\n/* Print Style \"Redesign\" gives .letter-head a 30px margin; tighten it. */\n.print-format .letter-head {\n\tmargin-bottom: 5px;\n}\n\n/* One vertical rhythm for every label/value line. \"Redesign\" stacks field rows with\n margin-top: 15px (doubled where Total/Taxes nest a layout .row) while the custom\n Customer / Bank tables and the heading dates had none; use line-height 1.8 everywhere\n and no row margins instead. */\n.print-format .row:not(.section-break) {\n\tmargin-top: 0 !important;\n\tline-height: 1.8;\n}\n.print-format .heading-dates,\n.print-format .customer-details td,\n.print-format .bank-account-details td {\n\tline-height: 1.8;\n}\n\n/* standard.css gives .table 20px top/bottom margins; keep the item table close to its neighbours. */\n.print-format [data-fieldname=\"items\"] > .table {\n\tmargin-top: 5px;\n\tmargin-bottom: 5px;\n}",
"custom_format": 1,
"default_print_language": null,
"disabled": 0,
@@ -12,13 +12,13 @@
"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{#- 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\"posting_date\", \"due_date\",\n\t\"customer_name\", \"address_display\", \"contact_display\", \"contact_mobile\", \"contact_email\", \"tax_id\",\n\t\"in_words\",\n] -%}\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{% if doc.address_display %}\n\t\t\t\t\t
{{ _(doc.meta.get_label(\"address_display\")) }}
{{ doc.address_display }}
\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
\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
\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{#- 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\"posting_date\", \"due_date\",\n\t\"customer_name\", \"address_display\", \"contact_display\", \"contact_mobile\", \"contact_email\", \"tax_id\",\n\t\"in_words\",\n] -%}\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{% if doc.address_display %}\n\t\t\t\t\t
{{ _(doc.meta.get_label(\"address_display\")) }}
{{ doc.address_display }}
\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
\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
\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",
"line_breaks": 0,
"margin_bottom": 15.0,
"margin_left": 15.0,
"margin_right": 15.0,
"margin_top": 15.0,
- "modified": "2026-09-14 16:35:32.130720",
+ "modified": "2026-09-15 06:44:37.653888",
"module": "Default Thai Company",
"name": "Default Standard Sales Invoice",
"page_number": "Hide",
@@ -36,7 +36,7 @@
{
"absolute_value": 0,
"align_labels_right": 0,
- "css": "/* Print Style \"Redesign\" gives .letter-head a 30px margin; tighten it. */\n.print-format .letter-head {\n\tmargin-bottom: 5px;\n}\n\n/* One vertical rhythm for every label/value line. \"Redesign\" stacks field rows with\n margin-top: 15px (doubled where Total/Taxes nest a layout .row) while the custom\n Customer / Bank tables and the heading dates had none; use line-height 1.8 everywhere\n and no row margins instead. */\n.print-format .row:not(.section-break) {\n\tmargin-top: 0 !important;\n\tline-height: 1.8;\n}\n.print-format .heading-dates,\n.print-format .customer-details td,\n.print-format .bank-account-details td {\n\tline-height: 1.8;\n}\n\n/* standard.css gives .table 20px top/bottom margins; keep the item table close to its neighbours. */\n.print-format [data-fieldname=\"items\"] > .table {\n\tmargin-top: 5px;\n\tmargin-bottom: 5px;\n}",
+ "css": "/* Thai text needs a font with Thai glyphs; wkhtmltopdf only sees the server's fonts and a\n headless server rarely has one, so ship Sarabun (OFL, see public/fonts/OFL.txt) with the app.\n Asset URLs are made absolute for the PDF renderer by frappe.utils.scrub_urls. */\n@font-face {\n\tfont-family: \"Sarabun\";\n\tfont-weight: 400;\n\tsrc: url(\"/assets/default_thai_company/fonts/Sarabun-Regular.ttf\");\n}\n@font-face {\n\tfont-family: \"Sarabun\";\n\tfont-weight: 700;\n\tsrc: url(\"/assets/default_thai_company/fonts/Sarabun-Bold.ttf\");\n}\n.print-format {\n\tfont-family: Sarabun, Inter, \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n}\n\n/* Print Style \"Redesign\" gives .letter-head a 30px margin; tighten it. */\n.print-format .letter-head {\n\tmargin-bottom: 5px;\n}\n\n/* One vertical rhythm for every label/value line. \"Redesign\" stacks field rows with\n margin-top: 15px (doubled where Total/Taxes nest a layout .row) while the custom\n Customer / Bank tables and the heading dates had none; use line-height 1.8 everywhere\n and no row margins instead. */\n.print-format .row:not(.section-break) {\n\tmargin-top: 0 !important;\n\tline-height: 1.8;\n}\n.print-format .heading-dates,\n.print-format .customer-details td,\n.print-format .bank-account-details td {\n\tline-height: 1.8;\n}\n\n/* standard.css gives .table 20px top/bottom margins; keep the item table close to its neighbours. */\n.print-format [data-fieldname=\"items\"] > .table {\n\tmargin-top: 5px;\n\tmargin-bottom: 5px;\n}",
"custom_format": 1,
"default_print_language": null,
"disabled": 0,
@@ -46,13 +46,13 @@
"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{#- 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\"posting_date\", \"due_date\",\n\t\"customer_name\", \"address_display\", \"contact_display\", \"contact_mobile\", \"contact_email\", \"tax_id\",\n\t\"in_words\",\n] -%}\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{% if doc.address_display %}\n\t\t\t\t\t
{{ _(doc.meta.get_label(\"address_display\")) }}
{{ doc.address_display }}
\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
\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
\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{#- 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\"posting_date\", \"due_date\",\n\t\"customer_name\", \"address_display\", \"contact_display\", \"contact_mobile\", \"contact_email\", \"tax_id\",\n\t\"in_words\",\n] -%}\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{% if doc.address_display %}\n\t\t\t\t\t
{{ _(doc.meta.get_label(\"address_display\")) }}
{{ doc.address_display }}
\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
\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
\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",
"line_breaks": 0,
"margin_bottom": 15.0,
"margin_left": 15.0,
"margin_right": 15.0,
"margin_top": 15.0,
- "modified": "2026-09-14 17:06:33.830672",
+ "modified": "2026-09-15 06:44:37.653888",
"module": "Default Thai Company",
"name": "Default Standard Tax Invoice/Receipt",
"page_number": "Hide",
diff --git a/default_thai_company/hooks.py b/default_thai_company/hooks.py
index 1f64eb9..6be69be 100644
--- a/default_thai_company/hooks.py
+++ b/default_thai_company/hooks.py
@@ -78,6 +78,7 @@ jinja = {
"methods": [
"default_thai_company.utils.get_letter_head_company",
"default_thai_company.utils.get_company_bank_account",
+ "default_thai_company.utils.get_in_words",
],
}
diff --git a/default_thai_company/public/fonts/OFL.txt b/default_thai_company/public/fonts/OFL.txt
new file mode 100644
index 0000000..a7d3a12
--- /dev/null
+++ b/default_thai_company/public/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2018 The Sarabun Project Authors (https://github.com/cadsondemak/Sarabun)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/default_thai_company/public/fonts/Sarabun-Bold.ttf b/default_thai_company/public/fonts/Sarabun-Bold.ttf
new file mode 100644
index 0000000..7e5d768
Binary files /dev/null and b/default_thai_company/public/fonts/Sarabun-Bold.ttf differ
diff --git a/default_thai_company/public/fonts/Sarabun-Regular.ttf b/default_thai_company/public/fonts/Sarabun-Regular.ttf
new file mode 100644
index 0000000..161c9ac
Binary files /dev/null and b/default_thai_company/public/fonts/Sarabun-Regular.ttf differ
diff --git a/default_thai_company/tax_withholding.py b/default_thai_company/tax_withholding.py
index e5c5774..a287e49 100644
--- a/default_thai_company/tax_withholding.py
+++ b/default_thai_company/tax_withholding.py
@@ -10,6 +10,8 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
from frappe import _
from frappe.utils import cint, flt
+from default_thai_company.utils import money_in_words
+
# Liability: tax we withhold from suppliers and remit on P.N.D.3/53 (linked on every category).
# Asset: tax customers withhold from us, creditable against corporate income tax.
LIABILITY_ACCOUNT = "Withholding Tax Payable"
@@ -158,6 +160,17 @@ class ThaiSalesInvoice(SalesInvoice):
self.precision("amount_after_withholding"),
)
+ def set_total_in_words(self):
+ """SellingController's version, with Thai wording when the saving user's
+ language is Thai. Print formats re-derive the text per print language
+ (`get_in_words`)."""
+ base_amount = abs(
+ self.base_grand_total if self.is_rounded_total_disabled() else self.base_rounded_total
+ )
+ self.base_in_words = money_in_words(base_amount, self.company_currency)
+ amount = abs(self.grand_total if self.is_rounded_total_disabled() else self.rounded_total)
+ self.in_words = money_in_words(amount, self.currency)
+
class ThaiPaymentEntry(PaymentEntry):
def build_gl_map(self):
@@ -333,7 +346,8 @@ def get_customer_withholding(doc):
total += withholding_on(details_by_category[category], base_taxable, base_taxable, precision)
if len(details_by_category) == 1:
- description = next(iter(details_by_category.values())).description
+ # category_name; translations/th.csv carries the fixture categories
+ description = _(next(iter(details_by_category.values())).description)
else:
description = _("Withholding tax deducted by customer")
return flt(total, precision), description
diff --git a/default_thai_company/translations/th.csv b/default_thai_company/translations/th.csv
new file mode 100644
index 0000000..a50b695
--- /dev/null
+++ b/default_thai_company/translations/th.csv
@@ -0,0 +1,55 @@
+Net Payable After Withholding Tax,ยอดชำระสุทธิหลังหักภาษี ณ ที่จ่าย
+Withholding Tax,ภาษีหัก ณ ที่จ่าย
+Tax Withholding Category,ประเภทภาษีหัก ณ ที่จ่าย
+Withholding tax deducted by customer,ภาษีหัก ณ ที่จ่ายที่ลูกค้าหักไว้
+Tax Withholding Category {0} has no account for Company {1},ประเภทภาษีหัก ณ ที่จ่าย {0} ไม่มีบัญชีสำหรับบริษัท {1}
+Account {0} not found for Company {1},ไม่พบบัญชี {0} สำหรับบริษัท {1}
+Please set Tax Withholding Category on {0} {1} or on this Payment Entry,กรุณาระบุประเภทภาษีหัก ณ ที่จ่ายใน {0} {1} หรือในรายการชำระเงินนี้
+Tax ID,เลขประจำตัวผู้เสียภาษี
+Tax Id,เลขประจำตัวผู้เสียภาษี
+Signatures,ลายเซ็น
+On behalf of,ในนาม
+Receiver,ผู้รับ
+Issuer,ผู้ออก
+Tax Invoice,ใบกำกับภาษี
+Tax Invoice/Receipt,ใบกำกับภาษี/ใบเสร็จรับเงิน
+"Commission, brokerage, agency fees - Sec. 40(2), juristic payee (P.N.D.53)",ค่านายหน้า ค่าตัวแทน - มาตรา 40(2) ผู้รับเป็นนิติบุคคล (ภ.ง.ด.53)
+"Royalties, goodwill, copyright, other rights - Sec. 40(3), juristic payee (P.N.D.53)",ค่าแห่งกู๊ดวิลล์ ค่าลิขสิทธิ์ หรือสิทธิอย่างอื่น - มาตรา 40(3) ผู้รับเป็นนิติบุคคล (ภ.ง.ด.53)
+"Interest - Sec. 40(4)(a), juristic payee excl. banks/finance companies (P.N.D.53)",ดอกเบี้ย - มาตรา 40(4)(ก) ผู้รับเป็นนิติบุคคล ยกเว้นธนาคาร/บริษัทเงินทุน (ภ.ง.ด.53)
+"Interest - Sec. 40(4)(a), individual payee, Sec. 50(2) (P.N.D.2)",ดอกเบี้ย - มาตรา 40(4)(ก) ผู้รับเป็นบุคคลธรรมดา มาตรา 50(2) (ภ.ง.ด.2)
+"Dividends, share of profits - Sec. 40(4)(b) (P.N.D.2 / P.N.D.53)",เงินปันผล เงินส่วนแบ่งของกำไร - มาตรา 40(4)(ข) (ภ.ง.ด.2 / ภ.ง.ด.53)
+"Rent of property, hire-purchase of movable property - Sec. 40(5) (P.N.D.3 / P.N.D.53)",ค่าเช่าทรัพย์สิน ค่าเช่าซื้อสังหาริมทรัพย์ - มาตรา 40(5) (ภ.ง.ด.3 / ภ.ง.ด.53)
+Rent of ships under the Merchant Marine Promotion Act - Sec. 40(5) (P.N.D.53),ค่าเช่าเรือตามกฎหมายว่าด้วยการส่งเสริมการพาณิชยนาวี - มาตรา 40(5) (ภ.ง.ด.53)
+"Law, medicine, engineering, architecture, accounting, fine arts - Sec. 40(6) (P.N.D.3 / P.N.D.53)",วิชาชีพอิสระ กฎหมาย การประกอบโรคศิลปะ วิศวกรรม สถาปัตยกรรม การบัญชี ประณีตศิลปกรรม - มาตรา 40(6) (ภ.ง.ด.3 / ภ.ง.ด.53)
+"Contract work, contractor supplies essential materials - Sec. 40(7) (P.N.D.3 / P.N.D.53)",ค่ารับเหมาที่ผู้รับเหมาต้องลงทุนจัดหาสัมภาระในส่วนสำคัญ - มาตรา 40(7) (ภ.ง.ด.3 / ภ.ง.ด.53)
+"Hire of work, service fees - Sec. 40(8) (P.N.D.3 / P.N.D.53)",ค่าจ้างทำของ ค่าบริการ - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
+"Rewards, discounts, sales promotion benefits - Sec. 40(8) (P.N.D.3 / P.N.D.53)",รางวัล ส่วนลด หรือประโยชน์ใด ๆ เนื่องจากการส่งเสริมการขาย - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
+Advertising fees - Sec. 40(8) (P.N.D.3 / P.N.D.53),ค่าโฆษณา - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
+"Transport of goods or passengers, excl. public transport - Sec. 40(8) (P.N.D.3 / P.N.D.53)",ค่าขนส่งสินค้าหรือผู้โดยสาร ยกเว้นการขนส่งสาธารณะ - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
+Non-life insurance premiums - Sec. 40(8) (P.N.D.53),ค่าเบี้ยประกันวินาศภัย - มาตรา 40(8) (ภ.ง.ด.53)
+"Prizes from contests, competitions, lucky draws - Sec. 40(8) (P.N.D.3 / P.N.D.53)",รางวัลจากการประกวด การแข่งขัน การชิงโชค - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
+Public entertainers resident in Thailand - Sec. 40(8) (P.N.D.3),นักแสดงสาธารณะที่มีภูมิลำเนาในประเทศไทย - มาตรา 40(8) (ภ.ง.ด.3)
+"Rubber, cassava, jute, rice, maize, sugarcane, coffee beans, oil palm - Sec. 40(8) (P.N.D.3 / P.N.D.53)",ยางแผ่น มันสำปะหลัง ปอ ข้าว ข้าวโพด อ้อย เมล็ดกาแฟ ปาล์มน้ำมัน - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
+Aquatic animals bought from non-registered sellers - Sec. 40(8) (P.N.D.3 / P.N.D.53),สัตว์น้ำที่ซื้อจากผู้ขายที่ไม่ได้จดทะเบียนภาษีมูลค่าเพิ่ม - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
+"Sale of immovable property, juristic seller - Sec. 69 Ter (remitted at the Land Office)",การขายอสังหาริมทรัพย์ ผู้ขายเป็นนิติบุคคล - มาตรา 69 ตรี (นำส่ง ณ สำนักงานที่ดิน)
+Sec. 40(2)-(6) income paid to non-resident individuals - Sec. 50(2) (P.N.D.3),เงินได้ตามมาตรา 40(2)-(6) จ่ายให้บุคคลธรรมดาที่มิได้เป็นผู้อยู่ในประเทศไทย - มาตรา 50(2) (ภ.ง.ด.3)
+"Sec. 40(2)-(6) income excl. dividends to foreign companies not doing business in Thailand - Sec. 70, pre-DTA (P.N.D.54)",เงินได้ตามมาตรา 40(2)-(6) ยกเว้นเงินปันผล จ่ายให้บริษัทต่างประเทศที่มิได้ประกอบกิจการในประเทศไทย - มาตรา 70 ก่อนใช้อนุสัญญาภาษีซ้อน (ภ.ง.ด.54)
+"Dividends to foreign companies not doing business in Thailand - Sec. 70, pre-DTA (P.N.D.54)",เงินปันผลจ่ายให้บริษัทต่างประเทศที่มิได้ประกอบกิจการในประเทศไทย - มาตรา 70 ก่อนใช้อนุสัญญาภาษีซ้อน (ภ.ง.ด.54)
+Sales Invoice,ใบแจ้งหนี้
+Sales Order,ใบสั่งขาย
+Posting Date,วันที่
+Payment Due Date,วันครบกำหนดชำระ
+Contact Person,ผู้ติดต่อ
+Mobile No,โทรศัพท์มือถือ
+Contact Email,อีเมล
+Description,รายละเอียด
+Rate,ราคาต่อหน่วย
+Amount,จำนวนเงิน
+Total,รวม
+Total (Without Tax),รวมก่อนภาษี
+Net Total,ยอดรวมสุทธิ
+Total Taxes and Charges,รวมภาษีและค่าธรรมเนียม
+Grand Total,ยอดรวมทั้งสิ้น
+Rounded Total,ยอดรวมปัดเศษ
+Terms and Conditions Details,รายละเอียดข้อตกลงและเงื่อนไข
+Page {0} of {1},หน้า {0} จาก {1}
diff --git a/default_thai_company/utils.py b/default_thai_company/utils.py
index 4b9952b..9179c6d 100644
--- a/default_thai_company/utils.py
+++ b/default_thai_company/utils.py
@@ -1,6 +1,30 @@
import frappe
+import frappe.utils
from erpnext import get_default_company
from frappe.contacts.doctype.address.address import get_default_address, render_address
+from num2words import num2words
+
+
+def money_in_words(amount, currency):
+ """Amount in words for the active language.
+
+ frappe.utils.money_in_words renders " only." in every
+ language; Thai documents write "บาทถ้วน" or "บาทสตางค์".
+ Currencies num2words cannot spell in Thai keep frappe's wording.
+ """
+ if frappe.local.lang == "th":
+ try:
+ return num2words(amount, lang="th", to="currency", currency=currency)
+ except NotImplementedError:
+ pass
+ return frappe.utils.money_in_words(amount, currency)
+
+
+def get_in_words(doc):
+ """`in_words` for print formats: re-derived in the print language instead
+ of the language of whoever saved the document."""
+ amount = abs(doc.grand_total if doc.is_rounded_total_disabled() else doc.rounded_total)
+ return money_in_words(amount, doc.currency)
def get_letter_head_company(doc=None):