mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-19 21:19:19 +00:00
feat: charging tcs on sales invoice
This commit is contained in:
committed by
Deepesh Garg
parent
f000b3b362
commit
62890adef4
@@ -23,6 +23,7 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
|
|||||||
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||||
from frappe.model.utils import get_fetch_values
|
from frappe.model.utils import get_fetch_values
|
||||||
from frappe.contacts.doctype.address.address import get_address_display
|
from frappe.contacts.doctype.address.address import get_address_display
|
||||||
|
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
|
||||||
|
|
||||||
from erpnext.healthcare.utils import manage_invoice_submit_cancel
|
from erpnext.healthcare.utils import manage_invoice_submit_cancel
|
||||||
|
|
||||||
@@ -76,6 +77,8 @@ class SalesInvoice(SellingController):
|
|||||||
if not self.is_pos:
|
if not self.is_pos:
|
||||||
self.so_dn_required()
|
self.so_dn_required()
|
||||||
|
|
||||||
|
self.set_tax_withholding()
|
||||||
|
|
||||||
self.validate_proj_cust()
|
self.validate_proj_cust()
|
||||||
self.validate_pos_return()
|
self.validate_pos_return()
|
||||||
self.validate_with_previous_doc()
|
self.validate_with_previous_doc()
|
||||||
@@ -154,6 +157,30 @@ class SalesInvoice(SellingController):
|
|||||||
if cost_center_company != self.company:
|
if cost_center_company != self.company:
|
||||||
frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company)))
|
frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company)))
|
||||||
|
|
||||||
|
def set_tax_withholding(self):
|
||||||
|
tax_withholding_details = get_party_tax_withholding_details(self)
|
||||||
|
|
||||||
|
if not tax_withholding_details:
|
||||||
|
return
|
||||||
|
|
||||||
|
accounts = []
|
||||||
|
for d in self.taxes:
|
||||||
|
if d.account_head == tax_withholding_details.get("account_head"):
|
||||||
|
d.update(tax_withholding_details)
|
||||||
|
accounts.append(d.account_head)
|
||||||
|
|
||||||
|
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
||||||
|
self.append("taxes", tax_withholding_details)
|
||||||
|
|
||||||
|
to_remove = [d for d in self.taxes
|
||||||
|
if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")]
|
||||||
|
|
||||||
|
for d in to_remove:
|
||||||
|
self.remove(d)
|
||||||
|
|
||||||
|
# calculate totals again after applying TDS
|
||||||
|
self.calculate_taxes_and_totals()
|
||||||
|
|
||||||
def before_save(self):
|
def before_save(self):
|
||||||
set_account_for_mode_of_payment(self)
|
set_account_for_mode_of_payment(self)
|
||||||
|
|
||||||
|
|||||||
@@ -104,31 +104,30 @@ def get_lower_deduction_certificate(fiscal_year, pan_no):
|
|||||||
def get_tax_amount(party_type, parties, ref_doc, tax_details, fiscal_year_details, pan_no=None):
|
def get_tax_amount(party_type, parties, ref_doc, tax_details, fiscal_year_details, pan_no=None):
|
||||||
fiscal_year = fiscal_year_details[0]
|
fiscal_year = fiscal_year_details[0]
|
||||||
|
|
||||||
vouchers = get_invoice_vouchers(parties, fiscal_year, ref_doc.company, party_type=party_type) or [""]
|
vouchers = get_invoice_vouchers(parties, fiscal_year, ref_doc.company, party_type=party_type)
|
||||||
advance_vouchers = get_advance_vouchers(parties, fiscal_year, ref_doc.company, party_type=party_type)
|
advance_vouchers = get_advance_vouchers(parties, fiscal_year, ref_doc.company, party_type=party_type)
|
||||||
tax_vouchers = vouchers + advance_vouchers
|
taxable_vouchers = vouchers + advance_vouchers
|
||||||
|
|
||||||
tax_deducted = 0
|
tax_deducted = 0
|
||||||
dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit'
|
if taxable_vouchers:
|
||||||
if tax_vouchers:
|
# check if tds / tcs is already charged on taxable vouchers
|
||||||
filters = {
|
filters = {
|
||||||
dr_or_cr: ['>', 0],
|
'is_cancelled': 0,
|
||||||
'account': tax_details.account_head,
|
'credit': ['>', 0],
|
||||||
'fiscal_year': fiscal_year,
|
'fiscal_year': fiscal_year,
|
||||||
'voucher_no': ['in', tax_vouchers],
|
'account': tax_details.account_head,
|
||||||
'is_cancelled': 0
|
'voucher_no': ['in', taxable_vouchers],
|
||||||
}
|
}
|
||||||
field = "sum({})".format(dr_or_cr)
|
field = "sum(credit)"
|
||||||
|
|
||||||
tax_deducted = frappe.db.get_value('GL Entry', filters, field) or 0.0
|
tax_deducted = frappe.db.get_value('GL Entry', filters, field) or 0.0
|
||||||
|
|
||||||
tax_amount = 0
|
tax_amount = 0
|
||||||
|
posting_date = ref_doc.posting_date
|
||||||
if party_type == 'Supplier':
|
if party_type == 'Supplier':
|
||||||
net_total = ref_doc.net_total
|
|
||||||
posting_date = ref_doc.posting_date
|
|
||||||
ldc = get_lower_deduction_certificate(fiscal_year, pan_no)
|
ldc = get_lower_deduction_certificate(fiscal_year, pan_no)
|
||||||
|
|
||||||
if tax_deducted:
|
if tax_deducted:
|
||||||
|
net_total = ref_doc.net_total
|
||||||
if ldc:
|
if ldc:
|
||||||
tax_amount = get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total)
|
tax_amount = get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total)
|
||||||
else:
|
else:
|
||||||
@@ -139,6 +138,19 @@ def get_tax_amount(party_type, parties, ref_doc, tax_details, fiscal_year_detail
|
|||||||
fiscal_year_details, vouchers
|
fiscal_year_details, vouchers
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif party_type == 'Customer':
|
||||||
|
if tax_deducted:
|
||||||
|
grand_total = get_invoice_total_without_tcs(ref_doc, tax_details)
|
||||||
|
# if already tcs is charged, then (net total + gst amount) of invoice is chargeable
|
||||||
|
tax_amount = grand_total * tax_details.rate / 100 if grand_total > 0 else 0
|
||||||
|
else:
|
||||||
|
# if no tcs has been charged in FY,
|
||||||
|
# then (prev invoices + advances) value crossing the threshold are chargeable
|
||||||
|
tax_amount = get_tcs_amount(
|
||||||
|
parties, ref_doc, tax_details,
|
||||||
|
fiscal_year_details, vouchers, advance_vouchers
|
||||||
|
)
|
||||||
|
|
||||||
return tax_amount
|
return tax_amount
|
||||||
|
|
||||||
def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'):
|
def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'):
|
||||||
@@ -154,7 +166,7 @@ def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'):
|
|||||||
'is_cancelled': 0
|
'is_cancelled': 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck="voucher_no")
|
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck="voucher_no") or [""]
|
||||||
|
|
||||||
def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None, to_date=None, party_type='Supplier'):
|
def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None, to_date=None, party_type='Supplier'):
|
||||||
# for advance vouchers, debit and credit is reversed
|
# for advance vouchers, debit and credit is reversed
|
||||||
@@ -162,10 +174,11 @@ def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None
|
|||||||
|
|
||||||
filters = {
|
filters = {
|
||||||
dr_or_cr: ['>', 0],
|
dr_or_cr: ['>', 0],
|
||||||
|
'is_opening': 'No',
|
||||||
|
'is_cancelled': 0,
|
||||||
'party_type': party_type,
|
'party_type': party_type,
|
||||||
'party': ['in', parties],
|
'party': ['in', parties],
|
||||||
'is_opening': 'No',
|
'against_voucher': ['is', 'not set']
|
||||||
'is_cancelled': 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if fiscal_year:
|
if fiscal_year:
|
||||||
@@ -175,7 +188,7 @@ def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None
|
|||||||
if from_date and to_date:
|
if from_date and to_date:
|
||||||
filters['posting_date'] = ['between', (from_date, to_date)]
|
filters['posting_date'] = ['between', (from_date, to_date)]
|
||||||
|
|
||||||
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no')
|
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""]
|
||||||
|
|
||||||
def get_tds_amount(ldc, parties, ref_doc, tax_details, fiscal_year_details, vouchers):
|
def get_tds_amount(ldc, parties, ref_doc, tax_details, fiscal_year_details, vouchers):
|
||||||
tds_amount = 0
|
tds_amount = 0
|
||||||
@@ -210,6 +223,53 @@ def get_tds_amount(ldc, parties, ref_doc, tax_details, fiscal_year_details, vouc
|
|||||||
|
|
||||||
return tds_amount
|
return tds_amount
|
||||||
|
|
||||||
|
def get_tcs_amount(parties, ref_doc, tax_details, fiscal_year_details, vouchers, adv_vouchers):
|
||||||
|
tcs_amount = 0
|
||||||
|
fiscal_year, _, _ = fiscal_year_details
|
||||||
|
|
||||||
|
# sum of debit entries made from sales invoices
|
||||||
|
invoiced_amt = frappe.db.get_value('GL Entry', {
|
||||||
|
'is_cancelled': 0,
|
||||||
|
'party': ['in', parties],
|
||||||
|
'company': ref_doc.company,
|
||||||
|
'voucher_no': ['in', vouchers],
|
||||||
|
}, 'sum(debit)') or 0.0
|
||||||
|
|
||||||
|
# sum of credit entries made from PE / JV with unset 'against voucher'
|
||||||
|
advance_amt = frappe.db.get_value('GL Entry', {
|
||||||
|
'is_cancelled': 0,
|
||||||
|
'party': ['in', parties],
|
||||||
|
'company': ref_doc.company,
|
||||||
|
'voucher_no': ['in', adv_vouchers],
|
||||||
|
}, 'sum(credit)') or 0.0
|
||||||
|
|
||||||
|
# sum of credit entries made from sales invoice
|
||||||
|
credit_note_amt = frappe.db.get_value('GL Entry', {
|
||||||
|
'is_cancelled': 0,
|
||||||
|
'credit': ['>', 0],
|
||||||
|
'party': ['in', parties],
|
||||||
|
'fiscal_year': fiscal_year,
|
||||||
|
'company': ref_doc.company,
|
||||||
|
'voucher_type': 'Sales Invoice',
|
||||||
|
}, 'sum(credit)') or 0.0
|
||||||
|
|
||||||
|
current_invoice_total = get_invoice_total_without_tcs(ref_doc, tax_details)
|
||||||
|
chargeable_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt
|
||||||
|
|
||||||
|
threshold = tax_details.get('threshold', 0)
|
||||||
|
cumulative_threshold = tax_details.get('cumulative_threshold', 0)
|
||||||
|
|
||||||
|
if ((threshold and chargeable_amt >= threshold) or (cumulative_threshold and chargeable_amt >= cumulative_threshold)):
|
||||||
|
tcs_amount = chargeable_amt * tax_details.rate / 100 if chargeable_amt > 0 else 0
|
||||||
|
|
||||||
|
return tcs_amount
|
||||||
|
|
||||||
|
def get_invoice_total_without_tcs(ref_doc, tax_details):
|
||||||
|
tcs_tax_row = [d for d in ref_doc.taxes 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
|
||||||
|
|
||||||
|
return ref_doc.grand_total - tcs_tax_row_amount
|
||||||
|
|
||||||
def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total):
|
def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total):
|
||||||
tds_amount = 0
|
tds_amount = 0
|
||||||
limit_consumed = frappe.db.get_value('Purchase Invoice', {
|
limit_consumed = frappe.db.get_value('Purchase Invoice', {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from frappe.utils import today
|
|||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||||
|
|
||||||
test_dependencies = ["Supplier Group"]
|
test_dependencies = ["Supplier Group", "Customer Group"]
|
||||||
|
|
||||||
class TestTaxWithholdingCategory(unittest.TestCase):
|
class TestTaxWithholdingCategory(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -128,9 +128,42 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
|||||||
for d in invoices:
|
for d in invoices:
|
||||||
d.cancel()
|
d.cancel()
|
||||||
|
|
||||||
|
def test_cumulative_threshold_tcs(self):
|
||||||
|
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS")
|
||||||
|
invoices = []
|
||||||
|
|
||||||
|
# create invoices for lower than single threshold tax rate
|
||||||
|
for _ in range(2):
|
||||||
|
si = create_sales_invoice(customer = "Test TCS Customer")
|
||||||
|
si.submit()
|
||||||
|
invoices.append(si)
|
||||||
|
|
||||||
|
# create another invoice whose total when added to previously created invoice,
|
||||||
|
# surpasses cumulative threshhold
|
||||||
|
si = create_sales_invoice(customer = "Test TCS Customer")
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
# assert tax collection on total invoice amount created until now
|
||||||
|
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC'])
|
||||||
|
self.assertEqual(tcs_charged, 3000)
|
||||||
|
self.assertEqual(si.grand_total, 13000)
|
||||||
|
invoices.append(si)
|
||||||
|
|
||||||
|
# TCS is already collected once, so going forward system will collect TCS on every invoice
|
||||||
|
si = create_sales_invoice(customer = "Test TCS Customer", rate=5000)
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC'])
|
||||||
|
self.assertEqual(tcs_charged, 500)
|
||||||
|
invoices.append(si)
|
||||||
|
|
||||||
|
#delete invoices to avoid clashing
|
||||||
|
for d in invoices:
|
||||||
|
d.cancel()
|
||||||
|
|
||||||
def create_purchase_invoice(**args):
|
def create_purchase_invoice(**args):
|
||||||
# return sales invoice doc object
|
# return sales invoice doc object
|
||||||
item = frappe.get_doc('Item', {'item_name': 'TDS Item'})
|
item = frappe.db.get_value('Item', {'item_name': 'TDS Item'}, "name")
|
||||||
|
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
pi = frappe.get_doc({
|
pi = frappe.get_doc({
|
||||||
@@ -145,7 +178,7 @@ def create_purchase_invoice(**args):
|
|||||||
"taxes": [],
|
"taxes": [],
|
||||||
"items": [{
|
"items": [{
|
||||||
'doctype': 'Purchase Invoice Item',
|
'doctype': 'Purchase Invoice Item',
|
||||||
'item_code': item.name,
|
'item_code': item,
|
||||||
'qty': args.qty or 1,
|
'qty': args.qty or 1,
|
||||||
'rate': args.rate or 10000,
|
'rate': args.rate or 10000,
|
||||||
'cost_center': 'Main - _TC',
|
'cost_center': 'Main - _TC',
|
||||||
@@ -156,6 +189,33 @@ def create_purchase_invoice(**args):
|
|||||||
pi.save()
|
pi.save()
|
||||||
return pi
|
return pi
|
||||||
|
|
||||||
|
def create_sales_invoice(**args):
|
||||||
|
# return sales invoice doc object
|
||||||
|
item = frappe.db.get_value('Item', {'item_name': 'TCS Item'}, "name")
|
||||||
|
|
||||||
|
args = frappe._dict(args)
|
||||||
|
si = frappe.get_doc({
|
||||||
|
"doctype": "Sales Invoice",
|
||||||
|
"posting_date": today(),
|
||||||
|
"customer": args.customer,
|
||||||
|
"company": '_Test Company',
|
||||||
|
"taxes_and_charges": "",
|
||||||
|
"currency": "INR",
|
||||||
|
"debit_to": "Debtors - _TC",
|
||||||
|
"taxes": [],
|
||||||
|
"items": [{
|
||||||
|
'doctype': 'Sales Invoice Item',
|
||||||
|
'item_code': item,
|
||||||
|
'qty': args.qty or 1,
|
||||||
|
'rate': args.rate or 10000,
|
||||||
|
'cost_center': 'Main - _TC',
|
||||||
|
'expense_account': 'Cost of Goods Sold - _TC'
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
si.save()
|
||||||
|
return si
|
||||||
|
|
||||||
def create_records():
|
def create_records():
|
||||||
# create a new suppliers
|
# create a new suppliers
|
||||||
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']:
|
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']:
|
||||||
@@ -168,7 +228,17 @@ def create_records():
|
|||||||
"doctype": "Supplier",
|
"doctype": "Supplier",
|
||||||
}).insert()
|
}).insert()
|
||||||
|
|
||||||
# create an item
|
for name in ['Test TCS Customer']:
|
||||||
|
if frappe.db.exists('Customer', name):
|
||||||
|
continue
|
||||||
|
|
||||||
|
frappe.get_doc({
|
||||||
|
"customer_group": "_Test Customer Group",
|
||||||
|
"customer_name": name,
|
||||||
|
"doctype": "Customer"
|
||||||
|
}).insert()
|
||||||
|
|
||||||
|
# create item
|
||||||
if not frappe.db.exists('Item', "TDS Item"):
|
if not frappe.db.exists('Item', "TDS Item"):
|
||||||
frappe.get_doc({
|
frappe.get_doc({
|
||||||
"doctype": "Item",
|
"doctype": "Item",
|
||||||
@@ -178,7 +248,16 @@ def create_records():
|
|||||||
"is_stock_item": 0,
|
"is_stock_item": 0,
|
||||||
}).insert()
|
}).insert()
|
||||||
|
|
||||||
# create an account
|
if not frappe.db.exists('Item', "TCS Item"):
|
||||||
|
frappe.get_doc({
|
||||||
|
"doctype": "Item",
|
||||||
|
"item_code": "TCS Item",
|
||||||
|
"item_name": "TCS Item",
|
||||||
|
"item_group": "All Item Groups",
|
||||||
|
"is_stock_item": 1
|
||||||
|
}).insert()
|
||||||
|
|
||||||
|
# create tds account
|
||||||
if not frappe.db.exists("Account", "TDS - _TC"):
|
if not frappe.db.exists("Account", "TDS - _TC"):
|
||||||
frappe.get_doc({
|
frappe.get_doc({
|
||||||
'doctype': 'Account',
|
'doctype': 'Account',
|
||||||
@@ -189,6 +268,17 @@ def create_records():
|
|||||||
'root_type': 'Asset'
|
'root_type': 'Asset'
|
||||||
}).insert()
|
}).insert()
|
||||||
|
|
||||||
|
# create tcs account
|
||||||
|
if not frappe.db.exists("Account", "TCS - _TC"):
|
||||||
|
frappe.get_doc({
|
||||||
|
'doctype': 'Account',
|
||||||
|
'company': '_Test Company',
|
||||||
|
'account_name': 'TCS',
|
||||||
|
'parent_account': 'Duties and Taxes - _TC',
|
||||||
|
'report_type': 'Balance Sheet',
|
||||||
|
'root_type': 'Liability'
|
||||||
|
}).insert()
|
||||||
|
|
||||||
def create_tax_with_holding_category():
|
def create_tax_with_holding_category():
|
||||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")[0]
|
fiscal_year = get_fiscal_year(today(), company="_Test Company")[0]
|
||||||
|
|
||||||
@@ -210,6 +300,23 @@ def create_tax_with_holding_category():
|
|||||||
}]
|
}]
|
||||||
}).insert()
|
}).insert()
|
||||||
|
|
||||||
|
if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TCS"):
|
||||||
|
frappe.get_doc({
|
||||||
|
"doctype": "Tax Withholding Category",
|
||||||
|
"name": "Cumulative Threshold TCS",
|
||||||
|
"category_name": "10% TCS",
|
||||||
|
"rates": [{
|
||||||
|
'fiscal_year': fiscal_year,
|
||||||
|
'tax_withholding_rate': 10,
|
||||||
|
'single_threshold': 0,
|
||||||
|
'cumulative_threshold': 30000.00
|
||||||
|
}],
|
||||||
|
"accounts": [{
|
||||||
|
'company': '_Test Company',
|
||||||
|
'account': 'TCS - _TC'
|
||||||
|
}]
|
||||||
|
}).insert()
|
||||||
|
|
||||||
# Single thresold
|
# Single thresold
|
||||||
if not frappe.db.exists("Tax Withholding Category", "Single Threshold TDS"):
|
if not frappe.db.exists("Tax Withholding Category", "Single Threshold TDS"):
|
||||||
frappe.get_doc({
|
frappe.get_doc({
|
||||||
|
|||||||
Reference in New Issue
Block a user