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.
This commit is contained in:
@@ -100,5 +100,39 @@
|
|||||||
"report": null,
|
"report": null,
|
||||||
"show_section_headings": 0,
|
"show_section_headings": 0,
|
||||||
"standard": "No"
|
"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}",
|
||||||
|
"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-22 09: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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -79,6 +79,7 @@ jinja = {
|
|||||||
"default_thai_company.utils.get_letter_head_company",
|
"default_thai_company.utils.get_letter_head_company",
|
||||||
"default_thai_company.utils.get_company_bank_account",
|
"default_thai_company.utils.get_company_bank_account",
|
||||||
"default_thai_company.utils.get_in_words",
|
"default_thai_company.utils.get_in_words",
|
||||||
|
"default_thai_company.tax_withholding.get_withholding_certificate",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +101,7 @@ fixtures = [
|
|||||||
"Default Standard Sales Invoice",
|
"Default Standard Sales Invoice",
|
||||||
"Default Standard Tax Invoice/Receipt",
|
"Default Standard Tax Invoice/Receipt",
|
||||||
"Default Standard Quotation",
|
"Default Standard Quotation",
|
||||||
|
"Withholding Tax Certificate",
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 226 KiB |
@@ -11,8 +11,9 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
|||||||
normal_round,
|
normal_round,
|
||||||
)
|
)
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.contacts.doctype.address.address import get_default_address
|
||||||
from frappe.modules.utils import sync_customizations_for_doctype
|
from frappe.modules.utils import sync_customizations_for_doctype
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt, fmt_money, getdate
|
||||||
|
|
||||||
from default_thai_company.utils import money_in_words
|
from default_thai_company.utils import money_in_words
|
||||||
|
|
||||||
@@ -389,3 +390,128 @@ def get_customer_withholding(doc):
|
|||||||
else:
|
else:
|
||||||
description = _("Withholding tax deducted by customer")
|
description = _("Withholding tax deducted by customer")
|
||||||
return flt(total, precision), description
|
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."""
|
||||||
|
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,
|
||||||
|
address.pincode,
|
||||||
|
]
|
||||||
|
if address.country and address.country != "Thailand":
|
||||||
|
parts.append(address.country)
|
||||||
|
return " ".join(part.strip() for part in parts if part and part.strip())
|
||||||
|
|
||||||
|
|
||||||
|
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,87 @@
|
|||||||
|
{#- หนังสือรับรองการหักภาษี ณ ที่จ่าย (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="">
|
||||||
|
|
||||||
|
{#- 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 -%}
|
||||||
@@ -12,12 +12,15 @@ from default_thai_company.tax_withholding import (
|
|||||||
LIABILITY_ACCOUNT,
|
LIABILITY_ACCOUNT,
|
||||||
OUTPUT_VAT_ACCOUNT,
|
OUTPUT_VAT_ACCOUNT,
|
||||||
get_payment_entry,
|
get_payment_entry,
|
||||||
|
get_withholding_certificate,
|
||||||
thai_companies,
|
thai_companies,
|
||||||
)
|
)
|
||||||
|
|
||||||
COMPANY = "_Test WHT Company"
|
COMPANY = "_Test WHT Company"
|
||||||
ABBR = "_TWC"
|
ABBR = "_TWC"
|
||||||
CUSTOMER = "_Test WHT Customer"
|
CUSTOMER = "_Test WHT Customer"
|
||||||
|
SUPPLIER = "_Test WHT Supplier"
|
||||||
|
INDIVIDUAL = "_Test WHT Individual"
|
||||||
ITEM = "_Test WHT Service"
|
ITEM = "_Test WHT Service"
|
||||||
FIXTURE = frappe.get_app_path("default_thai_company", "fixtures", "tax_withholding_category.json")
|
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")
|
CUSTOM_DIR = frappe.get_app_path("default_thai_company", "default_thai_company", "custom")
|
||||||
@@ -43,6 +46,7 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
"country": "Thailand",
|
"country": "Thailand",
|
||||||
"default_currency": "THB",
|
"default_currency": "THB",
|
||||||
"chart_of_accounts": "Standard",
|
"chart_of_accounts": "Standard",
|
||||||
|
"tax_id": "0105551234567",
|
||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
cls.payable = f"{LIABILITY_ACCOUNT} - {ABBR}"
|
cls.payable = f"{LIABILITY_ACCOUNT} - {ABBR}"
|
||||||
@@ -70,6 +74,35 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
}
|
}
|
||||||
).insert()
|
).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):
|
def make_invoice(self, rate=10000, category=None, inclusive=False):
|
||||||
si = frappe.get_doc(
|
si = frappe.get_doc(
|
||||||
{
|
{
|
||||||
@@ -122,6 +155,31 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
def withheld(self, pe):
|
def withheld(self, pe):
|
||||||
return [(t.account_head, t.add_deduct_tax, t.tax_amount) for t in pe.taxes]
|
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):
|
def category_account(self, category, company=COMPANY):
|
||||||
return frappe.db.get_value(
|
return frappe.db.get_value(
|
||||||
"Tax Withholding Account",
|
"Tax Withholding Account",
|
||||||
@@ -331,3 +389,34 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(pe.references[0].allocated_amount, 5350.0)
|
self.assertEqual(pe.references[0].allocated_amount, 5350.0)
|
||||||
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 150.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)"
|
||||||
|
)
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ from frappe.contacts.doctype.address.address import get_default_address, render_
|
|||||||
from num2words import num2words
|
from num2words import num2words
|
||||||
|
|
||||||
|
|
||||||
def money_in_words(amount, currency):
|
def money_in_words(amount, currency, lang=None):
|
||||||
"""Amount in words for the active language.
|
"""Amount in words for `lang` (default: the active language).
|
||||||
|
|
||||||
frappe.utils.money_in_words renders "<currency> <words> only." in every
|
frappe.utils.money_in_words renders "<currency> <words> only." in every
|
||||||
language; Thai documents write "<words>บาทถ้วน" or "<words>บาท<words>สตางค์".
|
language; Thai documents write "<words>บาทถ้วน" or "<words>บาท<words>สตางค์".
|
||||||
Currencies num2words cannot spell in Thai keep frappe's wording.
|
Currencies num2words cannot spell in Thai keep frappe's wording.
|
||||||
"""
|
"""
|
||||||
if frappe.local.lang == "th":
|
if (lang or frappe.local.lang) == "th":
|
||||||
try:
|
try:
|
||||||
return num2words(amount, lang="th", to="currency", currency=currency)
|
return num2words(amount, lang="th", to="currency", currency=currency)
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
|
|||||||
Reference in New Issue
Block a user