From ff12f914867e22734f5cf2d9a280b43401988d06 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 27 Jan 2021 19:17:38 +0530 Subject: [PATCH] feat: charging tcs on sales invoice --- .../doctype/sales_invoice/sales_invoice.py | 27 ++++ .../tax_withholding_category.py | 92 +++++++++++--- .../test_tax_withholding_category.py | 117 +++++++++++++++++- 3 files changed, 215 insertions(+), 21 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 566734e7d1..0be63a8c31 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -21,6 +21,7 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente from erpnext.accounts.doctype.loyalty_program.loyalty_program import \ get_loyalty_program_details_with_points, get_loyalty_details, validate_loyalty_points from erpnext.accounts.deferred_revenue import validate_service_stop_date +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 @@ -73,6 +74,8 @@ class SalesInvoice(SellingController): if not self.is_pos: self.so_dn_required() + + self.set_tax_withholding() self.validate_proj_cust() self.validate_pos_return() @@ -151,6 +154,30 @@ class SalesInvoice(SellingController): 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))) + 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): set_account_for_mode_of_payment(self) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 3e0ba9ac6a..36ed6decad 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -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): 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) - tax_vouchers = vouchers + advance_vouchers + taxable_vouchers = vouchers + advance_vouchers tax_deducted = 0 - dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit' - if tax_vouchers: + if taxable_vouchers: + # check if tds / tcs is already charged on taxable vouchers filters = { - dr_or_cr: ['>', 0], - 'account': tax_details.account_head, + 'is_cancelled': 0, + 'credit': ['>', 0], 'fiscal_year': fiscal_year, - 'voucher_no': ['in', tax_vouchers], - 'is_cancelled': 0 + 'account': tax_details.account_head, + '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_amount = 0 + posting_date = ref_doc.posting_date 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) - if tax_deducted: + net_total = ref_doc.net_total if ldc: tax_amount = get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total) else: @@ -139,6 +138,19 @@ def get_tax_amount(party_type, parties, ref_doc, tax_details, fiscal_year_detail 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 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 } - 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'): # 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 = { dr_or_cr: ['>', 0], + 'is_opening': 'No', + 'is_cancelled': 0, 'party_type': party_type, 'party': ['in', parties], - 'is_opening': 'No', - 'is_cancelled': 0 + 'against_voucher': ['is', 'not set'] } 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: 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): tds_amount = 0 @@ -210,6 +223,53 @@ def get_tds_amount(ldc, parties, ref_doc, tax_details, fiscal_year_details, vouc 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): tds_amount = 0 limit_consumed = frappe.db.get_value('Purchase Invoice', { diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index ef77674372..c8bd0834d8 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -9,7 +9,7 @@ from frappe.utils import today from erpnext.accounts.utils import get_fiscal_year from erpnext.buying.doctype.supplier.test_supplier import create_supplier -test_dependencies = ["Supplier Group"] +test_dependencies = ["Supplier Group", "Customer Group"] class TestTaxWithholdingCategory(unittest.TestCase): @classmethod @@ -128,9 +128,42 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in invoices: 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): # 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) pi = frappe.get_doc({ @@ -145,7 +178,7 @@ def create_purchase_invoice(**args): "taxes": [], "items": [{ 'doctype': 'Purchase Invoice Item', - 'item_code': item.name, + 'item_code': item, 'qty': args.qty or 1, 'rate': args.rate or 10000, 'cost_center': 'Main - _TC', @@ -156,6 +189,33 @@ def create_purchase_invoice(**args): pi.save() 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(): # create a new suppliers for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']: @@ -168,7 +228,17 @@ def create_records(): "doctype": "Supplier", }).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"): frappe.get_doc({ "doctype": "Item", @@ -178,7 +248,16 @@ def create_records(): "is_stock_item": 0, }).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"): frappe.get_doc({ 'doctype': 'Account', @@ -189,6 +268,17 @@ def create_records(): 'root_type': 'Asset' }).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(): fiscal_year = get_fiscal_year(today(), company="_Test Company")[0] @@ -210,6 +300,23 @@ def create_tax_with_holding_category(): }] }).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 if not frappe.db.exists("Tax Withholding Category", "Single Threshold TDS"): frappe.get_doc({