mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-13 20:05:09 +00:00
Co-authored-by: ljain112 <ljain112@gmail.com> Co-authored-by: Smit Vora <smitvora203@gmail.com> Co-authored-by: Diptanil Saha <diptanil@frappe.io>
This commit is contained in:
@@ -33,6 +33,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
|
|||||||
get_depr_schedule,
|
get_depr_schedule,
|
||||||
)
|
)
|
||||||
from erpnext.controllers.accounts_controller import AccountsController
|
from erpnext.controllers.accounts_controller import AccountsController
|
||||||
|
from erpnext.setup.utils import get_exchange_rate as _get_exchange_rate
|
||||||
|
|
||||||
|
|
||||||
class StockAccountInvalidTransaction(frappe.ValidationError):
|
class StockAccountInvalidTransaction(frappe.ValidationError):
|
||||||
@@ -273,93 +274,7 @@ class JournalEntry(AccountsController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def apply_tax_withholding(self):
|
def apply_tax_withholding(self):
|
||||||
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
|
JournalEntryTaxWithholding(self).apply()
|
||||||
|
|
||||||
if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"):
|
|
||||||
return
|
|
||||||
|
|
||||||
parties = [d.party for d in self.get("accounts") if d.party]
|
|
||||||
parties = list(set(parties))
|
|
||||||
|
|
||||||
if len(parties) > 1:
|
|
||||||
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
|
|
||||||
|
|
||||||
account_type_map = get_account_type_map(self.company)
|
|
||||||
party_type = "supplier" if self.voucher_type == "Credit Note" else "customer"
|
|
||||||
doctype = "Purchase Invoice" if self.voucher_type == "Credit Note" else "Sales Invoice"
|
|
||||||
debit_or_credit = (
|
|
||||||
"debit_in_account_currency"
|
|
||||||
if self.voucher_type == "Credit Note"
|
|
||||||
else "credit_in_account_currency"
|
|
||||||
)
|
|
||||||
rev_debit_or_credit = (
|
|
||||||
"credit_in_account_currency"
|
|
||||||
if debit_or_credit == "debit_in_account_currency"
|
|
||||||
else "debit_in_account_currency"
|
|
||||||
)
|
|
||||||
|
|
||||||
party_account = get_party_account(party_type.title(), parties[0], self.company)
|
|
||||||
|
|
||||||
net_total = sum(
|
|
||||||
d.get(debit_or_credit)
|
|
||||||
for d in self.get("accounts")
|
|
||||||
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
|
|
||||||
)
|
|
||||||
|
|
||||||
party_amount = sum(
|
|
||||||
d.get(rev_debit_or_credit) for d in self.get("accounts") if d.account == party_account
|
|
||||||
)
|
|
||||||
|
|
||||||
inv = frappe._dict(
|
|
||||||
{
|
|
||||||
party_type: parties[0],
|
|
||||||
"doctype": doctype,
|
|
||||||
"company": self.company,
|
|
||||||
"posting_date": self.posting_date,
|
|
||||||
"net_total": net_total,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
|
|
||||||
inv, self.tax_withholding_category
|
|
||||||
)
|
|
||||||
|
|
||||||
if not tax_withholding_details:
|
|
||||||
return
|
|
||||||
|
|
||||||
accounts = []
|
|
||||||
for d in self.get("accounts"):
|
|
||||||
if d.get("account") == tax_withholding_details.get("account_head"):
|
|
||||||
d.update(
|
|
||||||
{
|
|
||||||
"account": tax_withholding_details.get("account_head"),
|
|
||||||
debit_or_credit: tax_withholding_details.get("tax_amount"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
accounts.append(d.get("account"))
|
|
||||||
|
|
||||||
if d.get("account") == party_account:
|
|
||||||
d.update({rev_debit_or_credit: party_amount - tax_withholding_details.get("tax_amount")})
|
|
||||||
|
|
||||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
|
||||||
self.append(
|
|
||||||
"accounts",
|
|
||||||
{
|
|
||||||
"account": tax_withholding_details.get("account_head"),
|
|
||||||
rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
|
|
||||||
"against_account": parties[0],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
to_remove = [
|
|
||||||
d
|
|
||||||
for d in self.get("accounts")
|
|
||||||
if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")
|
|
||||||
]
|
|
||||||
|
|
||||||
for d in to_remove:
|
|
||||||
self.remove(d)
|
|
||||||
|
|
||||||
def update_asset_value(self):
|
def update_asset_value(self):
|
||||||
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
|
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
|
||||||
@@ -1281,6 +1196,230 @@ class JournalEntry(AccountsController):
|
|||||||
frappe.throw(_("Accounts table cannot be blank."))
|
frappe.throw(_("Accounts table cannot be blank."))
|
||||||
|
|
||||||
|
|
||||||
|
class JournalEntryTaxWithholding:
|
||||||
|
def __init__(self, journal_entry):
|
||||||
|
self.doc: JournalEntry = journal_entry
|
||||||
|
self.party = None
|
||||||
|
self.party_type = None
|
||||||
|
self.party_account = None
|
||||||
|
self.party_row = None
|
||||||
|
self.existing_tds_rows = []
|
||||||
|
self.precision = None
|
||||||
|
self.has_multiple_parties = False
|
||||||
|
|
||||||
|
# Direction fields based on party type
|
||||||
|
self.party_field = None # "credit" for Supplier, "debit" for Customer
|
||||||
|
self.reverse_field = None # opposite of party_field
|
||||||
|
|
||||||
|
def apply(self):
|
||||||
|
if not self._set_party_info():
|
||||||
|
return
|
||||||
|
|
||||||
|
self._setup_direction_fields()
|
||||||
|
self._reset_existing_tds()
|
||||||
|
|
||||||
|
if not self._should_apply_tds():
|
||||||
|
self._cleanup_duplicate_tds_rows(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.has_multiple_parties:
|
||||||
|
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
|
||||||
|
|
||||||
|
net_total = self._calculate_net_total()
|
||||||
|
if net_total <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
tds_details = self._get_tds_details(net_total)
|
||||||
|
if not tds_details or not tds_details.get("tax_amount"):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._create_or_update_tds_row(tds_details)
|
||||||
|
self._update_party_amount(tds_details.get("tax_amount"), is_reversal=False)
|
||||||
|
|
||||||
|
self._recalculate_totals()
|
||||||
|
|
||||||
|
def _should_apply_tds(self):
|
||||||
|
return self.doc.apply_tds and self.doc.voucher_type in ("Debit Note", "Credit Note")
|
||||||
|
|
||||||
|
def _set_party_info(self):
|
||||||
|
for row in self.doc.get("accounts"):
|
||||||
|
if row.party_type in ("Customer", "Supplier") and row.party:
|
||||||
|
if self.party and row.party != self.party:
|
||||||
|
self.has_multiple_parties = True
|
||||||
|
|
||||||
|
if not self.party:
|
||||||
|
self.party = row.party
|
||||||
|
self.party_type = row.party_type
|
||||||
|
self.party_account = row.account
|
||||||
|
self.party_row = row
|
||||||
|
|
||||||
|
if row.get("is_tax_withholding_account"):
|
||||||
|
self.existing_tds_rows.append(row)
|
||||||
|
|
||||||
|
return bool(self.party)
|
||||||
|
|
||||||
|
def _setup_direction_fields(self):
|
||||||
|
"""
|
||||||
|
For Supplier (TDS): party has credit, TDS reduces credit
|
||||||
|
For Customer (TCS): party has debit, TCS increases debit
|
||||||
|
"""
|
||||||
|
if self.party_type == "Supplier":
|
||||||
|
self.party_field = "credit"
|
||||||
|
self.reverse_field = "debit"
|
||||||
|
else: # Customer
|
||||||
|
self.party_field = "debit"
|
||||||
|
self.reverse_field = "credit"
|
||||||
|
|
||||||
|
self.precision = self.doc.precision(self.party_field, self.party_row)
|
||||||
|
|
||||||
|
def _reset_existing_tds(self):
|
||||||
|
for row in self.existing_tds_rows:
|
||||||
|
# TDS amount is always in credit (liability to government)
|
||||||
|
tds_amount = flt(row.get("credit") - row.get("debit"), self.precision)
|
||||||
|
if not tds_amount:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._update_party_amount(tds_amount, is_reversal=True)
|
||||||
|
|
||||||
|
# zero_out_tds_row
|
||||||
|
row.update(
|
||||||
|
{
|
||||||
|
"credit": 0,
|
||||||
|
"credit_in_account_currency": 0,
|
||||||
|
"debit": 0,
|
||||||
|
"debit_in_account_currency": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _update_party_amount(self, amount, is_reversal=False):
|
||||||
|
amount = flt(amount, self.precision)
|
||||||
|
amount_in_party_currency = flt(amount / self.party_row.get("exchange_rate", 1), self.precision)
|
||||||
|
|
||||||
|
# Determine which field the party amount is in
|
||||||
|
active_field = self.party_field if self.party_row.get(self.party_field) else self.reverse_field
|
||||||
|
|
||||||
|
# If amount is in reverse field, flip the signs
|
||||||
|
if active_field == self.reverse_field:
|
||||||
|
amount = -amount
|
||||||
|
amount_in_party_currency = -amount_in_party_currency
|
||||||
|
|
||||||
|
# Direction multiplier based on party type:
|
||||||
|
# Customer (TCS): +1 (add to debit)
|
||||||
|
# Supplier (TDS): -1 (subtract from credit)
|
||||||
|
direction = 1 if self.party_type == "Customer" else -1
|
||||||
|
|
||||||
|
# Reversal inverts the direction
|
||||||
|
if is_reversal:
|
||||||
|
direction = -direction
|
||||||
|
|
||||||
|
adjustment = amount * direction
|
||||||
|
adjustment_in_party_currency = amount_in_party_currency * direction
|
||||||
|
|
||||||
|
active_field_account_currency = f"{active_field}_in_account_currency"
|
||||||
|
|
||||||
|
self.party_row.update(
|
||||||
|
{
|
||||||
|
active_field: flt(self.party_row.get(active_field) + adjustment, self.precision),
|
||||||
|
active_field_account_currency: flt(
|
||||||
|
self.party_row.get(active_field_account_currency) + adjustment_in_party_currency,
|
||||||
|
self.precision,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _calculate_net_total(self):
|
||||||
|
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
|
||||||
|
|
||||||
|
account_type_map = get_account_type_map(self.doc.company)
|
||||||
|
|
||||||
|
return flt(
|
||||||
|
sum(
|
||||||
|
d.get(self.reverse_field) - d.get(self.party_field)
|
||||||
|
for d in self.doc.get("accounts")
|
||||||
|
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
|
||||||
|
and d.account != self.party_account
|
||||||
|
and not d.get("is_tax_withholding_account")
|
||||||
|
),
|
||||||
|
self.precision,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_tds_details(self, net_total):
|
||||||
|
return get_party_tax_withholding_details(
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"party_type": self.party_type,
|
||||||
|
"party": self.party,
|
||||||
|
"doctype": self.doc.doctype,
|
||||||
|
"company": self.doc.company,
|
||||||
|
"posting_date": self.doc.posting_date,
|
||||||
|
"tax_withholding_net_total": net_total,
|
||||||
|
"base_tax_withholding_net_total": net_total,
|
||||||
|
"grand_total": net_total,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
self.doc.tax_withholding_category,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_or_update_tds_row(self, tds_details):
|
||||||
|
tax_account = tds_details.get("account_head")
|
||||||
|
account_currency = get_account_currency(tax_account)
|
||||||
|
company_currency = frappe.get_cached_value("Company", self.doc.company, "default_currency")
|
||||||
|
exchange_rate = _get_exchange_rate(account_currency, company_currency, self.doc.posting_date)
|
||||||
|
|
||||||
|
tax_amount = flt(tds_details.get("tax_amount"), self.precision)
|
||||||
|
tax_amount_in_account_currency = flt(tax_amount / exchange_rate, self.precision)
|
||||||
|
|
||||||
|
# Find existing TDS row for this account
|
||||||
|
tax_row = None
|
||||||
|
for row in self.doc.get("accounts"):
|
||||||
|
if row.account == tax_account and row.get("is_tax_withholding_account"):
|
||||||
|
tax_row = row
|
||||||
|
break
|
||||||
|
|
||||||
|
if not tax_row:
|
||||||
|
tax_row = self.doc.append(
|
||||||
|
"accounts",
|
||||||
|
{
|
||||||
|
"account": tax_account,
|
||||||
|
"account_currency": account_currency,
|
||||||
|
"exchange_rate": exchange_rate,
|
||||||
|
"cost_center": tds_details.get("cost_center"),
|
||||||
|
"credit": 0,
|
||||||
|
"credit_in_account_currency": 0,
|
||||||
|
"debit": 0,
|
||||||
|
"debit_in_account_currency": 0,
|
||||||
|
"is_tax_withholding_account": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# TDS/TCS is always credited (liability to government)
|
||||||
|
tax_row.update(
|
||||||
|
{
|
||||||
|
"credit": tax_amount,
|
||||||
|
"credit_in_account_currency": tax_amount_in_account_currency,
|
||||||
|
"debit": 0,
|
||||||
|
"debit_in_account_currency": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self._cleanup_duplicate_tds_rows(tax_row)
|
||||||
|
|
||||||
|
def _cleanup_duplicate_tds_rows(self, current_tax_row):
|
||||||
|
rows_to_remove = [
|
||||||
|
row
|
||||||
|
for row in self.doc.get("accounts")
|
||||||
|
if row.get("is_tax_withholding_account") and row != current_tax_row
|
||||||
|
]
|
||||||
|
|
||||||
|
for row in rows_to_remove:
|
||||||
|
self.doc.remove(row)
|
||||||
|
|
||||||
|
def _recalculate_totals(self):
|
||||||
|
self.doc.set_amounts_in_company_currency()
|
||||||
|
self.doc.set_total_debit_credit()
|
||||||
|
self.doc.set_against_account()
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
|
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
|
||||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
|
||||||
@@ -1649,8 +1788,6 @@ def get_exchange_rate(
|
|||||||
credit=None,
|
credit=None,
|
||||||
exchange_rate=None,
|
exchange_rate=None,
|
||||||
):
|
):
|
||||||
from erpnext.setup.utils import get_exchange_rate
|
|
||||||
|
|
||||||
account_details = frappe.get_cached_value(
|
account_details = frappe.get_cached_value(
|
||||||
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
||||||
)
|
)
|
||||||
@@ -1672,8 +1809,8 @@ def get_exchange_rate(
|
|||||||
|
|
||||||
# The date used to retreive the exchange rate here is the date passed
|
# The date used to retreive the exchange rate here is the date passed
|
||||||
# in as an argument to this function.
|
# in as an argument to this function.
|
||||||
elif (not exchange_rate or flt(exchange_rate) == 1) and account_currency and posting_date:
|
elif (not flt(exchange_rate) or flt(exchange_rate) == 1) and account_currency and posting_date:
|
||||||
exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
|
exchange_rate = _get_exchange_rate(account_currency, company_currency, posting_date)
|
||||||
else:
|
else:
|
||||||
exchange_rate = 1
|
exchange_rate = 1
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"reference_detail_no",
|
"reference_detail_no",
|
||||||
"advance_voucher_type",
|
"advance_voucher_type",
|
||||||
"advance_voucher_no",
|
"advance_voucher_no",
|
||||||
|
"is_tax_withholding_account",
|
||||||
"col_break3",
|
"col_break3",
|
||||||
"is_advance",
|
"is_advance",
|
||||||
"user_remark",
|
"user_remark",
|
||||||
@@ -282,12 +283,19 @@
|
|||||||
"options": "advance_voucher_type",
|
"options": "advance_voucher_type",
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_tax_withholding_account",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Tax Withholding Account",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-27 13:48:32.805100",
|
"modified": "2025-11-27 12:23:33.157655",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Journal Entry Account",
|
"name": "Journal Entry Account",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class JournalEntryAccount(Document):
|
|||||||
debit_in_account_currency: DF.Currency
|
debit_in_account_currency: DF.Currency
|
||||||
exchange_rate: DF.Float
|
exchange_rate: DF.Float
|
||||||
is_advance: DF.Literal["No", "Yes"]
|
is_advance: DF.Literal["No", "Yes"]
|
||||||
|
is_tax_withholding_account: DF.Check
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ def get_party_details(inv):
|
|||||||
if inv.doctype == "Sales Invoice":
|
if inv.doctype == "Sales Invoice":
|
||||||
party_type = "Customer"
|
party_type = "Customer"
|
||||||
party = inv.customer
|
party = inv.customer
|
||||||
|
elif inv.doctype == "Journal Entry":
|
||||||
|
party_type = inv.party_type
|
||||||
|
party = inv.party
|
||||||
else:
|
else:
|
||||||
party_type = "Supplier"
|
party_type = "Supplier"
|
||||||
party = inv.supplier
|
party = inv.supplier
|
||||||
@@ -155,7 +158,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
|||||||
party_type, parties, inv, tax_details, posting_date, pan_no
|
party_type, parties, inv, tax_details, posting_date, pan_no
|
||||||
)
|
)
|
||||||
|
|
||||||
if party_type == "Supplier":
|
if party_type == "Supplier" or inv.doctype == "Journal Entry":
|
||||||
tax_row = get_tax_row_for_tds(tax_details, tax_amount)
|
tax_row = get_tax_row_for_tds(tax_details, tax_amount)
|
||||||
else:
|
else:
|
||||||
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
|
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
|
||||||
@@ -346,7 +349,10 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
|||||||
elif party_type == "Customer":
|
elif party_type == "Customer":
|
||||||
if tax_deducted:
|
if tax_deducted:
|
||||||
# if already TCS is charged, then amount will be calculated based on 'Previous Row Total'
|
# if already TCS is charged, then amount will be calculated based on 'Previous Row Total'
|
||||||
tax_amount = 0
|
if inv.doctype == "Sales Invoice":
|
||||||
|
tax_amount = 0
|
||||||
|
else:
|
||||||
|
tax_amount = inv.base_tax_withholding_net_total * tax_details.rate / 100
|
||||||
else:
|
else:
|
||||||
# if no TCS has been charged in FY,
|
# if no TCS has been charged in FY,
|
||||||
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
|
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
|
||||||
@@ -718,7 +724,7 @@ def get_advance_adjusted_in_invoice(inv):
|
|||||||
|
|
||||||
|
|
||||||
def get_invoice_total_without_tcs(inv, tax_details):
|
def get_invoice_total_without_tcs(inv, tax_details):
|
||||||
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
|
tcs_tax_row = [d for d in inv.get("taxes") or [] if d.account_head == tax_details.account_head]
|
||||||
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
|
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
|
||||||
|
|
||||||
return inv.grand_total - tcs_tax_row_amount
|
return inv.grand_total - tcs_tax_row_amount
|
||||||
|
|||||||
@@ -848,6 +848,90 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
|||||||
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||||
self.assertEqual(payment.taxes[0].allocated_amount, 6000)
|
self.assertEqual(payment.taxes[0].allocated_amount, 6000)
|
||||||
|
|
||||||
|
def test_tds_on_journal_entry_for_supplier(self):
|
||||||
|
"""Test TDS deduction for Supplier in Debit Note"""
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||||
|
)
|
||||||
|
|
||||||
|
jv = make_journal_entry_with_tax_withholding(
|
||||||
|
party_type="Supplier",
|
||||||
|
party="Test TDS Supplier",
|
||||||
|
voucher_type="Debit Note",
|
||||||
|
amount=50000,
|
||||||
|
save=False,
|
||||||
|
)
|
||||||
|
jv.apply_tds = 1
|
||||||
|
jv.tax_withholding_category = "Cumulative Threshold TDS"
|
||||||
|
jv.save()
|
||||||
|
|
||||||
|
# Again saving should not change tds amount
|
||||||
|
jv.user_remark = "Test TDS on Journal Entry for Supplier"
|
||||||
|
jv.save()
|
||||||
|
jv.submit()
|
||||||
|
|
||||||
|
# TDS = 50000 * 10% = 5000
|
||||||
|
self.assertEqual(len(jv.accounts), 3)
|
||||||
|
|
||||||
|
# Find TDS account row
|
||||||
|
tds_row = None
|
||||||
|
supplier_row = None
|
||||||
|
for row in jv.accounts:
|
||||||
|
if row.account == "TDS - _TC":
|
||||||
|
tds_row = row
|
||||||
|
elif row.party == "Test TDS Supplier":
|
||||||
|
supplier_row = row
|
||||||
|
|
||||||
|
self.assertEqual(tds_row.credit, 5000)
|
||||||
|
self.assertEqual(tds_row.debit, 0)
|
||||||
|
|
||||||
|
# Supplier amount should be reduced by TDS
|
||||||
|
self.assertEqual(supplier_row.credit, 45000)
|
||||||
|
jv.cancel()
|
||||||
|
|
||||||
|
def test_tcs_on_journal_entry_for_customer(self):
|
||||||
|
"""Test TCS collection for Customer in Credit Note"""
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create Credit Note with amount exceeding threshold
|
||||||
|
jv = make_journal_entry_with_tax_withholding(
|
||||||
|
party_type="Customer",
|
||||||
|
party="Test TCS Customer",
|
||||||
|
voucher_type="Credit Note",
|
||||||
|
amount=50000,
|
||||||
|
save=False,
|
||||||
|
)
|
||||||
|
jv.apply_tds = 1
|
||||||
|
jv.tax_withholding_category = "Cumulative Threshold TCS"
|
||||||
|
jv.save()
|
||||||
|
|
||||||
|
# Again saving should not change tds amount
|
||||||
|
jv.user_remark = "Test TCS on Journal Entry for Customer"
|
||||||
|
jv.save()
|
||||||
|
jv.submit()
|
||||||
|
|
||||||
|
# Assert TCS calculation (10% on amount above threshold of 30000)
|
||||||
|
self.assertEqual(len(jv.accounts), 3)
|
||||||
|
|
||||||
|
# Find TCS account row
|
||||||
|
tcs_row = None
|
||||||
|
customer_row = None
|
||||||
|
for row in jv.accounts:
|
||||||
|
if row.account == "TCS - _TC":
|
||||||
|
tcs_row = row
|
||||||
|
elif row.party == "Test TCS Customer":
|
||||||
|
customer_row = row
|
||||||
|
|
||||||
|
# TCS should be credited (liability to government)
|
||||||
|
self.assertEqual(tcs_row.credit, 2000) # above threshold 20000*10%
|
||||||
|
self.assertEqual(tcs_row.debit, 0)
|
||||||
|
|
||||||
|
# Customer amount should be increased by TCS
|
||||||
|
self.assertEqual(customer_row.debit, 52000)
|
||||||
|
jv.cancel()
|
||||||
|
|
||||||
|
|
||||||
def cancel_invoices():
|
def cancel_invoices():
|
||||||
purchase_invoices = frappe.get_all(
|
purchase_invoices = frappe.get_all(
|
||||||
@@ -996,6 +1080,88 @@ def create_payment_entry(**args):
|
|||||||
return pe
|
return pe
|
||||||
|
|
||||||
|
|
||||||
|
def make_journal_entry_with_tax_withholding(
|
||||||
|
party_type,
|
||||||
|
party,
|
||||||
|
voucher_type,
|
||||||
|
amount,
|
||||||
|
cost_center=None,
|
||||||
|
posting_date=None,
|
||||||
|
save=True,
|
||||||
|
submit=False,
|
||||||
|
):
|
||||||
|
"""Helper function to create Journal Entry for tax withholding"""
|
||||||
|
if not cost_center:
|
||||||
|
cost_center = "_Test Cost Center - _TC"
|
||||||
|
|
||||||
|
jv = frappe.new_doc("Journal Entry")
|
||||||
|
jv.posting_date = posting_date or today()
|
||||||
|
jv.company = "_Test Company"
|
||||||
|
jv.voucher_type = voucher_type
|
||||||
|
jv.multi_currency = 0
|
||||||
|
|
||||||
|
if party_type == "Supplier":
|
||||||
|
# Debit Note: Expense Dr, Supplier Cr
|
||||||
|
expense_account = "Stock Received But Not Billed - _TC"
|
||||||
|
party_account = "Creditors - _TC"
|
||||||
|
|
||||||
|
jv.append(
|
||||||
|
"accounts",
|
||||||
|
{
|
||||||
|
"account": expense_account,
|
||||||
|
"cost_center": cost_center,
|
||||||
|
"debit_in_account_currency": amount,
|
||||||
|
"exchange_rate": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
jv.append(
|
||||||
|
"accounts",
|
||||||
|
{
|
||||||
|
"account": party_account,
|
||||||
|
"party_type": party_type,
|
||||||
|
"party": party,
|
||||||
|
"cost_center": cost_center,
|
||||||
|
"credit_in_account_currency": amount,
|
||||||
|
"exchange_rate": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else: # Customer
|
||||||
|
# Credit Note: Customer Dr, Income Cr
|
||||||
|
party_account = "Debtors - _TC"
|
||||||
|
income_account = "Sales - _TC"
|
||||||
|
|
||||||
|
jv.append(
|
||||||
|
"accounts",
|
||||||
|
{
|
||||||
|
"account": party_account,
|
||||||
|
"party_type": party_type,
|
||||||
|
"party": party,
|
||||||
|
"cost_center": cost_center,
|
||||||
|
"debit_in_account_currency": amount,
|
||||||
|
"exchange_rate": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
jv.append(
|
||||||
|
"accounts",
|
||||||
|
{
|
||||||
|
"account": income_account,
|
||||||
|
"cost_center": cost_center,
|
||||||
|
"credit_in_account_currency": amount,
|
||||||
|
"exchange_rate": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if save or submit:
|
||||||
|
jv.insert()
|
||||||
|
|
||||||
|
if submit:
|
||||||
|
jv.submit()
|
||||||
|
|
||||||
|
return jv
|
||||||
|
|
||||||
|
|
||||||
def create_records():
|
def create_records():
|
||||||
# create a new suppliers
|
# create a new suppliers
|
||||||
for name in [
|
for name in [
|
||||||
|
|||||||
Reference in New Issue
Block a user