Author SHA1 Message Date
kurogeek 813a5402f4 fix: label tax id as "Tax ID" in the letter head 2026-09-10 02:52:25 +00:00
kurogeek 33a0fc2603 fix: company logo on the left of the letter head 2026-09-10 02:48:28 +00:00
kurogeek 84e23a6b71 feat: place company logo on the right of the letter head 2026-09-10 02:45:57 +00:00
kurogeek a6620d3743 fix: right-align letter head with the document
Text block flex-fills remaining width and right-aligns its text, so it sits at
the document's right edge with or without a company logo. Drop the fixed 500px
width; keep -webkit-box fallbacks for wkhtmltopdf.
2026-09-10 02:40:46 +00:00
kurogeek 9091caffab feat: company-driven letter head and Thai address template
- Letter Head fixture renders company details (logo, name, tax id, address,
  phone, email) from the printed document's company via a jinja helper
  instead of hardcoded values; falls back to the default company for
  doctypes without a company field.
- Register fixtures in hooks (Letter Head, Address Template); they were
  never imported before.
- Preserve the fixture's `source = HTML` on import: LetterHead.before_insert
  forces "Image" and fixture import is delete + insert.
- Thailand Address Template: two-line address, phone/fax/email lines kept.
2026-09-09 10:30:01 +00:00
kurogeek 3d520cbd92 mob next [ci-skip] [ci skip] [skip ci]
lastFile:default_thai_company/fixtures/letter_head.json
2026-09-09 16:29:25 +07:00
17 changed files with 15 additions and 1798 deletions
-8
View File
@@ -12,14 +12,6 @@ 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:
@@ -1,32 +0,0 @@
{
"custom_fields": [],
"custom_perms": [],
"doctype": "Payment Entry",
"links": [],
"property_setters": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"creation": "2026-09-11 17:18:28.670630",
"default_value": null,
"doc_type": "Payment Entry",
"docstatus": 0,
"doctype_or_field": "DocField",
"field_name": "apply_tax_withholding_amount",
"idx": 0,
"is_system_generated": 0,
"modified": "2026-09-11 12:00:00",
"modified_by": "Administrator",
"module": null,
"name": "Payment Entry-apply_tax_withholding_amount-depends_on",
"owner": "Administrator",
"property": "depends_on",
"property_type": "Data",
"row_name": null,
"value": "eval:doc.party_type == 'Supplier' || (doc.party_type == 'Customer' && doc.payment_type == 'Receive')"
}
],
"sync_on_migrate": 1
}
@@ -1,225 +0,0 @@
{
"custom_fields": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2026-09-11 17:18:48.182536",
"default": null,
"depends_on": "eval:doc.tax_withholding_category",
"description": null,
"docstatus": 0,
"dt": "Sales Invoice",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "amount_after_withholding",
"fieldtype": "Currency",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 80,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "withholding_tax_amount",
"is_system_generated": 0,
"is_virtual": 0,
"label": "Net Payable After Withholding Tax",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2026-09-11 17:18:48.182536",
"modified_by": "Administrator",
"module": null,
"name": "Sales Invoice-amount_after_withholding",
"no_copy": 1,
"non_negative": 0,
"options": "currency",
"owner": "Administrator",
"permlevel": 0,
"placeholder": null,
"precision": null,
"print_hide": 0,
"print_hide_if_no_value": 1,
"print_width": null,
"read_only": 1,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2026-09-11 17:18:44.710456",
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"dt": "Sales Invoice",
"fetch_from": "customer.tax_withholding_category",
"fetch_if_empty": 1,
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 57,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "taxes_and_charges",
"is_system_generated": 0,
"is_virtual": 0,
"label": "Tax Withholding Category",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2026-09-11 18:00:00",
"modified_by": "Administrator",
"module": null,
"name": "Sales Invoice-tax_withholding_category",
"no_copy": 0,
"non_negative": 0,
"options": "Tax Withholding Category",
"owner": "Administrator",
"permlevel": 0,
"placeholder": null,
"precision": null,
"print_hide": 1,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2026-09-11 17:18:47.244523",
"default": null,
"depends_on": "eval:doc.tax_withholding_category",
"description": null,
"docstatus": 0,
"dt": "Sales Invoice",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "withholding_tax_amount",
"fieldtype": "Currency",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 79,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "rounded_total",
"is_system_generated": 0,
"is_virtual": 0,
"label": "Withholding Tax",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2026-09-11 18:00:00",
"modified_by": "Administrator",
"module": null,
"name": "Sales Invoice-withholding_tax_amount",
"no_copy": 1,
"non_negative": 0,
"options": "currency",
"owner": "Administrator",
"permlevel": 0,
"placeholder": null,
"precision": null,
"print_hide": 0,
"print_hide_if_no_value": 1,
"print_width": null,
"read_only": 1,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
}
],
"custom_perms": [],
"doctype": "Sales Invoice",
"links": [],
"property_setters": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"creation": "2026-09-11 17:18:28.581896",
"default_value": null,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype_or_field": "DocType",
"field_name": null,
"idx": 0,
"is_system_generated": 1,
"modified": "2026-09-10 13:55:13.996942",
"modified_by": "Administrator",
"module": null,
"name": "Sales Invoice-main-default_print_format",
"owner": "Administrator",
"property": "default_print_format",
"property_type": "Data",
"row_name": null,
"value": "Default Standard Sales Invoice"
}
],
"sync_on_migrate": 1
}
@@ -6,6 +6,6 @@
"is_default": 1,
"modified": "2026-09-09 17:23:33.559202",
"name": "Thailand",
"template": "{{ address_line1 }}{% if address_line2 %} {{ address_line2 }}{% endif %}<br>\n{% if county %}{{ county }} {% endif -%}{{ city }}{% if state %} {{ state }}{% endif -%}{% if pincode %} {{ pincode }}{% endif -%}{% if country != \"Thailand\" %} {{ country }}{% endif %}<br>\n{% if phone %}{{ _(\"Phone\") }}: {{ phone }}<br>{% endif -%}\n{% if fax %}{{ _(\"Fax\") }}: {{ fax }}<br>{% endif -%}\n{% if email_id %}{{ _(\"Email\") }}: {{ email_id }}<br>{% endif -%}\n"
"template": "{{ address_line1 }}{% if address_line2 %} {{ address_line2 }}{% endif %}<br>\n{% if county %}{{ county }} {% endif -%}{{ city }}{% if state %} {{ state }}{% endif -%}{% if pincode %} {{ pincode }}{% endif -%}{% if country != \"Thailand\" %} {{ country }}{% endif %}<br>\n{% if phone or fax or email_id %}<br>{% endif -%}\n{% if phone %}{{ _(\"Phone\") }}: {{ phone }}<br>{% endif -%}\n{% if fax %}{{ _(\"Fax\") }}: {{ fax }}<br>{% endif -%}\n{% if email_id %}{{ _(\"Email\") }}: {{ email_id }}<br>{% endif -%}\n"
}
]
]
@@ -1,7 +1,7 @@
[
{
"align": "Left",
"content": "<style>\n .bpt-title {\n display: -webkit-box;\n display: flex;\n -webkit-box-align: center;\n align-items: center;\n width: 100%;\n }\n .bpt-title img {\n margin-right: 1em;\n }\n .bpt-title .bpt-company {\n -webkit-box-flex: 1;\n flex: 1;\n text-align: right;\n }\n</style>\n{% set company = get_letter_head_company(doc) %}\n{% if company %}\n<div class=\"bpt-title\">\n {% if company.company_logo %}\n <img alt=\"{{ company.company_name }}\" height=\"125\" src=\"{{ company.company_logo }}\">\n {% endif %}\n <div class=\"bpt-company\">\n <h1>{{ company.company_name }}</h1>\n <small>\n {% if company.tax_id %}<div>{{ _(\"Tax ID\") }}: {{ company.tax_id }}</div>{% endif %}\n {% if company.address %}<div>{{ company.address }}</div>{% endif %}\n {% if company.phone_no %}<div>{{ _(\"Phone\") }}: {{ company.phone_no }}</div>{% endif %}\n {% if company.email %}<div>{{ _(\"Email\") }}: {{ company.email }}</div>{% endif %}\n </small>\n </div>\n</div>\n{% endif %}\n",
"content": "<style>\n .bpt-title {\n display: -webkit-box;\n display: flex;\n -webkit-box-align: center;\n align-items: center;\n width: 100%;\n }\n .bpt-title img {\n margin-right: 1em;\n }\n .bpt-title .bpt-company {\n -webkit-box-flex: 1;\n flex: 1;\n text-align: right;\n }\n</style>\n{% set company = get_letter_head_company(doc) %}\n{% if company %}\n<div class=\"bpt-title\">\n {% if company.company_logo %}\n <img alt=\"{{ company.company_name }}\" height=\"125\" src=\"{{ company.company_logo }}\">\n {% endif %}\n <div class=\"bpt-company\">\n <h1>{{ company.company_name }}</h1>\n <small>\n {% if company.tax_id %}<div>Tax ID: {{ company.tax_id }}</div>{% endif %}\n {% if company.address %}<div>{{ company.address }}</div>{% endif %}\n {% if company.phone_no %}<div>Phone: {{ company.phone_no }}</div>{% endif %}\n {% if company.email %}<div>Email: {{ company.email }}</div>{% endif %}\n </small>\n </div>\n</div>\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-15 06:44:37.653888",
"modified": "2026-09-10 09:51:23.933752",
"name": "Default Letter Head",
"source": "HTML"
}
File diff suppressed because one or more lines are too long
@@ -1,574 +0,0 @@
[
{
"accounts": [],
"category_name": "Commission, brokerage, agency fees - Sec. 40(2), juristic payee (P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 3% - Commission",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 3% - Commission",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 3.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Royalties, goodwill, copyright, other rights - Sec. 40(3), juristic payee (P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 3% - Royalty",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 3% - Royalty",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 3.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Interest - Sec. 40(4)(a), juristic payee excl. banks/finance companies (P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 1% - Interest (Company)",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 1% - Interest (Company)",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 1.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Interest - Sec. 40(4)(a), individual payee, Sec. 50(2) (P.N.D.2)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 15% - Interest (Individual)",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 15% - Interest (Individual)",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1.0,
"tax_withholding_rate": 15.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Dividends, share of profits - Sec. 40(4)(b) (P.N.D.2 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 10% - Dividend",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 10% - Dividend",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1.0,
"tax_withholding_rate": 10.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Rent of property, hire-purchase of movable property - Sec. 40(5) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 5% - Rent",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 5% - Rent",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 5.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Rent of ships under the Merchant Marine Promotion Act - Sec. 40(5) (P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 1% - Ship Rental",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 1% - Ship Rental",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 1.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Law, medicine, engineering, architecture, accounting, fine arts - Sec. 40(6) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 3% - Professional Fee",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 3% - Professional Fee",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 3.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Contract work, contractor supplies essential materials - Sec. 40(7) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 3% - Contract Work",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 3% - Contract Work",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 3.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Hire of work, service fees - Sec. 40(8) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 3% - Service",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 3% - Service",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 3.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Rewards, discounts, sales promotion benefits - Sec. 40(8) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 3% - Sales Promotion",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 3% - Sales Promotion",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 3.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Advertising fees - Sec. 40(8) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 2% - Advertising",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 2% - Advertising",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 2.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Transport of goods or passengers, excl. public transport - Sec. 40(8) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 1% - Transportation",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 1% - Transportation",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 1.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Non-life insurance premiums - Sec. 40(8) (P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 1% - Non-Life Insurance",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 1% - Non-Life Insurance",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 1.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Prizes from contests, competitions, lucky draws - Sec. 40(8) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 5% - Prize",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 5% - Prize",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 5.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Public entertainers resident in Thailand - Sec. 40(8) (P.N.D.3)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 5% - Public Entertainer",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 5% - Public Entertainer",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 5.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Rubber, cassava, jute, rice, maize, sugarcane, coffee beans, oil palm - Sec. 40(8) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 0.75% - Agricultural Produce",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 0.75% - Agricultural Produce",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 0.75,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Aquatic animals bought from non-registered sellers - Sec. 40(8) (P.N.D.3 / P.N.D.53)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 1% - Aquatic Animals",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 1% - Aquatic Animals",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1000.0,
"tax_withholding_rate": 1.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Sale of immovable property, juristic seller - Sec. 69 Ter (remitted at the Land Office)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 1% - Immovable Property (Company)",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 1% - Immovable Property (Company)",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1.0,
"tax_withholding_rate": 1.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Sec. 40(2)-(6) income paid to non-resident individuals - Sec. 50(2) (P.N.D.3)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 15% - Non-Resident Individual",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 15% - Non-Resident Individual",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1.0,
"tax_withholding_rate": 15.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Sec. 40(2)-(6) income excl. dividends to foreign companies not doing business in Thailand - Sec. 70, pre-DTA (P.N.D.54)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 15% - Foreign Company",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 15% - Foreign Company",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1.0,
"tax_withholding_rate": 15.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
},
{
"accounts": [],
"category_name": "Dividends to foreign companies not doing business in Thailand - Sec. 70, pre-DTA (P.N.D.54)",
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"name": "WHT 10% - Foreign Company Dividend",
"rates": [
{
"cumulative_threshold": 0.0,
"docstatus": 0,
"doctype": "Tax Withholding Rate",
"from_date": "2024-01-01",
"idx": 1,
"parent": "WHT 10% - Foreign Company Dividend",
"parentfield": "rates",
"parenttype": "Tax Withholding Category",
"single_threshold": 1.0,
"tax_withholding_rate": 10.0,
"to_date": "2099-12-31"
}
],
"round_off_tax_amount": 0,
"tax_on_excess_amount": 0
}
]
+11 -34
View File
@@ -43,7 +43,7 @@ required_apps = ["erpnext"]
# page_js = {"page" : "public/js/file.js"}
# include js in doctype views
doctype_js = {"Payment Entry": "public/js/payment_entry.js"}
# doctype_js = {"doctype" : "public/js/doctype.js"}
# doctype_list_js = {"doctype" : "public/js/doctype_list.js"}
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"}
@@ -75,28 +75,15 @@ doctype_js = {"Payment Entry": "public/js/payment_entry.js"}
# add methods and filters to jinja environment
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",
],
"methods": ["default_thai_company.utils.get_letter_head_company"],
}
# Fixtures
# --------
# Custom Fields and Property Setters live in default_thai_company/custom/*.json
# (synced in place on install and migrate); fixtures are for whole documents.
fixtures = [
{"doctype": "Letter Head", "filters": [["name", "in", ["Default Letter Head"]]]},
{"doctype": "Address Template", "filters": [["name", "in", ["Thailand"]]]},
{
"doctype": "Print Format",
"filters": [
["name", "in", ["Default Standard Sales Invoice", "Default Standard Tax Invoice/Receipt"]]
],
},
{"doctype": "Tax Withholding Category", "filters": [["name", "like", "WHT %"]]},
]
# Installation
@@ -149,10 +136,9 @@ fixtures = [
# ---------------
# Override standard doctype classes
override_doctype_class = {
"Sales Invoice": "default_thai_company.tax_withholding.ThaiSalesInvoice",
"Payment Entry": "default_thai_company.tax_withholding.ThaiPaymentEntry",
}
# override_doctype_class = {
# "ToDo": "custom_app.overrides.CustomToDo"
# }
# Document Events
# ---------------
@@ -162,16 +148,7 @@ doc_events = {
"Letter Head": {
"before_import": "default_thai_company.utils.stash_letter_head_source",
"before_insert": "default_thai_company.utils.restore_letter_head_source",
},
"Tax Withholding Category": {
"before_import": "default_thai_company.tax_withholding.prepare_fixture_accounts",
},
"Company": {
"on_update": "default_thai_company.tax_withholding.setup_company",
},
"Payment Entry": {
"validate": "default_thai_company.tax_withholding.set_customer_withholding",
},
}
}
# Scheduled Tasks
@@ -202,11 +179,11 @@ doc_events = {
# Overriding Methods
# ------------------------------
override_whitelisted_methods = {
"erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry": "default_thai_company.tax_withholding.get_payment_entry",
}
#
# override_whitelisted_methods = {
# "frappe.desk.doctype.event.event.get_events": "default_thai_company.event.get_events"
# }
#
# each overriding function accepts a `data` argument;
# generated from the base implementation of the doctype dashboard,
# along with any modifications made in other Frappe apps
-93
View File
@@ -1,93 +0,0 @@
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.
Binary file not shown.
@@ -1,29 +0,0 @@
frappe.ui.form.on("Payment Entry", {
apply_tax_withholding_amount(frm) {
if (frm.doc.party_type !== "Customer" || frm.doc.payment_type !== "Receive") return;
if (!frm.doc.apply_tax_withholding_amount) {
frm.events.recompute_customer_withholding(frm);
return;
}
// ERPNext's handler looks the category up on Supplier and clears it for a
// Customer; wait for that request to settle, then set the Customer's value.
frappe.db.get_value("Customer", frm.doc.party, "tax_withholding_category").then(({ message }) => {
frappe.after_ajax(() => {
frm.set_value("tax_withholding_category", message.tax_withholding_category);
frm.events.recompute_customer_withholding(frm);
});
});
},
tax_withholding_category(frm) {
if (frm.doc.party_type === "Customer" && frm.doc.payment_type === "Receive") {
frm.events.recompute_customer_withholding(frm);
}
},
recompute_customer_withholding(frm) {
// allocate_amount_to_references is overridden server-side to refresh the withholding row
if (!frm.doc.references || !frm.doc.references.length) return;
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, false);
},
});
-353
View File
@@ -1,353 +0,0 @@
import erpnext
import frappe
from erpnext.accounts.doctype.payment_entry.payment_entry import PaymentEntry
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry as _get_payment_entry
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_tax_withholding_details,
normal_round,
)
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"
ASSET_ACCOUNT = "Withholding Tax Receivable"
# Preferred parent groups by root type; first match wins, else the root account.
PARENT_GROUPS = {
"Liability": ("Duties and Taxes", "Current Liabilities"),
"Asset": ("Tax Assets", "Current Assets"),
}
CATEGORY_FILTERS = {"name": ("like", "WHT %")}
def thai_companies():
return frappe.get_all("Company", filters={"country": "Thailand"}, pluck="name")
def get_or_create_account(company, account_name, root_type):
name = frappe.db.get_value(
"Account", {"company": company, "account_name": account_name, "is_group": 0, "root_type": root_type}
)
if name:
return name
account = frappe.get_doc(
{
"doctype": "Account",
"company": company,
"account_name": account_name,
"parent_account": find_parent_group(company, root_type),
"root_type": root_type,
"account_type": "Tax",
"is_group": 0,
}
).insert(ignore_permissions=True)
return account.name
def find_parent_group(company, root_type):
for account_name in PARENT_GROUPS[root_type]:
parent = frappe.db.get_value(
"Account",
{"company": company, "account_name": account_name, "is_group": 1, "root_type": root_type},
)
if parent:
return parent
return frappe.db.get_value(
"Account",
{"company": company, "root_type": root_type, "is_group": 1, "parent_account": ("is", "not set")},
)
def ensure_company_accounts(company):
"""Create both withholding accounts for `company`; return the liability account name."""
get_or_create_account(company, ASSET_ACCOUNT, "Asset")
return get_or_create_account(company, LIABILITY_ACCOUNT, "Liability")
def link_company(category, company, account):
"""Append `account` for `company` to the category's accounts table if missing."""
if any(row.company == company for row in category.accounts):
return False
category.append("accounts", {"company": company, "account": account})
return True
def prepare_fixture_accounts(doc, method=None):
"""Tax Withholding Category fixtures ship without `accounts` (company-specific).
Fixture import re-inserts the doc on every migrate, so: carry over the rows
already configured on this site, then link every Thai company, creating the
withholding accounts on first use. Mandatory is relaxed for sites without a
Thai company yet (setup wizard not run); `setup_company` links them later.
"""
doc.flags.ignore_mandatory = True
if not doc.accounts and frappe.db.exists(doc.doctype, doc.name):
for row in frappe.get_all(
"Tax Withholding Account",
filters={"parent": doc.name, "parenttype": doc.doctype},
fields=["company", "account"],
order_by="idx",
):
doc.append("accounts", row)
for company in thai_companies():
if not any(row.company == company for row in doc.accounts):
link_company(doc, company, ensure_company_accounts(company))
def setup_company(doc, method=None):
"""Company.on_update: create withholding accounts and link every WHT category."""
if doc.country != "Thailand" or not frappe.db.exists("Account", {"company": doc.name}):
return
account = ensure_company_accounts(doc.name)
for name in frappe.get_all("Tax Withholding Category", filters=CATEGORY_FILTERS, pluck="name"):
category = frappe.get_doc("Tax Withholding Category", name)
if link_company(category, doc.name, account):
category.save(ignore_permissions=True)
def is_thai_company(company):
return frappe.get_cached_value("Company", company, "country") == "Thailand"
def get_withholding_details(category, posting_date, company):
details = get_tax_withholding_details(category, posting_date, company)
if not details:
frappe.throw(
_("Tax Withholding Category {0} has no account for Company {1}").format(category, company)
)
return details
def withholding_on(details, taxable, base_taxable, precision):
"""Withholding for a taxable amount; the single threshold is checked in company currency."""
if details.threshold and flt(base_taxable) < flt(details.threshold):
return 0.0
amount = flt(taxable) * flt(details.rate) / 100
return normal_round(amount) if cint(details.round_off_tax_amount) else flt(amount, precision)
class ThaiSalesInvoice(SalesInvoice):
def set_tax_withholding(self):
"""Thai customers withhold at payment (see `set_customer_withholding`);
ERPNext's customer-side handling is Indian TCS, which adds tax on top of
the invoice. For Thai companies only show the expected withholding and
the net payable; totals and GL are untouched."""
if not is_thai_company(self.company):
return super().set_tax_withholding()
self.withholding_tax_amount = self.amount_after_withholding = 0
if not self.tax_withholding_category:
return
details = get_withholding_details(self.tax_withholding_category, self.posting_date, self.company)
self.withholding_tax_amount = withholding_on(
details, self.net_total, self.base_net_total, self.precision("withholding_tax_amount")
)
if self.withholding_tax_amount:
self.amount_after_withholding = flt(
(self.rounded_total or self.grand_total) - self.withholding_tax_amount,
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):
"""A "Deduct" tax row on a receipt posts bank Dr gross and bank Cr
withholding; ERPNext merges them into one entry but only nets it when
the bank entry itself carries `post_net_value`. Set it so the bank
ledger shows the amount that actually arrived."""
gl_entries = super().build_gl_map()
if self.payment_type == "Receive" and self.party_type == "Customer" and self.get("taxes"):
for entry in gl_entries:
if entry.account == self.paid_to:
entry.post_net_value = True
return gl_entries
@frappe.whitelist()
def allocate_amount_to_references(self, *args, **kwargs):
"""Called by the form after Get Outstanding Invoices and on Paid Amount
changes; keep the withholding row in step with the allocation."""
super().allocate_amount_to_references(*args, **kwargs)
if self.party_type == "Customer" and self.payment_type == "Receive" and self.source_exchange_rate:
auto_apply_customer_withholding(self)
set_customer_withholding(self)
@frappe.whitelist()
def get_payment_entry(
dt,
dn,
party_amount=None,
bank_account=None,
bank_amount=None,
party_type=None,
payment_type=None,
reference_date=None,
created_from_payment_request=False,
):
"""Create > Payment from a Sales Invoice: arrive with withholding applied.
Signature mirrors ERPNext's so `frappe.call` drops request-only args
(`cmd`, ...) instead of forwarding them.
"""
pe = _get_payment_entry(
dt,
dn,
party_amount=party_amount,
bank_account=bank_account,
bank_amount=bank_amount,
party_type=party_type,
payment_type=payment_type,
reference_date=reference_date,
created_from_payment_request=created_from_payment_request,
)
if auto_apply_customer_withholding(pe) and pe.source_exchange_rate:
set_customer_withholding(pe)
return pe
def auto_apply_customer_withholding(doc):
"""Tick Apply Tax Withholding Amount on a new customer receipt whose
references include a Sales Invoice that carries withholding. Only for
unsaved entries: after the first save the checkbox is the user's."""
if (
doc.party_type != "Customer"
or doc.payment_type != "Receive"
or doc.apply_tax_withholding_amount
or not doc.is_new()
):
return False
invoices = [r.reference_name for r in doc.references if r.reference_doctype == "Sales Invoice"]
if not invoices or not frappe.db.exists(
"Sales Invoice", {"name": ("in", invoices), "withholding_tax_amount": (">", 0)}
):
return False
doc.apply_tax_withholding_amount = 1
return True
def set_customer_withholding(doc, method=None):
"""Payment Entry.validate: tax withheld by the customer on a Receive entry
is posted like ERPNext's supplier TDS, as a "Deduct" row in the taxes
table against the withholding receivable account.
`paid_amount` is the gross amount settled against the invoices (what
"Get Outstanding Invoices" fills in); the bank receives paid minus the
withholding (`received_amount_after_tax`). Withholding is rate x pre-VAT
amount of each allocated reference, prorated by the allocation. Runs after
the controller's validate, then re-runs the tax computation so the row's
base amounts and totals are final within this save.
"""
if doc.party_type != "Customer" or doc.payment_type != "Receive":
return
if method == "validate":
auto_apply_customer_withholding(doc)
account = frappe.db.get_value(
"Account",
{"company": doc.company, "account_name": ASSET_ACCOUNT, "root_type": "Asset", "is_group": 0},
)
row = next((d for d in doc.taxes if d.account_head == account), None) if account else None
if not doc.apply_tax_withholding_amount:
if row:
doc.remove(row)
doc.apply_taxes()
doc.set_amounts_after_tax()
return
if not account:
frappe.throw(_("Account {0} not found for Company {1}").format(ASSET_ACCOUNT, doc.company))
if not doc.tax_withholding_category:
doc.tax_withholding_category = frappe.db.get_value("Customer", doc.party, "tax_withholding_category")
amount, description = get_customer_withholding(doc)
if not amount:
if row:
doc.remove(row)
else:
if not row:
row = doc.append(
"taxes", {"account_head": account, "charge_type": "Actual", "add_deduct_tax": "Deduct"}
)
row.tax_amount = amount
row.description = description
row.cost_center = doc.cost_center or erpnext.get_default_cost_center(doc.company)
doc.apply_taxes()
doc.set_amounts_after_tax()
def get_customer_withholding(doc):
"""Total withheld across allocated references, in company currency.
A Sales Invoice's own category wins over the Payment Entry's; Sales Orders
use the Payment Entry's. Withholding is prorated by allocated / grand total
on the pre-VAT amount, per reference.
"""
total = 0.0
details_by_category = {}
precision = doc.precision("difference_amount")
for ref in doc.references:
if ref.reference_doctype not in ("Sales Invoice", "Sales Order") or not ref.allocated_amount:
continue
fields = ["net_total", "grand_total"]
if ref.reference_doctype == "Sales Invoice":
fields.append("tax_withholding_category")
values = frappe.db.get_value(ref.reference_doctype, ref.reference_name, fields, as_dict=True)
if not values.grand_total:
continue
category = values.get("tax_withholding_category") or doc.tax_withholding_category
if not category:
frappe.throw(
_("Please set Tax Withholding Category on {0} {1} or on this Payment Entry").format(
_(ref.reference_doctype), ref.reference_name
)
)
if category not in details_by_category:
details_by_category[category] = get_withholding_details(category, doc.posting_date, doc.company)
base_taxable = (
flt(ref.allocated_amount)
* flt(values.net_total)
/ flt(values.grand_total)
* flt(doc.source_exchange_rate)
)
total += withholding_on(details_by_category[category], base_taxable, base_taxable, precision)
if len(details_by_category) == 1:
# 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
@@ -1,275 +0,0 @@
import json
import frappe
from frappe.modules.import_file import import_file_by_path
from frappe.modules.utils import sync_customizations_for_doctype
from frappe.tests.utils import FrappeTestCase
from frappe.utils import nowdate
from default_thai_company.tax_withholding import (
ASSET_ACCOUNT,
LIABILITY_ACCOUNT,
get_payment_entry,
thai_companies,
)
COMPANY = "_Test WHT Company"
ABBR = "_TWC"
CUSTOMER = "_Test WHT Customer"
ITEM = "_Test WHT Service"
FIXTURE = frappe.get_app_path("default_thai_company", "fixtures", "tax_withholding_category.json")
CUSTOM_DIR = frappe.get_app_path("default_thai_company", "default_thai_company", "custom")
class TestTaxWithholding(FrappeTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Fixture categories must exist before the company is created so the
# Company hook has something to link; force=True re-imports.
import_file_by_path(FIXTURE, force=True, data_import=True)
for fname in ("sales_invoice.json", "payment_entry.json"):
with open(f"{CUSTOM_DIR}/{fname}") as f:
sync_customizations_for_doctype(json.load(f), CUSTOM_DIR, fname)
frappe.get_doc(
{
"doctype": "Company",
"company_name": COMPANY,
"abbr": ABBR,
"country": "Thailand",
"default_currency": "THB",
"chart_of_accounts": "Standard",
}
).insert()
cls.payable = f"{LIABILITY_ACCOUNT} - {ABBR}"
cls.receivable = f"{ASSET_ACCOUNT} - {ABBR}"
cls.vat = frappe.get_doc(
{
"doctype": "Account",
"company": COMPANY,
"account_name": "Output VAT",
"parent_account": f"Duties and Taxes - {ABBR}",
"account_type": "Tax",
}
).insert()
frappe.get_doc(
{
"doctype": "Customer",
"customer_name": CUSTOMER,
"customer_type": "Company",
"customer_group": frappe.db.get_value("Customer Group", {"is_group": 0}),
"territory": frappe.db.get_value("Territory", {"is_group": 0}),
"tax_withholding_category": "WHT 3% - Service",
}
).insert()
frappe.get_doc(
{
"doctype": "Item",
"item_code": ITEM,
"item_group": frappe.db.get_value("Item Group", {"is_group": 0}),
"is_stock_item": 0,
"stock_uom": "Nos",
}
).insert()
def make_invoice(self, rate=10000, category=None, inclusive=False):
si = frappe.get_doc(
{
"doctype": "Sales Invoice",
"company": COMPANY,
"customer": CUSTOMER,
"posting_date": nowdate(),
"due_date": nowdate(),
"tax_withholding_category": category,
"items": [{"item_code": ITEM, "qty": 1, "rate": rate}],
"taxes": [
{
"charge_type": "On Net Total",
"account_head": self.vat.name,
"rate": 7,
"description": "VAT 7%",
"included_in_print_rate": int(inclusive),
}
],
}
)
si.set_missing_values()
return si.submit()
def make_receipt(self, invoice, allocated, apply=1):
"""Paid amount is the gross allocation; withholding reduces what reaches the bank."""
return frappe.get_doc(
{
"doctype": "Payment Entry",
"company": COMPANY,
"payment_type": "Receive",
"party_type": "Customer",
"party": CUSTOMER,
"posting_date": nowdate(),
"paid_from": f"Debtors - {ABBR}",
"paid_to": f"Cash - {ABBR}",
"paid_amount": allocated,
"received_amount": allocated,
"apply_tax_withholding_amount": apply,
"references": [
{
"reference_doctype": "Sales Invoice",
"reference_name": invoice.name,
"allocated_amount": allocated,
}
],
}
).insert()
def withheld(self, pe):
return [(t.account_head, t.add_deduct_tax, t.tax_amount) for t in pe.taxes]
def category_account(self, category, company=COMPANY):
return frappe.db.get_value(
"Tax Withholding Account",
{"parent": category, "parenttype": "Tax Withholding Category", "company": company},
"account",
)
def test_company_creation_adds_accounts_and_links_categories(self):
self.assertIn(COMPANY, thai_companies())
self.assertEqual(
frappe.db.get_value("Account", self.payable, ["root_type", "parent_account"]),
("Liability", f"Duties and Taxes - {ABBR}"),
)
self.assertEqual(
frappe.db.get_value("Account", self.receivable, ["root_type", "parent_account"]),
("Asset", f"Tax Assets - {ABBR}"),
)
categories = frappe.get_all(
"Tax Withholding Category", filters={"name": ("like", "WHT %")}, pluck="name"
)
self.assertEqual(len(categories), 22)
for name in categories:
self.assertEqual(self.category_account(name), self.payable, name)
def test_fixture_reimport_keeps_site_account_and_relinks(self):
alt = frappe.get_doc(
{
"doctype": "Account",
"company": COMPANY,
"account_name": "Alternative WHT Payable",
"parent_account": f"Duties and Taxes - {ABBR}",
"account_type": "Tax",
}
).insert()
rent = frappe.get_doc("Tax Withholding Category", "WHT 5% - Rent")
for row in rent.accounts:
if row.company == COMPANY:
row.account = alt.name
rent.save()
import_file_by_path(FIXTURE, force=True, data_import=True)
self.assertEqual(self.category_account("WHT 5% - Rent"), alt.name)
self.assertEqual(self.category_account("WHT 3% - Service"), self.payable)
rates = frappe.get_doc("Tax Withholding Category", "WHT 5% - Rent").rates
self.assertEqual([(r.tax_withholding_rate, r.single_threshold) for r in rates], [(5.0, 1000.0)])
def test_sales_invoice_shows_withholding_without_grossing_up(self):
si = self.make_invoice()
self.assertEqual((si.net_total, si.grand_total), (10000.0, 10700.0))
self.assertEqual([t.account_head for t in si.taxes], [self.vat.name])
self.assertEqual(si.tax_withholding_category, "WHT 3% - Service") # fetched from Customer
self.assertEqual((si.withholding_tax_amount, si.amount_after_withholding), (300.0, 10400.0))
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 10700.0)
def test_sales_invoice_below_threshold_shows_nothing(self):
si = self.make_invoice(rate=800)
self.assertEqual((si.withholding_tax_amount, si.amount_after_withholding), (0.0, 0.0))
def test_receipt_uses_invoice_category_over_customer_category(self):
si = self.make_invoice(category="WHT 5% - Rent")
self.assertEqual(si.withholding_tax_amount, 500.0)
pe = self.make_receipt(si, allocated=10700)
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 500.0)])
self.assertEqual(pe.received_amount_after_tax, 10200.0)
def test_receipt_deducts_withholding_and_settles_invoice(self):
si = self.make_invoice()
pe = self.make_receipt(si, allocated=10700)
self.assertEqual(pe.tax_withholding_category, "WHT 3% - Service")
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 300.0)])
self.assertEqual((pe.paid_amount, pe.received_amount_after_tax), (10700.0, 10400.0))
self.assertEqual((pe.unallocated_amount, pe.difference_amount), (0, 0))
pe.submit()
gl = {}
for g in frappe.get_all(
"GL Entry", filters={"voucher_no": pe.name}, fields=["account", "debit", "credit"]
):
gl.setdefault(g.account, [0, 0])
gl[g.account][0] += g.debit
gl[g.account][1] += g.credit
self.assertEqual(gl[f"Cash - {ABBR}"], [10400.0, 0.0])
self.assertEqual(gl[self.receivable], [300.0, 0.0])
self.assertEqual(gl[f"Debtors - {ABBR}"], [0.0, 10700.0])
self.assertEqual(len(gl), 3)
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 0)
def test_inclusive_vat_invoice_withholds_on_pre_vat_amount(self):
si = self.make_invoice(inclusive=True) # 10,000 incl. 7% VAT -> net 9,345.79
self.assertEqual((si.net_total, si.grand_total), (9345.79, 10000.0))
self.assertEqual(si.withholding_tax_amount, 280.37)
pe = self.make_receipt(si, allocated=10000)
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 280.37)])
self.assertEqual(pe.received_amount_after_tax, 9719.63)
def test_partial_allocation_prorates_withholding(self):
si = self.make_invoice()
pe = self.make_receipt(si, allocated=5350)
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 150.0)])
self.assertEqual(pe.received_amount_after_tax, 5200.0)
def test_below_single_threshold_has_no_deduction(self):
si = self.make_invoice(rate=800) # net 800 < 1,000 threshold
pe = self.make_receipt(si, allocated=856, apply=0)
self.assertEqual(pe.apply_tax_withholding_amount, 0) # invoice carries no withholding
self.assertEqual(pe.taxes, [])
self.assertEqual(pe.received_amount_after_tax, 856.0)
def test_create_payment_from_invoice_applies_withholding(self):
si = self.make_invoice()
pe = get_payment_entry("Sales Invoice", si.name, bank_account=f"Cash - {ABBR}")
self.assertEqual(pe.apply_tax_withholding_amount, 1)
self.assertEqual(pe.tax_withholding_category, "WHT 3% - Service")
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 300.0)])
self.assertEqual((pe.paid_amount, pe.received_amount_after_tax), (10700.0, 10400.0))
pe.insert()
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 300.0)])
def test_new_receipt_referencing_withheld_invoice_applies_on_save(self):
si = self.make_invoice()
pe = self.make_receipt(si, allocated=10700, apply=0)
self.assertEqual(pe.apply_tax_withholding_amount, 1)
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 300.0)])
def test_unticking_after_save_removes_withholding(self):
si = self.make_invoice()
pe = self.make_receipt(si, allocated=10700)
pe.apply_tax_withholding_amount = 0
pe.save()
self.assertEqual(pe.apply_tax_withholding_amount, 0) # saved docs keep the user's choice
self.assertEqual(pe.taxes, [])
self.assertEqual(pe.received_amount_after_tax, 10700.0)
def test_reallocation_on_form_recomputes_withholding(self):
si = self.make_invoice()
pe = get_payment_entry("Sales Invoice", si.name, bank_account=f"Cash - {ABBR}")
pe.paid_amount = pe.received_amount = 5350
pe.allocate_amount_to_references(
paid_amount=5350, paid_amount_change=True, allocate_payment_amount=True
)
self.assertEqual(pe.references[0].allocated_amount, 5350.0)
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 150.0)])
-55
View File
@@ -1,55 +0,0 @@
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}
1 Net Payable After Withholding Tax ยอดชำระสุทธิหลังหักภาษี ณ ที่จ่าย
2 Withholding Tax ภาษีหัก ณ ที่จ่าย
3 Tax Withholding Category ประเภทภาษีหัก ณ ที่จ่าย
4 Withholding tax deducted by customer ภาษีหัก ณ ที่จ่ายที่ลูกค้าหักไว้
5 Tax Withholding Category {0} has no account for Company {1} ประเภทภาษีหัก ณ ที่จ่าย {0} ไม่มีบัญชีสำหรับบริษัท {1}
6 Account {0} not found for Company {1} ไม่พบบัญชี {0} สำหรับบริษัท {1}
7 Please set Tax Withholding Category on {0} {1} or on this Payment Entry กรุณาระบุประเภทภาษีหัก ณ ที่จ่ายใน {0} {1} หรือในรายการชำระเงินนี้
8 Tax ID เลขประจำตัวผู้เสียภาษี
9 Tax Id เลขประจำตัวผู้เสียภาษี
10 Signatures ลายเซ็น
11 On behalf of ในนาม
12 Receiver ผู้รับ
13 Issuer ผู้ออก
14 Tax Invoice ใบกำกับภาษี
15 Tax Invoice/Receipt ใบกำกับภาษี/ใบเสร็จรับเงิน
16 Commission, brokerage, agency fees - Sec. 40(2), juristic payee (P.N.D.53) ค่านายหน้า ค่าตัวแทน - มาตรา 40(2) ผู้รับเป็นนิติบุคคล (ภ.ง.ด.53)
17 Royalties, goodwill, copyright, other rights - Sec. 40(3), juristic payee (P.N.D.53) ค่าแห่งกู๊ดวิลล์ ค่าลิขสิทธิ์ หรือสิทธิอย่างอื่น - มาตรา 40(3) ผู้รับเป็นนิติบุคคล (ภ.ง.ด.53)
18 Interest - Sec. 40(4)(a), juristic payee excl. banks/finance companies (P.N.D.53) ดอกเบี้ย - มาตรา 40(4)(ก) ผู้รับเป็นนิติบุคคล ยกเว้นธนาคาร/บริษัทเงินทุน (ภ.ง.ด.53)
19 Interest - Sec. 40(4)(a), individual payee, Sec. 50(2) (P.N.D.2) ดอกเบี้ย - มาตรา 40(4)(ก) ผู้รับเป็นบุคคลธรรมดา มาตรา 50(2) (ภ.ง.ด.2)
20 Dividends, share of profits - Sec. 40(4)(b) (P.N.D.2 / P.N.D.53) เงินปันผล เงินส่วนแบ่งของกำไร - มาตรา 40(4)(ข) (ภ.ง.ด.2 / ภ.ง.ด.53)
21 Rent of property, hire-purchase of movable property - Sec. 40(5) (P.N.D.3 / P.N.D.53) ค่าเช่าทรัพย์สิน ค่าเช่าซื้อสังหาริมทรัพย์ - มาตรา 40(5) (ภ.ง.ด.3 / ภ.ง.ด.53)
22 Rent of ships under the Merchant Marine Promotion Act - Sec. 40(5) (P.N.D.53) ค่าเช่าเรือตามกฎหมายว่าด้วยการส่งเสริมการพาณิชยนาวี - มาตรา 40(5) (ภ.ง.ด.53)
23 Law, medicine, engineering, architecture, accounting, fine arts - Sec. 40(6) (P.N.D.3 / P.N.D.53) วิชาชีพอิสระ กฎหมาย การประกอบโรคศิลปะ วิศวกรรม สถาปัตยกรรม การบัญชี ประณีตศิลปกรรม - มาตรา 40(6) (ภ.ง.ด.3 / ภ.ง.ด.53)
24 Contract work, contractor supplies essential materials - Sec. 40(7) (P.N.D.3 / P.N.D.53) ค่ารับเหมาที่ผู้รับเหมาต้องลงทุนจัดหาสัมภาระในส่วนสำคัญ - มาตรา 40(7) (ภ.ง.ด.3 / ภ.ง.ด.53)
25 Hire of work, service fees - Sec. 40(8) (P.N.D.3 / P.N.D.53) ค่าจ้างทำของ ค่าบริการ - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
26 Rewards, discounts, sales promotion benefits - Sec. 40(8) (P.N.D.3 / P.N.D.53) รางวัล ส่วนลด หรือประโยชน์ใด ๆ เนื่องจากการส่งเสริมการขาย - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
27 Advertising fees - Sec. 40(8) (P.N.D.3 / P.N.D.53) ค่าโฆษณา - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
28 Transport of goods or passengers, excl. public transport - Sec. 40(8) (P.N.D.3 / P.N.D.53) ค่าขนส่งสินค้าหรือผู้โดยสาร ยกเว้นการขนส่งสาธารณะ - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
29 Non-life insurance premiums - Sec. 40(8) (P.N.D.53) ค่าเบี้ยประกันวินาศภัย - มาตรา 40(8) (ภ.ง.ด.53)
30 Prizes from contests, competitions, lucky draws - Sec. 40(8) (P.N.D.3 / P.N.D.53) รางวัลจากการประกวด การแข่งขัน การชิงโชค - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
31 Public entertainers resident in Thailand - Sec. 40(8) (P.N.D.3) นักแสดงสาธารณะที่มีภูมิลำเนาในประเทศไทย - มาตรา 40(8) (ภ.ง.ด.3)
32 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)
33 Aquatic animals bought from non-registered sellers - Sec. 40(8) (P.N.D.3 / P.N.D.53) สัตว์น้ำที่ซื้อจากผู้ขายที่ไม่ได้จดทะเบียนภาษีมูลค่าเพิ่ม - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
34 Sale of immovable property, juristic seller - Sec. 69 Ter (remitted at the Land Office) การขายอสังหาริมทรัพย์ ผู้ขายเป็นนิติบุคคล - มาตรา 69 ตรี (นำส่ง ณ สำนักงานที่ดิน)
35 Sec. 40(2)-(6) income paid to non-resident individuals - Sec. 50(2) (P.N.D.3) เงินได้ตามมาตรา 40(2)-(6) จ่ายให้บุคคลธรรมดาที่มิได้เป็นผู้อยู่ในประเทศไทย - มาตรา 50(2) (ภ.ง.ด.3)
36 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)
37 Dividends to foreign companies not doing business in Thailand - Sec. 70, pre-DTA (P.N.D.54) เงินปันผลจ่ายให้บริษัทต่างประเทศที่มิได้ประกอบกิจการในประเทศไทย - มาตรา 70 ก่อนใช้อนุสัญญาภาษีซ้อน (ภ.ง.ด.54)
38 Sales Invoice ใบแจ้งหนี้
39 Sales Order ใบสั่งขาย
40 Posting Date วันที่
41 Payment Due Date วันครบกำหนดชำระ
42 Contact Person ผู้ติดต่อ
43 Mobile No โทรศัพท์มือถือ
44 Contact Email อีเมล
45 Description รายละเอียด
46 Rate ราคาต่อหน่วย
47 Amount จำนวนเงิน
48 Total รวม
49 Total (Without Tax) รวมก่อนภาษี
50 Net Total ยอดรวมสุทธิ
51 Total Taxes and Charges รวมภาษีและค่าธรรมเนียม
52 Grand Total ยอดรวมทั้งสิ้น
53 Rounded Total ยอดรวมปัดเศษ
54 Terms and Conditions Details รายละเอียดข้อตกลงและเงื่อนไข
55 Page {0} of {1} หน้า {0} จาก {1}
-46
View File
@@ -1,30 +1,6 @@
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 "<currency> <words> only." in every
language; Thai documents write "<words>บาทถ้วน" or "<words>บาท<words>สตางค์".
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):
@@ -53,28 +29,6 @@ def get_letter_head_company(doc=None):
)
def get_company_bank_account(company=None):
"""Default company Bank Account for print formats.
Returns the enabled Bank Account flagged `Is Company Account` and
`Is Default Account` for `company`, or None when there is none.
"""
if not company:
return None
account = frappe.db.get_value(
"Bank Account",
{"company": company, "is_company_account": 1, "is_default": 1, "disabled": 0},
["account_name", "bank", "bank_account_no", "iban", "branch_code"],
as_dict=True,
)
if not account:
return None
account.swift_number = frappe.db.get_value("Bank", account.bank, "swift_number")
return account
def stash_letter_head_source(doc, method=None):
"""Fixture import re-inserts the Letter Head; remember the fixture's `source`."""
doc.flags.fixture_source = doc.source