fix: post customer withholding as a Deduct tax row, not a deduction

With a deduction row ERPNext treats Paid Amount as cash received and
credits the party for paid + deduction, so a gross Paid Amount (what Get
Outstanding Invoices fills) over-credited Debtors and posted the gross
to the bank. Use the taxes table with add_deduct_tax=Deduct like
supplier TDS: Paid Amount is the gross settlement, bank receives paid
minus withholding, receivable is debited. Override Payment Entry
build_gl_map to flag the bank entry post_net_value so the merged bank
line shows the net amount.
This commit is contained in:
2026-09-13 07:45:11 +00:00
parent abeb2af0c8
commit 6a082edc56
3 changed files with 75 additions and 39 deletions
+30 -11
View File
@@ -1,5 +1,6 @@
import erpnext
import frappe
from erpnext.accounts.doctype.payment_entry.payment_entry import PaymentEntry
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,
@@ -157,15 +158,31 @@ class ThaiSalesInvoice(SalesInvoice):
)
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
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.
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
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.
`paid_amount` is the gross amount settled against the invoices (what
"Get Outstanding Invoices" fills in); the bank receives paid minus the
withholding (`received_amount_after_tax`). Withholding is rate x pre-VAT
amount of each allocated reference, prorated by the allocation. Runs after
the controller's validate, then re-runs the tax computation so the row's
base amounts and totals are final within this save.
"""
if doc.party_type != "Customer" or doc.payment_type != "Receive" or not doc.apply_tax_withholding_amount:
return
@@ -181,20 +198,22 @@ def set_customer_withholding(doc, method=None):
frappe.throw(_("Account {0} not found for Company {1}").format(ASSET_ACCOUNT, doc.company))
amount, description = get_customer_withholding(doc)
row = next((d for d in doc.deductions if d.account == account), None)
row = next((d for d in doc.taxes if d.account_head == account), None)
if not amount:
if row:
doc.remove(row)
else:
if not row:
row = doc.append("deductions", {"account": account})
row.amount = amount
row = doc.append(
"taxes", {"account_head": account, "charge_type": "Actual", "add_deduct_tax": "Deduct"}
)
row.tax_amount = amount
row.description = description
row.cost_center = doc.cost_center or erpnext.get_default_cost_center(doc.company)
doc.set_unallocated_amount()
doc.set_difference_amount()
doc.apply_taxes()
doc.set_amounts_after_tax()
def get_customer_withholding(doc):