Compare commits

...
23 Commits
Author SHA1 Message Date
kurogeek c8fb983749 feat: bank details on the Sales Invoice print in the document's language
Bank Account.account_name and Bank.bank_name are translatable (the Link
field Bank Account.bank cannot be; the Bank's name is what prints), and the
Sales Invoice format renders both through _().
2026-10-21 03:36:46 +00:00
kurogeek 5c15b6637b feat: addresses print in the document's language
Address lines, city, county and state are translatable (Property Setters),
so the Address form offers a translate button that stores the other language
as a Translation record. The Thailand Address Template renders those fields
through _(), and the print formats re-render party addresses from the linked
Address with get_print_address instead of printing the stored
address_display, which is frozen in the language of whoever saved the
document. The letter head already rendered the company address at print
time. The withholding tax certificate takes the Thai translation whatever
the print language, like its tax in words.
2026-10-21 03:36:46 +00:00
kurogeek b6492f75e1 feat: Payment Entry Cheque/Reference Date follows Posting Date
Set on new entries and whenever Posting Date changes; the field stays
editable for cheques dated differently.
2026-10-05 07:12:36 +00:00
kurogeek 2d184a5b11 feat: Tax Invoice/Receipt prints Paid Date instead of Payment Due Date
Paid Date is the posting date of the latest submitted Payment Entry
referencing the invoice; omitted when no Payment Entry exists.
2026-10-05 05:06:57 +00:00
kurogeek c6e6c1238d feat: Default Standard Sales Order as Sales Order default print format
Property Setter synced on migrate, mirroring the Sales Invoice default.
2026-10-01 04:48:24 +00:00
kurogeek 4793e980b1 feat: position line under signatures on print formats
Add a 'Position' (ตำแหน่ง) underline beneath each signature cell in the
six Jinja print formats. Withholding Tax Certificate has no signature
block and is unchanged.
2026-10-01 04:48:23 +00:00
kurogeek e7d0a545b1 feat: print formats for Purchase Invoice, Purchase Order, Sales Order
Default Standard Purchase Invoice / Purchase Order / Sales Order, laid out
like the Sales Invoice and Quotation formats: dates (and the supplier's or
customer's reference numbers) in the heading, party block with Tax ID and
contact, essentials-only item table, right-aligned totals with the amount in
words beside Grand Total, signature lines.

taxes include: purchase "Deduct" charges (withholding tax) print negative and
"Valuation" charges are skipped so the lines add up to Grand Total; charge
descriptions are translated so the WHT category names print in Thai.
2026-09-23 08:31:14 +00:00
kurogeek e82d82dbcc feat: DRAFT watermark on certificates of unsubmitted invoices
Both copies are stamped while the Purchase Invoice is a draft; the
rgba colour keeps the form legible since wkhtmltopdf drops opacity on
transformed elements.
2026-09-23 03:22:02 +00:00
kurogeek baff8de0d3 fix: certificate button only on paid Purchase Invoices
The certificate is issued when the tax is withheld, i.e. on payment;
show the button once the submitted invoice's status is Paid.
2026-09-22 03:58:04 +00:00
kurogeek 2ee381f369 feat: Withholding Tax Certificate button on Purchase Invoice
Shown while Apply Tax Withholding Amount is ticked on a saved, not
cancelled invoice; downloads the certificate PDF in Thai.
2026-09-22 03:29:54 +00:00
kurogeek a1ca8439fc feat: Withholding Tax Certificate print format for Purchase Invoice
The Revenue Department's Sec. 50 bis form (approve_wh3_081156.pdf) as
the page background, with the invoice's data positioned in the form's
fields; two copies per certificate. Its AcroForm fields carry no Thai
font, so the PDF is not filled directly.

get_withholding_certificate(doc) resolves the payer and payee (13-digit
tax ID, one-line address), the certificate row from the category's
income_type, the P.N.D. return from the supplier type and income, and
the amounts in company currency with the tax in Thai words. Dates are
Buddhist Era.
2026-09-22 03:29:54 +00:00
kurogeek 42857b1377 feat: Type of Income Paid on Tax Withholding Category
Custom Select income_type: the row of the withholding tax certificate
(50 tawi) a payment under the category is reported on, set for every
shipped category.

Frappe syncs fixtures before customizations on install and migrate, so a
fixture value for a Custom Field that does not exist yet is dropped;
before_install / before_migrate create the category fields first.
2026-09-22 03:29:39 +00:00
kurogeek 270e44c080 feat: default calendar-year Payroll Period per Thai company 2026-09-18 03:52:35 +00:00
kurogeek 68f0817125 fix: gross discount layout for VAT-inclusive invoices
With inclusive pricing and a discount on Grand Total the Total line is
the tax-inclusive item total and the discount is printed as entered,
followed by Total After Discount, which the Net Total and charge lines
then break down. Other cases keep the net layout: Total (net before
discount), the discount's net share, Net Total, charges. Thai label for
Total After Discount.
2026-09-17 10:30:43 +00:00
kurogeek 4a19c60ba2 fix: print the posted discount and tax amounts
ERPNext takes an Additional Discount off the items' net amounts and,
for a discount on Grand Total, splits it between net and taxes: each
charge is booked at tax_amount_after_discount_amount and Total Taxes
and Charges sums those. The print showed the discount as entered and
the pre-discount tax_amount, so on a Grand Total discount the VAT line
disagreed with the invoice.

The totals includes now print the discount's net share (sum of
item.distributed_discount_amount), Net Total, and each charge after
discount, so the lines add up to Grand Total whichever total the
discount applies on and with inclusive or exclusive tax. A discount
that is not distributed (cash / non-trade) stays after the charges.
2026-09-17 10:19:30 +00:00
kurogeek 71eda0bec9 fix: print Total, Additional Discount, Net Total in that order
Default Standard Sales Invoice and Tax Invoice/Receipt printed the
Additional Discount Amount among the charges, after the Total line but
with no total after the discount. App copies of ERPNext's totals
includes are swapped in via doc.print_templates:

- taxes.html: a discount on Net Total is followed by the Net Total line
  ahead of the charges; a discount on Grand Total stays after them.
- total.html: with inclusive tax, "Total (Without Tax)" is the total
  before the discount (net_total + discount_amount); upstream printed
  net_total, which already has the discount taken off, so the same
  figure appeared before and after the discount line.
2026-09-17 10:02:23 +00:00
kurogeek 8cba58ba02 feat: default Shipping Rule per Thai company
"Shipping Charges - <abbr>": Selling, Fixed, posting to a "Shipping
Charges" income account under Direct Income (created if the chart lacks
it) with the default cost center. Shipping Rule is named by its label,
so the company abbreviation keeps one rule per company, as ERPNext
names its tax templates. Created on Company save, after install, and by
a patch for existing companies.

The amount is entered on the transaction, not the rule: ERPNext
re-applies the rule on every recalculation and would reset the charge
row to the rule's fixed amount. ThaiShippingRule overrides the doctype
class so a Fixed rule with no amount only seeds the row and leaves the
entered amount alone.
2026-09-17 10:02:23 +00:00
kurogeek 4785878025 feat: default Location tree and Asset Categories
Location fixture: "All Locations" group with Head Office, Branch Office and
Warehouse. Fixture import re-inserts tree nodes with fresh lft/rgt, which
strands locations users add beneath them, so after_migrate rebuilds the
Location tree.

Asset Category fixture: Land (non-depreciable), Buildings (20 years),
Plant and Machinery, Vehicles, Furniture and Fixtures, Office Equipment,
Computers and Electronics, Software (5 years) and Intangible Assets
(10 years), per Royal Decree 145 rates; monthly straight-line, daily
pro-rata. The accounts table is company-specific and therefore not
shipped: an Asset Category before_import hook links every Thai company
(same contract as the WHT categories), reusing the Standard chart's
fixed asset accounts and creating Land, Vehicles and Intangible Assets
under Fixed Assets. Company.on_update links new companies. Depreciation
accounts are left to the Company defaults.

get_or_create_account now takes account_type and parent_groups.
2026-09-17 05:13:00 +00:00
kurogeek 2a44aed574 feat: Thailand Personal Income Tax slab fixture
Submitted Income Tax Slab "Thailand Personal Income Tax" (THB, effective
2017-01-01) with the eight progressive brackets 0% to 35%, using the HRMS
from/to convention (150,001-300,000 etc.). standard_tax_exemption_amount
is 160,000: the Sec. 42 bis expense deduction (50%, max 100,000) plus the
personal allowance (60,000), which every employee gets without filing a
declaration. The "Expense Deduction" category and "Personal allowance"
sub-category are dropped from the exemption fixtures so they cannot be
counted twice; Personal and Family Allowances cap becomes 270,000.
2026-09-17 01:10:43 +00:00
kurogeek 6a969841e5 feat: Thai personal income tax exemption categories for HRMS
Fixtures for Employee Tax Exemption Category / Sub Category covering the
Thai PIT allowances (personal and family, insurance, retirement savings,
Thai ESG/ESGX, social security, home loan interest, donations, income
exemptions, stimulus measures) plus the Sec. 42 bis expense deduction.

Category caps follow HRMS validation: a sub-category max must not exceed
its category max and the declaration caps the category total, so groups
with a real combined limit carry it, groups without one use the sum of
their sub caps, and uncapped per-person/percentage items live in 0-cap
categories. Social Security uses the 2026 ceiling (10,500/yr); SSF is
kept inactive. sync_fixtures skips these files on sites without hrms.
2026-09-16 11:22:28 +00:00
kurogeek d074657511 feat: add Default Standard Quotation print format 2026-09-16 07:46:59 +00:00
kurogeek 04735ca813 feat: Thai VAT schemes as Sales/Purchase Taxes and Charges Templates
Per Thai company: Thailand VAT 7%, Thailand VAT 7% (Included) for
VAT-inclusive pricing, Thailand VAT 0% for zero-rated supplies. Sales
schemes post to Output VAT, purchase schemes to Input VAT (category
Total, so recoverable VAT stays out of valuation). The 7% scheme is the
default when the company has none. Created on Company save, after
install, and by a patch for existing companies.
2026-09-15 09:35:15 +00:00
kurogeek 381aab2c83 feat: Output VAT and Input VAT accounts for every Thai company
Thai VAT is filed on P.P.30 as output tax (ภาษีขาย, collected on sales)
net of input tax (ภาษีซื้อ, paid on purchases); ERPNext's wizard makes a
single VAT account. Both join the per-company tax accounts created on
Company save and fixture import; a patch adds them to existing companies.
Account names are translated for the chart of accounts tree.
2026-09-15 09:19:43 +00:00
35 changed files with 2338 additions and 91 deletions

No files matched your search

+24
View File
@@ -20,6 +20,30 @@ file overrides the upstream Thai for the standard labels the print formats show.
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.
### Addresses in more than one language
Address lines, city, county and state are *translatable*: the Address form shows a translate
button next to each, which stores the other language in a Translation record. The Thailand
Address Template renders those fields through `_()`, so the print formats and letter head
show the company's and the party's address in the document's print language (the company
address is re-rendered at print time; party addresses are re-rendered from the linked
Address instead of printing the stored `address_display`). The withholding tax certificate
always takes the Thai translation. Translations are keyed on the exact text, so one
translation of a city or province serves every address that uses it. The same goes for
the bank details on the Sales Invoice: *Account Name* on the Bank Account and *Bank Name*
on the Bank are translatable.
### Withholding tax certificate
A paid Purchase Invoice with *Apply Tax Withholding Amount* gets a **Withholding Tax Certificate**
button that downloads the Revenue Department's Sec. 50 bis form (หนังสือรับรองการหักภาษี ณ ที่จ่าย,
two copies) as a PDF: the *Withholding Tax Certificate* print format lays the invoice's data
over the official form (`public/images`). The row the payment is reported on comes from
*Type of Income Paid* on the Tax Withholding Category (set for the shipped categories; pick it
for your own), and the ภ.ง.ด. return is ticked from the Supplier's type: ภ.ง.ด.53 for juristic
payees, ภ.ง.ด.1ก / 2 / 3 by income type for individuals. เล่มที่ / เลขที่ (certificate book
numbers) and ลำดับที่ (the line in the return) are left for the accountant.
### 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:
+91
View File
@@ -0,0 +1,91 @@
import frappe
from frappe.utils.nestedset import rebuild_tree
from default_thai_company.tax_withholding import company_ready, get_or_create_account, thai_companies
# Asset Category -> fixed asset account. Names follow ERPNext's Standard chart so
# setup-wizard companies reuse their accounts; the rest are created under the
# same "Fixed Assets" group. Depreciation accounts are left to the Company
# defaults (Asset falls back to them), so one change there covers every category.
FIXED_ASSET_ACCOUNTS = {
"Land": "Land",
"Buildings": "Buildings",
"Plant and Machinery": "Plants and Machineries",
"Vehicles": "Vehicles",
"Furniture and Fixtures": "Furnitures and Fixtures",
"Office Equipment": "Office Equipments",
"Computers and Electronics": "Electronic Equipments",
"Software": "Softwares",
"Intangible Assets": "Intangible Assets",
}
FIXED_ASSET_GROUPS = ("Fixed Assets",)
def fixed_asset_account(company, category):
return get_or_create_account(
company, FIXED_ASSET_ACCOUNTS[category], "Asset", "Fixed Asset", FIXED_ASSET_GROUPS
)
def link_company(category, company):
"""Append the company's fixed asset account row to the category if missing."""
if any(row.company_name == company for row in category.accounts):
return False
category.append(
"accounts",
{"company_name": company, "fixed_asset_account": fixed_asset_account(company, category.name)},
)
return True
def prepare_fixture_accounts(doc, method=None):
"""Asset Category fixtures ship without `accounts` (company-specific).
Same contract as the Tax Withholding Category fixtures: fixture import
re-inserts the doc on every migrate, so carry over the rows already on this
site, then link every Thai company. Mandatory is relaxed for sites without a
Thai company yet; `setup_company` links them later.
"""
if doc.name not in FIXED_ASSET_ACCOUNTS:
return
doc.flags.ignore_mandatory = True
if not doc.accounts and frappe.db.exists(doc.doctype, doc.name):
for row in frappe.get_all(
"Asset Category Account",
filters={"parent": doc.name, "parenttype": doc.doctype},
fields=[
"company_name",
"fixed_asset_account",
"accumulated_depreciation_account",
"depreciation_expense_account",
"capital_work_in_progress_account",
],
order_by="idx",
):
doc.append("accounts", row)
for company in thai_companies():
link_company(doc, company)
def setup_company(doc, method=None):
"""Company.on_update: link a Thai company on every default Asset Category."""
if not company_ready(doc):
return
for name in FIXED_ASSET_ACCOUNTS:
if not frappe.db.exists("Asset Category", name):
continue
category = frappe.get_doc("Asset Category", name)
if link_company(category, doc.name):
category.save(ignore_permissions=True)
def rebuild_locations():
"""after_migrate: fixture import re-inserts the Location tree nodes (fresh
lft/rgt), which strands locations users added beneath them; rebuild."""
if frappe.db.exists("Location", "All Locations"):
rebuild_tree("Location")
@@ -0,0 +1,124 @@
{
"custom_fields": [],
"custom_perms": [],
"doctype": "Address",
"links": [],
"property_setters": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"creation": "2026-10-20 12:00:00",
"default_value": null,
"doc_type": "Address",
"docstatus": 0,
"doctype_or_field": "DocField",
"field_name": "address_line1",
"idx": 0,
"is_system_generated": 0,
"modified": "2026-10-20 12:00:00",
"modified_by": "Administrator",
"module": null,
"name": "Address-address_line1-translatable",
"owner": "Administrator",
"property": "translatable",
"property_type": "Check",
"row_name": null,
"value": "1"
},
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"creation": "2026-10-20 12:00:00",
"default_value": null,
"doc_type": "Address",
"docstatus": 0,
"doctype_or_field": "DocField",
"field_name": "address_line2",
"idx": 0,
"is_system_generated": 0,
"modified": "2026-10-20 12:00:00",
"modified_by": "Administrator",
"module": null,
"name": "Address-address_line2-translatable",
"owner": "Administrator",
"property": "translatable",
"property_type": "Check",
"row_name": null,
"value": "1"
},
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"creation": "2026-10-20 12:00:00",
"default_value": null,
"doc_type": "Address",
"docstatus": 0,
"doctype_or_field": "DocField",
"field_name": "city",
"idx": 0,
"is_system_generated": 0,
"modified": "2026-10-20 12:00:00",
"modified_by": "Administrator",
"module": null,
"name": "Address-city-translatable",
"owner": "Administrator",
"property": "translatable",
"property_type": "Check",
"row_name": null,
"value": "1"
},
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"creation": "2026-10-20 12:00:00",
"default_value": null,
"doc_type": "Address",
"docstatus": 0,
"doctype_or_field": "DocField",
"field_name": "county",
"idx": 0,
"is_system_generated": 0,
"modified": "2026-10-20 12:00:00",
"modified_by": "Administrator",
"module": null,
"name": "Address-county-translatable",
"owner": "Administrator",
"property": "translatable",
"property_type": "Check",
"row_name": null,
"value": "1"
},
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"creation": "2026-10-20 12:00:00",
"default_value": null,
"doc_type": "Address",
"docstatus": 0,
"doctype_or_field": "DocField",
"field_name": "state",
"idx": 0,
"is_system_generated": 0,
"modified": "2026-10-20 12:00:00",
"modified_by": "Administrator",
"module": null,
"name": "Address-state-translatable",
"owner": "Administrator",
"property": "translatable",
"property_type": "Check",
"row_name": null,
"value": "1"
}
],
"sync_on_migrate": 1
}
@@ -0,0 +1,32 @@
{
"custom_fields": [],
"custom_perms": [],
"doctype": "Bank",
"links": [],
"property_setters": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"creation": "2026-10-20 12:00:00",
"default_value": null,
"doc_type": "Bank",
"docstatus": 0,
"doctype_or_field": "DocField",
"field_name": "bank_name",
"idx": 0,
"is_system_generated": 0,
"modified": "2026-10-20 12:00:00",
"modified_by": "Administrator",
"module": null,
"name": "Bank-bank_name-translatable",
"owner": "Administrator",
"property": "translatable",
"property_type": "Check",
"row_name": null,
"value": "1"
}
],
"sync_on_migrate": 1
}
@@ -0,0 +1,32 @@
{
"custom_fields": [],
"custom_perms": [],
"doctype": "Bank Account",
"links": [],
"property_setters": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"creation": "2026-10-20 12:00:00",
"default_value": null,
"doc_type": "Bank Account",
"docstatus": 0,
"doctype_or_field": "DocField",
"field_name": "account_name",
"idx": 0,
"is_system_generated": 0,
"modified": "2026-10-20 12:00:00",
"modified_by": "Administrator",
"module": null,
"name": "Bank Account-account_name-translatable",
"owner": "Administrator",
"property": "translatable",
"property_type": "Check",
"row_name": null,
"value": "1"
}
],
"sync_on_migrate": 1
}
@@ -0,0 +1,32 @@
{
"custom_fields": [],
"custom_perms": [],
"doctype": "Sales Order",
"links": [],
"property_setters": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"creation": "2026-10-01 12:00:00",
"default_value": null,
"doc_type": "Sales Order",
"docstatus": 0,
"doctype_or_field": "DocType",
"field_name": null,
"idx": 0,
"is_system_generated": 1,
"modified": "2026-10-01 12:00:00",
"modified_by": "Administrator",
"module": null,
"name": "Sales Order-main-default_print_format",
"owner": "Administrator",
"property": "default_print_format",
"property_type": "Data",
"row_name": null,
"value": "Default Standard Sales Order"
}
],
"sync_on_migrate": 1
}
@@ -0,0 +1,73 @@
{
"custom_fields": [
{
"_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-22 09:00:00.000000",
"default": null,
"depends_on": null,
"description": "Row of the withholding tax certificate (50 Tawi) the payment is reported on; Other prints the category name in row 6.",
"docstatus": 0,
"dt": "Tax Withholding Category",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "income_type",
"fieldtype": "Select",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 2,
"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": "category_name",
"is_system_generated": 0,
"is_virtual": 0,
"label": "Type of Income Paid",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2026-09-22 09:00:00.000000",
"modified_by": "Administrator",
"module": null,
"name": "Tax Withholding Category-income_type",
"no_copy": 0,
"non_negative": 0,
"options": "\nSalary and Wages - Sec. 40(1)\nFees and Commissions - Sec. 40(2)\nRoyalties - Sec. 40(3)\nInterest - Sec. 40(4)(a)\nDividends - Sec. 40(4)(b)\nSec. 3 Tera (Services, Rent, Contract Work etc.)\nOther",
"owner": "Administrator",
"permlevel": 0,
"placeholder": null,
"precision": null,
"print_hide": 0,
"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
}
],
"custom_perms": [],
"doctype": "Tax Withholding Category",
"links": [],
"property_setters": [],
"sync_on_migrate": 1
}
@@ -4,8 +4,8 @@
"docstatus": 0,
"doctype": "Address Template",
"is_default": 1,
"modified": "2026-09-09 17:23:33.559202",
"modified": "2026-10-20 12:00:00.000000",
"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": "{#- Fields go through _() so Translation records (the field's translate button) render the address in the print language. -#}\n{{ _(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"
}
]
@@ -0,0 +1,237 @@
[
{
"accounts": [],
"asset_category_name": "Land",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [],
"modified": "2026-09-16 09:00:00.000000",
"name": "Land",
"non_depreciable_category": 1
},
{
"accounts": [],
"asset_category_name": "Buildings",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Buildings",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 240
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Buildings",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Plant and Machinery",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Plant and Machinery",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Plant and Machinery",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Vehicles",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Vehicles",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Vehicles",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Furniture and Fixtures",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Furniture and Fixtures",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Furniture and Fixtures",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Office Equipment",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Office Equipment",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Office Equipment",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Computers and Electronics",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Computers and Electronics",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Computers and Electronics",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Software",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Software",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 60
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Software",
"non_depreciable_category": 0
},
{
"accounts": [],
"asset_category_name": "Intangible Assets",
"docstatus": 0,
"doctype": "Asset Category",
"enable_cwip_accounting": 0,
"finance_books": [
{
"daily_prorata_based": 1,
"depreciation_method": "Straight Line",
"docstatus": 0,
"doctype": "Asset Finance Book",
"finance_book": null,
"frequency_of_depreciation": 1,
"idx": 1,
"parent": "Intangible Assets",
"parentfield": "finance_books",
"parenttype": "Asset Category",
"rate_of_depreciation": 0.0,
"salvage_value_percentage": 0.0,
"shift_based": 0,
"total_number_of_depreciations": 120
}
],
"modified": "2026-09-16 09:00:00.000000",
"name": "Intangible Assets",
"non_depreciable_category": 0
}
]
@@ -0,0 +1,106 @@
[
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 270000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Personal and Family Allowances"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Child and Dependant Allowances"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 100000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Life and Health Insurance"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 25000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Family Insurance"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 500000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Retirement Savings and Investments"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 300000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Thai ESG Fund"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 600000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Thai ESGX Fund"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 10500.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Social Security Fund"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 100000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Housing"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Donations"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 10000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Political Party Donation"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 190000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Income Exemptions"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Category",
"is_active": 1,
"max_amount": 250000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Government Stimulus Measures"
}
]
@@ -0,0 +1,299 @@
[
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Personal and Family Allowances",
"is_active": 1,
"max_amount": 60000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Spouse allowance (spouse without income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Personal and Family Allowances",
"is_active": 1,
"max_amount": 90000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Adopted child allowance (30,000 per child, max 3)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Personal and Family Allowances",
"is_active": 1,
"max_amount": 60000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Parent allowance (30,000 per parent, max 2)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Personal and Family Allowances",
"is_active": 1,
"max_amount": 60000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Spouse's parent allowance (30,000 per parent, max 2)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Child and Dependant Allowances",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Child allowance (30,000 per child)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Child and Dependant Allowances",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Child allowance - 2nd child onward born 2018 or later (60,000 per child)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Child and Dependant Allowances",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Disabled or incapacitated dependant care (60,000 per person)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Child and Dependant Allowances",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Prenatal care and childbirth expenses (60,000 per pregnancy)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Life and Health Insurance",
"is_active": 1,
"max_amount": 100000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Life insurance premium (policy term 10 years or more)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Life and Health Insurance",
"is_active": 1,
"max_amount": 25000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Health insurance premium (self)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Family Insurance",
"is_active": 1,
"max_amount": 10000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Spouse life insurance premium (spouse without income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Family Insurance",
"is_active": 1,
"max_amount": 15000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Parents' health insurance premium"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 500000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Provident fund contribution (15% of wages)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 500000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Government Pension Fund contribution (30% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 500000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Private Teacher Aid Fund contribution (15% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 500000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Retirement Mutual Fund - RMF (30% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 200000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Pension life insurance premium (15% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 1,
"max_amount": 30000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "National Savings Fund contribution"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Retirement Savings and Investments",
"is_active": 0,
"max_amount": 200000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Super Savings Fund - SSF (30% of income, tax years 2020-2024)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Thai ESG Fund",
"is_active": 1,
"max_amount": 300000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Thai ESG fund (30% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Thai ESGX Fund",
"is_active": 1,
"max_amount": 300000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Thai ESGX fund - new units (30% of income)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Thai ESGX Fund",
"is_active": 1,
"max_amount": 300000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Thai ESGX fund - LTF switch (500,000 spread over tax years 2025-2029)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Social Security Fund",
"is_active": 1,
"max_amount": 10500.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Social Security contribution - Sec. 33 employee"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Social Security Fund",
"is_active": 1,
"max_amount": 5184.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Social Security contribution - Sec. 39 voluntary"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Social Security Fund",
"is_active": 1,
"max_amount": 3600.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Social Security contribution - Sec. 40 informal worker"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Housing",
"is_active": 1,
"max_amount": 100000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Home loan interest"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Donations",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "General donations (10% of income after allowances)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Donations",
"is_active": 1,
"max_amount": 0.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Education, sports, hospital and social development donations (2x, 10% of income after allowances)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Political Party Donation",
"is_active": 1,
"max_amount": 10000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Political party donation"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Income Exemptions",
"is_active": 1,
"max_amount": 190000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Taxpayer aged 65 or over - income exemption"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Income Exemptions",
"is_active": 1,
"max_amount": 190000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Disabled taxpayer under 65 - income exemption"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Government Stimulus Measures",
"is_active": 1,
"max_amount": 50000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Easy E-Receipt (e-Tax Invoice / e-Receipt purchases)"
},
{
"docstatus": 0,
"doctype": "Employee Tax Exemption Sub Category",
"exemption_category": "Government Stimulus Measures",
"is_active": 1,
"max_amount": 200000.0,
"modified": "2026-09-16 09:00:00.000000",
"name": "Solar rooftop installation (tax years 2025-2027)"
}
]
@@ -0,0 +1,113 @@
[
{
"allow_tax_exemption": 1,
"currency": "THB",
"disabled": 0,
"docstatus": 1,
"doctype": "Income Tax Slab",
"effective_from": "2017-01-01",
"modified": "2026-09-16 09:00:00.000000",
"name": "Thailand Personal Income Tax",
"other_taxes_and_charges": [],
"slabs": [
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 0.0,
"idx": 1,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 0.0,
"to_amount": 150000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 150001.0,
"idx": 2,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 5.0,
"to_amount": 300000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 300001.0,
"idx": 3,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 10.0,
"to_amount": 500000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 500001.0,
"idx": 4,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 15.0,
"to_amount": 750000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 750001.0,
"idx": 5,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 20.0,
"to_amount": 1000000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 1000001.0,
"idx": 6,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 25.0,
"to_amount": 2000000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 2000001.0,
"idx": 7,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 30.0,
"to_amount": 5000000.0
},
{
"condition": "",
"docstatus": 1,
"doctype": "Taxable Salary Slab",
"from_amount": 5000001.0,
"idx": 8,
"parent": "Thailand Personal Income Tax",
"parentfield": "slabs",
"parenttype": "Income Tax Slab",
"percent_deduction": 35.0,
"to_amount": 0.0
}
],
"standard_tax_exemption_amount": 160000.0,
"tax_relief_limit": 0.0
}
]
@@ -0,0 +1,42 @@
[
{
"docstatus": 0,
"doctype": "Location",
"is_container": 0,
"is_group": 1,
"location_name": "All Locations",
"modified": "2026-09-16 09:00:00.000000",
"name": "All Locations",
"parent_location": null
},
{
"docstatus": 0,
"doctype": "Location",
"is_container": 0,
"is_group": 0,
"location_name": "Head Office",
"modified": "2026-09-16 09:00:00.000000",
"name": "Head Office",
"parent_location": "All Locations"
},
{
"docstatus": 0,
"doctype": "Location",
"is_container": 0,
"is_group": 0,
"location_name": "Branch Office",
"modified": "2026-09-16 09:00:00.000000",
"name": "Branch Office",
"parent_location": "All Locations"
},
{
"docstatus": 0,
"doctype": "Location",
"is_container": 0,
"is_group": 0,
"location_name": "Warehouse",
"modified": "2026-09-16 09:00:00.000000",
"name": "Warehouse",
"parent_location": "All Locations"
}
]
+175 -5
View File
@@ -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\"disable_rounded_total\",\n\t\"posting_date\", \"due_date\",\n\t\"customer_name\", \"address_display\", \"contact_display\", \"contact_mobile\", \"contact_email\", \"tax_id\",\n\t\"in_words\",\n] -%}\n{#- rounded_total keeps a value even when rounding is disabled, so the standard layout would print it. -#}\n{%- if doc.is_rounded_total_disabled() -%}{%- set ignored = hidden_fields.append(\"rounded_total\") -%}{%- endif -%}\n{#- add_header renders this string via frappe.render_template with {\"doc\": doc}, so it is kept\n unrendered with {% raw %}. Same markup as the stock heading plus a right-aligned date block. -#}\n{%- set heading_template -%}\n{% raw %}\n<div class=\"print-heading\">\n\t<style>\n\t\t.print-heading .heading-dates { float: right; text-align: right; margin-top: 4px; white-space: nowrap; }\n\t\t.print-heading .heading-dates label { display: inline; font-weight: 700; margin: 0 4px 0 0; }\n\t\t.print-heading .heading-clear { clear: both; }\n\t</style>\n\t<div class=\"heading-dates\">\n\t\t<div><label>{{ _(doc.meta.get_label(\"posting_date\")) }}:</label> {{ doc.get_formatted(\"posting_date\") }}</div>\n\t\t{%- if doc.due_date %}\n\t\t<div><label>{{ _(doc.meta.get_label(\"due_date\")) }}:</label> {{ doc.get_formatted(\"due_date\") }}</div>\n\t\t{%- endif %}\n\t</div>\n\t<h2>\n\t\t<div>{{ _(doc.select_print_heading) or (_(doc.print_heading) if doc.print_heading != None else _(doc.doctype)) }}</div>\n\t\t<small class=\"sub-heading\">{{ _(doc.sub_heading) if doc.sub_heading != None else _(doc.name) }}</small>\n\t</h2>\n\t<div class=\"heading-clear\"></div>\n</div>\n{% endraw %}\n{%- endset -%}\n{#- Standard layout prints every non-empty child column; keep the item table to the essentials -#}\n{%- set item_columns = [\n\t{\"fieldname\": \"item_code\"},\n\t{\"fieldname\": \"description\"},\n\t{\"fieldname\": \"qty\"},\n\t{\"fieldname\": \"rate\"},\n\t{\"fieldname\": \"amount\"},\n] -%}\n\n{%- macro render_fields(fields, no_of_cols) -%}\n\t{%- for df in fields -%}\n\t\t{%- if df.fieldname not in hidden_fields -%}\n\t\t\t{%- if df.fieldname == \"items\" -%}\n\t\t\t\t{%- set df = df.as_dict() -%}\n\t\t\t\t{%- set ignored = df.update({\"visible_columns\": item_columns}) -%}\n\t\t\t{%- endif -%}\n\t\t\t{{ render_field(df, doc, no_of_cols) }}\n\t\t{%- endif -%}\n\t{%- endfor -%}\n{%- endmacro -%}\n\n{% for page in layout %}\n<div class=\"page-break\">\n\t<div {% if print_settings.repeat_header_footer %} id=\"header-html\" class=\"hidden-pdf\" {% endif %}>\n\t\t{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings, heading_template) }}\n\t</div>\n\n\t{% if loop.first %}\n\t<div class=\"row section-break\" data-label=\"{{ _(\"Customer\") }}\">\n\t\t<div class=\"col-xs-12 column-break\">\n\t\t\t<h4>{{ _(\"Customer\") }}</h4>\n\t\t\t<style>\n\t\t\t\t.customer-details { width: 100%; }\n\t\t\t\t.customer-details td { padding: 0 !important; border: 0; vertical-align: top; }\n\t\t\t\t.customer-details td.text-muted { width: 40%; padding-right: 1em !important; white-space: nowrap; }\n\t\t\t</style>\n\t\t</div>\n\t\t<div class=\"col-xs-6 column-break\">\n\t\t\t<table class=\"customer-details\">\n\t\t\t\t<tbody>\n\t\t\t\t\t<tr><td class=\"text-muted\">{{ _(doc.meta.get_label(\"customer_name\")) }}</td><td>{{ doc.customer_name or doc.customer }}</td></tr>\n\t\t\t\t\t{% if doc.address_display %}\n\t\t\t\t\t<tr><td class=\"text-muted\">{{ _(doc.meta.get_label(\"address_display\")) }}</td><td>{{ doc.address_display }}</td></tr>\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t{% if doc.tax_id %}\n\t\t\t\t\t<tr><td class=\"text-muted\">{{ _(doc.meta.get_label(\"tax_id\")) }}</td><td>{{ doc.tax_id }}</td></tr>\n\t\t\t\t\t{% endif %}\n\t\t\t\t</tbody>\n\t\t\t</table>\n\t\t</div>\n\t\t<div class=\"col-xs-6 column-break\">\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_valLine truncated
"html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header, render_field with context -%}\n{%- set bank_account = get_company_bank_account(doc.company) -%}\n{#- ERPNext hides taxes marked \"Is this Tax included in Basic Rate?\" unless Accounts Settings\n enables \"Show Inclusive Tax in Print\"; always show them here so the tax line and\n \"Total (Without Tax)\" appear on invoices with inclusive tax. -#}\n{%- if doc.get(\"taxes\", {\"included_in_print_rate\": 1}) -%}\n\t{%- set ignored = doc.flags.update({\"show_inclusive_tax_in_print\": 1}) -%}\n{%- endif -%}\n{#- App copies of ERPNext's totals includes, printing the posted figures: \"total\" is the net before\n the Additional Discount; \"taxes\" prints the discount's net share, Net Total and each charge after\n discount, so the lines add up to Grand Total whichever total the discount applies on. -#}\n{%- set ignored = doc.print_templates.update({\"total\": \"default_thai_company/templates/print_formats/includes/total.html\"}) -%}\n{%- if doc.print_templates.get(\"taxes\") -%}\n\t{%- set ignored = doc.print_templates.update({\"taxes\": \"default_thai_company/templates/print_formats/includes/taxes.html\"}) -%}\n{%- endif -%}\n{#- Standard layout prints every non-empty field; drop the ones this invoice does not need.\n posting_date / due_date are printed in the heading, customer fields in the Customer section,\n in_words in the left column of the Totals section. -#}\n{%- set hidden_fields = [\n\t\"total_qty\", \"update_billed_amount_in_delivery_note\", \"amount_eligible_for_commission\", \"company_tax_id\",\n\t\"disable_rounded_total\",\n\t\"posting_date\", \"due_date\",\n\t\"customer_name\", \"address_display\", \"contact_display\", \"contact_mobile\", \"contact_email\", \"tax_id\",\n\t\"in_words\",\n] -%}\n{#- rounded_total keeps a value even when rounding is disabled, so the standard layout would print it. -#}\n{%- if doc.is_rounded_total_disabled() -%}{%- set ignored = hidden_fields.append(\"rounded_total\") -%}{%- endif -%}\n{#- add_header renders this string via frappe.render_template with {\"doc\": doc}, so it is kept\n unrendered with {% raw %}. Same markup as the stock heading plus a right-aligned date block. -#}\n{%- set heading_template -%}\n{% raw %}\n<div class=\"print-heading\">\n\t<style>\n\t\t.print-heading .heading-dates { float: right; text-align: right; margin-top: 4px; white-space: nowrap; }\n\t\t.print-heading .heading-dates label { display: inline; font-weight: 700; margin: 0 4px 0 0; }\n\t\t.print-heading .heading-clear { clear: both; }\n\t</style>\n\t<div class=\"heading-dates\">\n\t\t<div><label>{{ _(doc.meta.get_label(\"posting_date\")) }}:</label> {{ doc.get_formatted(\"posting_date\") }}</div>\n\t\t{%- if doc.due_date %}\n\t\t<div><label>{{ _(doc.meta.get_label(\"due_date\")) }}:</label> {{ doc.get_formatted(\"due_date\") }}</div>\n\t\t{%- endif %}\n\t</div>\n\t<h2>\n\t\t<div>{{ _(doc.select_print_heading) or (_(doc.print_heading) if doc.print_heading != None else _(doc.doctype)) }}</div>\n\t\t<small class=\"sub-heading\">{{ _(doc.sub_heading) if doc.sub_heading != None else _(doc.name) }}</small>\n\t</h2>\n\t<div class=\"heading-clear\"></div>\n</div>\n{% endraw %}\n{%- endset -%}\n{#- Standard layout prints every non-empty child column; keep the item table to the essentials -#}\n{%- set item_columns = [\n\t{\"fieldname\": \"item_code\"},\n\t{\"fieldname\": \"description\"},\n\t{\"fieldname\": \"qty\"},\n\t{\"fieldname\": \"rate\"},\n\t{\"fieldname\": \"amount\"},\n] -%}\n\n{%- macro render_fields(fields, no_of_cols) -%}\n\t{%- for df in fields -%}\n\t\t{%- if df.fieldname not in hidden_fields -%}\n\t\t\t{%- if df.fieldname == \"items\" -%}\n\t\t\t\t{%- set df = df.as_dict() -%}\n\t\t\t\t{%- set ignored = df.update({\"visible_columns\": item_columns}) -%}\n\t\t\t{%- endif -%}\n\t\t\t{{ render_field(df, doc, no_of_cols) }}\n\t\t{%- endif -%}\n\t{%- endfor -%}\n{%- endmacro -%}\n\n{% for page in layout %}\n<div class=\"page-break\">\n\t<div {% if print_settings.repeat_header_footer %} id=\"header-html\" class=\"hidden-pdf\" {% endif %}>\n\t\t{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings, heading_template) }}\n\t</div>\n\n\t{% if loop.first %}\n\t<div class=\"row section-break\" data-label=\"{{ _(\"Customer\") }}\">\n\t\t<div class=\"col-xs-12 column-break\">\n\t\t\t<h4>{{ _(\"Customer\") }}</h4>\n\t\t\t<style>\n\t\t\t\t.customer-details { width: 100%; }\n\t\t\t\t.customer-details td { padding: 0 !important; border: 0; vertical-align: top; }\n\t\t\t\t.customer-details td.text-muted { width: 40%; padding-right: 1em !important; white-space: nowrap; }\n\t\t\t</style>\n\t\t</div>\n\t\t<div class=\"col-xs-6 column-break\">\n\t\t\t<table class=\"customer-details\">\n\t\t\t\t<tbody>\n\t\t\t\t\t<tr><td class=\"text-muted\">{{ _(doc.meta.get_label(\"customer_name\")) }}</td><td>{{ doc.customer_name or doc.customer }}</td></tr>\n\t\t\t\t\t{#- Re-rLine truncated
"line_breaks": 0,
"margin_bottom": 15.0,
"margin_left": 15.0,
"margin_right": 15.0,
"margin_top": 15.0,
"modified": "2026-09-15 07:10:28.117714",
"modified": "2026-10-20 12:00:00.000000",
"module": "Default Thai Company",
"name": "Default Standard Sales Invoice",
"page_number": "Hide",
@@ -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{#- 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\"disable_rounded_total\",\n\t\"posting_date\", \"due_date\",\n\t\"customer_name\", \"address_display\", \"contact_display\", \"contact_mobile\", \"contact_email\", \"tax_id\",\n\t\"in_words\",\n] -%}\n{#- rounded_total keeps a value even when rounding is disabled, so the standard layout would print it. -#}\n{%- if doc.is_rounded_total_disabled() -%}{%- set ignored = hidden_fields.append(\"rounded_total\") -%}{%- endif -%}\n{#- add_header renders this string via frappe.render_template with {\"doc\": doc}, so it is kept\n unrendered with {% raw %}. Same markup as the stock heading plus a right-aligned date block. -#}\n{%- set heading_template -%}\n{% raw %}\n<div class=\"print-heading\">\n\t<style>\n\t\t.print-heading .heading-dates { float: right; text-align: right; margin-top: 4px; white-space: nowrap; }\n\t\t.print-heading .heading-dates label { display: inline; font-weight: 700; margin: 0 4px 0 0; }\n\t\t.print-heading .heading-clear { clear: both; }\n\t</style>\n\t<div class=\"heading-dates\">\n\t\t<div><label>{{ _(doc.meta.get_label(\"posting_date\")) }}:</label> {{ doc.get_formatted(\"posting_date\") }}</div>\n\t\t{%- if doc.due_date %}\n\t\t<div><label>{{ _(doc.meta.get_label(\"due_date\")) }}:</label> {{ doc.get_formatted(\"due_date\") }}</div>\n\t\t{%- endif %}\n\t</div>\n\t<h2>\n\t\t<div>{{ _(doc.select_print_heading) or (_(doc.print_heading) if doc.print_heading != None else _(\"Tax Invoice/Receipt\")) }}</div>\n\t\t<small class=\"sub-heading\">{{ _(doc.sub_heading) if doc.sub_heading != None else _(doc.name) }}</small>\n\t</h2>\n\t<div class=\"heading-clear\"></div>\n</div>\n{% endraw %}\n{%- endset -%}\n{#- Standard layout prints every non-empty child column; keep the item table to the essentials -#}\n{%- set item_columns = [\n\t{\"fieldname\": \"item_code\"},\n\t{\"fieldname\": \"description\"},\n\t{\"fieldname\": \"qty\"},\n\t{\"fieldname\": \"rate\"},\n\t{\"fieldname\": \"amount\"},\n] -%}\n\n{%- macro render_fields(fields, no_of_cols) -%}\n\t{%- for df in fields -%}\n\t\t{%- if df.fieldname not in hidden_fields -%}\n\t\t\t{%- if df.fieldname == \"items\" -%}\n\t\t\t\t{%- set df = df.as_dict() -%}\n\t\t\t\t{%- set ignored = df.update({\"visible_columns\": item_columns}) -%}\n\t\t\t{%- endif -%}\n\t\t\t{{ render_field(df, doc, no_of_cols) }}\n\t\t{%- endif -%}\n\t{%- endfor -%}\n{%- endmacro -%}\n\n{% for page in layout %}\n<div class=\"page-break\">\n\t<div {% if print_settings.repeat_header_footer %} id=\"header-html\" class=\"hidden-pdf\" {% endif %}>\n\t\t{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings, heading_template) }}\n\t</div>\n\n\t{% if loop.first %}\n\t<div class=\"row section-break\" data-label=\"{{ _(\"Customer\") }}\">\n\t\t<div class=\"col-xs-12 column-break\">\n\t\t\t<h4>{{ _(\"Customer\") }}</h4>\n\t\t\t<style>\n\t\t\t\t.customer-details { width: 100%; }\n\t\t\t\t.customer-details td { padding: 0 !important; border: 0; vertical-align: top; }\n\t\t\t\t.customer-details td.text-muted { width: 40%; padding-right: 1em !important; white-space: nowrap; }\n\t\t\t</style>\n\t\t</div>\n\t\t<div class=\"col-xs-6 column-break\">\n\t\t\t<table class=\"customer-details\">\n\t\t\t\t<tbody>\n\t\t\t\t\t<tr><td class=\"text-muted\">{{ _(doc.meta.get_label(\"customer_name\")) }}</td><td>{{ doc.customer_name or doc.customer }}</td></tr>\n\t\t\t\t\t{% if doc.address_display %}\n\t\t\t\t\t<tr><td class=\"text-muted\">{{ _(doc.meta.get_label(\"address_display\")) }}</td><td>{{ doc.address_display }}</td></tr>\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t{% if doc.tax_id %}\n\t\t\t\t\t<tr><td class=\"text-muted\">{{ _(doc.meta.get_label(\"tax_id\")) }}</td><td>{{ doc.tax_id }}</td></tr>\n\t\t\t\t\t{% endif %}\n\t\t\t\t</tbody>\n\t\t\t</table>\n\t\t</div>\n\t\t<div class=\"col-xs-6 column-break\">\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\tLine truncated
"html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header, render_field with context -%}\n{#- ERPNext hides taxes marked \"Is this Tax included in Basic Rate?\" unless Accounts Settings\n enables \"Show Inclusive Tax in Print\"; always show them here so the tax line and\n \"Total (Without Tax)\" appear on invoices with inclusive tax. -#}\n{%- if doc.get(\"taxes\", {\"included_in_print_rate\": 1}) -%}\n\t{%- set ignored = doc.flags.update({\"show_inclusive_tax_in_print\": 1}) -%}\n{%- endif -%}\n{#- App copies of ERPNext's totals includes, printing the posted figures: \"total\" is the net before\n the Additional Discount; \"taxes\" prints the discount's net share, Net Total and each charge after\n discount, so the lines add up to Grand Total whichever total the discount applies on. -#}\n{%- set ignored = doc.print_templates.update({\"total\": \"default_thai_company/templates/print_formats/includes/total.html\"}) -%}\n{%- if doc.print_templates.get(\"taxes\") -%}\n\t{%- set ignored = doc.print_templates.update({\"taxes\": \"default_thai_company/templates/print_formats/includes/taxes.html\"}) -%}\n{%- endif -%}\n{#- Standard layout prints every non-empty field; drop the ones this invoice does not need.\n posting_date is printed in the heading (due_date dropped: a receipt shows the paid date), customer fields in the Customer section,\n in_words in the left column of the Totals section. -#}\n{%- set hidden_fields = [\n\t\"total_qty\", \"update_billed_amount_in_delivery_note\", \"amount_eligible_for_commission\", \"company_tax_id\",\n\t\"disable_rounded_total\",\n\t\"posting_date\", \"due_date\",\n\t\"customer_name\", \"address_display\", \"contact_display\", \"contact_mobile\", \"contact_email\", \"tax_id\",\n\t\"in_words\",\n] -%}\n{#- rounded_total keeps a value even when rounding is disabled, so the standard layout would print it. -#}\n{%- if doc.is_rounded_total_disabled() -%}{%- set ignored = hidden_fields.append(\"rounded_total\") -%}{%- endif -%}\n{#- add_header renders this string via frappe.render_template with {\"doc\": doc}, so it is kept\n unrendered with {% raw %}. Same markup as the stock heading plus a right-aligned date block. -#}\n{%- set heading_template -%}\n{% raw %}\n<div class=\"print-heading\">\n\t<style>\n\t\t.print-heading .heading-dates { float: right; text-align: right; margin-top: 4px; white-space: nowrap; }\n\t\t.print-heading .heading-dates label { display: inline; font-weight: 700; margin: 0 4px 0 0; }\n\t\t.print-heading .heading-clear { clear: both; }\n\t</style>\n\t<div class=\"heading-dates\">\n\t\t<div><label>{{ _(doc.meta.get_label(\"posting_date\")) }}:</label> {{ doc.get_formatted(\"posting_date\") }}</div>\n\t\t{#- A receipt prints the date the invoice was paid: the latest submitted Payment Entry\n\t\t that references this invoice. -#}\n\t\t{%- set payment = frappe.get_all(\"Payment Entry\",\n\t\t\tfilters=[\n\t\t\t\t[\"docstatus\", \"=\", 1],\n\t\t\t\t[\"Payment Entry Reference\", \"reference_doctype\", \"=\", doc.doctype],\n\t\t\t\t[\"Payment Entry Reference\", \"reference_name\", \"=\", doc.name],\n\t\t\t],\n\t\t\tfields=[\"posting_date\"], order_by=\"posting_date desc\", limit_page_length=1) -%}\n\t\t{%- if payment %}\n\t\t<div><label>{{ _(\"Paid Date\") }}:</label> {{ frappe.format(payment[0].posting_date, {\"fieldtype\": \"Date\"}) }}</div>\n\t\t{%- endif %}\n\t</div>\n\t<h2>\n\t\t<div>{{ _(doc.select_print_heading) or (_(doc.print_heading) if doc.print_heading != None else _(\"Tax Invoice/Receipt\")) }}</div>\n\t\t<small class=\"sub-heading\">{{ _(doc.sub_heading) if doc.sub_heading != None else _(doc.name) }}</small>\n\t</h2>\n\t<div class=\"heading-clear\"></div>\n</div>\n{% endraw %}\n{%- endset -%}\n{#- Standard layout prints every non-empty child column; keep the item table to the essentials -#}\n{%- set item_columns = [\n\t{\"fieldname\": \"item_code\"},\n\t{\"fieldname\": \"description\"},\n\t{\"fieldname\": \"qty\"},\n\t{\"fieldname\": \"rate\"},\n\t{\"fieldname\": \"amount\"},\n] -%}\n\n{%- macro render_fields(fields, no_of_cols) -%}\n\t{%- for df in fields -%}\n\t\t{%- if df.fieldname not in hidden_fields -%}\n\t\t\t{%- if df.fieldname == \"items\" -%}\n\t\t\t\t{%- set df = df.as_dict() -%}\n\t\t\t\t{%- set ignored = df.update({\"visible_columns\": item_columns}) -%}\n\t\t\t{%- endif -%}\n\t\t\t{{ render_field(df, doc, no_of_cols) }}\n\t\t{%- endif -%}\n\t{%- endfor -%}\n{%- endmacro -%}\n\n{% for page in layout %}\n<div class=\"page-break\">\n\t<div {% if print_settings.repeat_header_footer %} id=\"header-html\" class=\"hidden-pdf\" {% endif %}>\n\t\t{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings, heading_template) }}\n\t</div>\n\n\t{% if loop.first %}\n\t<div class=\"row section-break\" data-label=\"{{ _(\"Customer\") }}\">\n\t\t<div class=\"col-xs-12 column-break\">\n\t\t\t<h4>{{ _(\"Customer\") }}</h4>\n\t\t\t<style>\n\t\t\t\t.customer-details { width: Line truncated
"line_breaks": 0,
"margin_bottom": 15.0,
"margin_left": 15.0,
"margin_right": 15.0,
"margin_top": 15.0,
"modified": "2026-09-15 07:10:28.117714",
"modified": "2026-10-20 12:00:00.000000",
"module": "Default Thai Company",
"name": "Default Standard Tax Invoice/Receipt",
"page_number": "Hide",
@@ -66,5 +66,175 @@
"report": null,
"show_section_headings": 0,
"standard": "No"
},
{
"absolute_value": 0,
"align_labels_right": 0,
"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 table 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\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,
"doc_type": "Quotation",
"docstatus": 0,
"doctype": "Print Format",
"font": null,
"font_size": 14,
"format_data": null,
"html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header, render_field with context -%}\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 quotations 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{#- Lead has no Tax ID; only a Customer party carries one. -#}\n{%- set tax_id = frappe.db.get_value(\"Customer\", doc.party_name, \"tax_id\") if doc.quotation_to == \"Customer\" else None -%}\n{#- Standard layout prints every non-empty field; drop the ones this quotation does not need.\n transaction_date / valid_till are printed in the heading, customer fields in the Customer section,\n in_words in the left column of the Totals section, the company address in the letter head. -#}\n{%- set hidden_fields = [\n\t\"total_qty\", \"company_address\", \"company_address_display\", \"has_unit_price_items\",\n\t\"disable_rounded_total\",\n\t\"transaction_date\", \"valid_till\",\n\t\"customer_name\", \"address_display\", \"contact_display\", \"contact_mobile\", \"contact_email\",\n\t\"in_words\",\n] -%}\n{#- rounded_total keeps a value even when rounding is disabled, so the standard layout would print it. -#}\n{%- if doc.is_rounded_total_disabled() -%}{%- set ignored = hidden_fields.append(\"rounded_total\") -%}{%- endif -%}\n{#- add_header renders this string via frappe.render_template with {\"doc\": doc}, so it is kept\n unrendered with {% raw %}. Same markup as the stock heading plus a right-aligned date block. -#}\n{%- set heading_template -%}\n{% raw %}\n<div class=\"print-heading\">\n\t<style>\n\t\t.print-heading .heading-dates { float: right; text-align: right; margin-top: 4px; white-space: nowrap; }\n\t\t.print-heading .heading-dates label { display: inline; font-weight: 700; margin: 0 4px 0 0; }\n\t\t.print-heading .heading-clear { clear: both; }\n\t</style>\n\t<div class=\"heading-dates\">\n\t\t<div><label>{{ _(doc.meta.get_label(\"transaction_date\")) }}:</label> {{ doc.get_formatted(\"transaction_date\") }}</div>\n\t\t{%- if doc.valid_till %}\n\t\t<div><label>{{ _(doc.meta.get_label(\"valid_till\")) }}:</label> {{ doc.get_formatted(\"valid_till\") }}</div>\n\t\t{%- endif %}\n\t</div>\n\t<h2>\n\t\t<div>{{ _(doc.select_print_heading) or (_(doc.print_heading) if doc.print_heading != None else _(doc.doctype)) }}</div>\n\t\t<small class=\"sub-heading\">{{ _(doc.sub_heading) if doc.sub_heading != None else _(doc.name) }}</small>\n\t</h2>\n\t<div class=\"heading-clear\"></div>\n</div>\n{% endraw %}\n{%- endset -%}\n{#- Standard layout prints every non-empty child column; keep the item table to the essentials -#}\n{%- set item_columns = [\n\t{\"fieldname\": \"item_code\"},\n\t{\"fieldname\": \"description\"},\n\t{\"fieldname\": \"qty\"},\n\t{\"fieldname\": \"rate\"},\n\t{\"fieldname\": \"amount\"},\n] -%}\n\n{%- macro render_fields(fields, no_of_cols) -%}\n\t{%- for df in fields -%}\n\t\t{%- if df.fieldname not in hidden_fields -%}\n\t\t\t{%- if df.fieldname == \"items\" -%}\n\t\t\t\t{%- set df = df.as_dict() -%}\n\t\t\t\t{%- set ignored = df.update({\"visible_columns\": item_columns}) -%}\n\t\t\t{%- endif -%}\n\t\t\t{{ render_field(df, doc, no_of_cols) }}\n\t\t{%- endif -%}\n\t{%- endfor -%}\n{%- endmacro -%}\n\n{% for page in layout %}\n<div class=\"page-break\">\n\t<div {% if print_settings.repeat_header_footer %} id=\"header-html\" class=\"hidden-pdf\" {% endif %}>\n\t\t{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings, heading_template) }}\n\t</div>\n\n\t{% if loop.first %}\n\t<div class=\"row section-break\" data-label=\"{{ _(\"Customer\") }}\">\n\t\t<div class=\"col-xs-12 column-break\">\n\t\t\t<h4>{{ _(\"Customer\") }}</h4>\n\t\t\t<style>\n\t\t\t\t.customer-details { width: 100%; }\n\t\t\t\t.customer-details td { padding: 0 !important; border: 0; vertical-align: top; }\n\t\t\t\t.customer-details td.text-muted { width: 40%; padding-right: 1em !important; white-space: nowrap; }\n\t\t\t</style>\n\t\t</div>\n\t\t<div class=\"col-xs-6 column-break\">\n\t\t\t<table class=\"customer-details\">\n\t\t\t\t<tbody>\n\t\t\t\t\t<tr><td class=\"text-muted\">{{ _(doc.meta.get_label(\"customer_name\")) }}</td><td>{{ doc.customer_name or doc.party_name }}</td></tr>\n\t\t\t\t\t{#- Re-rendered from the Address so its translations follow the print language. -#}\n\t\t\t\t\t{%- set address = get_print_address(doc.customer_address) -%}\n\t\t\t\t\t{% if address %}\n\t\t\t\t\t<tr><td class=\"text-muted\">{{ _(doc.meta.get_label(\"address_display\")) }}</td><td>{{ address }}</td></tr>\n\t\t\t\t\t{% endif %}\n\t\t\t\t\t{% if tax_id %}\n\t\t\t\t\t<tr><td class=\"text-muted\">{{ _(\"Tax ID\") }}</td><td>{{ tax_id }}</td></tr>\n\t\t\t\t\t{% endif %}\n\t\t\Line truncated
"line_breaks": 0,
"margin_bottom": 15.0,
"margin_left": 15.0,
"margin_right": 15.0,
"margin_top": 15.0,
"modified": "2026-10-20 12:00:00.000000",
"module": "Default Thai Company",
"name": "Default Standard Quotation",
"page_number": "Hide",
"pdf_generator": "wkhtmltopdf",
"print_format_builder": 0,
"print_format_builder_beta": 0,
"print_format_for": "DocType",
"print_format_type": "Jinja",
"raw_commands": null,
"raw_printing": 0,
"report": null,
"show_section_headings": 0,
"standard": "No"
},
{
"absolute_value": 0,
"align_labels_right": 0,
"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 Supplier table 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 .supplier-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,
"doc_type": "Purchase Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font": null,
"font_size": 14,
"format_data": null,
"html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header, render_field with context -%}\n{#- ERPNext hides taxes marked \"Is this Tax included in Basic Rate?\" unless Accounts Settings\n enables \"Show Inclusive Tax in Print\"; always show them here so the tax line and\n \"Total (Without Tax)\" appear on invoices with inclusive tax. -#}\n{%- if doc.get(\"taxes\", {\"included_in_print_rate\": 1}) -%}\n\t{%- set ignored = doc.flags.update({\"show_inclusive_tax_in_print\": 1}) -%}\n{%- endif -%}\n{#- App copies of ERPNext's totals includes, printing the posted figures: \"total\" is the net before\n the Additional Discount; \"taxes\" prints the discount's net share, Net Total and each charge after\n discount (withholding tax as a deduction), so the lines add up to Grand Total. -#}\n{%- set ignored = doc.print_templates.update({\"total\": \"default_thai_company/templates/print_formats/includes/total.html\"}) -%}\n{%- if doc.print_templates.get(\"taxes\") -%}\n\t{%- set ignored = doc.print_templates.update({\"taxes\": \"default_thai_company/templates/print_formats/includes/taxes.html\"}) -%}\n{%- endif -%}\n{#- Standard layout prints every non-empty field; drop the ones this invoice does not need.\n posting_date / due_date and the supplier's invoice reference are printed in the heading,\n supplier fields in the Supplier section, in_words in the left column of the Totals section,\n the company's billing address in the letter head. -#}\n{%- set hidden_fields = [\n\t\"total_qty\", \"tax_withholding_net_total\", \"update_outstanding_for_self\",\n\t\"update_billed_amount_in_purchase_order\", \"update_billed_amount_in_purchase_receipt\",\n\t\"billing_address\", \"billing_address_display\", \"supplier_group\", \"disable_rounded_total\",\n\t\"posting_date\", \"due_date\", \"bill_no\", \"bill_date\",\n\t\"supplier_name\", \"address_display\", \"contact_display\", \"contact_mobile\", \"contact_email\", \"tax_id\",\n\t\"in_words\",\n] -%}\n{#- rounded_total keeps a value even when rounding is disabled, so the standard layout would print it. -#}\n{%- if doc.is_rounded_total_disabled() -%}{%- set ignored = hidden_fields.append(\"rounded_total\") -%}{%- endif -%}\n{#- add_header renders this string via frappe.render_template with {\"doc\": doc}, so it is kept\n unrendered with {% raw %}. Same markup as the stock heading plus a right-aligned date block. -#}\n{%- set heading_template -%}\n{% raw %}\n<div class=\"print-heading\">\n\t<style>\n\t\t.print-heading .heading-dates { float: right; text-align: right; margin-top: 4px; white-space: nowrap; }\n\t\t.print-heading .heading-dates label { display: inline; font-weight: 700; margin: 0 4px 0 0; }\n\t\t.print-heading .heading-clear { clear: both; }\n\t</style>\n\t<div class=\"heading-dates\">\n\t\t<div><label>{{ _(doc.meta.get_label(\"posting_date\")) }}:</label> {{ doc.get_formatted(\"posting_date\") }}</div>\n\t\t{%- if doc.due_date %}\n\t\t<div><label>{{ _(doc.meta.get_label(\"due_date\")) }}:</label> {{ doc.get_formatted(\"due_date\") }}</div>\n\t\t{%- endif %}\n\t\t{%- if doc.bill_no %}\n\t\t<div><label>{{ _(doc.meta.get_label(\"bill_no\")) }}:</label> {{ doc.bill_no }}</div>\n\t\t{%- endif %}\n\t\t{%- if doc.bill_date %}\n\t\t<div><label>{{ _(doc.meta.get_label(\"bill_date\")) }}:</label> {{ doc.get_formatted(\"bill_date\") }}</div>\n\t\t{%- endif %}\n\t</div>\n\t<h2>\n\t\t<div>{{ _(doc.select_print_heading) or (_(doc.print_heading) if doc.print_heading != None else _(doc.doctype)) }}</div>\n\t\t<small class=\"sub-heading\">{{ _(doc.sub_heading) if doc.sub_heading != None else _(doc.name) }}</small>\n\t</h2>\n\t<div class=\"heading-clear\"></div>\n</div>\n{% endraw %}\n{%- endset -%}\n{#- Standard layout prints every non-empty child column; keep the item table to the essentials.\n Purchase Invoice Item marks item_code print_hide (it prints item_name instead); show the code\n as the other formats do. -#}\n{%- set item_columns = [\n\t{\"fieldname\": \"item_code\", \"print_hide\": 0},\n\t{\"fieldname\": \"description\"},\n\t{\"fieldname\": \"qty\"},\n\t{\"fieldname\": \"rate\"},\n\t{\"fieldname\": \"amount\"},\n] -%}\n\n{%- macro render_fields(fields, no_of_cols) -%}\n\t{%- for df in fields -%}\n\t\t{%- if df.fieldname not in hidden_fields -%}\n\t\t\t{%- if df.fieldname == \"items\" -%}\n\t\t\t\t{%- set df = df.as_dict() -%}\n\t\t\t\t{%- set ignored = df.update({\"visible_columns\": item_columns}) -%}\n\t\t\t{%- endif -%}\n\t\t\t{{ render_field(df, doc, no_of_cols) }}\n\t\t{%- endif -%}\n\t{%- endfor -%}\n{%- endmacro -%}\n\n{% for page in layout %}\n<div class=\"page-break\">\n\t<div {% if print_settings.repeat_header_footer %} id=\"header-html\" class=\"hidden-pdf\" {% endif %}>\n\t\t{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings, heading_template) }}\n\t</div>\n\n\t{% if loop.first %}\n\t<div class=\"row section-break\" data-label=\"{{ _(\"Supplier\") }}\">\n\t\t<div clLine truncated
"line_breaks": 0,
"margin_bottom": 15.0,
"margin_left": 15.0,
"margin_right": 15.0,
"margin_top": 15.0,
"modified": "2026-10-20 12:00:00.000000",
"module": "Default Thai Company",
"name": "Default Standard Purchase Invoice",
"page_number": "Hide",
"pdf_generator": "wkhtmltopdf",
"print_format_builder": 0,
"print_format_builder_beta": 0,
"print_format_for": "DocType",
"print_format_type": "Jinja",
"raw_commands": null,
"raw_printing": 0,
"report": null,
"show_section_headings": 0,
"standard": "No"
},
{
"absolute_value": 0,
"align_labels_right": 0,
"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 Supplier table 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 .supplier-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,
"doc_type": "Purchase Order",
"docstatus": 0,
"doctype": "Print Format",
"font": null,
"font_size": 14,
"format_data": null,
"html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header, render_field with context -%}\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 orders with inclusive tax. -#}\n{%- if doc.get(\"taxes\", {\"included_in_print_rate\": 1}) -%}\n\t{%- set ignored = doc.flags.update({\"show_inclusive_tax_in_print\": 1}) -%}\n{%- endif -%}\n{#- App copies of ERPNext's totals includes, printing the posted figures: \"total\" is the net before\n the Additional Discount; \"taxes\" prints the discount's net share, Net Total and each charge after\n discount (withholding tax as a deduction), so the lines add up to Grand Total. -#}\n{%- set ignored = doc.print_templates.update({\"total\": \"default_thai_company/templates/print_formats/includes/total.html\"}) -%}\n{%- if doc.print_templates.get(\"taxes\") -%}\n\t{%- set ignored = doc.print_templates.update({\"taxes\": \"default_thai_company/templates/print_formats/includes/taxes.html\"}) -%}\n{%- endif -%}\n{#- Purchase Order has no Tax ID field; read the supplier's. -#}\n{%- set tax_id = frappe.db.get_value(\"Supplier\", doc.supplier, \"tax_id\") -%}\n{#- Standard layout prints every non-empty field; drop the ones this order does not need.\n transaction_date / schedule_date are printed in the heading, supplier and shipping fields in the\n Supplier section, in_words in the left column of the Totals section, the company's billing\n address in the letter head. Withholding shows as a deduction in the taxes, not as its flag. -#}\n{%- set hidden_fields = [\n\t\"total_qty\", \"tax_withholding_net_total\", \"apply_tds\", \"tax_withholding_category\", \"has_unit_price_items\",\n\t\"billing_address\", \"billing_address_display\", \"disable_rounded_total\",\n\t\"transaction_date\", \"schedule_date\",\n\t\"supplier_name\", \"address_display\", \"contact_display\", \"contact_mobile\", \"contact_email\", \"shipping_address_display\",\n\t\"in_words\",\n] -%}\n{#- rounded_total keeps a value even when rounding is disabled, so the standard layout would print it. -#}\n{%- if doc.is_rounded_total_disabled() -%}{%- set ignored = hidden_fields.append(\"rounded_total\") -%}{%- endif -%}\n{#- add_header renders this string via frappe.render_template with {\"doc\": doc}, so it is kept\n unrendered with {% raw %}. Same markup as the stock heading plus a right-aligned date block. -#}\n{%- set heading_template -%}\n{% raw %}\n<div class=\"print-heading\">\n\t<style>\n\t\t.print-heading .heading-dates { float: right; text-align: right; margin-top: 4px; white-space: nowrap; }\n\t\t.print-heading .heading-dates label { display: inline; font-weight: 700; margin: 0 4px 0 0; }\n\t\t.print-heading .heading-clear { clear: both; }\n\t</style>\n\t<div class=\"heading-dates\">\n\t\t<div><label>{{ _(doc.meta.get_label(\"transaction_date\")) }}:</label> {{ doc.get_formatted(\"transaction_date\") }}</div>\n\t\t{%- if doc.schedule_date %}\n\t\t<div><label>{{ _(doc.meta.get_label(\"schedule_date\")) }}:</label> {{ doc.get_formatted(\"schedule_date\") }}</div>\n\t\t{%- endif %}\n\t</div>\n\t<h2>\n\t\t<div>{{ _(doc.select_print_heading) or (_(doc.print_heading) if doc.print_heading != None else _(doc.doctype)) }}</div>\n\t\t<small class=\"sub-heading\">{{ _(doc.sub_heading) if doc.sub_heading != None else _(doc.name) }}</small>\n\t</h2>\n\t<div class=\"heading-clear\"></div>\n</div>\n{% endraw %}\n{%- endset -%}\n{#- Standard layout prints every non-empty child column; keep the item table to the essentials -#}\n{%- set item_columns = [\n\t{\"fieldname\": \"item_code\"},\n\t{\"fieldname\": \"description\"},\n\t{\"fieldname\": \"qty\"},\n\t{\"fieldname\": \"rate\"},\n\t{\"fieldname\": \"amount\"},\n] -%}\n\n{%- macro render_fields(fields, no_of_cols) -%}\n\t{%- for df in fields -%}\n\t\t{%- if df.fieldname not in hidden_fields -%}\n\t\t\t{%- if df.fieldname == \"items\" -%}\n\t\t\t\t{%- set df = df.as_dict() -%}\n\t\t\t\t{%- set ignored = df.update({\"visible_columns\": item_columns}) -%}\n\t\t\t{%- endif -%}\n\t\t\t{{ render_field(df, doc, no_of_cols) }}\n\t\t{%- endif -%}\n\t{%- endfor -%}\n{%- endmacro -%}\n\n{% for page in layout %}\n<div class=\"page-break\">\n\t<div {% if print_settings.repeat_header_footer %} id=\"header-html\" class=\"hidden-pdf\" {% endif %}>\n\t\t{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings, heading_template) }}\n\t</div>\n\n\t{% if loop.first %}\n\t<div class=\"row section-break\" data-label=\"{{ _(\"Supplier\") }}\">\n\t\t<div class=\"col-xs-12 column-break\">\n\t\t\t<h4>{{ _(\"Supplier\") }}</h4>\n\t\t\t<style>\n\t\t\t\t.supplier-details { width: 100%; }\n\t\t\t\t.supplier-details td { padding: 0 !important; border: 0; vertical-align: top; }\n\t\t\t\t.supplier-details td.text-muted { width: 40%; padding-right: 1em !important; whLine truncated
"line_breaks": 0,
"margin_bottom": 15.0,
"margin_left": 15.0,
"margin_right": 15.0,
"margin_top": 15.0,
"modified": "2026-10-20 12:00:00.000000",
"module": "Default Thai Company",
"name": "Default Standard Purchase Order",
"page_number": "Hide",
"pdf_generator": "wkhtmltopdf",
"print_format_builder": 0,
"print_format_builder_beta": 0,
"print_format_for": "DocType",
"print_format_type": "Jinja",
"raw_commands": null,
"raw_printing": 0,
"report": null,
"show_section_headings": 0,
"standard": "No"
},
{
"absolute_value": 0,
"align_labels_right": 0,
"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 table 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\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,
"doc_type": "Sales Order",
"docstatus": 0,
"doctype": "Print Format",
"font": null,
"font_size": 14,
"format_data": null,
"html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header, render_field with context -%}\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 orders with inclusive tax. -#}\n{%- if doc.get(\"taxes\", {\"included_in_print_rate\": 1}) -%}\n\t{%- set ignored = doc.flags.update({\"show_inclusive_tax_in_print\": 1}) -%}\n{%- endif -%}\n{#- App copies of ERPNext's totals includes, printing the posted figures: \"total\" is the net before\n the Additional Discount; \"taxes\" prints the discount's net share, Net Total and each charge after\n discount, so the lines add up to Grand Total whichever total the discount applies on. -#}\n{%- set ignored = doc.print_templates.update({\"total\": \"default_thai_company/templates/print_formats/includes/total.html\"}) -%}\n{%- if doc.print_templates.get(\"taxes\") -%}\n\t{%- set ignored = doc.print_templates.update({\"taxes\": \"default_thai_company/templates/print_formats/includes/taxes.html\"}) -%}\n{%- endif -%}\n{#- Standard layout prints every non-empty field; drop the ones this order does not need.\n transaction_date / delivery_date and the customer's PO reference are printed in the heading,\n customer and shipping fields in the Customer section, in_words in the left column of the Totals\n section, the company address in the letter head. -#}\n{%- set hidden_fields = [\n\t\"total_qty\", \"has_unit_price_items\", \"amount_eligible_for_commission\", \"per_picked\", \"loyalty_points\",\n\t\"company_address\", \"company_address_display\", \"disable_rounded_total\",\n\t\"transaction_date\", \"delivery_date\", \"po_no\", \"po_date\",\n\t\"customer_name\", \"tax_id\", \"address_display\", \"contact_display\", \"contact_phone\", \"contact_mobile\", \"contact_email\", \"shipping_address\",\n\t\"in_words\",\n] -%}\n{#- rounded_total keeps a value even when rounding is disabled, so the standard layout would print it. -#}\n{%- if doc.is_rounded_total_disabled() -%}{%- set ignored = hidden_fields.append(\"rounded_total\") -%}{%- endif -%}\n{#- add_header renders this string via frappe.render_template with {\"doc\": doc}, so it is kept\n unrendered with {% raw %}. Same markup as the stock heading plus a right-aligned date block. -#}\n{%- set heading_template -%}\n{% raw %}\n<div class=\"print-heading\">\n\t<style>\n\t\t.print-heading .heading-dates { float: right; text-align: right; margin-top: 4px; white-space: nowrap; }\n\t\t.print-heading .heading-dates label { display: inline; font-weight: 700; margin: 0 4px 0 0; }\n\t\t.print-heading .heading-clear { clear: both; }\n\t</style>\n\t<div class=\"heading-dates\">\n\t\t<div><label>{{ _(doc.meta.get_label(\"transaction_date\")) }}:</label> {{ doc.get_formatted(\"transaction_date\") }}</div>\n\t\t{%- if doc.delivery_date %}\n\t\t<div><label>{{ _(doc.meta.get_label(\"delivery_date\")) }}:</label> {{ doc.get_formatted(\"delivery_date\") }}</div>\n\t\t{%- endif %}\n\t\t{%- if doc.po_no %}\n\t\t<div><label>{{ _(doc.meta.get_label(\"po_no\")) }}:</label> {{ doc.po_no }}</div>\n\t\t{%- endif %}\n\t\t{%- if doc.po_date %}\n\t\t<div><label>{{ _(doc.meta.get_label(\"po_date\")) }}:</label> {{ doc.get_formatted(\"po_date\") }}</div>\n\t\t{%- endif %}\n\t</div>\n\t<h2>\n\t\t<div>{{ _(doc.select_print_heading) or (_(doc.print_heading) if doc.print_heading != None else _(doc.doctype)) }}</div>\n\t\t<small class=\"sub-heading\">{{ _(doc.sub_heading) if doc.sub_heading != None else _(doc.name) }}</small>\n\t</h2>\n\t<div class=\"heading-clear\"></div>\n</div>\n{% endraw %}\n{%- endset -%}\n{#- Standard layout prints every non-empty child column; keep the item table to the essentials -#}\n{%- set item_columns = [\n\t{\"fieldname\": \"item_code\"},\n\t{\"fieldname\": \"description\"},\n\t{\"fieldname\": \"qty\"},\n\t{\"fieldname\": \"rate\"},\n\t{\"fieldname\": \"amount\"},\n] -%}\n\n{%- macro render_fields(fields, no_of_cols) -%}\n\t{%- for df in fields -%}\n\t\t{%- if df.fieldname not in hidden_fields -%}\n\t\t\t{%- if df.fieldname == \"items\" -%}\n\t\t\t\t{%- set df = df.as_dict() -%}\n\t\t\t\t{%- set ignored = df.update({\"visible_columns\": item_columns}) -%}\n\t\t\t{%- endif -%}\n\t\t\t{{ render_field(df, doc, no_of_cols) }}\n\t\t{%- endif -%}\n\t{%- endfor -%}\n{%- endmacro -%}\n\n{% for page in layout %}\n<div class=\"page-break\">\n\t<div {% if print_settings.repeat_header_footer %} id=\"header-html\" class=\"hidden-pdf\" {% endif %}>\n\t\t{{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings, heading_template) }}\n\t</div>\n\n\t{% if loop.first %}\n\t<div class=\"row section-break\" data-label=\"{{ _(\"Customer\") }}\">\n\t\t<div class=\"col-xs-12 column-break\">\n\t\t\t<h4>{{ _(\"Customer\") }}</h4>\n\t\t\t<style>\n\t\t\t\t.customer-details { width: 100%; }\n\t\t\t\t.customer-details td { Line truncated
"line_breaks": 0,
"margin_bottom": 15.0,
"margin_left": 15.0,
"margin_right": 15.0,
"margin_top": 15.0,
"modified": "2026-10-20 12:00:00.000000",
"module": "Default Thai Company",
"name": "Default Standard Sales Order",
"page_number": "Hide",
"pdf_generator": "wkhtmltopdf",
"print_format_builder": 0,
"print_format_builder_beta": 0,
"print_format_for": "DocType",
"print_format_type": "Jinja",
"raw_commands": null,
"raw_printing": 0,
"report": null,
"show_section_headings": 0,
"standard": "No"
},
{
"absolute_value": 0,
"align_labels_right": 0,
"css": "/* Thai text needs a font with Thai glyphs; ship Sarabun as in Default Standard Sales Invoice. */\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\n/* The Revenue Department form fills the whole A4 sheet: no page margins. frappe.utils.pdf reads\n these .print-format properties as the wkhtmltopdf page options. */\n.print-format {\n\tmargin-top: 0mm;\n\tmargin-bottom: 0mm;\n\tmargin-left: 0mm;\n\tmargin-right: 0mm;\n\tpage-size: A4;\n}\n/* On screen the standard sheet padding would clip the 595pt form. */\n@media screen {\n\t.print-format {\n\t\tpadding: 0;\n\t\tmax-width: 595pt;\n\t\tmin-height: 0;\n\t}\n}\n\n/* One sheet per copy; values are positioned in pt against the form's PDF coordinates. */\n.wht-page {\n\tposition: relative;\n\twidth: 595pt;\n\theight: 841pt;\n\toverflow: hidden;\n\tpage-break-after: always;\n\tfont-family: Sarabun, sans-serif;\n\tfont-size: 10pt;\n\tcolor: #000;\n}\n.wht-page:last-child {\n\tpage-break-after: auto;\n}\n.wht-form {\n\tposition: absolute;\n\tleft: 0;\n\ttop: 0;\n\twidth: 100%;\n\theight: 100%;\n}\n.wht-page .f {\n\tposition: absolute;\n\twhite-space: nowrap;\n\toverflow: hidden;\n}\n/* Check mark drawn in CSS: the shipped font has no U+2713 and the PDF renderer sees no system fonts. */\n.wht-page .tick {\n\tposition: absolute;\n}\n.wht-page .tick:after {\n\tcontent: \"\";\n\tposition: absolute;\n\tleft: 4pt;\n\ttop: 0;\n\twidth: 4pt;\n\theight: 8pt;\n\tborder: solid #000;\n\tborder-width: 0 1.5pt 1.5pt 0;\n\t-webkit-transform: rotate(45deg);\n\ttransform: rotate(45deg);\n}\n\n/* Unsubmitted invoice: stamp every copy so a preview cannot pass for a certificate. */\n.wht-page .wht-draft {\n\tposition: absolute;\n\tleft: 0;\n\t/* a square centred on the sheet: the rotated text stays inside its own box */\n\ttop: 123pt;\n\twidth: 595pt;\n\theight: 595pt;\n\tline-height: 595pt;\n\ttext-align: center;\n\tfont-size: 120pt;\n\tfont-weight: 700;\n\tletter-spacing: 6pt;\n\t/* wkhtmltopdf drops opacity on transformed elements; an rgba colour keeps the form legible */\n\tcolor: rgba(204, 0, 0, 0.3);\n\t-webkit-transform: rotate(-35deg);\n\ttransform: rotate(-35deg);\n}",
"custom_format": 1,
"default_print_language": null,
"disabled": 0,
"doc_type": "Purchase Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font": null,
"font_size": 10,
"format_data": null,
"html": "{% include \"default_thai_company/templates/print_formats/withholding_tax_certificate.html\" %}",
"line_breaks": 0,
"margin_bottom": 0.0,
"margin_left": 0.0,
"margin_right": 0.0,
"margin_top": 0.0,
"modified": "2026-09-23 11:00:00.000000",
"module": "Default Thai Company",
"name": "Withholding Tax Certificate",
"page_number": "Hide",
"pdf_generator": "wkhtmltopdf",
"print_format_builder": 0,
"print_format_builder_beta": 0,
"print_format_for": "DocType",
"print_format_type": "Jinja",
"raw_commands": null,
"raw_printing": 0,
"report": null,
"show_section_headings": 0,
"standard": "No"
}
]
]
@@ -5,7 +5,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Fees and Commissions - Sec. 40(2)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 3% - Commission",
"rates": [
{
@@ -31,7 +32,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Royalties - Sec. 40(3)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 3% - Royalty",
"rates": [
{
@@ -57,7 +59,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Interest - Sec. 40(4)(a)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 1% - Interest (Company)",
"rates": [
{
@@ -83,7 +86,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Interest - Sec. 40(4)(a)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 15% - Interest (Individual)",
"rates": [
{
@@ -109,7 +113,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Dividends - Sec. 40(4)(b)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 10% - Dividend",
"rates": [
{
@@ -135,7 +140,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 5% - Rent",
"rates": [
{
@@ -161,7 +167,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 1% - Ship Rental",
"rates": [
{
@@ -187,7 +194,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 3% - Professional Fee",
"rates": [
{
@@ -213,7 +221,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 3% - Contract Work",
"rates": [
{
@@ -239,7 +248,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 3% - Service",
"rates": [
{
@@ -265,7 +275,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 3% - Sales Promotion",
"rates": [
{
@@ -291,7 +302,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 2% - Advertising",
"rates": [
{
@@ -317,7 +329,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 1% - Transportation",
"rates": [
{
@@ -343,7 +356,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 1% - Non-Life Insurance",
"rates": [
{
@@ -369,7 +383,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 5% - Prize",
"rates": [
{
@@ -395,7 +410,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 5% - Public Entertainer",
"rates": [
{
@@ -421,7 +437,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 0.75% - Agricultural Produce",
"rates": [
{
@@ -447,7 +464,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Sec. 3 Tera (Services, Rent, Contract Work etc.)",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 1% - Aquatic Animals",
"rates": [
{
@@ -473,7 +491,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Other",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 1% - Immovable Property (Company)",
"rates": [
{
@@ -499,7 +518,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Other",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 15% - Non-Resident Individual",
"rates": [
{
@@ -525,7 +545,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Other",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 15% - Foreign Company",
"rates": [
{
@@ -551,7 +572,8 @@
"consider_party_ledger_amount": 0,
"docstatus": 0,
"doctype": "Tax Withholding Category",
"modified": "2026-09-11 09:00:00.000000",
"income_type": "Other",
"modified": "2026-09-22 12:00:00.000000",
"name": "WHT 10% - Foreign Company Dividend",
"rates": [
{
+77 -23
View File
@@ -43,7 +43,10 @@ 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 = {
"Payment Entry": "public/js/payment_entry.js",
"Purchase Invoice": "public/js/purchase_invoice.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"}
@@ -78,7 +81,9 @@ jinja = {
"methods": [
"default_thai_company.utils.get_letter_head_company",
"default_thai_company.utils.get_company_bank_account",
"default_thai_company.utils.get_print_address",
"default_thai_company.utils.get_in_words",
"default_thai_company.tax_withholding.get_withholding_certificate",
],
}
@@ -93,17 +98,68 @@ fixtures = [
{
"doctype": "Print Format",
"filters": [
["name", "in", ["Default Standard Sales Invoice", "Default Standard Tax Invoice/Receipt"]]
[
"name",
"in",
[
"Default Standard Sales Invoice",
"Default Standard Tax Invoice/Receipt",
"Default Standard Quotation",
"Default Standard Sales Order",
"Default Standard Purchase Order",
"Default Standard Purchase Invoice",
"Withholding Tax Certificate",
],
]
],
},
{"doctype": "Tax Withholding Category", "filters": [["name", "like", "WHT %"]]},
{
"doctype": "Location",
"filters": [["name", "in", ["All Locations", "Head Office", "Branch Office", "Warehouse"]]],
},
{
"doctype": "Asset Category",
"filters": [
[
"name",
"in",
[
"Land",
"Buildings",
"Plant and Machinery",
"Vehicles",
"Furniture and Fixtures",
"Office Equipment",
"Computers and Electronics",
"Software",
"Intangible Assets",
],
]
],
},
# HRMS doctypes; skipped by sync_fixtures on sites without hrms
{"doctype": "Employee Tax Exemption Category"},
{"doctype": "Employee Tax Exemption Sub Category"},
{"doctype": "Income Tax Slab", "filters": [["name", "=", "Thailand Personal Income Tax"]]},
]
# Installation
# ------------
# before_install = "default_thai_company.install.before_install"
# after_install = "default_thai_company.install.after_install"
# Fixtures are synced before Custom Fields on install and migrate; the Tax Withholding
# Category fixtures carry values for the app's Custom Fields, so create those first.
before_install = "default_thai_company.tax_withholding.sync_category_customizations"
before_migrate = "default_thai_company.tax_withholding.sync_category_customizations"
# Fixtures are synced before this runs; VAT schemes and Shipping Rules for companies
# that already exist.
after_install = [
"default_thai_company.vat.setup_companies",
"default_thai_company.shipping.setup_companies",
"default_thai_company.payroll.setup_companies",
]
# Fixture import re-inserts the Location tree; restore lft/rgt for user-added nodes.
after_migrate = "default_thai_company.assets.rebuild_locations"
# Uninstallation
# ------------
@@ -117,7 +173,8 @@ fixtures = [
# Name of the app being installed is passed as an argument
# before_app_install = "default_thai_company.utils.before_app_install"
# after_app_install = "default_thai_company.utils.after_app_install"
# Payroll Period for companies created before hrms was installed.
after_app_install = "default_thai_company.payroll.after_app_install"
# Integration Cleanup
# -------------------
@@ -152,6 +209,7 @@ fixtures = [
override_doctype_class = {
"Sales Invoice": "default_thai_company.tax_withholding.ThaiSalesInvoice",
"Payment Entry": "default_thai_company.tax_withholding.ThaiPaymentEntry",
"Shipping Rule": "default_thai_company.shipping.ThaiShippingRule",
}
# Document Events
@@ -166,8 +224,17 @@ doc_events = {
"Tax Withholding Category": {
"before_import": "default_thai_company.tax_withholding.prepare_fixture_accounts",
},
"Asset Category": {
"before_import": "default_thai_company.assets.prepare_fixture_accounts",
},
"Company": {
"on_update": "default_thai_company.tax_withholding.setup_company",
"on_update": [
"default_thai_company.tax_withholding.setup_company",
"default_thai_company.vat.setup_company",
"default_thai_company.assets.setup_company",
"default_thai_company.shipping.setup_company",
"default_thai_company.payroll.setup_company",
],
},
"Payment Entry": {
"validate": "default_thai_company.tax_withholding.set_customer_withholding",
@@ -177,23 +244,10 @@ doc_events = {
# Scheduled Tasks
# ---------------
# scheduler_events = {
# "all": [
# "default_thai_company.tasks.all"
# ],
# "daily": [
# "default_thai_company.tasks.daily"
# ],
# "hourly": [
# "default_thai_company.tasks.hourly"
# ],
# "weekly": [
# "default_thai_company.tasks.weekly"
# ],
# "monthly": [
# "default_thai_company.tasks.monthly"
# ],
# }
# Payroll Period is per calendar year; roll the default over on 1 January.
scheduler_events = {
"daily": ["default_thai_company.payroll.setup_companies"],
}
# Testing
# -------
+5 -1
View File
@@ -3,4 +3,8 @@
# Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations
[post_model_sync]
# Patches added in this section will be executed after doctypes are migrated
# Patches added in this section will be executed after doctypes are migrated
default_thai_company.patches.create_vat_accounts
default_thai_company.patches.create_vat_templates
default_thai_company.patches.create_shipping_rules
default_thai_company.patches.create_payroll_periods
Whitespace-only changes.
@@ -0,0 +1,5 @@
from default_thai_company.payroll import setup_companies
def execute():
setup_companies()
@@ -0,0 +1,5 @@
from default_thai_company.shipping import setup_companies
def execute():
setup_companies()
@@ -0,0 +1,8 @@
from default_thai_company.tax_withholding import ensure_company_accounts, thai_companies
def execute():
"""Output VAT / Input VAT were added to COMPANY_ACCOUNTS; Company.on_update
only runs on save, so give existing Thai companies the pair."""
for company in thai_companies():
ensure_company_accounts(company)
@@ -0,0 +1,5 @@
from default_thai_company.vat import setup_companies
def execute():
setup_companies()
+62
View File
@@ -0,0 +1,62 @@
import frappe
from frappe.utils import getdate
from default_thai_company.tax_withholding import thai_companies
# Thai personal income tax (P.N.D.1/91) is assessed per calendar year whatever
# the company's fiscal year, so the Payroll Period is January to December.
# Payroll Period is named by prompt (site-wide unique) but bound to one company,
# so each Thai company gets "<year> - <abbr>", like the Shipping Rule.
def hrms_installed():
return "hrms" in frappe.get_installed_apps()
def payroll_period_name(company, year):
abbr = frappe.get_cached_value("Company", company, "abbr")
return f"{year} - {abbr}"
def ensure_payroll_period(company, year):
"""Calendar-year Payroll Period for `company`. No-op when any period of the
company touches the year: an existing one covers it, or the company runs its
own scheme and a second period would only fail the overlap check."""
start, end = f"{year}-01-01", f"{year}-12-31"
if frappe.db.exists(
"Payroll Period", {"company": company, "start_date": ("<=", end), "end_date": (">=", start)}
):
return
frappe.get_doc(
{
"doctype": "Payroll Period",
"name": payroll_period_name(company, year),
"company": company,
"start_date": start,
"end_date": end,
}
).insert(ignore_permissions=True)
def setup_company(doc, method=None):
"""Company.on_update: this year's Payroll Period for a Thai company."""
if doc.country == "Thailand" and hrms_installed():
ensure_payroll_period(doc.name, getdate().year)
def setup_companies():
"""after_install, the daily scheduler, and the create_payroll_periods patch:
this year's Payroll Period for every Thai company. The scheduler rolls the
default over on 1 January."""
if not hrms_installed():
return
year = getdate().year
for company in thai_companies():
ensure_payroll_period(company, year)
def after_app_install(app_name):
"""Payroll Period only exists once hrms is installed; catch companies that
were created before it."""
if app_name == "hrms":
setup_companies()
Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

@@ -1,4 +1,18 @@
frappe.ui.form.on("Payment Entry", {
onload(frm) {
if (frm.is_new() && frm.doc.posting_date && !frm.doc.reference_date) {
frm.set_value("reference_date", frm.doc.posting_date);
}
},
// Cheque/Reference Date follows Posting Date; the field stays editable for the
// cases where the cheque is dated differently.
posting_date(frm) {
if (frm.doc.posting_date) {
frm.set_value("reference_date", frm.doc.posting_date);
}
},
apply_tax_withholding_amount(frm) {
if (frm.doc.party_type !== "Customer" || frm.doc.payment_type !== "Receive") return;
if (!frm.doc.apply_tax_withholding_amount) {
@@ -0,0 +1,23 @@
frappe.ui.form.on("Purchase Invoice", {
refresh(frm) {
// The certificate is issued once the tax has been withheld, i.e. on payment.
if (!frm.doc.apply_tds || frm.doc.docstatus !== 1 || frm.doc.status !== "Paid") return;
// The Withholding Tax Certificate print format renders the Revenue Department's
// Sec. 50 bis form (fixtures/print_format.json) as a PDF.
frm.add_custom_button(__("Withholding Tax Certificate"), () => {
const params = new URLSearchParams({
doctype: frm.doctype,
name: frm.docname,
format: "Withholding Tax Certificate",
no_letterhead: 1,
_lang: "th",
});
const w = window.open(
frappe.urllib.get_full_url(
`/api/method/frappe.utils.print_format.download_pdf?${params}`
)
);
if (!w) frappe.msgprint(__("Please enable pop-ups"));
});
},
});
+66
View File
@@ -0,0 +1,66 @@
import erpnext
import frappe
from erpnext.accounts.doctype.shipping_rule.shipping_rule import ShippingRule
from default_thai_company.tax_withholding import company_ready, get_or_create_account, thai_companies
# Shipping Rule is named by its label (site-wide unique) but bound to one company,
# so each Thai company gets "<LABEL> - <abbr>", the way ERPNext names accounts and
# tax templates. The label doubles as the charge description on the transaction.
LABEL = "Shipping Charges"
# Shipping billed to customers is revenue; the courier's bill stays an expense.
SHIPPING_ACCOUNT = "Shipping Charges"
SHIPPING_ACCOUNT_GROUPS = ("Direct Income", "Income")
class ThaiShippingRule(ShippingRule):
def add_shipping_rule_to_tax_table(self, doc, shipping_amount):
"""A Fixed rule without an amount only seeds the charge row; the amount is
entered on the transaction. ERPNext re-applies the rule on every
recalculation, which would otherwise reset the row to 0."""
manual = self.calculate_based_on == "Fixed" and not self.shipping_amount
entered = [(row, row.tax_amount) for row in doc.get("taxes")] if manual else ()
super().add_shipping_rule_to_tax_table(doc, shipping_amount)
for row, amount in entered:
row.tax_amount = amount
def shipping_rule_name(company):
abbr = frappe.get_cached_value("Company", company, "abbr")
return f"{LABEL} - {abbr}"
def ensure_shipping_rule(company):
"""Selling Shipping Rule for `company` with the amount entered per
transaction (see ThaiShippingRule). No-op when the rule exists."""
name = shipping_rule_name(company)
if frappe.db.exists("Shipping Rule", name):
return
frappe.get_doc(
{
"doctype": "Shipping Rule",
"label": name,
"company": company,
"shipping_rule_type": "Selling",
"calculate_based_on": "Fixed",
"shipping_amount": 0,
"account": get_or_create_account(
company, SHIPPING_ACCOUNT, "Income", "Income Account", SHIPPING_ACCOUNT_GROUPS
),
"cost_center": erpnext.get_default_cost_center(company),
}
).insert(ignore_permissions=True)
def setup_company(doc, method=None):
"""Company.on_update: default Shipping Rule for a Thai company."""
if company_ready(doc):
ensure_shipping_rule(doc.name)
def setup_companies():
"""after_install and the create_shipping_rules patch: companies that exist
before this code did never pass through `setup_company`."""
for company in thai_companies():
ensure_shipping_rule(company)
+182 -13
View File
@@ -1,3 +1,6 @@
import json
import os
import erpnext
import frappe
from erpnext.accounts.doctype.payment_entry.payment_entry import PaymentEntry
@@ -8,14 +11,29 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
normal_round,
)
from frappe import _
from frappe.utils import cint, flt
from frappe.contacts.doctype.address.address import get_default_address
from frappe.modules.utils import sync_customizations_for_doctype
from frappe.utils import cint, flt, fmt_money, getdate
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.
# Tax accounts every Thai company gets (Company.on_update, fixture import, and the
# create_vat_accounts patch for companies that predate the VAT pair).
# Withholding: liability = tax we withhold from suppliers, remitted on P.N.D.3/53
# (linked on every category); asset = tax customers withhold from us, creditable
# against corporate income tax.
# VAT: output (ภาษีขาย) collected on sales is payable, input (ภาษีซื้อ) paid on
# purchases is recoverable; the P.P.30 return nets the two, so they are kept apart.
LIABILITY_ACCOUNT = "Withholding Tax Payable"
ASSET_ACCOUNT = "Withholding Tax Receivable"
OUTPUT_VAT_ACCOUNT = "Output VAT"
INPUT_VAT_ACCOUNT = "Input VAT"
COMPANY_ACCOUNTS = (
(LIABILITY_ACCOUNT, "Liability"),
(ASSET_ACCOUNT, "Asset"),
(OUTPUT_VAT_ACCOUNT, "Liability"),
(INPUT_VAT_ACCOUNT, "Asset"),
)
# Preferred parent groups by root type; first match wins, else the root account.
PARENT_GROUPS = {
@@ -30,7 +48,9 @@ def thai_companies():
return frappe.get_all("Company", filters={"country": "Thailand"}, pluck="name")
def get_or_create_account(company, account_name, root_type):
def get_or_create_account(company, account_name, root_type, account_type="Tax", parent_groups=None):
"""Leaf account `account_name` for `company`, created under the first existing
group in `parent_groups` (default: PARENT_GROUPS[root_type]), else the root."""
name = frappe.db.get_value(
"Account", {"company": company, "account_name": account_name, "is_group": 0, "root_type": root_type}
)
@@ -42,17 +62,19 @@ def get_or_create_account(company, account_name, root_type):
"doctype": "Account",
"company": company,
"account_name": account_name,
"parent_account": find_parent_group(company, root_type),
"parent_account": find_parent_group(
company, root_type, parent_groups or PARENT_GROUPS[root_type]
),
"root_type": root_type,
"account_type": "Tax",
"account_type": account_type,
"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]:
def find_parent_group(company, root_type, group_names):
for account_name in group_names:
parent = frappe.db.get_value(
"Account",
{"company": company, "account_name": account_name, "is_group": 1, "root_type": root_type},
@@ -67,9 +89,10 @@ def find_parent_group(company, root_type):
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")
"""Create every account in COMPANY_ACCOUNTS for `company`; return the
withholding liability account name (the one Tax Withholding Categories link)."""
accounts = {name: get_or_create_account(company, name, root_type) for name, root_type in COMPANY_ACCOUNTS}
return accounts[LIABILITY_ACCOUNT]
def link_company(category, company, account):
@@ -104,9 +127,25 @@ def prepare_fixture_accounts(doc, method=None):
link_company(doc, company, ensure_company_accounts(company))
def sync_category_customizations():
"""before_install / before_migrate: create the Tax Withholding Category Custom
Fields (custom/tax_withholding_category.json) ahead of the fixture import.
Frappe syncs fixtures before customizations, and a fixture value for a field
that does not exist yet is dropped."""
folder = frappe.get_app_path("default_thai_company", "default_thai_company", "custom")
filename = "tax_withholding_category.json"
with open(os.path.join(folder, filename)) as f:
sync_customizations_for_doctype(json.load(f), folder, filename)
def company_ready(doc):
"""A Thai company whose chart of accounts exists."""
return doc.country == "Thailand" and frappe.db.exists("Account", {"company": doc.name})
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}):
"""Company.on_update: create the tax accounts and link every WHT category."""
if not company_ready(doc):
return
account = ensure_company_accounts(doc.name)
@@ -351,3 +390,133 @@ def get_customer_withholding(doc):
else:
description = _("Withholding tax deducted by customer")
return flt(total, precision), description
# Withholding tax certificate (หนังสือรับรองการหักภาษี ณ ที่จ่าย, Sec. 50 bis): the income
# row the payment is reported on, from Tax Withholding Category.income_type (custom field).
# Rows: 1 = Sec. 40(1), 2 = 40(2), 3 = 40(3), 4a = 40(4)(a), 4b = 40(4)(b) dividends
# paid from profits taxed at the standard 20% rate (form row 4(b)(1)(1.3)), 5 = payments
# withheld under Revenue Department orders issued under Sec. 3 tera, 6 = other (specified).
CERTIFICATE_ROWS = {
"Salary and Wages - Sec. 40(1)": "1",
"Fees and Commissions - Sec. 40(2)": "2",
"Royalties - Sec. 40(3)": "3",
"Interest - Sec. 40(4)(a)": "4a",
"Dividends - Sec. 40(4)(b)": "4b",
"Sec. 3 Tera (Services, Rent, Contract Work etc.)": "5",
}
THAI_MONTHS = (
"มกราคม",
"กุมภาพันธ์",
"มีนาคม",
"เมษายน",
"พฤษภาคม",
"มิถุนายน",
"กรกฎาคม",
"สิงหาคม",
"กันยายน",
"ตุลาคม",
"พฤศจิกายน",
"ธันวาคม",
)
def pnd_form(row, supplier_type):
"""P.N.D. return the certificate row is filed on, as numbered on the form:
1 = ภ.ง.ด.1ก, 2 = ภ.ง.ด.2, 3 = ภ.ง.ด.3, 53 = ภ.ง.ด.53.
Juristic payees (Company; Partnership, taken as registered) file on
ภ.ง.ด.53 whatever the income; individuals by income type: salary on
ภ.ง.ด.1ก, Sec. 40(3)/(4) on ภ.ง.ด.2, the rest on ภ.ง.ด.3.
"""
if supplier_type != "Individual":
return "53"
if row == "1":
return "1"
if row in ("3", "4a", "4b"):
return "2"
return "3"
def one_line_address(address_name):
"""Address in the order of the Thailand Address Template, on one line.
The form is Thai, so each part goes through its Thai translation (the
Address field's translate button) whatever the print language."""
if not address_name:
return None
address = frappe.get_cached_doc("Address", address_name)
parts = [
address.address_line1,
address.address_line2,
address.county,
address.city,
address.state,
]
parts = [_(part.strip(), lang="th") for part in parts if part and part.strip()]
if address.pincode and address.pincode.strip():
parts.append(address.pincode.strip())
if address.country and address.country != "Thailand":
parts.append(_(address.country, lang="th"))
return " ".join(parts)
def tax_id_digits(tax_id):
"""The 13 digits of a Thai tax ID for the form's boxes, or None when it is not one."""
digits = "".join(ch for ch in (tax_id or "") if ch.isdigit())
return digits if len(digits) == 13 else None
def get_withholding_certificate(doc):
"""Certificate data for a Purchase Invoice with Apply Tax Withholding Amount.
Amounts are in company currency; the form is Thai, so dates are Buddhist Era
and the tax in words is Thai whatever the print language.
"""
company = frappe.get_cached_doc("Company", doc.company)
currency = erpnext.get_company_currency(doc.company)
precision = doc.precision("base_grand_total")
category = (
frappe.get_cached_doc("Tax Withholding Category", doc.tax_withholding_category)
if doc.tax_withholding_category
else None
)
row = CERTIFICATE_ROWS.get(category.income_type if category else None, "6")
supplier_type = frappe.get_cached_value("Supplier", doc.supplier, "supplier_type")
amount = flt(doc.base_tax_withholding_net_total, precision)
tax = flt(
sum(abs(flt(t.base_tax_amount)) for t in doc.get("taxes") if cint(t.is_tax_withholding_account)),
precision,
)
posting_date = getdate(doc.posting_date)
return frappe._dict(
payer=frappe._dict(
name=company.company_name,
tax_id=company.tax_id,
tax_id_digits=tax_id_digits(company.tax_id),
address=one_line_address(get_default_address("Company", doc.company)),
),
payee=frappe._dict(
name=doc.supplier_name,
tax_id=doc.tax_id,
tax_id_digits=tax_id_digits(doc.tax_id),
address=one_line_address(doc.supplier_address or get_default_address("Supplier", doc.supplier)),
),
row=row,
# row 6 prints what was paid for; translations/th.csv carries the fixture categories
row_note=_(category.category_name) if row == "6" and category else None,
pnd=pnd_form(row, supplier_type),
date=f"{posting_date.day:02d}/{posting_date.month:02d}/{posting_date.year + 543}",
amount=fmt_money(amount, precision),
tax=fmt_money(tax, precision),
tax_in_words=money_in_words(tax, currency, lang="th"),
issued=frappe._dict(
day=posting_date.day,
month=THAI_MONTHS[posting_date.month - 1],
year=posting_date.year + 543,
),
)
@@ -0,0 +1,54 @@
{#- ERPNext's templates/print_formats/includes/taxes.html, printing the amounts that are posted.
ERPNext takes an Additional Discount off the items' net amounts (item.distributed_discount_amount;
a discount on Grand Total is split between net and taxes) and books each charge at
tax_amount_after_discount_amount, while upstream prints the discount as entered and the
pre-discount tax_amount, so the lines do not add up to Grand Total.
Inclusive tax with a discount on Grand Total: the Total line is the tax-inclusive item total
and the discount is printed as entered; Total After Discount is then broken down into
Net Total and the charges. Otherwise the Total line is the net before the discount, so the
discount is its net share, followed by Net Total and the charges. A discount that is not
distributed (cash / non-trade) comes off the grand total only and stays after the charges,
as upstream.
Purchase documents: a "Deduct" charge (withholding tax) is taken off the total, so it is
printed negative; a "Valuation" charge only affects stock value and is not printed. Charge
descriptions are translated: the withholding categories' are in translations/th.csv. -#}
{%- macro amount_row(label, value) -%}
<div class="row">
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ label }}</label>
</div>
<div class="col-xs-7 text-right">
{{ value }}
</div>
</div>
{%- endmacro -%}
{%- macro currency(value) -%}
{{ frappe.format_value(value, {"fieldtype": "Currency", "options": "currency"}, doc) }}
{%- endmacro -%}
{%- set net_discount = doc.get("items")|map(attribute="distributed_discount_amount")|select|sum -%}
{%- set gross_discount = net_discount and doc.apply_discount_on == "Grand Total" and doc.flags.show_inclusive_tax_in_print -%}
<div class="row">
<div class="col-xs-6"></div>
<div class="col-xs-6">
{%- if gross_discount -%}
{{ amount_row(_(doc.meta.get_label("discount_amount")), "- " ~ doc.get_formatted("discount_amount", doc)) }}
{{ amount_row(_("Total After Discount"), currency(doc.total - doc.discount_amount)) }}
{{ amount_row(_(doc.meta.get_label("net_total")), doc.get_formatted("net_total", doc)) }}
{%- elif net_discount -%}
{{ amount_row(_(doc.meta.get_label("discount_amount")), "- " ~ currency(net_discount)) }}
{{ amount_row(_(doc.meta.get_label("net_total")), doc.get_formatted("net_total", doc)) }}
{%- endif -%}
{%- for charge in data -%}
{%- if (charge.tax_amount or print_settings.print_taxes_with_zero_amount) and (not charge.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) and charge.get("category") != "Valuation" -%}
{{ amount_row(charge.get_formatted("description", translated=True), ("- " if charge.get("add_deduct_tax") == "Deduct" else "") ~ charge.get_formatted("tax_amount_after_discount_amount", doc)) }}
{%- endif -%}
{%- endfor -%}
{%- if doc.discount_amount and not net_discount -%}
{{ amount_row(_(doc.meta.get_label("discount_amount")), "- " ~ doc.get_formatted("discount_amount", doc)) }}
{%- endif -%}
</div>
</div>
@@ -0,0 +1,22 @@
{#- ERPNext's templates/print_formats/includes/total.html, paired with the taxes include:
with inclusive tax and a discount on Grand Total the Total line is the tax-inclusive item
total (the discount is printed as entered below it); with inclusive tax otherwise it is
the net before the Additional Discount, since net_total already has the discount's net
share (item.distributed_discount_amount) taken off. -#}
{%- set net_discount = doc.get("items")|map(attribute="distributed_discount_amount")|select|sum -%}
{%- set gross_discount = net_discount and doc.apply_discount_on == "Grand Total" and doc.flags.show_inclusive_tax_in_print -%}
<div class="row {% if df.bold %}important{% endif %} data-field">
{% if doc.flags.show_inclusive_tax_in_print and not gross_discount %}
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ _("Total (Without Tax)") }}</label></div>
<div class="col-xs-7 text-right value">
{{ frappe.format_value(doc.net_total + net_discount, {"fieldtype": "Currency", "options": "currency"}, doc) }}
</div>
{% else %}
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ _(df.label) }}</label></div>
<div class="col-xs-7 text-right value">
{{ doc.get_formatted("total", doc) }}
</div>
{% endif %}
</div>
@@ -0,0 +1,90 @@
{#- หนังสือรับรองการหักภาษี ณ ที่จ่าย (Sec. 50 bis certificate) for a Purchase Invoice.
The Revenue Department's fillable form (approve_wh3_081156.pdf, A4 = 595 x 842 pt) is the page
background; every value sits in the rectangle of the corresponding AcroForm field, given here as
the PDF's (x0, y0, x1, y1) with the origin at the bottom-left so the numbers can be checked against
the form. Two identical copies are printed: copy 1 goes with the payee's tax return, copy 2 is the
payee's record. -#}
{%- set c = get_withholding_certificate(doc) -%}
{%- set rows = {
"1": {"date": (327, 533, 403, 546), "pay": (411, 533, 490, 547), "tax": (496, 534, 560, 547)},
"2": {"date": (328, 519, 403, 533), "pay": (410, 520, 489, 534), "tax": (496, 519, 560, 532)},
"3": {"date": (328, 504, 403, 518), "pay": (411, 504, 490, 517), "tax": (496, 504, 560, 517)},
"4a": {"date": (328, 490, 403, 505), "pay": (412, 490, 491, 504), "tax": (496, 490, 560, 503)},
"4b": {"date": (328, 402, 404, 416), "pay": (410, 403, 489, 417), "tax": (497, 403, 562, 416)},
"5": {"date": (327, 216, 403, 230), "pay": (409, 216, 489, 230), "tax": (496, 215, 561, 230)},
"6": {"date": (327, 199, 403, 213), "pay": (409, 199, 489, 213), "tax": (496, 198, 561, 213)},
} -%}
{%- set pnd_boxes = {
"1": (209, 603, 222, 615),
"2": (395, 602, 407, 615),
"3": (471, 602, 484, 615),
"53": (395, 584, 407, 597),
} -%}
{#- Left edges of the printed 13-digit ID boxes (12 pt wide, grouped 1-4-5-2-1), measured on the
form; the AcroForm comb field does not line up with them. -#}
{%- set id_cells = (375, 393, 405, 417, 429, 447.5, 459.5, 471.5, 483.5, 495.5, 513.5, 525.5, 545) -%}
{%- macro box(rect, text, align="left", size=10) -%}
<div class="f" style="left: {{ rect[0] }}pt; top: {{ 842 - rect[3] }}pt; width: {{ rect[2] - rect[0] }}pt; height: {{ rect[3] - rect[1] }}pt; line-height: {{ rect[3] - rect[1] }}pt; text-align: {{ align }}; font-size: {{ size }}pt;">{{ text }}</div>
{%- endmacro -%}
{%- macro tick(rect) -%}
<div class="tick" style="left: {{ rect[0] }}pt; top: {{ 842 - rect[3] }}pt; width: {{ rect[2] - rect[0] }}pt; height: {{ rect[3] - rect[1] }}pt;"></div>
{%- endmacro -%}
{%- macro tax_id(rect, party) -%}
{%- if party.tax_id_digits -%}
{%- for digit in party.tax_id_digits -%}
{%- set x = id_cells[loop.index0] -%}
{{ box((x, rect[1], x + 12, rect[3]), digit, "center") }}
{%- endfor -%}
{%- elif party.tax_id -%}
{{ box(rect, party.tax_id, "center") }}
{%- endif -%}
{%- endmacro -%}
{%- macro amount_row(key) -%}
{{ box(rows[key].date, c.date, "center") }}
{{ box(rows[key].pay, c.amount, "right") }}
{{ box(rows[key].tax, c.tax, "right") }}
{%- endmacro -%}
{%- for _ in range(2) -%}
<div class="wht-page">
<img class="wht-form" src="/assets/default_thai_company/images/withholding_tax_certificate.png" alt="">
{%- if doc.docstatus == 0 %}
<div class="wht-draft">DRAFT</div>
{%- endif %}
{#- Invoice reference in the top-right margin; เล่มที่ / เลขที่ are the certificate book's numbers. -#}
{{ box((400, 821, 560, 835), doc.name, "right", 8) }}
{#- ผู้มีหน้าที่หักภาษี ณ ที่จ่าย: the company -#}
{{ tax_id((375, 744, 558, 759), c.payer) }}
{{ box((54, 729, 316, 745), c.payer.name) }}
{{ box((61, 706, 550, 722), c.payer.address or "", "left", 9) }}
{#- ผู้ถูกหักภาษี ณ ที่จ่าย: the supplier -#}
{{ tax_id((375, 676, 558, 690), c.payee) }}
{{ box((53, 658, 315, 671), c.payee.name) }}
{{ box((59, 627, 550, 643), c.payee.address or "", "left", 9) }}
{{ tick(pnd_boxes[c.pnd]) }}
{%- if c.row == "6" %}
{{ box((96, 197, 325, 214), c.row_note or "", "left", 8) }}
{%- endif %}
{{ amount_row(c.row) }}
{{ box((409, 180, 488, 196), c.amount, "right") }}
{{ box((496, 180, 560, 196), c.tax, "right") }}
{{ box((185, 158, 558, 177), c.tax_in_words) }}
{#- ผู้จ่ายเงิน (1) หัก ณ ที่จ่าย -#}
{{ tick((82, 119, 94, 131)) }}
{{ box((342, 72, 366, 87), c.issued.day, "center") }}
{{ box((364, 71, 428, 87), c.issued.month, "center") }}
{{ box((429, 72, 470, 87), c.issued.year, "center") }}
</div>
{%- endfor -%}
@@ -8,14 +8,19 @@ from frappe.utils import nowdate
from default_thai_company.tax_withholding import (
ASSET_ACCOUNT,
INPUT_VAT_ACCOUNT,
LIABILITY_ACCOUNT,
OUTPUT_VAT_ACCOUNT,
get_payment_entry,
get_withholding_certificate,
thai_companies,
)
COMPANY = "_Test WHT Company"
ABBR = "_TWC"
CUSTOMER = "_Test WHT Customer"
SUPPLIER = "_Test WHT Supplier"
INDIVIDUAL = "_Test WHT Individual"
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")
@@ -25,12 +30,13 @@ 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"):
# Custom Fields first: the category fixtures carry income_type. Fixture
# categories must exist before the company is created so the Company hook
# has something to link; force=True re-imports.
for fname in ("tax_withholding_category.json", "sales_invoice.json", "payment_entry.json"):
with open(f"{CUSTOM_DIR}/{fname}") as f:
sync_customizations_for_doctype(json.load(f), CUSTOM_DIR, fname)
import_file_by_path(FIXTURE, force=True, data_import=True)
frappe.get_doc(
{
@@ -40,20 +46,12 @@ class TestTaxWithholding(FrappeTestCase):
"country": "Thailand",
"default_currency": "THB",
"chart_of_accounts": "Standard",
"tax_id": "0105551234567",
}
).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()
cls.vat = frappe.get_doc("Account", f"{OUTPUT_VAT_ACCOUNT} - {ABBR}")
frappe.get_doc(
{
@@ -76,6 +74,35 @@ class TestTaxWithholding(FrappeTestCase):
}
).insert()
supplier_group = frappe.db.get_value("Supplier Group", {"is_group": 0})
for name, supplier_type, tax_id in (
(SUPPLIER, "Company", "0-1234-56789-01-2"),
(INDIVIDUAL, "Individual", "1234567890123"),
):
frappe.get_doc(
{
"doctype": "Supplier",
"supplier_name": name,
"supplier_type": supplier_type,
"supplier_group": supplier_group,
"tax_id": tax_id,
}
).insert()
frappe.get_doc(
{
"doctype": "Address",
"address_title": SUPPLIER,
"address_type": "Billing",
"address_line1": "99/9 Moo 5",
"address_line2": "Soi Sukhumvit 24",
"city": "Khlong Toei",
"state": "Bangkok",
"pincode": "10110",
"country": "Thailand",
"links": [{"link_doctype": "Supplier", "link_name": SUPPLIER}],
}
).insert()
def make_invoice(self, rate=10000, category=None, inclusive=False):
si = frappe.get_doc(
{
@@ -128,6 +155,31 @@ class TestTaxWithholding(FrappeTestCase):
def withheld(self, pe):
return [(t.account_head, t.add_deduct_tax, t.tax_amount) for t in pe.taxes]
def make_purchase_invoice(self, supplier, category, rate=100000):
pi = frappe.get_doc(
{
"doctype": "Purchase Invoice",
"company": COMPANY,
"supplier": supplier,
"set_posting_time": 1,
"posting_date": "2026-09-16",
"due_date": "2026-09-16",
"apply_tds": 1,
"tax_withholding_category": category,
"items": [{"item_code": ITEM, "qty": 1, "rate": rate}],
"taxes": [
{
"charge_type": "On Net Total",
"account_head": f"{INPUT_VAT_ACCOUNT} - {ABBR}",
"rate": 7,
"description": "VAT 7%",
}
],
}
)
pi.set_missing_values()
return pi.insert()
def category_account(self, category, company=COMPANY):
return frappe.db.get_value(
"Tax Withholding Account",
@@ -137,14 +189,17 @@ class TestTaxWithholding(FrappeTestCase):
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}"),
)
for account, expected in (
(self.payable, ("Liability", f"Duties and Taxes - {ABBR}", "Tax")),
(self.receivable, ("Asset", f"Tax Assets - {ABBR}", "Tax")),
(f"{OUTPUT_VAT_ACCOUNT} - {ABBR}", ("Liability", f"Duties and Taxes - {ABBR}", "Tax")),
(f"{INPUT_VAT_ACCOUNT} - {ABBR}", ("Asset", f"Tax Assets - {ABBR}", "Tax")),
):
self.assertEqual(
frappe.db.get_value("Account", account, ["root_type", "parent_account", "account_type"]),
expected,
account,
)
categories = frappe.get_all(
"Tax Withholding Category", filters={"name": ("like", "WHT %")}, pluck="name"
@@ -153,6 +208,67 @@ class TestTaxWithholding(FrappeTestCase):
for name in categories:
self.assertEqual(self.category_account(name), self.payable, name)
def test_company_creation_adds_vat_schemes(self):
for doctype, account in (
("Sales Taxes and Charges Template", f"{OUTPUT_VAT_ACCOUNT} - {ABBR}"),
("Purchase Taxes and Charges Template", f"{INPUT_VAT_ACCOUNT} - {ABBR}"),
):
schemes = {}
for name in frappe.get_all(
doctype, filters={"company": COMPANY, "title": ("like", "Thailand VAT%")}, pluck="name"
):
doc = frappe.get_doc(doctype, name)
(row,) = doc.taxes
schemes[doc.title] = (
doc.is_default,
row.account_head,
row.charge_type,
row.rate,
row.included_in_print_rate,
)
self.assertEqual(
schemes,
{
"Thailand VAT 7%": (1, account, "On Net Total", 7.0, 0),
"Thailand VAT 7% (Included)": (0, account, "On Net Total", 7.0, 1),
"Thailand VAT 0%": (0, account, "On Net Total", 0.0, 0),
},
doctype,
)
def test_company_creation_adds_shipping_rule(self):
rule = frappe.get_doc("Shipping Rule", f"Shipping Charges - {ABBR}")
self.assertEqual(
(rule.company, rule.shipping_rule_type, rule.calculate_based_on, rule.account, rule.cost_center),
(COMPANY, "Selling", "Fixed", f"Shipping Charges - {ABBR}", f"Main - {ABBR}"),
)
self.assertEqual(frappe.db.get_value("Account", rule.account, "root_type"), "Income")
def test_shipping_amount_entered_on_transaction_survives_recalculation(self):
rule = f"Shipping Charges - {ABBR}"
so = frappe.get_doc(
{
"doctype": "Sales Order",
"company": COMPANY,
"customer": CUSTOMER,
"transaction_date": nowdate(),
"delivery_date": nowdate(),
"shipping_rule": rule,
"items": [{"item_code": ITEM, "qty": 1, "rate": 1000}],
}
)
so.set_missing_values()
so.apply_shipping_rule()
(charge,) = so.taxes
self.assertEqual((charge.description, charge.tax_amount), (rule, 0))
charge.tax_amount = 150
so.insert()
so.apply_shipping_rule()
so.save()
self.assertEqual([t.tax_amount for t in so.taxes], [150])
self.assertEqual(so.grand_total, 1150)
def test_fixture_reimport_keeps_site_account_and_relinks(self):
alt = frappe.get_doc(
{
@@ -273,3 +389,34 @@ class TestTaxWithholding(FrappeTestCase):
)
self.assertEqual(pe.references[0].allocated_amount, 5350.0)
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 150.0)])
def test_certificate_reports_withholding_on_the_category_row(self):
pi = self.make_purchase_invoice(SUPPLIER, "WHT 3% - Professional Fee")
self.assertEqual(pi.grand_total, 104000.0) # 100,000 + 7% VAT - 3% withheld
c = get_withholding_certificate(pi)
self.assertEqual((c.row, c.row_note, c.pnd), ("5", None, "53"))
self.assertEqual((c.amount, c.tax, c.tax_in_words), ("100,000.00", "3,000.00", "สามพันบาทถ้วน"))
self.assertEqual(
(c.date, dict(c.issued)), ("16/09/2569", {"day": 16, "month": "กันยายน", "year": 2569})
)
self.assertEqual((c.payer.name, c.payer.tax_id_digits), (COMPANY, "0105551234567"))
self.assertEqual(
(c.payee.name, c.payee.tax_id_digits, c.payee.address),
(SUPPLIER, "0123456789012", "99/9 Moo 5 Soi Sukhumvit 24 Khlong Toei Bangkok 10110"),
)
def test_certificate_return_follows_payee_type_and_income(self):
c = get_withholding_certificate(
self.make_purchase_invoice(INDIVIDUAL, "WHT 15% - Interest (Individual)")
)
self.assertEqual((c.row, c.pnd, c.tax), ("4a", "2", "15,000.00"))
self.assertIsNone(c.payee.address)
c = get_withholding_certificate(
self.make_purchase_invoice(INDIVIDUAL, "WHT 15% - Non-Resident Individual")
)
self.assertEqual((c.row, c.pnd), ("6", "3"))
self.assertEqual(
c.row_note, "Sec. 40(2)-(6) income paid to non-resident individuals - Sec. 50(2) (P.N.D.3)"
)
+32
View File
@@ -11,6 +11,12 @@ Signatures,ลายเซ็น
On behalf of,ในนาม
Receiver,ผู้รับ
Issuer,ผู้ออก
Accepted By,ผู้อนุมัติสั่งซื้อ
Quoted By,ผู้เสนอราคา
Approved By,ผู้อนุมัติ
Prepared By,ผู้จัดทำ
Acknowledged By,ผู้รับใบสั่งซื้อ
Position,ตำแหน่ง
Tax Invoice,ใบกำกับภาษี
Tax Invoice/Receipt,ใบกำกับภาษี/ใบเสร็จรับเงิน
"Commission, brokerage, agency fees - Sec. 40(2), juristic payee (P.N.D.53)",ค่านายหน้า ค่าตัวแทน - มาตรา 40(2) ผู้รับเป็นนิติบุคคล (ภ.ง.ด.53)
@@ -37,8 +43,21 @@ Sec. 40(2)-(6) income paid to non-resident individuals - Sec. 50(2) (P.N.D.3),
"Dividends to foreign companies not doing business in Thailand - Sec. 70, pre-DTA (P.N.D.54)",เงินปันผลจ่ายให้บริษัทต่างประเทศที่มิได้ประกอบกิจการในประเทศไทย - มาตรา 70 ก่อนใช้อนุสัญญาภาษีซ้อน (ภ.ง.ด.54)
Sales Invoice,ใบแจ้งหนี้
Sales Order,ใบสั่งขาย
Purchase Invoice,ใบแจ้งหนี้ซื้อ
Purchase Order,ใบสั่งซื้อ
Required By,วันที่ต้องการรับสินค้า
Supplier Invoice No,เลขที่ใบแจ้งหนี้ผู้จัดจำหน่าย
Supplier Invoice Date,วันที่ใบแจ้งหนี้ผู้จัดจำหน่าย
Customer's Purchase Order,เลขที่ใบสั่งซื้อของลูกค้า
Customer's Purchase Order Date,วันที่ใบสั่งซื้อของลูกค้า
Contact Name,ชื่อผู้ติดต่อ
Contact Mobile No,โทรศัพท์มือถือ
Supplier Contact,ผู้ติดต่อ
Shipping Address,ที่อยู่จัดส่ง
Accepted Qty,จำนวนที่รับ
Posting Date,วันที่
Payment Due Date,วันครบกำหนดชำระ
Paid Date,วันที่ชำระ
Contact Person,ผู้ติดต่อ
Mobile No,โทรศัพท์มือถือ
Contact Email,อีเมล
@@ -48,8 +67,21 @@ Amount,จำนวนเงิน
Total,รวม
Total (Without Tax),รวมก่อนภาษี
Net Total,ยอดรวมสุทธิ
Total After Discount,ยอดรวมหลังหักส่วนลด
Total Taxes and Charges,รวมภาษีและค่าธรรมเนียม
Grand Total,ยอดรวมทั้งสิ้น
Rounded Total,ยอดรวมปัดเศษ
Terms and Conditions Details,รายละเอียดข้อตกลงและเงื่อนไข
Page {0} of {1},หน้า {0} จาก {1}
Withholding Tax Payable,ภาษีหัก ณ ที่จ่ายค้างจ่าย
Withholding Tax Receivable,ภาษีถูกหัก ณ ที่จ่าย
Output VAT,ภาษีขาย
Input VAT,ภาษีซื้อ
Withholding Tax Certificate,หนังสือรับรองการหักภาษี ณ ที่จ่าย
Type of Income Paid,ประเภทเงินได้พึงประเมินที่จ่าย
Salary and Wages - Sec. 40(1),เงินเดือน ค่าจ้าง ฯลฯ ตามมาตรา 40(1)
Fees and Commissions - Sec. 40(2),ค่าธรรมเนียม ค่านายหน้า ฯลฯ ตามมาตรา 40(2)
Royalties - Sec. 40(3),ค่าแห่งลิขสิทธิ์ ฯลฯ ตามมาตรา 40(3)
Interest - Sec. 40(4)(a),ดอกเบี้ย ฯลฯ ตามมาตรา 40(4)(ก)
Dividends - Sec. 40(4)(b),เงินปันผล เงินส่วนแบ่งกำไร ฯลฯ ตามมาตรา 40(4)(ข)
"Sec. 3 Tera (Services, Rent, Contract Work etc.)",ตามคำสั่งกรมสรรพากรที่ออกตามมาตรา 3 เตรส (ค่าบริการ ค่าเช่า ค่าจ้างทำของ ฯลฯ)
1 Net Payable After Withholding Tax ยอดชำระสุทธิหลังหักภาษี ณ ที่จ่าย
11 On behalf of ในนาม
12 Receiver ผู้รับ
13 Issuer ผู้ออก
14 Accepted By ผู้อนุมัติสั่งซื้อ
15 Quoted By ผู้เสนอราคา
16 Approved By ผู้อนุมัติ
17 Prepared By ผู้จัดทำ
18 Acknowledged By ผู้รับใบสั่งซื้อ
19 Position ตำแหน่ง
20 Tax Invoice ใบกำกับภาษี
21 Tax Invoice/Receipt ใบกำกับภาษี/ใบเสร็จรับเงิน
22 Commission, brokerage, agency fees - Sec. 40(2), juristic payee (P.N.D.53) ค่านายหน้า ค่าตัวแทน - มาตรา 40(2) ผู้รับเป็นนิติบุคคล (ภ.ง.ด.53)
43 Dividends to foreign companies not doing business in Thailand - Sec. 70, pre-DTA (P.N.D.54) เงินปันผลจ่ายให้บริษัทต่างประเทศที่มิได้ประกอบกิจการในประเทศไทย - มาตรา 70 ก่อนใช้อนุสัญญาภาษีซ้อน (ภ.ง.ด.54)
44 Sales Invoice ใบแจ้งหนี้
45 Sales Order ใบสั่งขาย
46 Purchase Invoice ใบแจ้งหนี้ซื้อ
47 Purchase Order ใบสั่งซื้อ
48 Required By วันที่ต้องการรับสินค้า
49 Supplier Invoice No เลขที่ใบแจ้งหนี้ผู้จัดจำหน่าย
50 Supplier Invoice Date วันที่ใบแจ้งหนี้ผู้จัดจำหน่าย
51 Customer's Purchase Order เลขที่ใบสั่งซื้อของลูกค้า
52 Customer's Purchase Order Date วันที่ใบสั่งซื้อของลูกค้า
53 Contact Name ชื่อผู้ติดต่อ
54 Contact Mobile No โทรศัพท์มือถือ
55 Supplier Contact ผู้ติดต่อ
56 Shipping Address ที่อยู่จัดส่ง
57 Accepted Qty จำนวนที่รับ
58 Posting Date วันที่
59 Payment Due Date วันครบกำหนดชำระ
60 Paid Date วันที่ชำระ
61 Contact Person ผู้ติดต่อ
62 Mobile No โทรศัพท์มือถือ
63 Contact Email อีเมล
67 Total รวม
68 Total (Without Tax) รวมก่อนภาษี
69 Net Total ยอดรวมสุทธิ
70 Total After Discount ยอดรวมหลังหักส่วนลด
71 Total Taxes and Charges รวมภาษีและค่าธรรมเนียม
72 Grand Total ยอดรวมทั้งสิ้น
73 Rounded Total ยอดรวมปัดเศษ
74 Terms and Conditions Details รายละเอียดข้อตกลงและเงื่อนไข
75 Page {0} of {1} หน้า {0} จาก {1}
76 Withholding Tax Payable ภาษีหัก ณ ที่จ่ายค้างจ่าย
77 Withholding Tax Receivable ภาษีถูกหัก ณ ที่จ่าย
78 Output VAT ภาษีขาย
79 Input VAT ภาษีซื้อ
80 Withholding Tax Certificate หนังสือรับรองการหักภาษี ณ ที่จ่าย
81 Type of Income Paid ประเภทเงินได้พึงประเมินที่จ่าย
82 Salary and Wages - Sec. 40(1) เงินเดือน ค่าจ้าง ฯลฯ ตามมาตรา 40(1)
83 Fees and Commissions - Sec. 40(2) ค่าธรรมเนียม ค่านายหน้า ฯลฯ ตามมาตรา 40(2)
84 Royalties - Sec. 40(3) ค่าแห่งลิขสิทธิ์ ฯลฯ ตามมาตรา 40(3)
85 Interest - Sec. 40(4)(a) ดอกเบี้ย ฯลฯ ตามมาตรา 40(4)(ก)
86 Dividends - Sec. 40(4)(b) เงินปันผล เงินส่วนแบ่งกำไร ฯลฯ ตามมาตรา 40(4)(ข)
87 Sec. 3 Tera (Services, Rent, Contract Work etc.) ตามคำสั่งกรมสรรพากรที่ออกตามมาตรา 3 เตรส (ค่าบริการ ค่าเช่า ค่าจ้างทำของ ฯลฯ)
+15 -3
View File
@@ -5,14 +5,14 @@ from frappe.contacts.doctype.address.address import get_default_address, render_
from num2words import num2words
def money_in_words(amount, currency):
"""Amount in words for the active language.
def money_in_words(amount, currency, lang=None):
"""Amount in words for `lang` (default: 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":
if (lang or frappe.local.lang) == "th":
try:
return num2words(amount, lang="th", to="currency", currency=currency)
except NotImplementedError:
@@ -53,6 +53,18 @@ def get_letter_head_company(doc=None):
)
def get_print_address(address_name):
"""Address rendered in the print language for print formats.
The `*_address_display` fields on a document are rendered once, when it is
saved, in the language of whoever saved it; re-render the linked Address
here so the Address Template's translations follow the print language.
"""
if not address_name:
return None
return render_address(address_name, check_permissions=False)
def get_company_bank_account(company=None):
"""Default company Bank Account for print formats.
+78
View File
@@ -0,0 +1,78 @@
import erpnext
import frappe
from default_thai_company.tax_withholding import (
INPUT_VAT_ACCOUNT,
OUTPUT_VAT_ACCOUNT,
company_ready,
get_or_create_account,
thai_companies,
)
RATE = 7.0
# title, rate, included_in_print_rate. Exempt supplies take no template.
SCHEMES = (
("Thailand VAT 7%", RATE, 0),
("Thailand VAT 7% (Included)", RATE, 1), # VAT-inclusive prices, the retail norm
("Thailand VAT 0%", 0.0, 0), # zero-rated: exports, international transport
)
DEFAULT_SCHEME = "Thailand VAT 7%"
# doctype, account, root type, extra row fields. Input VAT is recoverable, so on
# purchases it is "Total" (added to the bill, kept out of item valuation).
TEMPLATES = (
("Sales Taxes and Charges Template", OUTPUT_VAT_ACCOUNT, "Liability", {}),
(
"Purchase Taxes and Charges Template",
INPUT_VAT_ACCOUNT,
"Asset",
{"category": "Total", "add_deduct_tax": "Add"},
),
)
def ensure_vat_templates(company):
"""Create the VAT schemes for `company`, skipping titles that exist. The 7%
scheme becomes the default when the company has no default template."""
cost_center = erpnext.get_default_cost_center(company)
for doctype, account_name, root_type, extra in TEMPLATES:
account = get_or_create_account(company, account_name, root_type)
has_default = frappe.db.exists(doctype, {"company": company, "is_default": 1})
for title, rate, included in SCHEMES:
if frappe.db.exists(doctype, {"company": company, "title": title}):
continue
is_default = title == DEFAULT_SCHEME and not has_default
frappe.get_doc(
{
"doctype": doctype,
"title": title,
"company": company,
"is_default": int(is_default),
"taxes": [
{
"charge_type": "On Net Total",
"account_head": account,
"rate": rate,
"description": f"VAT {rate:g}%",
"included_in_print_rate": included,
"cost_center": cost_center,
**extra,
}
],
}
).insert(ignore_permissions=True)
has_default = has_default or is_default
def setup_company(doc, method=None):
"""Company.on_update: VAT schemes for a Thai company."""
if company_ready(doc):
ensure_vat_templates(doc.name)
def setup_companies():
"""after_install and the create_vat_templates patch: companies that exist
before this code did never pass through `setup_company`."""
for company in thai_companies():
ensure_vat_templates(company)