feat: customer-side withholding tax on Payment Entry
Thai customers withhold tax at payment; ERPNext's customer-side handling is Indian TCS which adds tax on top of the Sales Invoice. Override SalesInvoice.set_tax_withholding to no-op for Thai companies, expose "Apply Tax Withholding Amount" on Customer/Receive Payment Entries, and on validate compute rate x pre-VAT amount of each allocated reference (prorated, single threshold honoured) as a deduction to "Withholding Tax Receivable". Client script fetches the Customer's category when the box is ticked.
This commit is contained in:
@@ -1,4 +1,12 @@
|
||||
import erpnext
|
||||
import frappe
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_tax_withholding_details,
|
||||
normal_round,
|
||||
)
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
# 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.
|
||||
@@ -102,3 +110,91 @@ def setup_company(doc, method=None):
|
||||
category = frappe.get_doc("Tax Withholding Category", name)
|
||||
if link_company(category, doc.name, account):
|
||||
category.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def is_thai_company(company):
|
||||
return frappe.get_cached_value("Company", company, "country") == "Thailand"
|
||||
|
||||
|
||||
class ThaiSalesInvoice(SalesInvoice):
|
||||
def set_tax_withholding(self):
|
||||
"""Thai customers withhold at payment (see `set_customer_withholding`);
|
||||
ERPNext's customer-side handling is Indian TCS, which adds tax on top of
|
||||
the invoice. Skip it for Thai companies."""
|
||||
if is_thai_company(self.company):
|
||||
return
|
||||
super().set_tax_withholding()
|
||||
|
||||
|
||||
def set_customer_withholding(doc, method=None):
|
||||
"""Payment Entry.validate: tax withheld by the customer on a Receive entry
|
||||
becomes a deduction to the company's withholding receivable account.
|
||||
|
||||
Withholding is rate x pre-VAT amount of each allocated reference, prorated
|
||||
by the allocation. `paid_amount` is the cash actually received, so the
|
||||
deduction closes the difference against the gross allocation. Runs after
|
||||
the controller's validate (exchange rates and allocations are final), then
|
||||
re-derives the two amounts that depend on deductions.
|
||||
"""
|
||||
if doc.party_type != "Customer" or doc.payment_type != "Receive" or not doc.apply_tax_withholding_amount:
|
||||
return
|
||||
|
||||
if not doc.tax_withholding_category:
|
||||
doc.tax_withholding_category = frappe.db.get_value("Customer", doc.party, "tax_withholding_category")
|
||||
if not doc.tax_withholding_category:
|
||||
frappe.throw(_("Please set Tax Withholding Category for Customer {0}").format(doc.party))
|
||||
|
||||
details = get_tax_withholding_details(doc.tax_withholding_category, doc.posting_date, doc.company)
|
||||
if not details:
|
||||
frappe.throw(
|
||||
_("Tax Withholding Category {0} has no account for Company {1}").format(
|
||||
doc.tax_withholding_category, doc.company
|
||||
)
|
||||
)
|
||||
|
||||
account = frappe.db.get_value(
|
||||
"Account",
|
||||
{"company": doc.company, "account_name": ASSET_ACCOUNT, "root_type": "Asset", "is_group": 0},
|
||||
)
|
||||
if not account:
|
||||
frappe.throw(_("Account {0} not found for Company {1}").format(ASSET_ACCOUNT, doc.company))
|
||||
|
||||
amount = get_customer_withholding_amount(doc, details)
|
||||
row = next((d for d in doc.deductions if d.account == account), None)
|
||||
|
||||
if not amount:
|
||||
if row:
|
||||
doc.remove(row)
|
||||
else:
|
||||
if not row:
|
||||
row = doc.append("deductions", {"account": account})
|
||||
row.amount = amount
|
||||
row.description = details.description
|
||||
row.cost_center = doc.cost_center or erpnext.get_default_cost_center(doc.company)
|
||||
|
||||
doc.set_unallocated_amount()
|
||||
doc.set_difference_amount()
|
||||
|
||||
|
||||
def get_customer_withholding_amount(doc, details):
|
||||
total = 0.0
|
||||
for ref in doc.references:
|
||||
if ref.reference_doctype not in ("Sales Invoice", "Sales Order") or not ref.allocated_amount:
|
||||
continue
|
||||
|
||||
net_total, grand_total = frappe.db.get_value(
|
||||
ref.reference_doctype, ref.reference_name, ["net_total", "grand_total"]
|
||||
)
|
||||
if not grand_total:
|
||||
continue
|
||||
|
||||
taxable = (
|
||||
flt(ref.allocated_amount) * flt(net_total) / flt(grand_total) * flt(doc.source_exchange_rate)
|
||||
)
|
||||
if details.threshold and taxable < details.threshold:
|
||||
continue
|
||||
total += taxable * flt(details.rate) / 100
|
||||
|
||||
if cint(details.round_off_tax_amount):
|
||||
return normal_round(total)
|
||||
return flt(total, doc.precision("difference_amount"))
|
||||
|
||||
Reference in New Issue
Block a user