Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caad6e77da | ||
|
|
8aeecfca2f | ||
|
|
e85ec3a585 | ||
|
|
015ce48639 | ||
|
|
9a8602f74e | ||
|
|
33310c51ae | ||
|
|
f0961de744 | ||
|
|
dc8509d6d7 | ||
|
|
2d93fe45c2 | ||
|
|
9b99be9abd | ||
|
|
6a082edc56 |
@@ -12,6 +12,14 @@ bench get-app $URL_OF_THIS_REPO --branch develop
|
|||||||
bench install-app default_thai_company
|
bench install-app default_thai_company
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Thai language
|
||||||
|
|
||||||
|
Every string the app adds (custom field labels, messages, print format and letter head
|
||||||
|
text, Tax Withholding Category names) is translated in `translations/th.csv`; the same
|
||||||
|
file overrides the upstream Thai for the standard labels the print formats show. The
|
||||||
|
print formats embed Sarabun (`public/fonts`, OFL) so PDFs render Thai on servers
|
||||||
|
without a Thai system font. Switch a user or the print language to `th` to use it.
|
||||||
|
|
||||||
### Contributing
|
### 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:
|
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:
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"is_default": 1,
|
"is_default": 1,
|
||||||
"modified": "2026-09-09 17:23:33.559202",
|
"modified": "2026-09-09 17:23:33.559202",
|
||||||
"name": "Thailand",
|
"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 or fax or email_id %}<br>{% endif -%}\n{% if phone %}{{ _(\"Phone\") }}: {{ phone }}<br>{% endif -%}\n{% if fax %}{{ _(\"Fax\") }}: {{ fax }}<br>{% endif -%}\n{% if email_id %}{{ _(\"Email\") }}: {{ email_id }}<br>{% endif -%}\n"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"align": "Left",
|
"align": "Left",
|
||||||
"content": "<style>\n .bpt-title {\n display: -webkit-box;\n display: flex;\n -webkit-box-align: center;\n align-items: center;\n width: 100%;\n }\n .bpt-title img {\n margin-right: 1em;\n }\n .bpt-title .bpt-company {\n -webkit-box-flex: 1;\n flex: 1;\n text-align: right;\n }\n</style>\n{% set company = get_letter_head_company(doc) %}\n{% if company %}\n<div class=\"bpt-title\">\n {% if company.company_logo %}\n <img alt=\"{{ company.company_name }}\" height=\"125\" src=\"{{ company.company_logo }}\">\n {% endif %}\n <div class=\"bpt-company\">\n <h1>{{ company.company_name }}</h1>\n <small>\n {% if company.tax_id %}<div>Tax ID: {{ company.tax_id }}</div>{% endif %}\n {% if company.address %}<div>{{ company.address }}</div>{% endif %}\n {% if company.phone_no %}<div>Phone: {{ company.phone_no }}</div>{% endif %}\n {% if company.email %}<div>Email: {{ company.email }}</div>{% endif %}\n </small>\n </div>\n</div>\n{% endif %}\n",
|
"content": "<style>\n .bpt-title {\n display: -webkit-box;\n display: flex;\n -webkit-box-align: center;\n align-items: center;\n width: 100%;\n }\n .bpt-title img {\n margin-right: 1em;\n }\n .bpt-title .bpt-company {\n -webkit-box-flex: 1;\n flex: 1;\n text-align: right;\n }\n</style>\n{% set company = get_letter_head_company(doc) %}\n{% if company %}\n<div class=\"bpt-title\">\n {% if company.company_logo %}\n <img alt=\"{{ company.company_name }}\" height=\"125\" src=\"{{ company.company_logo }}\">\n {% endif %}\n <div class=\"bpt-company\">\n <h1>{{ company.company_name }}</h1>\n <small>\n {% if company.tax_id %}<div>{{ _(\"Tax ID\") }}: {{ company.tax_id }}</div>{% endif %}\n {% if company.address %}<div>{{ company.address }}</div>{% endif %}\n {% if company.phone_no %}<div>{{ _(\"Phone\") }}: {{ company.phone_no }}</div>{% endif %}\n {% if company.email %}<div>{{ _(\"Email\") }}: {{ company.email }}</div>{% endif %}\n </small>\n </div>\n</div>\n{% endif %}\n",
|
||||||
"disabled": 0,
|
"disabled": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Letter Head",
|
"doctype": "Letter Head",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"image_width": 0.0,
|
"image_width": 0.0,
|
||||||
"is_default": 1,
|
"is_default": 1,
|
||||||
"letter_head_name": "Default Letter Head",
|
"letter_head_name": "Default Letter Head",
|
||||||
"modified": "2026-09-10 09:51:23.933752",
|
"modified": "2026-09-15 06:44:37.653888",
|
||||||
"name": "Default Letter Head",
|
"name": "Default Letter Head",
|
||||||
"source": "HTML"
|
"source": "HTML"
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -78,6 +78,7 @@ jinja = {
|
|||||||
"methods": [
|
"methods": [
|
||||||
"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",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +90,12 @@ jinja = {
|
|||||||
fixtures = [
|
fixtures = [
|
||||||
{"doctype": "Letter Head", "filters": [["name", "in", ["Default Letter Head"]]]},
|
{"doctype": "Letter Head", "filters": [["name", "in", ["Default Letter Head"]]]},
|
||||||
{"doctype": "Address Template", "filters": [["name", "in", ["Thailand"]]]},
|
{"doctype": "Address Template", "filters": [["name", "in", ["Thailand"]]]},
|
||||||
{"doctype": "Print Format", "filters": [["name", "in", ["Default Standard Sales Invoice"]]]},
|
{
|
||||||
|
"doctype": "Print Format",
|
||||||
|
"filters": [
|
||||||
|
["name", "in", ["Default Standard Sales Invoice", "Default Standard Tax Invoice/Receipt"]]
|
||||||
|
],
|
||||||
|
},
|
||||||
{"doctype": "Tax Withholding Category", "filters": [["name", "like", "WHT %"]]},
|
{"doctype": "Tax Withholding Category", "filters": [["name", "like", "WHT %"]]},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -145,6 +151,7 @@ fixtures = [
|
|||||||
|
|
||||||
override_doctype_class = {
|
override_doctype_class = {
|
||||||
"Sales Invoice": "default_thai_company.tax_withholding.ThaiSalesInvoice",
|
"Sales Invoice": "default_thai_company.tax_withholding.ThaiSalesInvoice",
|
||||||
|
"Payment Entry": "default_thai_company.tax_withholding.ThaiPaymentEntry",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Document Events
|
# Document Events
|
||||||
@@ -195,11 +202,11 @@ doc_events = {
|
|||||||
|
|
||||||
# Overriding Methods
|
# Overriding Methods
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
#
|
|
||||||
# override_whitelisted_methods = {
|
override_whitelisted_methods = {
|
||||||
# "frappe.desk.doctype.event.event.get_events": "default_thai_company.event.get_events"
|
"erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry": "default_thai_company.tax_withholding.get_payment_entry",
|
||||||
# }
|
}
|
||||||
#
|
|
||||||
# each overriding function accepts a `data` argument;
|
# each overriding function accepts a `data` argument;
|
||||||
# generated from the base implementation of the doctype dashboard,
|
# generated from the base implementation of the doctype dashboard,
|
||||||
# along with any modifications made in other Frappe apps
|
# along with any modifications made in other Frappe apps
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2018 The Sarabun Project Authors (https://github.com/cadsondemak/Sarabun)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,10 +1,29 @@
|
|||||||
frappe.ui.form.on("Payment Entry", {
|
frappe.ui.form.on("Payment Entry", {
|
||||||
apply_tax_withholding_amount(frm) {
|
apply_tax_withholding_amount(frm) {
|
||||||
if (frm.doc.party_type !== "Customer" || !frm.doc.apply_tax_withholding_amount) return;
|
if (frm.doc.party_type !== "Customer" || frm.doc.payment_type !== "Receive") return;
|
||||||
|
if (!frm.doc.apply_tax_withholding_amount) {
|
||||||
|
frm.events.recompute_customer_withholding(frm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// ERPNext's handler looks the category up on Supplier and clears it for a
|
// ERPNext's handler looks the category up on Supplier and clears it for a
|
||||||
// Customer; wait for that request to settle, then set the Customer's value.
|
// Customer; wait for that request to settle, then set the Customer's value.
|
||||||
frappe.db.get_value("Customer", frm.doc.party, "tax_withholding_category").then(({ message }) => {
|
frappe.db.get_value("Customer", frm.doc.party, "tax_withholding_category").then(({ message }) => {
|
||||||
frappe.after_ajax(() => frm.set_value("tax_withholding_category", message.tax_withholding_category));
|
frappe.after_ajax(() => {
|
||||||
|
frm.set_value("tax_withholding_category", message.tax_withholding_category);
|
||||||
|
frm.events.recompute_customer_withholding(frm);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
tax_withholding_category(frm) {
|
||||||
|
if (frm.doc.party_type === "Customer" && frm.doc.payment_type === "Receive") {
|
||||||
|
frm.events.recompute_customer_withholding(frm);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
recompute_customer_withholding(frm) {
|
||||||
|
// allocate_amount_to_references is overridden server-side to refresh the withholding row
|
||||||
|
if (!frm.doc.references || !frm.doc.references.length) return;
|
||||||
|
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, false);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import erpnext
|
import erpnext
|
||||||
import frappe
|
import frappe
|
||||||
|
from erpnext.accounts.doctype.payment_entry.payment_entry import PaymentEntry
|
||||||
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry as _get_payment_entry
|
||||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
|
||||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||||
get_tax_withholding_details,
|
get_tax_withholding_details,
|
||||||
@@ -8,6 +10,8 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt
|
||||||
|
|
||||||
|
from default_thai_company.utils import money_in_words
|
||||||
|
|
||||||
# Liability: tax we withhold from suppliers and remit on P.N.D.3/53 (linked on every category).
|
# 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.
|
# Asset: tax customers withhold from us, creditable against corporate income tax.
|
||||||
LIABILITY_ACCOUNT = "Withholding Tax Payable"
|
LIABILITY_ACCOUNT = "Withholding Tax Payable"
|
||||||
@@ -156,45 +160,149 @@ class ThaiSalesInvoice(SalesInvoice):
|
|||||||
self.precision("amount_after_withholding"),
|
self.precision("amount_after_withholding"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def set_total_in_words(self):
|
||||||
|
"""SellingController's version, with Thai wording when the saving user's
|
||||||
|
language is Thai. Print formats re-derive the text per print language
|
||||||
|
(`get_in_words`)."""
|
||||||
|
base_amount = abs(
|
||||||
|
self.base_grand_total if self.is_rounded_total_disabled() else self.base_rounded_total
|
||||||
|
)
|
||||||
|
self.base_in_words = money_in_words(base_amount, self.company_currency)
|
||||||
|
amount = abs(self.grand_total if self.is_rounded_total_disabled() else self.rounded_total)
|
||||||
|
self.in_words = money_in_words(amount, self.currency)
|
||||||
|
|
||||||
|
|
||||||
|
class ThaiPaymentEntry(PaymentEntry):
|
||||||
|
def build_gl_map(self):
|
||||||
|
"""A "Deduct" tax row on a receipt posts bank Dr gross and bank Cr
|
||||||
|
withholding; ERPNext merges them into one entry but only nets it when
|
||||||
|
the bank entry itself carries `post_net_value`. Set it so the bank
|
||||||
|
ledger shows the amount that actually arrived."""
|
||||||
|
gl_entries = super().build_gl_map()
|
||||||
|
if self.payment_type == "Receive" and self.party_type == "Customer" and self.get("taxes"):
|
||||||
|
for entry in gl_entries:
|
||||||
|
if entry.account == self.paid_to:
|
||||||
|
entry.post_net_value = True
|
||||||
|
return gl_entries
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def allocate_amount_to_references(self, *args, **kwargs):
|
||||||
|
"""Called by the form after Get Outstanding Invoices and on Paid Amount
|
||||||
|
changes; keep the withholding row in step with the allocation."""
|
||||||
|
super().allocate_amount_to_references(*args, **kwargs)
|
||||||
|
if self.party_type == "Customer" and self.payment_type == "Receive" and self.source_exchange_rate:
|
||||||
|
auto_apply_customer_withholding(self)
|
||||||
|
set_customer_withholding(self)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_payment_entry(
|
||||||
|
dt,
|
||||||
|
dn,
|
||||||
|
party_amount=None,
|
||||||
|
bank_account=None,
|
||||||
|
bank_amount=None,
|
||||||
|
party_type=None,
|
||||||
|
payment_type=None,
|
||||||
|
reference_date=None,
|
||||||
|
created_from_payment_request=False,
|
||||||
|
):
|
||||||
|
"""Create > Payment from a Sales Invoice: arrive with withholding applied.
|
||||||
|
|
||||||
|
Signature mirrors ERPNext's so `frappe.call` drops request-only args
|
||||||
|
(`cmd`, ...) instead of forwarding them.
|
||||||
|
"""
|
||||||
|
pe = _get_payment_entry(
|
||||||
|
dt,
|
||||||
|
dn,
|
||||||
|
party_amount=party_amount,
|
||||||
|
bank_account=bank_account,
|
||||||
|
bank_amount=bank_amount,
|
||||||
|
party_type=party_type,
|
||||||
|
payment_type=payment_type,
|
||||||
|
reference_date=reference_date,
|
||||||
|
created_from_payment_request=created_from_payment_request,
|
||||||
|
)
|
||||||
|
if auto_apply_customer_withholding(pe) and pe.source_exchange_rate:
|
||||||
|
set_customer_withholding(pe)
|
||||||
|
return pe
|
||||||
|
|
||||||
|
|
||||||
|
def auto_apply_customer_withholding(doc):
|
||||||
|
"""Tick Apply Tax Withholding Amount on a new customer receipt whose
|
||||||
|
references include a Sales Invoice that carries withholding. Only for
|
||||||
|
unsaved entries: after the first save the checkbox is the user's."""
|
||||||
|
if (
|
||||||
|
doc.party_type != "Customer"
|
||||||
|
or doc.payment_type != "Receive"
|
||||||
|
or doc.apply_tax_withholding_amount
|
||||||
|
or not doc.is_new()
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
invoices = [r.reference_name for r in doc.references if r.reference_doctype == "Sales Invoice"]
|
||||||
|
if not invoices or not frappe.db.exists(
|
||||||
|
"Sales Invoice", {"name": ("in", invoices), "withholding_tax_amount": (">", 0)}
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
doc.apply_tax_withholding_amount = 1
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def set_customer_withholding(doc, method=None):
|
def set_customer_withholding(doc, method=None):
|
||||||
"""Payment Entry.validate: tax withheld by the customer on a Receive entry
|
"""Payment Entry.validate: tax withheld by the customer on a Receive entry
|
||||||
becomes a deduction to the company's withholding receivable account.
|
is posted like ERPNext's supplier TDS, as a "Deduct" row in the taxes
|
||||||
|
table against the withholding receivable account.
|
||||||
|
|
||||||
Withholding is rate x pre-VAT amount of each allocated reference, prorated
|
`paid_amount` is the gross amount settled against the invoices (what
|
||||||
by the allocation. `paid_amount` is the cash actually received, so the
|
"Get Outstanding Invoices" fills in); the bank receives paid minus the
|
||||||
deduction closes the difference against the gross allocation. Runs after
|
withholding (`received_amount_after_tax`). Withholding is rate x pre-VAT
|
||||||
the controller's validate (exchange rates and allocations are final), then
|
amount of each allocated reference, prorated by the allocation. Runs after
|
||||||
re-derives the two amounts that depend on deductions.
|
the controller's validate, then re-runs the tax computation so the row's
|
||||||
|
base amounts and totals are final within this save.
|
||||||
"""
|
"""
|
||||||
if doc.party_type != "Customer" or doc.payment_type != "Receive" or not doc.apply_tax_withholding_amount:
|
if doc.party_type != "Customer" or doc.payment_type != "Receive":
|
||||||
return
|
return
|
||||||
|
|
||||||
if not doc.tax_withholding_category:
|
if method == "validate":
|
||||||
doc.tax_withholding_category = frappe.db.get_value("Customer", doc.party, "tax_withholding_category")
|
auto_apply_customer_withholding(doc)
|
||||||
|
|
||||||
account = frappe.db.get_value(
|
account = frappe.db.get_value(
|
||||||
"Account",
|
"Account",
|
||||||
{"company": doc.company, "account_name": ASSET_ACCOUNT, "root_type": "Asset", "is_group": 0},
|
{"company": doc.company, "account_name": ASSET_ACCOUNT, "root_type": "Asset", "is_group": 0},
|
||||||
)
|
)
|
||||||
|
row = next((d for d in doc.taxes if d.account_head == account), None) if account else None
|
||||||
|
|
||||||
|
if not doc.apply_tax_withholding_amount:
|
||||||
|
if row:
|
||||||
|
doc.remove(row)
|
||||||
|
doc.apply_taxes()
|
||||||
|
doc.set_amounts_after_tax()
|
||||||
|
return
|
||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
frappe.throw(_("Account {0} not found for Company {1}").format(ASSET_ACCOUNT, doc.company))
|
frappe.throw(_("Account {0} not found for Company {1}").format(ASSET_ACCOUNT, doc.company))
|
||||||
|
|
||||||
|
if not doc.tax_withholding_category:
|
||||||
|
doc.tax_withholding_category = frappe.db.get_value("Customer", doc.party, "tax_withholding_category")
|
||||||
|
|
||||||
amount, description = get_customer_withholding(doc)
|
amount, description = get_customer_withholding(doc)
|
||||||
row = next((d for d in doc.deductions if d.account == account), None)
|
|
||||||
|
|
||||||
if not amount:
|
if not amount:
|
||||||
if row:
|
if row:
|
||||||
doc.remove(row)
|
doc.remove(row)
|
||||||
else:
|
else:
|
||||||
if not row:
|
if not row:
|
||||||
row = doc.append("deductions", {"account": account})
|
row = doc.append(
|
||||||
row.amount = amount
|
"taxes", {"account_head": account, "charge_type": "Actual", "add_deduct_tax": "Deduct"}
|
||||||
|
)
|
||||||
|
row.tax_amount = amount
|
||||||
row.description = description
|
row.description = description
|
||||||
row.cost_center = doc.cost_center or erpnext.get_default_cost_center(doc.company)
|
row.cost_center = doc.cost_center or erpnext.get_default_cost_center(doc.company)
|
||||||
|
|
||||||
doc.set_unallocated_amount()
|
doc.apply_taxes()
|
||||||
doc.set_difference_amount()
|
doc.set_amounts_after_tax()
|
||||||
|
|
||||||
|
|
||||||
def get_customer_withholding(doc):
|
def get_customer_withholding(doc):
|
||||||
@@ -238,7 +346,8 @@ def get_customer_withholding(doc):
|
|||||||
total += withholding_on(details_by_category[category], base_taxable, base_taxable, precision)
|
total += withholding_on(details_by_category[category], base_taxable, base_taxable, precision)
|
||||||
|
|
||||||
if len(details_by_category) == 1:
|
if len(details_by_category) == 1:
|
||||||
description = next(iter(details_by_category.values())).description
|
# category_name; translations/th.csv carries the fixture categories
|
||||||
|
description = _(next(iter(details_by_category.values())).description)
|
||||||
else:
|
else:
|
||||||
description = _("Withholding tax deducted by customer")
|
description = _("Withholding tax deducted by customer")
|
||||||
return flt(total, precision), description
|
return flt(total, precision), description
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ from frappe.modules.utils import sync_customizations_for_doctype
|
|||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils import nowdate
|
from frappe.utils import nowdate
|
||||||
|
|
||||||
from default_thai_company.tax_withholding import ASSET_ACCOUNT, LIABILITY_ACCOUNT, thai_companies
|
from default_thai_company.tax_withholding import (
|
||||||
|
ASSET_ACCOUNT,
|
||||||
|
LIABILITY_ACCOUNT,
|
||||||
|
get_payment_entry,
|
||||||
|
thai_companies,
|
||||||
|
)
|
||||||
|
|
||||||
COMPANY = "_Test WHT Company"
|
COMPANY = "_Test WHT Company"
|
||||||
ABBR = "_TWC"
|
ABBR = "_TWC"
|
||||||
@@ -71,7 +76,7 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
|
|
||||||
def make_invoice(self, rate=10000, category=None):
|
def make_invoice(self, rate=10000, category=None, inclusive=False):
|
||||||
si = frappe.get_doc(
|
si = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Sales Invoice",
|
"doctype": "Sales Invoice",
|
||||||
@@ -87,6 +92,7 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
"account_head": self.vat.name,
|
"account_head": self.vat.name,
|
||||||
"rate": 7,
|
"rate": 7,
|
||||||
"description": "VAT 7%",
|
"description": "VAT 7%",
|
||||||
|
"included_in_print_rate": int(inclusive),
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -94,7 +100,8 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
si.set_missing_values()
|
si.set_missing_values()
|
||||||
return si.submit()
|
return si.submit()
|
||||||
|
|
||||||
def make_receipt(self, invoice, allocated, paid, apply=1):
|
def make_receipt(self, invoice, allocated, apply=1):
|
||||||
|
"""Paid amount is the gross allocation; withholding reduces what reaches the bank."""
|
||||||
return frappe.get_doc(
|
return frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Payment Entry",
|
"doctype": "Payment Entry",
|
||||||
@@ -105,8 +112,8 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
"posting_date": nowdate(),
|
"posting_date": nowdate(),
|
||||||
"paid_from": f"Debtors - {ABBR}",
|
"paid_from": f"Debtors - {ABBR}",
|
||||||
"paid_to": f"Cash - {ABBR}",
|
"paid_to": f"Cash - {ABBR}",
|
||||||
"paid_amount": paid,
|
"paid_amount": allocated,
|
||||||
"received_amount": paid,
|
"received_amount": allocated,
|
||||||
"apply_tax_withholding_amount": apply,
|
"apply_tax_withholding_amount": apply,
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
@@ -118,6 +125,9 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
}
|
}
|
||||||
).insert()
|
).insert()
|
||||||
|
|
||||||
|
def withheld(self, pe):
|
||||||
|
return [(t.account_head, t.add_deduct_tax, t.tax_amount) for t in pe.taxes]
|
||||||
|
|
||||||
def category_account(self, category, company=COMPANY):
|
def category_account(self, category, company=COMPANY):
|
||||||
return frappe.db.get_value(
|
return frappe.db.get_value(
|
||||||
"Tax Withholding Account",
|
"Tax Withholding Account",
|
||||||
@@ -181,44 +191,85 @@ class TestTaxWithholding(FrappeTestCase):
|
|||||||
def test_receipt_uses_invoice_category_over_customer_category(self):
|
def test_receipt_uses_invoice_category_over_customer_category(self):
|
||||||
si = self.make_invoice(category="WHT 5% - Rent")
|
si = self.make_invoice(category="WHT 5% - Rent")
|
||||||
self.assertEqual(si.withholding_tax_amount, 500.0)
|
self.assertEqual(si.withholding_tax_amount, 500.0)
|
||||||
pe = self.make_receipt(si, allocated=10700, paid=10200)
|
pe = self.make_receipt(si, allocated=10700)
|
||||||
self.assertEqual([(d.account, d.amount) for d in pe.deductions], [(self.receivable, 500.0)])
|
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 500.0)])
|
||||||
self.assertEqual(pe.difference_amount, 0)
|
self.assertEqual(pe.received_amount_after_tax, 10200.0)
|
||||||
|
|
||||||
def test_receipt_deducts_withholding_and_settles_invoice(self):
|
def test_receipt_deducts_withholding_and_settles_invoice(self):
|
||||||
si = self.make_invoice()
|
si = self.make_invoice()
|
||||||
pe = self.make_receipt(si, allocated=10700, paid=10400)
|
pe = self.make_receipt(si, allocated=10700)
|
||||||
|
|
||||||
self.assertEqual(pe.tax_withholding_category, "WHT 3% - Service")
|
self.assertEqual(pe.tax_withholding_category, "WHT 3% - Service")
|
||||||
self.assertEqual([(d.account, d.amount) for d in pe.deductions], [(self.receivable, 300.0)])
|
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 300.0)])
|
||||||
self.assertEqual(pe.difference_amount, 0)
|
self.assertEqual((pe.paid_amount, pe.received_amount_after_tax), (10700.0, 10400.0))
|
||||||
|
self.assertEqual((pe.unallocated_amount, pe.difference_amount), (0, 0))
|
||||||
|
|
||||||
pe.submit()
|
pe.submit()
|
||||||
gl = {
|
gl = {}
|
||||||
g.account: (g.debit, g.credit)
|
|
||||||
for g in frappe.get_all(
|
for g in frappe.get_all(
|
||||||
"GL Entry", filters={"voucher_no": pe.name}, fields=["account", "debit", "credit"]
|
"GL Entry", filters={"voucher_no": pe.name}, fields=["account", "debit", "credit"]
|
||||||
)
|
):
|
||||||
}
|
gl.setdefault(g.account, [0, 0])
|
||||||
self.assertEqual(gl[f"Cash - {ABBR}"], (10400.0, 0.0))
|
gl[g.account][0] += g.debit
|
||||||
self.assertEqual(gl[self.receivable], (300.0, 0.0))
|
gl[g.account][1] += g.credit
|
||||||
self.assertEqual(gl[f"Debtors - {ABBR}"], (0.0, 10700.0))
|
self.assertEqual(gl[f"Cash - {ABBR}"], [10400.0, 0.0])
|
||||||
|
self.assertEqual(gl[self.receivable], [300.0, 0.0])
|
||||||
|
self.assertEqual(gl[f"Debtors - {ABBR}"], [0.0, 10700.0])
|
||||||
|
self.assertEqual(len(gl), 3)
|
||||||
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 0)
|
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 0)
|
||||||
|
|
||||||
|
def test_inclusive_vat_invoice_withholds_on_pre_vat_amount(self):
|
||||||
|
si = self.make_invoice(inclusive=True) # 10,000 incl. 7% VAT -> net 9,345.79
|
||||||
|
self.assertEqual((si.net_total, si.grand_total), (9345.79, 10000.0))
|
||||||
|
self.assertEqual(si.withholding_tax_amount, 280.37)
|
||||||
|
pe = self.make_receipt(si, allocated=10000)
|
||||||
|
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 280.37)])
|
||||||
|
self.assertEqual(pe.received_amount_after_tax, 9719.63)
|
||||||
|
|
||||||
def test_partial_allocation_prorates_withholding(self):
|
def test_partial_allocation_prorates_withholding(self):
|
||||||
si = self.make_invoice()
|
si = self.make_invoice()
|
||||||
pe = self.make_receipt(si, allocated=5350, paid=5200)
|
pe = self.make_receipt(si, allocated=5350)
|
||||||
self.assertEqual([(d.account, d.amount) for d in pe.deductions], [(self.receivable, 150.0)])
|
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 150.0)])
|
||||||
self.assertEqual(pe.difference_amount, 0)
|
self.assertEqual(pe.received_amount_after_tax, 5200.0)
|
||||||
|
|
||||||
def test_below_single_threshold_has_no_deduction(self):
|
def test_below_single_threshold_has_no_deduction(self):
|
||||||
si = self.make_invoice(rate=800) # net 800 < 1,000 threshold
|
si = self.make_invoice(rate=800) # net 800 < 1,000 threshold
|
||||||
pe = self.make_receipt(si, allocated=856, paid=856)
|
pe = self.make_receipt(si, allocated=856, apply=0)
|
||||||
self.assertEqual(pe.deductions, [])
|
self.assertEqual(pe.apply_tax_withholding_amount, 0) # invoice carries no withholding
|
||||||
self.assertEqual(pe.difference_amount, 0)
|
self.assertEqual(pe.taxes, [])
|
||||||
|
self.assertEqual(pe.received_amount_after_tax, 856.0)
|
||||||
|
|
||||||
def test_unchecked_receipt_is_untouched(self):
|
def test_create_payment_from_invoice_applies_withholding(self):
|
||||||
si = self.make_invoice()
|
si = self.make_invoice()
|
||||||
pe = self.make_receipt(si, allocated=10700, paid=10700, apply=0)
|
pe = get_payment_entry("Sales Invoice", si.name, bank_account=f"Cash - {ABBR}")
|
||||||
self.assertEqual(pe.deductions, [])
|
self.assertEqual(pe.apply_tax_withholding_amount, 1)
|
||||||
self.assertEqual(pe.difference_amount, 0)
|
self.assertEqual(pe.tax_withholding_category, "WHT 3% - Service")
|
||||||
|
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 300.0)])
|
||||||
|
self.assertEqual((pe.paid_amount, pe.received_amount_after_tax), (10700.0, 10400.0))
|
||||||
|
pe.insert()
|
||||||
|
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 300.0)])
|
||||||
|
|
||||||
|
def test_new_receipt_referencing_withheld_invoice_applies_on_save(self):
|
||||||
|
si = self.make_invoice()
|
||||||
|
pe = self.make_receipt(si, allocated=10700, apply=0)
|
||||||
|
self.assertEqual(pe.apply_tax_withholding_amount, 1)
|
||||||
|
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 300.0)])
|
||||||
|
|
||||||
|
def test_unticking_after_save_removes_withholding(self):
|
||||||
|
si = self.make_invoice()
|
||||||
|
pe = self.make_receipt(si, allocated=10700)
|
||||||
|
pe.apply_tax_withholding_amount = 0
|
||||||
|
pe.save()
|
||||||
|
self.assertEqual(pe.apply_tax_withholding_amount, 0) # saved docs keep the user's choice
|
||||||
|
self.assertEqual(pe.taxes, [])
|
||||||
|
self.assertEqual(pe.received_amount_after_tax, 10700.0)
|
||||||
|
|
||||||
|
def test_reallocation_on_form_recomputes_withholding(self):
|
||||||
|
si = self.make_invoice()
|
||||||
|
pe = get_payment_entry("Sales Invoice", si.name, bank_account=f"Cash - {ABBR}")
|
||||||
|
pe.paid_amount = pe.received_amount = 5350
|
||||||
|
pe.allocate_amount_to_references(
|
||||||
|
paid_amount=5350, paid_amount_change=True, allocate_payment_amount=True
|
||||||
|
)
|
||||||
|
self.assertEqual(pe.references[0].allocated_amount, 5350.0)
|
||||||
|
self.assertEqual(self.withheld(pe), [(self.receivable, "Deduct", 150.0)])
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
Net Payable After Withholding Tax,ยอดชำระสุทธิหลังหักภาษี ณ ที่จ่าย
|
||||||
|
Withholding Tax,ภาษีหัก ณ ที่จ่าย
|
||||||
|
Tax Withholding Category,ประเภทภาษีหัก ณ ที่จ่าย
|
||||||
|
Withholding tax deducted by customer,ภาษีหัก ณ ที่จ่ายที่ลูกค้าหักไว้
|
||||||
|
Tax Withholding Category {0} has no account for Company {1},ประเภทภาษีหัก ณ ที่จ่าย {0} ไม่มีบัญชีสำหรับบริษัท {1}
|
||||||
|
Account {0} not found for Company {1},ไม่พบบัญชี {0} สำหรับบริษัท {1}
|
||||||
|
Please set Tax Withholding Category on {0} {1} or on this Payment Entry,กรุณาระบุประเภทภาษีหัก ณ ที่จ่ายใน {0} {1} หรือในรายการชำระเงินนี้
|
||||||
|
Tax ID,เลขประจำตัวผู้เสียภาษี
|
||||||
|
Tax Id,เลขประจำตัวผู้เสียภาษี
|
||||||
|
Signatures,ลายเซ็น
|
||||||
|
On behalf of,ในนาม
|
||||||
|
Receiver,ผู้รับ
|
||||||
|
Issuer,ผู้ออก
|
||||||
|
Tax Invoice,ใบกำกับภาษี
|
||||||
|
Tax Invoice/Receipt,ใบกำกับภาษี/ใบเสร็จรับเงิน
|
||||||
|
"Commission, brokerage, agency fees - Sec. 40(2), juristic payee (P.N.D.53)",ค่านายหน้า ค่าตัวแทน - มาตรา 40(2) ผู้รับเป็นนิติบุคคล (ภ.ง.ด.53)
|
||||||
|
"Royalties, goodwill, copyright, other rights - Sec. 40(3), juristic payee (P.N.D.53)",ค่าแห่งกู๊ดวิลล์ ค่าลิขสิทธิ์ หรือสิทธิอย่างอื่น - มาตรา 40(3) ผู้รับเป็นนิติบุคคล (ภ.ง.ด.53)
|
||||||
|
"Interest - Sec. 40(4)(a), juristic payee excl. banks/finance companies (P.N.D.53)",ดอกเบี้ย - มาตรา 40(4)(ก) ผู้รับเป็นนิติบุคคล ยกเว้นธนาคาร/บริษัทเงินทุน (ภ.ง.ด.53)
|
||||||
|
"Interest - Sec. 40(4)(a), individual payee, Sec. 50(2) (P.N.D.2)",ดอกเบี้ย - มาตรา 40(4)(ก) ผู้รับเป็นบุคคลธรรมดา มาตรา 50(2) (ภ.ง.ด.2)
|
||||||
|
"Dividends, share of profits - Sec. 40(4)(b) (P.N.D.2 / P.N.D.53)",เงินปันผล เงินส่วนแบ่งของกำไร - มาตรา 40(4)(ข) (ภ.ง.ด.2 / ภ.ง.ด.53)
|
||||||
|
"Rent of property, hire-purchase of movable property - Sec. 40(5) (P.N.D.3 / P.N.D.53)",ค่าเช่าทรัพย์สิน ค่าเช่าซื้อสังหาริมทรัพย์ - มาตรา 40(5) (ภ.ง.ด.3 / ภ.ง.ด.53)
|
||||||
|
Rent of ships under the Merchant Marine Promotion Act - Sec. 40(5) (P.N.D.53),ค่าเช่าเรือตามกฎหมายว่าด้วยการส่งเสริมการพาณิชยนาวี - มาตรา 40(5) (ภ.ง.ด.53)
|
||||||
|
"Law, medicine, engineering, architecture, accounting, fine arts - Sec. 40(6) (P.N.D.3 / P.N.D.53)",วิชาชีพอิสระ กฎหมาย การประกอบโรคศิลปะ วิศวกรรม สถาปัตยกรรม การบัญชี ประณีตศิลปกรรม - มาตรา 40(6) (ภ.ง.ด.3 / ภ.ง.ด.53)
|
||||||
|
"Contract work, contractor supplies essential materials - Sec. 40(7) (P.N.D.3 / P.N.D.53)",ค่ารับเหมาที่ผู้รับเหมาต้องลงทุนจัดหาสัมภาระในส่วนสำคัญ - มาตรา 40(7) (ภ.ง.ด.3 / ภ.ง.ด.53)
|
||||||
|
"Hire of work, service fees - Sec. 40(8) (P.N.D.3 / P.N.D.53)",ค่าจ้างทำของ ค่าบริการ - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
|
||||||
|
"Rewards, discounts, sales promotion benefits - Sec. 40(8) (P.N.D.3 / P.N.D.53)",รางวัล ส่วนลด หรือประโยชน์ใด ๆ เนื่องจากการส่งเสริมการขาย - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
|
||||||
|
Advertising fees - Sec. 40(8) (P.N.D.3 / P.N.D.53),ค่าโฆษณา - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
|
||||||
|
"Transport of goods or passengers, excl. public transport - Sec. 40(8) (P.N.D.3 / P.N.D.53)",ค่าขนส่งสินค้าหรือผู้โดยสาร ยกเว้นการขนส่งสาธารณะ - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
|
||||||
|
Non-life insurance premiums - Sec. 40(8) (P.N.D.53),ค่าเบี้ยประกันวินาศภัย - มาตรา 40(8) (ภ.ง.ด.53)
|
||||||
|
"Prizes from contests, competitions, lucky draws - Sec. 40(8) (P.N.D.3 / P.N.D.53)",รางวัลจากการประกวด การแข่งขัน การชิงโชค - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
|
||||||
|
Public entertainers resident in Thailand - Sec. 40(8) (P.N.D.3),นักแสดงสาธารณะที่มีภูมิลำเนาในประเทศไทย - มาตรา 40(8) (ภ.ง.ด.3)
|
||||||
|
"Rubber, cassava, jute, rice, maize, sugarcane, coffee beans, oil palm - Sec. 40(8) (P.N.D.3 / P.N.D.53)",ยางแผ่น มันสำปะหลัง ปอ ข้าว ข้าวโพด อ้อย เมล็ดกาแฟ ปาล์มน้ำมัน - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
|
||||||
|
Aquatic animals bought from non-registered sellers - Sec. 40(8) (P.N.D.3 / P.N.D.53),สัตว์น้ำที่ซื้อจากผู้ขายที่ไม่ได้จดทะเบียนภาษีมูลค่าเพิ่ม - มาตรา 40(8) (ภ.ง.ด.3 / ภ.ง.ด.53)
|
||||||
|
"Sale of immovable property, juristic seller - Sec. 69 Ter (remitted at the Land Office)",การขายอสังหาริมทรัพย์ ผู้ขายเป็นนิติบุคคล - มาตรา 69 ตรี (นำส่ง ณ สำนักงานที่ดิน)
|
||||||
|
Sec. 40(2)-(6) income paid to non-resident individuals - Sec. 50(2) (P.N.D.3),เงินได้ตามมาตรา 40(2)-(6) จ่ายให้บุคคลธรรมดาที่มิได้เป็นผู้อยู่ในประเทศไทย - มาตรา 50(2) (ภ.ง.ด.3)
|
||||||
|
"Sec. 40(2)-(6) income excl. dividends to foreign companies not doing business in Thailand - Sec. 70, pre-DTA (P.N.D.54)",เงินได้ตามมาตรา 40(2)-(6) ยกเว้นเงินปันผล จ่ายให้บริษัทต่างประเทศที่มิได้ประกอบกิจการในประเทศไทย - มาตรา 70 ก่อนใช้อนุสัญญาภาษีซ้อน (ภ.ง.ด.54)
|
||||||
|
"Dividends to foreign companies not doing business in Thailand - Sec. 70, pre-DTA (P.N.D.54)",เงินปันผลจ่ายให้บริษัทต่างประเทศที่มิได้ประกอบกิจการในประเทศไทย - มาตรา 70 ก่อนใช้อนุสัญญาภาษีซ้อน (ภ.ง.ด.54)
|
||||||
|
Sales Invoice,ใบแจ้งหนี้
|
||||||
|
Sales Order,ใบสั่งขาย
|
||||||
|
Posting Date,วันที่
|
||||||
|
Payment Due Date,วันครบกำหนดชำระ
|
||||||
|
Contact Person,ผู้ติดต่อ
|
||||||
|
Mobile No,โทรศัพท์มือถือ
|
||||||
|
Contact Email,อีเมล
|
||||||
|
Description,รายละเอียด
|
||||||
|
Rate,ราคาต่อหน่วย
|
||||||
|
Amount,จำนวนเงิน
|
||||||
|
Total,รวม
|
||||||
|
Total (Without Tax),รวมก่อนภาษี
|
||||||
|
Net Total,ยอดรวมสุทธิ
|
||||||
|
Total Taxes and Charges,รวมภาษีและค่าธรรมเนียม
|
||||||
|
Grand Total,ยอดรวมทั้งสิ้น
|
||||||
|
Rounded Total,ยอดรวมปัดเศษ
|
||||||
|
Terms and Conditions Details,รายละเอียดข้อตกลงและเงื่อนไข
|
||||||
|
Page {0} of {1},หน้า {0} จาก {1}
|
||||||
|
@@ -1,6 +1,30 @@
|
|||||||
import frappe
|
import frappe
|
||||||
|
import frappe.utils
|
||||||
from erpnext import get_default_company
|
from erpnext import get_default_company
|
||||||
from frappe.contacts.doctype.address.address import get_default_address, render_address
|
from frappe.contacts.doctype.address.address import get_default_address, render_address
|
||||||
|
from num2words import num2words
|
||||||
|
|
||||||
|
|
||||||
|
def money_in_words(amount, currency):
|
||||||
|
"""Amount in words for the active language.
|
||||||
|
|
||||||
|
frappe.utils.money_in_words renders "<currency> <words> only." in every
|
||||||
|
language; Thai documents write "<words>บาทถ้วน" or "<words>บาท<words>สตางค์".
|
||||||
|
Currencies num2words cannot spell in Thai keep frappe's wording.
|
||||||
|
"""
|
||||||
|
if frappe.local.lang == "th":
|
||||||
|
try:
|
||||||
|
return num2words(amount, lang="th", to="currency", currency=currency)
|
||||||
|
except NotImplementedError:
|
||||||
|
pass
|
||||||
|
return frappe.utils.money_in_words(amount, currency)
|
||||||
|
|
||||||
|
|
||||||
|
def get_in_words(doc):
|
||||||
|
"""`in_words` for print formats: re-derived in the print language instead
|
||||||
|
of the language of whoever saved the document."""
|
||||||
|
amount = abs(doc.grand_total if doc.is_rounded_total_disabled() else doc.rounded_total)
|
||||||
|
return money_in_words(amount, doc.currency)
|
||||||
|
|
||||||
|
|
||||||
def get_letter_head_company(doc=None):
|
def get_letter_head_company(doc=None):
|
||||||
|
|||||||
Reference in New Issue
Block a user