feat: apply customer withholding automatically on new receipts
Tick Apply Tax Withholding Amount on unsaved customer receipts whose references include a Sales Invoice carrying withholding: in the get_payment_entry override (Create > Payment), in the allocate_amount_to_references doc method (Get Outstanding Invoices / Paid Amount changes, so the tax row updates live) and on first save. Saved entries keep the user's choice; unticking now removes the row. Client script recomputes the row when the checkbox or category changes.
This commit is contained in:
@@ -196,11 +196,11 @@ doc_events = {
|
||||
|
||||
# Overriding Methods
|
||||
# ------------------------------
|
||||
#
|
||||
# override_whitelisted_methods = {
|
||||
# "frappe.desk.doctype.event.event.get_events": "default_thai_company.event.get_events"
|
||||
# }
|
||||
#
|
||||
|
||||
override_whitelisted_methods = {
|
||||
"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;
|
||||
# generated from the base implementation of the doctype dashboard,
|
||||
# along with any modifications made in other Frappe apps
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
frappe.ui.form.on("Payment Entry", {
|
||||
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
|
||||
// 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.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,6 +1,7 @@
|
||||
import erpnext
|
||||
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.tax_withholding_category.tax_withholding_category import (
|
||||
get_tax_withholding_details,
|
||||
@@ -171,6 +172,46 @@ class ThaiPaymentEntry(PaymentEntry):
|
||||
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(*args, **kwargs):
|
||||
"""Create > Payment from a Sales Invoice: arrive with withholding applied."""
|
||||
pe = _get_payment_entry(*args, **kwargs)
|
||||
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):
|
||||
"""Payment Entry.validate: tax withheld by the customer on a Receive entry
|
||||
@@ -184,21 +225,32 @@ def set_customer_withholding(doc, method=None):
|
||||
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
|
||||
|
||||
if not doc.tax_withholding_category:
|
||||
doc.tax_withholding_category = frappe.db.get_value("Customer", doc.party, "tax_withholding_category")
|
||||
if method == "validate":
|
||||
auto_apply_customer_withholding(doc)
|
||||
|
||||
account = frappe.db.get_value(
|
||||
"Account",
|
||||
{"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:
|
||||
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)
|
||||
row = next((d for d in doc.taxes if d.account_head == account), None)
|
||||
|
||||
if not amount:
|
||||
if row:
|
||||
|
||||
@@ -6,7 +6,12 @@ from frappe.modules.utils import sync_customizations_for_doctype
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
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"
|
||||
ABBR = "_TWC"
|
||||
@@ -229,12 +234,42 @@ class TestTaxWithholding(FrappeTestCase):
|
||||
|
||||
def test_below_single_threshold_has_no_deduction(self):
|
||||
si = self.make_invoice(rate=800) # net 800 < 1,000 threshold
|
||||
pe = self.make_receipt(si, allocated=856)
|
||||
pe = self.make_receipt(si, allocated=856, apply=0)
|
||||
self.assertEqual(pe.apply_tax_withholding_amount, 0) # invoice carries no withholding
|
||||
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()
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account=f"Cash - {ABBR}")
|
||||
self.assertEqual(pe.apply_tax_withholding_amount, 1)
|
||||
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)])
|
||||
|
||||
Reference in New Issue
Block a user