From 85664474ffb9e466db5f350eea8451968c0ee654 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Thu, 19 Oct 2023 22:33:14 +0530 Subject: [PATCH 1/7] feat: build automations for ineligible ITC --- .../gst_india/constants/custom_fields.py | 62 +- .../doctype/gstr_3b_report/gstr_3b_report.py | 12 +- .../gst_india/overrides/company.py | 10 + .../gst_india/overrides/ineligible_itc.py | 299 ++++++ .../gst_india/overrides/purchase_invoice.py | 17 +- .../overrides/test_ineligible_itc.py | 978 ++++++++++++++++++ .../gst_india/overrides/transaction.py | 5 +- .../report/gstr_3b_details/gstr_3b_details.py | 12 +- india_compliance/gst_india/utils/tests.py | 4 +- india_compliance/hooks.py | 15 + india_compliance/install.py | 1 + india_compliance/patches.txt | 4 +- .../rename_import_of_capital_goods.py | 3 + .../patches/post_install/set_gst_category.py | 3 + .../post_install/update_company_fixtures.py | 6 + .../post_install/update_itc_amounts.py | 3 + .../update_itc_classification_field.py | 54 + 17 files changed, 1460 insertions(+), 28 deletions(-) create mode 100644 india_compliance/gst_india/overrides/ineligible_itc.py create mode 100644 india_compliance/gst_india/overrides/test_ineligible_itc.py create mode 100644 india_compliance/patches/post_install/update_itc_classification_field.py diff --git a/india_compliance/gst_india/constants/custom_fields.py b/india_compliance/gst_india/constants/custom_fields.py index 16b2844c7..6917e5b91 100644 --- a/india_compliance/gst_india/constants/custom_fields.py +++ b/india_compliance/gst_india/constants/custom_fields.py @@ -67,6 +67,13 @@ "options": "Account", "insert_after": "default_finance_book", }, + { + "fieldname": "default_gst_expense_account", + "label": "Default GST Expense Account", + "fieldtype": "Link", + "options": "Account", + "insert_after": "default_customs_expense_account", + }, ], ("Customer", "Supplier"): party_fields, # Purchase Fields @@ -308,6 +315,7 @@ "Sales Invoice Item", "POS Invoice Item", "Purchase Invoice Item", + "Purchase Receipt Item", ): [ { "fieldname": "taxable_value", @@ -320,6 +328,22 @@ "no_copy": 1, }, ], + ( + "Supplier Quotation Item", + "Purchase Order Item", + "Purchase Receipt Item", + "Purchase Invoice Item", + ): [ + { + "fieldname": "is_ineligible_for_itc", + "label": "Is Ineligible for Input Tax Credit", + "fieldtype": "Check", + "fetch_from": "item_code.is_ineligible_for_itc", + "insert_after": "is_nil_exempt", + "fetch_if_empty": 1, + "print_hide": 1, + }, + ], "Sales Invoice": [ { "fieldname": "port_address", @@ -377,24 +401,34 @@ "collapsible": 1, }, { - "fieldname": "eligibility_for_itc", - "label": "Eligibility For ITC", + "fieldname": "itc_classification", + "label": "ITC Classification", "fieldtype": "Select", "insert_after": "gst_section", "print_hide": 1, "options": ( "Input Service Distributor\nImport Of Service\nImport Of" - " Goods\nITC on Reverse Charge\nIneligible As Per Section" - " 17(5)\nIneligible Others\nAll Other ITC" + " Goods\nITC on Reverse Charge\nAll Other ITC" ), "default": "All Other ITC", "translatable": 0, }, + { + "fieldname": "ineligibility_reason", + "label": "Reason for Ineligibility", + "fieldtype": "Select", + "insert_after": "itc_classification", + "options": ( + "\nIneligible As Per Section 17(5)\nITC restricted due to PoS rules" + ), + "read_only": 1, + "print_hide": 1, + }, { "fieldname": "reconciliation_status", "label": "Reconciliation Status", "fieldtype": "Select", - "insert_after": "eligibility_for_itc", + "insert_after": "ineligibility_reason", "print_hide": 1, "options": ("\nNot Applicable\nReconciled\nUnreconciled\nIgnored"), "no_copy": 1, @@ -407,26 +441,29 @@ }, { "fieldname": "itc_integrated_tax", - "label": "Availed ITC Integrated Tax", + "label": "Integrated Tax", "fieldtype": "Currency", "insert_after": "gst_col_break", "options": "Company:company:default_currency", + "read_only": 1, "print_hide": 1, }, { "fieldname": "itc_central_tax", - "label": "Availed ITC Central Tax", + "label": "Central Tax", "fieldtype": "Currency", "insert_after": "itc_integrated_tax", "options": "Company:company:default_currency", + "read_only": 1, "print_hide": 1, }, { "fieldname": "itc_state_tax", - "label": "Availed ITC State/UT Tax", + "label": "State/UT Tax", "fieldtype": "Currency", "insert_after": "itc_central_tax", "options": "Company:company:default_currency", + "read_only": 1, "print_hide": 1, }, { @@ -435,6 +472,7 @@ "fieldtype": "Currency", "insert_after": "itc_state_tax", "options": "Company:company:default_currency", + "read_only": 1, "print_hide": 1, }, ], @@ -584,7 +622,7 @@ ], "Journal Entry": [ { - "fieldname": "reversal_type", + "fieldname": "ineligibility_reason", "label": "Reversal Type", "fieldtype": "Select", "insert_after": "voucher_type", @@ -647,6 +685,12 @@ "fieldtype": "Check", "insert_after": "is_nil_exempt", }, + { + "fieldname": "is_ineligible_for_itc", + "label": "Is Ineligible for Input Tax Credit", + "fieldtype": "Check", + "insert_after": "item_tax_section_break", + }, ], } diff --git a/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py b/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py index 9bdf2c166..50ee29045 100644 --- a/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py +++ b/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py @@ -93,7 +93,7 @@ def set_itc_details(self, itc_details): def get_itc_reversal_entries(self): reversal_entries = frappe.db.sql( """ - SELECT ja.account, j.reversal_type, sum(credit_in_account_currency) as amount + SELECT ja.account, j.ineligibility_reason, sum(credit_in_account_currency) as amount FROM `tabJournal Entry` j, `tabJournal Entry Account` ja where j.docstatus = 1 and j.is_opening = 'No' @@ -101,7 +101,7 @@ def get_itc_reversal_entries(self): and j.voucher_type = 'Reversal Of ITC' and month(j.posting_date) = %s and year(j.posting_date) = %s and j.company = %s and j.company_gstin = %s - GROUP BY ja.account, j.reversal_type""", + GROUP BY ja.account, j.ineligibility_reason""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1, ) @@ -109,7 +109,7 @@ def get_itc_reversal_entries(self): net_itc = self.report_dict["itc_elg"]["itc_net"] for entry in reversal_entries: - if entry.reversal_type == "As per rules 42 & 43 of CGST Rules": + if entry.ineligibility_reason == "As per rules 42 & 43 of CGST Rules": index = 0 else: index = 1 @@ -124,7 +124,7 @@ def get_itc_reversal_entries(self): def get_itc_details(self): itc_amounts = frappe.db.sql( """ - SELECT eligibility_for_itc, sum(itc_integrated_tax) as itc_integrated_tax, + SELECT itc_classification, sum(itc_integrated_tax) as itc_integrated_tax, sum(itc_central_tax) as itc_central_tax, sum(itc_state_tax) as itc_state_tax, sum(itc_cess_amount) as itc_cess_amount @@ -133,7 +133,7 @@ def get_itc_details(self): and is_opening = 'No' and month(posting_date) = %s and year(posting_date) = %s and company = %s and company_gstin = %s - GROUP BY eligibility_for_itc + GROUP BY itc_classification """, (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1, @@ -142,7 +142,7 @@ def get_itc_details(self): itc_details = {} for d in itc_amounts: itc_details.setdefault( - d.eligibility_for_itc, + d.itc_classification, { "iamt": d.itc_integrated_tax, "camt": d.itc_central_tax, diff --git a/india_compliance/gst_india/overrides/company.py b/india_compliance/gst_india/overrides/company.py index 754d681f0..8e254ed77 100644 --- a/india_compliance/gst_india/overrides/company.py +++ b/india_compliance/gst_india/overrides/company.py @@ -28,6 +28,7 @@ def make_company_fixtures(doc, method=None): def create_company_fixtures(company): make_default_tax_templates(company) make_default_customs_accounts(company) + make_default_gst_expense_accounts(company) def make_default_customs_accounts(company): @@ -46,6 +47,15 @@ def make_default_customs_accounts(company): ) +def make_default_gst_expense_accounts(company): + create_default_company_account( + company, + account_name="GST Expense", + parent="Indirect Expenses", + default_fieldname="default_gst_expense_account", + ) + + @frappe.whitelist() def make_default_tax_templates(company: str): frappe.has_permission("Company", ptype="write", doc=company, throw=True) diff --git a/india_compliance/gst_india/overrides/ineligible_itc.py b/india_compliance/gst_india/overrides/ineligible_itc.py new file mode 100644 index 000000000..2ebbe4808 --- /dev/null +++ b/india_compliance/gst_india/overrides/ineligible_itc.py @@ -0,0 +1,299 @@ +import frappe +from frappe.utils import flt, rounded +from erpnext.assets.doctype.asset.asset import ( + get_asset_account, + is_cwip_accounting_enabled, +) + +from india_compliance.gst_india.utils import get_gst_accounts_by_type + + +class IneligibleITC: + def __init__(self, doc): + self.doc = doc + + self.company = frappe.get_cached_doc("Company", doc.company) + self.is_perpetual = self.company.enable_perpetual_inventory + self.cost_center = doc.cost_center or self.company.cost_center + + self.dr_or_cr = "credit" if doc.is_return else "debit" + self.cr_or_dr = "debit" if doc.is_return else "credit" + + def update_valuation_rate(self): + """ + Updates Valuation Rate for each item row + + - Only updates if its a stock item or fixed asset + - No updates for expense items + """ + self.doc._has_ineligible_itc_items = False + stock_items = self.doc.get_stock_items() + + for item in self.doc.items: + if ( + not self.is_eligibility_restricted_due_to_pos() + and not item.is_ineligible_for_itc + ): + continue + + self.update_ineligible_taxes(item) + + if item._ineligible_tax_amount: + self.doc._has_ineligible_itc_items = True + + if item.item_code in stock_items: + item._is_stock_item = True + + if item.get("_is_stock_item") or item.is_fixed_asset: + ineligible_tax_amount = item._ineligible_tax_amount + if self.doc.is_return: + ineligible_tax_amount = -ineligible_tax_amount + + # TODO: handle rounding off + item.valuation_rate += flt(ineligible_tax_amount / item.stock_qty, 2) + + def update_gl_entries(self, gl_entries): + self.gl_entries = gl_entries + + if self.doc.get("is_opening") == "Yes" or not self.doc.get( + "_has_ineligible_itc_items" + ): + return gl_entries + + for item in self.doc.items: + if not item.get("_ineligible_tax_amount"): + continue + + self.update_item_gl_entries(item) + + def update_item_gl_entries(self, item): + return + + def reverse_input_taxes_entry(self, item): + """ + Reverse Proportionate ITC for each tax component + and book GST Expense for same + + eg: GST Expense Dr 100 + Input CGST Cr 50 + Input SGST Cr 50 + """ + # Auto handled for returns as -ve amount + ineligible_item_tax_amount = item.get("_ineligible_tax_amount", 0) + self.gl_entries.append( + self.doc.get_gl_dict( + { + "account": self.company.default_gst_expense_account, + self.dr_or_cr: ineligible_item_tax_amount, + f"{self.dr_or_cr}_in_account_currency": ineligible_item_tax_amount, + "cost_center": self.cost_center, + } + ) + ) + + for account, amount in item.get("_ineligible_taxes", {}).items(): + self.gl_entries.append( + self.doc.get_gl_dict( + { + "account": account, + self.cr_or_dr: amount, + f"{self.cr_or_dr}_in_account_currency": amount, + "cost_center": self.cost_center, + } + ) + ) + + def make_gst_expense_entry(self, item): + """ + Reverse GST Expense and transfer it to respective + Asset / Stock Account / Expense Account + + eg: Fixed Asset Dr 100 + GST Expense Cr 100 + """ + + ineligible_item_tax_amount = item.get("_ineligible_tax_amount", 0) + self.gl_entries.append( + self.doc.get_gl_dict( + { + "account": self.company.default_gst_expense_account, + self.cr_or_dr: ineligible_item_tax_amount, + f"{self.cr_or_dr}_in_account_currency": ineligible_item_tax_amount, + "cost_center": self.cost_center, + } + ) + ) + + if item.is_fixed_asset: + item.expense_account = _get_asset_account( + item.asset_category, self.doc.company + ) + self.update_asset_valuation_rate(item) + + if self.is_debit_entry_required(item): + self.gl_entries.append( + self.doc.get_gl_dict( + { + "account": item.expense_account, + self.dr_or_cr: ineligible_item_tax_amount, + f"{self.dr_or_cr}_in_account_currency": ineligible_item_tax_amount, + "cost_center": item.cost_center or self.cost_center, + } + ) + ) + + def update_ineligible_taxes(self, item): + """ + Returns proportionate Ineligible ITC for each tax component + + :param item: Item Row + :return: dict + + Example: + { + "Input IGST - FC": 100, + "Input CGST - FC": 50, + "Input SGST - FC": 50, + } + """ + gst_accounts = get_gst_accounts_by_type(self.doc.company, "Input").values() + ineligible_taxes = frappe._dict() + + for tax in self.doc.taxes: + if tax.account_head not in gst_accounts: + continue + + ineligible_taxes[tax.account_head] = self.get_item_tax_amount(item, tax) + + item._ineligible_taxes = ineligible_taxes + item._ineligible_tax_amount = sum(ineligible_taxes.values()) + + def get_item_tax_amount(self, item, tax): + """ + Returns proportionate item tax amount for each tax component + """ + tax_rate = rounded( + frappe.parse_json(tax.item_wise_tax_detail).get( + item.item_code or item.item_name + )[0], + 3, + ) + + tax_amount = ( + tax_rate * item.qty + if tax.charge_type == "On Item Quantity" + else tax_rate * item.taxable_value / 100 + ) + + return abs(tax_amount) + + def is_debit_entry_required(self, item): + return True + + def update_asset_valuation_rate(self, item): + return + + def is_eligibility_restricted_due_to_pos(self): + return False + + +class PurchaseReceipt(IneligibleITC): + def update_item_gl_entries(self, item): + if (item.get("_is_stock_item") and self.is_perpetual) or item.get( + "is_fixed_asset" + ): + self.make_gst_expense_entry(item) + + def update_asset_valuation_rate(self, item): + # TODO: Remove this once its fixed in ERPNext + frappe.db.set_value( + "Asset", + {"item_code": item.item_code, "purchase_receipt": self.doc.name}, + { + "gross_purchase_amount": flt(item.valuation_rate), + "purchase_receipt_amount": flt(item.valuation_rate), + }, + ) + + def is_eligibility_restricted_due_to_pos(self): + return self.doc.place_of_supply[:2] != self.doc.company_gstin[:2] + + +class PurchaseInvoice(IneligibleITC): + def update_item_gl_entries(self, item): + if self.doc.update_stock or self.is_expense_item(item): + self.make_gst_expense_entry(item) + + self.reverse_input_taxes_entry(item) + + def is_debit_entry_required(self, item): + # For Stock Entry in PI, Additional Debit is accounted automatically from valuation rates + return self.is_expense_item(item) or item.is_fixed_asset + + def is_expense_item(self, item): + """ + Returns False if item is Stock Item or Fixed Asset + Else returns True + + :param item: Item Row + :return: bool + """ + if self.doc.update_stock: + if item.get("is_fixed_asset"): + return False + + if item.get("_is_stock_item") and self.is_perpetual: + return False + + return True + + account_root = frappe.db.get_value("Account", item.expense_account, "root_type") + if account_root in ["Asset", "Liability", "Equity"]: + return False + + return True + + def update_asset_valuation_rate(self, item): + # TODO: Remove this once its fixed in ERPNext + frappe.db.set_value( + "Asset", + {"item_code": item.item_code, "purchase_invoice": self.doc.name}, + { + "gross_purchase_amount": flt(item.valuation_rate), + "purchase_receipt_amount": flt(item.valuation_rate), + }, + ) + + def is_eligibility_restricted_due_to_pos(self): + return self.doc.get("ineligibility_reason") == "ITC restricted due to PoS rules" + + +class BillOfEntry(IneligibleITC): + pass + + +DOCTYPE_MAPPING = { + "Purchase Invoice": PurchaseInvoice, + "Purchase Receipt": PurchaseReceipt, + "Bill Of Entry": BillOfEntry, +} + + +def before_submit(doc, method=None): + if doc.doctype in DOCTYPE_MAPPING: + DOCTYPE_MAPPING[doc.doctype](doc).update_valuation_rate() + + +def update_regional_gl_entries(gl_entries, doc): + if doc.doctype in DOCTYPE_MAPPING: + DOCTYPE_MAPPING[doc.doctype](doc).update_gl_entries(gl_entries) + + return gl_entries + + +def _get_asset_account(asset_category, company): + fieldname = "fixed_asset_account" + if is_cwip_accounting_enabled(asset_category): + fieldname = "capital_work_in_progress_account" + + return get_asset_account(fieldname, asset_category=asset_category, company=company) diff --git a/india_compliance/gst_india/overrides/purchase_invoice.py b/india_compliance/gst_india/overrides/purchase_invoice.py index a2aa5f3c6..98a7ee1c2 100644 --- a/india_compliance/gst_india/overrides/purchase_invoice.py +++ b/india_compliance/gst_india/overrides/purchase_invoice.py @@ -47,6 +47,7 @@ def validate(doc, method=None): validate_supplier_invoice_number(doc) validate_with_inward_supply(doc) set_reconciliation_status(doc) + set_ineligibility_reason(doc) def set_reconciliation_status(doc): @@ -70,8 +71,8 @@ def is_b2b_invoice(doc): def update_itc_totals(doc, method=None): # Set default value - if not doc.eligibility_for_itc: - doc.eligibility_for_itc = "All Other ITC" + if not doc.itc_classification: + doc.itc_classification = "All Other ITC" # Initialize values doc.itc_integrated_tax = 0 @@ -200,3 +201,15 @@ def get_tax_amount(taxes, account_head): if tax.account_head == account_head ] ) + + +def set_ineligibility_reason(doc): + doc.ineligibility_reason = "" + + for item in doc.items: + if item.is_ineligible_for_itc: + doc.ineligibility_reason = "Ineligible As Per Section 17(5)" + break + + if doc.place_of_supply[:2] != doc.company_gstin[:2]: + doc.ineligibility_reason = "ITC restricted due to PoS rules" diff --git a/india_compliance/gst_india/overrides/test_ineligible_itc.py b/india_compliance/gst_india/overrides/test_ineligible_itc.py new file mode 100644 index 000000000..b6901d6b7 --- /dev/null +++ b/india_compliance/gst_india/overrides/test_ineligible_itc.py @@ -0,0 +1,978 @@ +import json + +import frappe +from frappe.tests.utils import FrappeTestCase +from erpnext.controllers.sales_and_purchase_return import make_return_doc +from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_invoice, +) + +from india_compliance.gst_india.utils.tests import create_transaction + +SAMPLE_ITEM_LIST = [ + {"item_code": "Test Stock Item", "qty": 5, "rate": 20}, + {"item_code": "Test Ineligible Stock Item", "qty": 3, "rate": 19}, + { + "item_code": "Test Fixed Asset", + "qty": 1, + "rate": 1000, + "asset_location": "Test Location", + }, + { + "item_code": "Test Ineligible Fixed Asset", + "qty": 1, + "rate": 999, + "asset_location": "Test Location", + }, + {"item_code": "Test Service Item", "qty": 3, "rate": 500}, + {"item_code": "Test Ineligible Service Item", "qty": 2, "rate": 499}, +] +# Item Total +# 20 * 5 + 19 * 3 + 1000 * 1 + 999 * 1 + 500 * 3 + 499 * 2 + 100 * 1 (Default) = 4754 + +# Tax Total +# 4754 * 18% = 855.72 or CGST + SGST = 427.86 + 427.86 = 855.72 + +# Ineligible Stock Item = 19 * 3 * 18% = 10.26 or CGST + SGST = 5.13 + 5.13 = 10.26 +# Ineligible Fixed Asset = 999 * 1 * 18% = 179.82 or CGST + SGST = 89.91 + 89.91 = 179.82 +# Ineligible Service Item = 499 * 2 * 18% = 179.64 or CGST + SGST = 89.82 + 89.82 = 179.64 + + +class TestIneligibleITC(FrappeTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + create_test_items() + + def test_purchase_invoice_with_update_stock(self): + transaction_details = { + "doctype": "Purchase Invoice", + "bill_no": "BILL-01", + "update_stock": 1, + "items": SAMPLE_ITEM_LIST, + "is_in_state": 1, + } + + doc = create_transaction(**transaction_details) + + self.assertEqual(doc.ineligibility_reason, "Ineligible As Per Section 17(5)") + + # Check GL Entries + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_no": doc.name}, + fields=["account", "debit", "credit"], + ) + + out_str = json.dumps(sorted(gl_entries, key=json.dumps)) + expected_out_str = json.dumps( + sorted( + [ + {"account": "Round Off - _TIRC", "debit": 0.28, "credit": 0.0}, + { + "account": "GST Expense - _TIRC", + "debit": 369.72, + "credit": 369.72, + }, # 179.64 + 179.82 + 10.26 + { + "account": "Input Tax SGST - _TIRC", + "debit": 427.86, + "credit": 184.86, # 369.72 / 2 + }, + { + "account": "Input Tax CGST - _TIRC", + "debit": 427.86, + "credit": 184.86, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 2677.64, # 500 * 3 + 499 * 2 + 179.64 + "credit": 0.0, + }, + { + "account": "CWIP Account - _TIRC", + "debit": 2178.82, + "credit": 0.0, + }, # 1000 + 999 + 179.82 + { + "account": "Stock In Hand - _TIRC", + "debit": 267.26, + "credit": 0.0, + }, # 20 * 5 + 19 * 3 + 100 * 1 + 10.26 + {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5610.0}, + ], + key=json.dumps, + ) + ) + + self.assertEqual(out_str, expected_out_str) + + # Check Stock Ledger Entries + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": doc.name, "item_code": "Test Stock Item"}, + "incoming_rate", + ) + self.assertEqual(incoming_rate, 20) + + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": doc.name, "item_code": "Test Ineligible Stock Item"}, + "incoming_rate", + ) + self.assertEqual(incoming_rate, 22.42) # 19 * 1.18 + + # Check Asset Valuation Rate + asset_purchase_value = frappe.db.get_value( + "Asset", + {"purchase_invoice": doc.name, "item_code": "Test Fixed Asset"}, + "gross_purchase_amount", + ) + self.assertEqual(asset_purchase_value, 1000) + + asset_purchase_value = frappe.db.get_value( + "Asset", + {"purchase_invoice": doc.name, "item_code": "Test Ineligible Fixed Asset"}, + "gross_purchase_amount", + ) + self.assertEqual(asset_purchase_value, 1178.82) # 999 + 179.82 + + def test_purchase_invoice_with_ineligible_pos(self): + transaction_details = { + "doctype": "Purchase Invoice", + "bill_no": "BILL-02", + "update_stock": 1, + "items": SAMPLE_ITEM_LIST, + "place_of_supply": "27-Maharashtra", + "is_out_state": 1, + } + + doc = create_transaction(**transaction_details) + + self.assertEqual(doc.ineligibility_reason, "ITC restricted due to PoS rules") + + # Check GL Entries + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_no": doc.name}, + fields=["account", "debit", "credit"], + ) + + out_str = json.dumps(sorted(gl_entries, key=json.dumps)) + expected_out_str = json.dumps( + sorted( + [ + {"account": "Round Off - _TIRC", "debit": 0.28, "credit": 0.0}, + { + "account": "GST Expense - _TIRC", + "debit": 855.72, + "credit": 855.72, + }, # full taxes reversed + { + "account": "Input Tax IGST - _TIRC", + "debit": 855.72, + "credit": 855.72, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 2947.64, + "credit": 0.0, + }, + { + "account": "CWIP Account - _TIRC", + "debit": 2358.82, + "credit": 0.0, + }, + { + "account": "Stock In Hand - _TIRC", + "debit": 303.26, + "credit": 0.0, + }, + {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5610.0}, + ], + key=json.dumps, + ) + ) + + self.assertEqual(out_str, expected_out_str) + + # Check Stock Ledger Entries + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": doc.name, "item_code": "Test Stock Item"}, + "incoming_rate", + ) + self.assertEqual(incoming_rate, 23.60) + + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": doc.name, "item_code": "Test Ineligible Stock Item"}, + "incoming_rate", + ) + self.assertEqual(incoming_rate, 22.42) + + # Check Asset Valuation Rate + asset_purchase_value = frappe.db.get_value( + "Asset", + {"purchase_invoice": doc.name, "item_code": "Test Fixed Asset"}, + "gross_purchase_amount", + ) + self.assertEqual(asset_purchase_value, 1180) + + asset_purchase_value = frappe.db.get_value( + "Asset", + {"purchase_invoice": doc.name, "item_code": "Test Ineligible Fixed Asset"}, + "gross_purchase_amount", + ) + self.assertEqual(asset_purchase_value, 1178.82) + + def test_purchase_receipt_and_then_purchase_invoice(self): + transaction_details = { + "doctype": "Purchase Receipt", + "items": SAMPLE_ITEM_LIST, + "is_in_state": 1, + } + + doc = create_transaction(**transaction_details) + + # Check GL Entries + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_no": doc.name}, + fields=["account", "debit", "credit"], + ) + + out_str = json.dumps(sorted(gl_entries, key=json.dumps)) + expected_out_str = json.dumps( + sorted( + [ + { + "account": "GST Expense - _TIRC", + "debit": 0.0, + "credit": 190.08, + }, # 10.26 + 179.82 + { + "account": "Asset Received But Not Billed - _TIRC", + "debit": 0.0, + "credit": 1999.0, + }, + { + "account": "CWIP Account - _TIRC", + "debit": 2178.82, # 1999 + 179.82 + "credit": 0.0, + }, + { + "account": "Stock Received But Not Billed - _TIRC", + "debit": 0.0, + "credit": 257.0, + }, + { + "account": "Stock In Hand - _TIRC", + "debit": 267.26, # 257 + 10.26 + "credit": 0.0, + }, + ], + key=json.dumps, + ) + ) + self.assertEqual(out_str, expected_out_str) + + # Check Stock Ledger Entries + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": doc.name, "item_code": "Test Stock Item"}, + "incoming_rate", + ) + self.assertEqual(incoming_rate, 20) + + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": doc.name, "item_code": "Test Ineligible Stock Item"}, + "incoming_rate", + ) + self.assertEqual(incoming_rate, 22.42) # 19 * 1.18 + + # Check Asset Valuation Rate + asset_purchase_value = frappe.db.get_value( + "Asset", + {"purchase_receipt": doc.name, "item_code": "Test Fixed Asset"}, + "gross_purchase_amount", + ) + self.assertEqual(asset_purchase_value, 1000) + + asset_purchase_value = frappe.db.get_value( + "Asset", + {"purchase_receipt": doc.name, "item_code": "Test Ineligible Fixed Asset"}, + "gross_purchase_amount", + ) + self.assertEqual(asset_purchase_value, 1178.82) # 999 + 179.82 + + # Create Purchase Invoice + doc = make_purchase_invoice(doc.name) + doc.bill_no = "BILL-03" + doc.submit() + + self.assertEqual(doc.ineligibility_reason, "Ineligible As Per Section 17(5)") + + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_no": doc.name}, + fields=["account", "debit", "credit"], + ) + + out_str = json.dumps(sorted(gl_entries, key=json.dumps)) + expected_out_str = json.dumps( + sorted( + [ + {"account": "Round Off - _TIRC", "debit": 0.0, "credit": 0.32}, + { + "account": "GST Expense - _TIRC", + "debit": 369.72, # 179.82 + 179.64 + 10.26 + "credit": 179.64, # Only Expense + }, + {"account": "TDS Payable - _TIRC", "debit": 0.0, "credit": 475.4}, + { + "account": "Input Tax SGST - _TIRC", + "debit": 427.86, + "credit": 184.86, + }, + { + "account": "Input Tax CGST - _TIRC", + "debit": 427.86, + "credit": 184.86, # 369.72 / 2 + }, + { + "account": "Asset Received But Not Billed - _TIRC", + "debit": 1999.0, + "credit": 0.0, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 2677.64, # 1500 + 998 + 179.64 + "credit": 0.0, + }, + { + "account": "Stock Received But Not Billed - _TIRC", + "debit": 257.0, + "credit": 0.0, + }, + {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5134.0}, + ], + key=json.dumps, + ) + ) + + def test_purchase_receipt_and_then_purchase_invoice_for_ineligible_pos(self): + transaction_details = { + "doctype": "Purchase Receipt", + "items": SAMPLE_ITEM_LIST, + "place_of_supply": "27-Maharashtra", + "is_out_state": 1, + } + + doc = create_transaction(**transaction_details) + + # Check GL Entries + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_no": doc.name}, + fields=["account", "debit", "credit"], + ) + + out_str = json.dumps(sorted(gl_entries, key=json.dumps)) + expected_out_str = json.dumps( + sorted( + [ + { + "account": "GST Expense - _TIRC", + "debit": 0.0, + "credit": 406.08, + }, # 855.72 - 449.64 (reversal on expense) + { + "account": "Asset Received But Not Billed - _TIRC", + "debit": 0.0, + "credit": 1999.0, + }, + { + "account": "CWIP Account - _TIRC", + "debit": 2358.82, + "credit": 0.0, + }, + { + "account": "Stock Received But Not Billed - _TIRC", + "debit": 0.0, + "credit": 257.0, + }, + { + "account": "Stock In Hand - _TIRC", + "debit": 303.26, + "credit": 0.0, + }, + ], + key=json.dumps, + ) + ) + self.assertEqual(out_str, expected_out_str) + + # Check Stock Ledger Entries + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": doc.name, "item_code": "Test Stock Item"}, + "incoming_rate", + ) + self.assertEqual(incoming_rate, 23.60) + + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": doc.name, "item_code": "Test Ineligible Stock Item"}, + "incoming_rate", + ) + self.assertEqual(incoming_rate, 22.42) + + # Check Asset Valuation Rate + asset_purchase_value = frappe.db.get_value( + "Asset", + {"purchase_receipt": doc.name, "item_code": "Test Fixed Asset"}, + "gross_purchase_amount", + ) + self.assertEqual(asset_purchase_value, 1180) + + asset_purchase_value = frappe.db.get_value( + "Asset", + {"purchase_receipt": doc.name, "item_code": "Test Ineligible Fixed Asset"}, + "gross_purchase_amount", + ) + self.assertEqual(asset_purchase_value, 1178.82) + + # Create Purchase Invoice + doc = make_purchase_invoice(doc.name) + doc.bill_no = "BILL-04" + doc.submit() + + self.assertEqual(doc.ineligibility_reason, "ITC restricted due to PoS rules") + + # Check GL Entries + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_no": doc.name}, + fields=["account", "debit", "credit"], + ) + + out_str = json.dumps(sorted(gl_entries, key=json.dumps)) + expected_out_str = json.dumps( + sorted( + [ + {"account": "Round Off - _TIRC", "debit": 0.0, "credit": 0.32}, + { + "account": "GST Expense - _TIRC", + "debit": 855.72, + "credit": 449.64, # expense reversal + }, + {"account": "TDS Payable - _TIRC", "debit": 0.0, "credit": 475.4}, + { + "account": "Input Tax IGST - _TIRC", + "debit": 855.72, + "credit": 855.72, + }, + { + "account": "Asset Received But Not Billed - _TIRC", + "debit": 1999.0, + "credit": 0.0, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 2947.64, + "credit": 0.0, + }, + { + "account": "Stock Received But Not Billed - _TIRC", + "debit": 257.0, + "credit": 0.0, + }, + {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5134.0}, + ], + key=json.dumps, + ) + ) + self.assertEqual(out_str, expected_out_str) + + def test_purchase_returns_with_update_stock(self): + transaction_details = { + "doctype": "Purchase Invoice", + "bill_no": "BILL-05", + "update_stock": 1, + "items": SAMPLE_ITEM_LIST, + "is_in_state": 1, + } + + doc = create_transaction(**transaction_details) + doc = make_return_doc("Purchase Invoice", doc.name) + doc.submit() + + # Check GL Entries + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_no": doc.name}, + fields=["account", "debit", "credit"], + ) + + out_str = json.dumps(sorted(gl_entries, key=json.dumps)) + expected_out_str = json.dumps( + sorted( + [ + {"account": "Round Off - _TIRC", "debit": 0.0, "credit": 0.28}, + { + "account": "GST Expense - _TIRC", + "debit": 369.72, + "credit": 369.72, + }, + { + "account": "Input Tax SGST - _TIRC", + "debit": 0.0, + "credit": 243.0, + }, + { + "account": "Input Tax CGST - _TIRC", + "debit": 0.0, + "credit": 243.0, + }, + { + "account": "CWIP Account - _TIRC", + "debit": 0.0, + "credit": 2178.82, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 0.0, + "credit": 2677.64, + }, + { + "account": "Stock In Hand - _TIRC", + "debit": 0.0, + "credit": 267.26, + }, + {"account": "Creditors - _TIRC", "debit": 5610.0, "credit": 0.0}, + ], + key=json.dumps, + ) + ) + self.assertEqual(out_str, expected_out_str) + + def test_purchase_receipt_and_then_purchase_invoice_for_non_perpetual_stock(self): + # Disable Perpetual Inventory + frappe.db.set_value( + "Company", + "_Test Indian Registered Company", + "enable_perpetual_inventory", + 0, + ) + # ERPNext uses erpnext.is_perpetual_inventory_enabled from local + del frappe.local.enable_perpetual_inventory + + transaction_details = { + "doctype": "Purchase Receipt", + "items": SAMPLE_ITEM_LIST, + "is_in_state": 1, + } + + doc = create_transaction(**transaction_details) + + # Check GL Entries + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_no": doc.name}, + fields=["account", "debit", "credit"], + ) + + out_str = json.dumps(sorted(gl_entries, key=json.dumps)) + expected_out_str = json.dumps( + sorted( + [ + { + "account": "GST Expense - _TIRC", + "debit": 0.0, + "credit": 179.82, + }, # only asset + { + "account": "Asset Received But Not Billed - _TIRC", + "debit": 0.0, + "credit": 1999.0, + }, + { + "account": "CWIP Account - _TIRC", + "debit": 2178.82, + "credit": 0.0, + }, + ], + key=json.dumps, + ) + ) + self.assertEqual(out_str, expected_out_str) + + # Check Asset Valuation Rate + asset_purchase_value = frappe.db.get_value( + "Asset", + {"purchase_receipt": doc.name, "item_code": "Test Fixed Asset"}, + "gross_purchase_amount", + ) + self.assertEqual(asset_purchase_value, 1000) + + asset_purchase_value = frappe.db.get_value( + "Asset", + {"purchase_receipt": doc.name, "item_code": "Test Ineligible Fixed Asset"}, + "gross_purchase_amount", + ) + self.assertEqual(asset_purchase_value, 1178.82) + + # Create Purchase Invoice + doc = make_purchase_invoice(doc.name) + doc.bill_no = "BILL-06" + doc.submit() + + self.assertEqual(doc.ineligibility_reason, "Ineligible As Per Section 17(5)") + + # Check GL Entries + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_no": doc.name}, + fields=["account", "debit", "credit"], + ) + + out_str = json.dumps(sorted(gl_entries, key=json.dumps)) + expected_out_str = json.dumps( + sorted( + [ + {"account": "Round Off - _TIRC", "debit": 0.0, "credit": 0.32}, + { + "account": "GST Expense - _TIRC", + "debit": 369.72, + "credit": 189.9, + }, + {"account": "TDS Payable - _TIRC", "debit": 0.0, "credit": 475.4}, + { + "account": "Input Tax SGST - _TIRC", + "debit": 427.86, + "credit": 184.86, + }, + { + "account": "Input Tax CGST - _TIRC", + "debit": 427.86, + "credit": 184.86, + }, + { + "account": "Asset Received But Not Billed - _TIRC", + "debit": 1999.0, + "credit": 0.0, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 2677.64, + "credit": 0.0, + }, + { + "account": "Cost of Goods Sold - _TIRC", + "debit": 267.26, # stock with gst expense + "credit": 0.0, + }, + {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5134.0}, + ], + key=json.dumps, + ) + ) + + self.assertEqual(out_str, expected_out_str) + + # Check Stock Ledger Entries + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": doc.name, "item_code": "Test Stock Item"}, + "incoming_rate", + ) + self.assertEqual(incoming_rate, None) + + # Enable Perpetual Inventory + frappe.db.set_value( + "Company", + "_Test Indian Registered Company", + "enable_perpetual_inventory", + 1, + ) + del frappe.local.enable_perpetual_inventory + + def test_purchase_receipt_and_then_purchase_invoice_for_provisional_expense(self): + """ + No change in accounting because of provisional accounting as it's reversed on purchase invoice + """ + # Enable Provisional Expense + frappe.db.set_value( + "Company", + "_Test Indian Registered Company", + { + "enable_provisional_accounting_for_non_stock_items": 1, + "default_provisional_account": "Unsecured Loans - _TIRC", + }, + ) + + transaction_details = { + "doctype": "Purchase Receipt", + "items": SAMPLE_ITEM_LIST, + "is_in_state": 1, + } + + doc = create_transaction(**transaction_details) + + # Check GL Entries + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_no": doc.name}, + fields=["account", "debit", "credit"], + ) + + out_str = json.dumps(sorted(gl_entries, key=json.dumps)) + expected_out_str = json.dumps( + sorted( + [ + {"account": "GST Expense - _TIRC", "debit": 0.0, "credit": 190.08}, + { + "account": "Asset Received But Not Billed - _TIRC", + "debit": 0.0, + "credit": 1999.0, + }, + { + "account": "CWIP Account - _TIRC", + "debit": 2178.82, + "credit": 0.0, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 998.0, + "credit": 0.0, + }, + { + "account": "Unsecured Loans - _TIRC", + "debit": 0.0, + "credit": 998.0, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 1500.0, + "credit": 0.0, + }, + { + "account": "Unsecured Loans - _TIRC", + "debit": 0.0, + "credit": 1500.0, + }, + { + "account": "Stock Received But Not Billed - _TIRC", + "debit": 0.0, + "credit": 257.0, + }, + { + "account": "Stock In Hand - _TIRC", + "debit": 267.26, + "credit": 0.0, + }, + ], + key=json.dumps, + ) + ) + self.assertEqual(out_str, expected_out_str) + + # Check Stock Ledger Entries + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": doc.name, "item_code": "Test Stock Item"}, + "incoming_rate", + ) + self.assertEqual(incoming_rate, 20) + + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": doc.name, "item_code": "Test Ineligible Stock Item"}, + "incoming_rate", + ) + self.assertEqual(incoming_rate, 22.42) # 19 * 1.18 + + # Check Asset Valuation Rate + asset_purchase_value = frappe.db.get_value( + "Asset", + {"purchase_receipt": doc.name, "item_code": "Test Fixed Asset"}, + "gross_purchase_amount", + ) + self.assertEqual(asset_purchase_value, 1000) + + asset_purchase_value = frappe.db.get_value( + "Asset", + {"purchase_receipt": doc.name, "item_code": "Test Ineligible Fixed Asset"}, + "gross_purchase_amount", + ) + self.assertEqual(asset_purchase_value, 1178.82) + + # Create Purchase Invoice + doc = make_purchase_invoice(doc.name) + doc.bill_no = "BILL-07" + doc.submit() + + self.assertEqual(doc.ineligibility_reason, "Ineligible As Per Section 17(5)") + + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_no": doc.name}, + fields=["account", "debit", "credit"], + ) + + out_str = json.dumps(sorted(gl_entries, key=json.dumps)) + expected_out_str = json.dumps( + sorted( + [ + {"account": "Round Off - _TIRC", "debit": 0.0, "credit": 0.32}, + { + "account": "GST Expense - _TIRC", + "debit": 369.72, + "credit": 179.64, + }, + {"account": "TDS Payable - _TIRC", "debit": 0.0, "credit": 475.4}, + { + "account": "Input Tax SGST - _TIRC", + "debit": 427.86, + "credit": 184.86, + }, + { + "account": "Input Tax CGST - _TIRC", + "debit": 427.86, + "credit": 184.86, + }, + { + "account": "Asset Received But Not Billed - _TIRC", + "debit": 1999.0, + "credit": 0.0, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 2677.64, + "credit": 0.0, + }, + { + "account": "Stock Received But Not Billed - _TIRC", + "debit": 257.0, + "credit": 0.0, + }, + {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5134.0}, + ], + key=json.dumps, + ) + ) + self.assertEqual(out_str, expected_out_str) + + # Disable Provisional Expense + frappe.db.set_value( + "Company", + "_Test Indian Registered Company", + { + "enable_provisional_accounting_for_non_stock_items": 0, + "default_provisional_account": "", + }, + ) + + +def create_test_items(): + item_defaults = { + "company": "_Test Indian Registered Company", + "default_warehouse": "Stores - _TIRC", + "expense_account": "Cost of Goods Sold - _TIRC", + "buying_cost_center": "Main - _TIRC", + "selling_cost_center": "Main - _TIRC", + "income_account": "Sales - _TIRC", + } + + stock_item = { + "doctype": "Item", + "item_code": "Test Stock Item", + "item_group": "All Item Groups", + "gst_hsn_code": "730419", + "is_stock_item": 1, + "item_defaults": [item_defaults], + } + + asset_account = frappe.get_doc( + { + "doctype": "Account", + "account_name": "Asset Account", + "parent_account": "Fixed Assets - _TIRC", + "account_type": "Fixed Asset", + } + ) + asset_account.insert() + + asset_category = frappe.get_doc( + { + "doctype": "Asset Category", + "asset_category_name": "Test Asset Category", + # TODO: Ensure same accounting for without cwip after ERPNext PR 37542 is merged + "enable_cwip_accounting": 1, + "accounts": [ + { + "company_name": "_Test Indian Registered Company", + "fixed_asset_account": asset_account.name, + } + ], + } + ) + asset_category.insert() + + frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() + + asset_item = { + "doctype": "Item", + "item_code": "Test Fixed Asset", + "item_group": "All Item Groups", + "gst_hsn_code": "730419", + "is_stock_item": 0, + "is_fixed_asset": 1, + "auto_create_assets": 1, + "asset_category": asset_category.name, + "asset_naming_series": "ACC-ASS-.YYYY.-", + "item_defaults": [item_defaults], + } + + service_item = { + "doctype": "Item", + "item_code": "Test Service Item", + "item_group": "All Item Groups", + "gst_hsn_code": "730419", + "is_stock_item": 0, + "item_defaults": [ + {**item_defaults, "expense_account": "Administrative Expenses - _TIRC"} + ], + } + + # Stock Item + frappe.get_doc(stock_item).insert() + frappe.get_doc( + { + **stock_item, + "item_code": "Test Ineligible Stock Item", + "is_ineligible_for_itc": 1, + } + ).insert() + + # Fixed Asset + frappe.get_doc(asset_item).insert() + frappe.get_doc( + { + **asset_item, + "item_code": "Test Ineligible Fixed Asset", + "is_ineligible_for_itc": 1, + } + ).insert() + + # Service Item + frappe.get_doc(service_item).insert() + frappe.get_doc( + { + **service_item, + "item_code": "Test Ineligible Service Item", + "is_ineligible_for_itc": 1, + } + ).insert() diff --git a/india_compliance/gst_india/overrides/transaction.py b/india_compliance/gst_india/overrides/transaction.py index 4dfc0bb17..d35aed6c0 100644 --- a/india_compliance/gst_india/overrides/transaction.py +++ b/india_compliance/gst_india/overrides/transaction.py @@ -29,6 +29,7 @@ ) DOCTYPES_WITH_TAXABLE_VALUE = { + "Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice", @@ -867,8 +868,8 @@ def validate_reverse_charge_transaction(doc, method=None): frappe.throw(msg) - if doc.get("eligibility_for_itc") == "All Other ITC": - doc.eligibility_for_itc = "ITC on Reverse Charge" + if doc.get("itc_classification") == "All Other ITC": + doc.itc_classification = "ITC on Reverse Charge" def is_export_without_payment_of_gst(doc): diff --git a/india_compliance/gst_india/report/gstr_3b_details/gstr_3b_details.py b/india_compliance/gst_india/report/gstr_3b_details/gstr_3b_details.py index a26be30a1..75ae65a70 100644 --- a/india_compliance/gst_india/report/gstr_3b_details/gstr_3b_details.py +++ b/india_compliance/gst_india/report/gstr_3b_details/gstr_3b_details.py @@ -101,7 +101,7 @@ def extend_columns(self): "width": 100, }, { - "fieldname": "eligibility_for_itc", + "fieldname": "itc_classification", "label": _("Eligibility for ITC"), "fieldtype": "Data", "width": 100, @@ -118,7 +118,7 @@ def get_data(self): self.data = sorted( data, - key=lambda k: (k["eligibility_for_itc"], k["posting_date"]), + key=lambda k: (k["itc_classification"], k["posting_date"]), ) def get_itc_from_purchase(self): @@ -130,7 +130,7 @@ def get_itc_from_purchase(self): ConstantColumn("Purchase Invoice").as_("voucher_type"), purchase_invoice.name.as_("voucher_no"), purchase_invoice.posting_date, - purchase_invoice.eligibility_for_itc, + purchase_invoice.itc_classification, Sum(purchase_invoice.itc_integrated_tax).as_("integrated_tax"), Sum(purchase_invoice.itc_central_tax).as_("central_tax"), Sum(purchase_invoice.itc_state_tax).as_("state_tax"), @@ -142,7 +142,7 @@ def get_itc_from_purchase(self): & (purchase_invoice.posting_date[self.from_date : self.to_date]) & (purchase_invoice.company == self.company) & (purchase_invoice.company_gstin == self.company_gstin) - & (Ifnull(purchase_invoice.eligibility_for_itc, "") != "") + & (Ifnull(purchase_invoice.itc_classification, "") != "") ) .groupby(purchase_invoice.name) ) @@ -180,7 +180,7 @@ def get_itc_from_boe(self): ).as_("cess_amount"), LiteralValue(0).as_("central_tax"), LiteralValue(0).as_("state_tax"), - ConstantColumn("Import of Goods").as_("eligibility_for_itc"), + ConstantColumn("Import of Goods").as_("itc_classification"), ) .where( (boe.docstatus == 1) @@ -237,7 +237,7 @@ def get_itc_from_journal_entry(self): ) .else_(0) ).as_("cess_amount"), - journal_entry.reversal_type.as_("eligibility_for_itc"), + journal_entry.ineligibility_reason.as_("itc_classification"), ) .where( (journal_entry.docstatus == 1) diff --git a/india_compliance/gst_india/utils/tests.py b/india_compliance/gst_india/utils/tests.py index b6d9a7526..6558b6f55 100644 --- a/india_compliance/gst_india/utils/tests.py +++ b/india_compliance/gst_india/utils/tests.py @@ -49,9 +49,9 @@ def create_transaction(**data): if ( transaction.doctype == "Purchase Invoice" - and not transaction.eligibility_for_itc + and not transaction.itc_classification ): - transaction.eligibility_for_itc = "All Other ITC" + transaction.itc_classification = "All Other ITC" if transaction.doctype == "POS Invoice": transaction.append( diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py index 1d783a604..ba1ade5a9 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -111,6 +111,9 @@ "before_validate": ( "india_compliance.gst_india.overrides.transaction.before_validate" ), + "before_submit": "india_compliance.gst_india.overrides.ineligible_itc.before_submit", + "before_gl_preview": "india_compliance.gst_india.overrides.ineligible_itc.before_submit", + "before_sl_preview": "india_compliance.gst_india.overrides.ineligible_itc.before_submit", }, "Purchase Order": { "validate": ( @@ -127,6 +130,9 @@ "before_validate": ( "india_compliance.gst_india.overrides.transaction.before_validate" ), + "before_submit": "india_compliance.gst_india.overrides.ineligible_itc.before_submit", + "before_gl_preview": "india_compliance.gst_india.overrides.ineligible_itc.before_submit", + "before_sl_preview": "india_compliance.gst_india.overrides.ineligible_itc.before_submit", }, "Sales Invoice": { "onload": "india_compliance.gst_india.overrides.sales_invoice.onload", @@ -192,6 +198,15 @@ "erpnext.accounts.doctype.payment_reconciliation.payment_reconciliation.adjust_allocations_for_taxes": ( "india_compliance.gst_india.overrides.payment_entry.adjust_allocations_for_taxes_in_payment_reconciliation" ), + "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries": ( + "india_compliance.gst_india.overrides.ineligible_itc.update_regional_gl_entries" + ), + "erpnext.stock.doctype.purchase_receipt.purchase_receipt.update_regional_gl_entries": ( + "india_compliance.gst_india.overrides.ineligible_itc.update_regional_gl_entries" + ), + "erpnext.controllers.stock_controller.update_regional_gl_entries": ( + "india_compliance.gst_india.overrides.ineligible_itc.update_regional_gl_entries" + ), "erpnext.accounts.party.get_regional_address_details": ( "india_compliance.gst_india.overrides.transaction.update_party_details" ), diff --git a/india_compliance/install.py b/india_compliance/install.py index 53405187f..e11c0a509 100644 --- a/india_compliance/install.py +++ b/india_compliance/install.py @@ -38,6 +38,7 @@ "update_custom_role_for_e_invoice_summary", "update_company_gstin", "update_payment_entry_fields", + "update_itc_classification_field", ) diff --git a/india_compliance/patches.txt b/india_compliance/patches.txt index 4e0d91d0b..a07acd475 100644 --- a/india_compliance/patches.txt +++ b/india_compliance/patches.txt @@ -3,7 +3,7 @@ execute:import frappe; frappe.delete_doc_if_exists("DocType", "GSTIN") [post_model_sync] india_compliance.patches.v14.set_default_for_overridden_accounts_setting -execute:from india_compliance.gst_india.setup import create_custom_fields; create_custom_fields() #31 +execute:from india_compliance.gst_india.setup import create_custom_fields; create_custom_fields() #32 execute:from india_compliance.gst_india.setup import create_property_setters; create_property_setters() #3 india_compliance.patches.post_install.remove_old_fields india_compliance.patches.post_install.update_company_gstin @@ -31,3 +31,5 @@ execute:from india_compliance.gst_india.setup import create_email_template; crea india_compliance.patches.post_install.update_reconciliation_status india_compliance.patches.post_install.update_payment_entry_fields india_compliance.patches.v15.remove_ignore_reconciliation_field +india_compliance.patches.post_install.update_company_fixtures +india_compliance.patches.post_install.update_itc_classification_field diff --git a/india_compliance/patches/post_install/rename_import_of_capital_goods.py b/india_compliance/patches/post_install/rename_import_of_capital_goods.py index f1290425b..9fdca1d0c 100644 --- a/india_compliance/patches/post_install/rename_import_of_capital_goods.py +++ b/india_compliance/patches/post_install/rename_import_of_capital_goods.py @@ -2,6 +2,9 @@ def execute(): + if "eligibility_for_itc" not in frappe.db.get_table_columns("Purchase Invoice"): + return + frappe.db.set_value( "Purchase Invoice", {"eligibility_for_itc": "Import Of Capital Goods"}, diff --git a/india_compliance/patches/post_install/set_gst_category.py b/india_compliance/patches/post_install/set_gst_category.py index 8b33c8a88..4b69c7dbf 100644 --- a/india_compliance/patches/post_install/set_gst_category.py +++ b/india_compliance/patches/post_install/set_gst_category.py @@ -28,6 +28,9 @@ def execute(): delete_old_fields("invoice_type", doctypes) + if "eligibility_for_itc" not in frappe.db.get_table_columns("Purchase Invoice"): + return + # update eligibility_for_itc with new options for old_value, new_value in { "ineligible": "Ineligible", diff --git a/india_compliance/patches/post_install/update_company_fixtures.py b/india_compliance/patches/post_install/update_company_fixtures.py index 174d43843..a7f4d426e 100644 --- a/india_compliance/patches/post_install/update_company_fixtures.py +++ b/india_compliance/patches/post_install/update_company_fixtures.py @@ -3,6 +3,7 @@ from india_compliance.gst_india.overrides.company import ( make_default_customs_accounts, + make_default_gst_expense_accounts, make_default_tax_templates, ) from india_compliance.income_tax_india.overrides.company import ( @@ -35,6 +36,11 @@ def execute(): ): make_default_customs_accounts(company) + if not frappe.db.exists( + "Account", {"company": company, "account_name": "GST Expense"} + ): + make_default_gst_expense_accounts(company) + def update_root_for_rcm(company): # Root type for RCM had been updated to "Liability". diff --git a/india_compliance/patches/post_install/update_itc_amounts.py b/india_compliance/patches/post_install/update_itc_amounts.py index 26a357732..e6526d6ac 100644 --- a/india_compliance/patches/post_install/update_itc_amounts.py +++ b/india_compliance/patches/post_install/update_itc_amounts.py @@ -4,6 +4,9 @@ def execute(): + if "eligibility_for_itc" not in frappe.db.get_table_columns("Purchase Invoice"): + return + itc_amounts = { "itc_integrated_tax": 0, "itc_state_tax": 0, diff --git a/india_compliance/patches/post_install/update_itc_classification_field.py b/india_compliance/patches/post_install/update_itc_classification_field.py new file mode 100644 index 000000000..2c25d5661 --- /dev/null +++ b/india_compliance/patches/post_install/update_itc_classification_field.py @@ -0,0 +1,54 @@ +import frappe +from frappe.query_builder.functions import IfNull + +from india_compliance.gst_india.utils.custom_fields import delete_old_fields + + +def execute(): + patch_field_in_purchase_invoice() + patch_journal_entry() + + delete_old_fields("eligibility_for_itc", "Purchase Invoice") + delete_old_fields("reversal_type", "Journal Entry") + + +def patch_field_in_purchase_invoice(): + if "eligibility_for_itc" not in frappe.db.get_table_columns("Purchase Invoice"): + return + + doctype = frappe.qb.DocType("Purchase Invoice") + depricated_eligibility_for_itc = ( + "Ineligible As Per Section 17(5)", + "Ineligible Others", + ) + ( + frappe.qb.update(doctype) + .set(doctype.itc_classification, doctype.eligibility_for_itc) + .where(doctype.eligibility_for_itc.notin(depricated_eligibility_for_itc)) + .run() + ) + ( + frappe.qb.update(doctype) + .set(doctype.itc_classification, "All Other ITC") + .where(IfNull(doctype.itc_classification, "") == "") + .run() + ) + ( + frappe.qb.update(doctype) + .set(doctype.ineligibility_reason, "Ineligible As Per Section 17(5)") + .where(doctype.eligibility_for_itc.isin(depricated_eligibility_for_itc)) + .run() + ) + + +def patch_journal_entry(): + if "reversal_type" not in frappe.db.get_table_columns("Journal Entry"): + return + + doctype = frappe.qb.DocType("Journal Entry") + ( + frappe.qb.update(doctype) + .set(doctype.reversal_type, doctype.ineligibility_reason) + .where(IfNull(doctype.reversal_type, "") != "") + .run() + ) From 165760bfd8ccd1aa1fc82c5bbad9dc44493e81fe Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Sat, 21 Oct 2023 19:31:54 +0530 Subject: [PATCH 2/7] feat: add boe support --- .../doctype/bill_of_entry/bill_of_entry.py | 47 +++- .../bill_of_entry_item.json | 12 +- .../gst_india/overrides/ineligible_itc.py | 102 ++++++- .../overrides/test_ineligible_itc.py | 250 +++++++++--------- india_compliance/hooks.py | 12 +- 5 files changed, 272 insertions(+), 151 deletions(-) diff --git a/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.py b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.py index 18b767c38..876e8b260 100644 --- a/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.py +++ b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.py @@ -9,10 +9,15 @@ from frappe.model.mapper import get_mapped_doc from frappe.utils import today import erpnext -from erpnext.accounts.general_ledger import make_gl_entries +from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.taxes_and_totals import get_round_off_applicable_accounts +from india_compliance.gst_india.overrides.ineligible_itc import ( + update_landed_cost_voucher_for_gst_expense, + update_regional_gl_entries, + update_valuation_rate, +) from india_compliance.gst_india.utils import get_gst_accounts_by_type @@ -45,13 +50,16 @@ def validate(self): self.validate_purchase_invoice() self.validate_taxes() self.reconciliation_status = "Unreconciled" + update_valuation_rate(self) def on_submit(self): - make_gl_entries(self.get_gl_entries()) + gl_entries = self.get_gl_entries() + update_regional_gl_entries(gl_entries, self) + make_gl_entries(gl_entries) def on_cancel(self): self.ignore_linked_doctypes = ("GL Entry",) - make_gl_entries(self.get_gl_entries(), cancel=True) + make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) # Code adapted from AccountsController.on_trash def on_trash(self): @@ -305,6 +313,32 @@ def get_rows_to_update(self, item_name=None, tax_name=None): return items, taxes + def get_stock_items(self): + stock_items = [] + item_codes = list(set(item.item_code for item in self.get("items"))) + if item_codes: + stock_items = frappe.db.get_values( + "Item", + {"name": ["in", item_codes], "is_stock_item": 1}, + pluck="name", + cache=True, + ) + + return stock_items + + def get_asset_items(self): + asset_items = [] + item_codes = list(set(item.item_code for item in self.get("items"))) + if item_codes: + asset_items = frappe.db.get_values( + "Item", + {"name": ["in", item_codes], "is_fixed_asset": 1}, + pluck="name", + cache=True, + ) + + return asset_items + @frappe.whitelist() def make_bill_of_entry(source_name, target_doc=None): @@ -454,6 +488,7 @@ def set_missing_values(source, target): for item in target.items: item.applicable_charges = items[item.purchase_receipt_item].customs_duty total_customs_duty += item.applicable_charges + item.boe_detail = items[item.purchase_receipt_item].boe_detail # add taxes target.append( @@ -473,6 +508,8 @@ def set_missing_values(source, target): ) ) + update_landed_cost_voucher_for_gst_expense(source, target) + doc = get_mapped_doc( "Bill of Entry", source_name, @@ -503,6 +540,7 @@ def get_items_for_landed_cost_voucher(boe): """ pi = frappe.get_doc("Purchase Invoice", boe.purchase_invoice) item_customs_map = {item.pi_detail: item.customs_duty for item in boe.items} + item_name_map = {item.pi_detail: item.name for item in boe.items} def _item_dict(items): return frappe._dict({item.name: item for item in items}) @@ -512,6 +550,7 @@ def _item_dict(items): pi_items = [pi_item.as_dict() for pi_item in pi.items] for pi_item in pi_items: pi_item.customs_duty = item_customs_map.get(pi_item.name) + pi_item.boe_detail = item_name_map.get(pi_item.name) return _item_dict(pi_items) @@ -526,6 +565,7 @@ def _item_dict(items): for pr_item in pr_items: pr_item.customs_duty = item_customs_map.get(pr_pi_map.get(pr_item.name)) + pr_item.boe_detail = item_name_map.get(pr_pi_map.get(pr_item.name)) return _item_dict(pr_items) @@ -542,5 +582,6 @@ def _item_dict(items): customs_duty_for_item = item_customs_map.get(pr_item.purchase_invoice_item) total_qty = item_qty_map.get(pr_item.purchase_invoice_item) pr_item.customs_duty = customs_duty_for_item * pr_item.qty / total_qty + pr_item.boe_detail = item_name_map.get(pr_item.purchase_invoice_item) return _item_dict(pr_items) diff --git a/india_compliance/gst_india/doctype/bill_of_entry_item/bill_of_entry_item.json b/india_compliance/gst_india/doctype/bill_of_entry_item/bill_of_entry_item.json index acba4cd5d..4d44ef58e 100644 --- a/india_compliance/gst_india/doctype/bill_of_entry_item/bill_of_entry_item.json +++ b/india_compliance/gst_india/doctype/bill_of_entry_item/bill_of_entry_item.json @@ -17,6 +17,7 @@ "column_break_ifxl", "taxable_value", "item_tax_template", + "is_ineligible_for_itc", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -106,12 +107,21 @@ "fieldtype": "Link", "label": "Project", "options": "Project" + }, + { + "default": "0", + "fetch_from": "item_code.is_ineligible_for_itc", + "fetch_if_empty": 1, + "fieldname": "is_ineligible_for_itc", + "fieldtype": "Check", + "label": "Is Ineligible for Input Tax Credit", + "print_hide": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-03-13 14:26:53.580618", + "modified": "2023-10-20 12:24:27.373448", "modified_by": "Administrator", "module": "GST India", "name": "Bill of Entry Item", diff --git a/india_compliance/gst_india/overrides/ineligible_itc.py b/india_compliance/gst_india/overrides/ineligible_itc.py index 2ebbe4808..484dfc940 100644 --- a/india_compliance/gst_india/overrides/ineligible_itc.py +++ b/india_compliance/gst_india/overrides/ineligible_itc.py @@ -5,6 +5,9 @@ is_cwip_accounting_enabled, ) +from india_compliance.gst_india.overrides.transaction import ( + is_indian_registered_company, +) from india_compliance.gst_india.utils import get_gst_accounts_by_type @@ -16,8 +19,8 @@ def __init__(self, doc): self.is_perpetual = self.company.enable_perpetual_inventory self.cost_center = doc.cost_center or self.company.cost_center - self.dr_or_cr = "credit" if doc.is_return else "debit" - self.cr_or_dr = "debit" if doc.is_return else "credit" + self.dr_or_cr = "credit" if doc.get("is_return") else "debit" + self.cr_or_dr = "debit" if doc.get("is_return") else "credit" def update_valuation_rate(self): """ @@ -26,6 +29,11 @@ def update_valuation_rate(self): - Only updates if its a stock item or fixed asset - No updates for expense items """ + if self.doc.get("is_opening") == "Yes" or not is_indian_registered_company( + self.doc + ): + return + self.doc._has_ineligible_itc_items = False stock_items = self.doc.get_stock_items() @@ -44,20 +52,18 @@ def update_valuation_rate(self): if item.item_code in stock_items: item._is_stock_item = True - if item.get("_is_stock_item") or item.is_fixed_asset: + if item.get("_is_stock_item") or item.get("is_fixed_asset"): ineligible_tax_amount = item._ineligible_tax_amount - if self.doc.is_return: + if self.doc.get("is_return"): ineligible_tax_amount = -ineligible_tax_amount # TODO: handle rounding off - item.valuation_rate += flt(ineligible_tax_amount / item.stock_qty, 2) + self.update_item_valuation_rate(item, ineligible_tax_amount) def update_gl_entries(self, gl_entries): self.gl_entries = gl_entries - if self.doc.get("is_opening") == "Yes" or not self.doc.get( - "_has_ineligible_itc_items" - ): + if not self.doc.get("_has_ineligible_itc_items"): return gl_entries for item in self.doc.items: @@ -124,7 +130,7 @@ def make_gst_expense_entry(self, item): ) ) - if item.is_fixed_asset: + if item.get("is_fixed_asset"): item.expense_account = _get_asset_account( item.asset_category, self.doc.company ) @@ -168,6 +174,9 @@ def update_ineligible_taxes(self, item): item._ineligible_taxes = ineligible_taxes item._ineligible_tax_amount = sum(ineligible_taxes.values()) + def update_item_valuation_rate(self, item, ineligible_tax_amount): + item.valuation_rate += flt(ineligible_tax_amount / item.stock_qty, 2) + def get_item_tax_amount(self, item, tax): """ Returns proportionate item tax amount for each tax component @@ -269,17 +278,82 @@ def is_eligibility_restricted_due_to_pos(self): class BillOfEntry(IneligibleITC): - pass + def update_valuation_rate(self): + # Update fixed assets + asset_items = self.doc.get_asset_items() + expense_account = frappe.db.get_values( + "Purchase Invoice Item", + {"parent": self.doc.purchase_invoice}, + ["expense_account", "name"], + as_dict=True, + ) + expense_account = {d.name: d.expense_account for d in expense_account} + + for item in self.doc.items: + if item.item_code in asset_items: + item.is_fixed_asset = True + + if item.pi_detail in expense_account: + item.expense_account = expense_account[item.pi_detail] + + super().update_valuation_rate() + + def get_item_tax_amount(self, item, tax): + tax_rate = frappe.parse_json(tax.item_wise_tax_rates).get(item.name) + if tax_rate is None: + return 0 + + tax_rate = rounded(tax_rate, 3) + tax_amount = tax_rate * item.taxable_value / 100 + + return abs(tax_amount) + + def update_item_valuation_rate(self, item, ineligible_tax_amount): + item.valuation_rate = ineligible_tax_amount + + def update_item_gl_entries(self, item): + if not ( + (item.get("_is_stock_item") and self.is_perpetual) + or item.get("is_fixed_asset") + ): + self.make_gst_expense_entry(item) + + self.reverse_input_taxes_entry(item) + + def update_landed_cost_voucher(self, landed_cost_voucher): + self.update_valuation_rate() + boe_items = frappe._dict({item.name: item for item in self.doc.items}) + total_gst_expense = 0 + + for item in landed_cost_voucher.items: + if item.get("boe_detail") not in boe_items: + continue + + gst_expense = boe_items[item.boe_detail].get("valuation_rate", 0) + if not gst_expense: + continue + + total_gst_expense += gst_expense + item.applicable_charges += gst_expense / item.qty + + landed_cost_voucher.append( + "taxes", + { + "expense_account": self.company.default_gst_expense_account, + "description": "Customs Duty", + "amount": total_gst_expense, + }, + ) DOCTYPE_MAPPING = { "Purchase Invoice": PurchaseInvoice, "Purchase Receipt": PurchaseReceipt, - "Bill Of Entry": BillOfEntry, + "Bill of Entry": BillOfEntry, } -def before_submit(doc, method=None): +def update_valuation_rate(doc, method=None): if doc.doctype in DOCTYPE_MAPPING: DOCTYPE_MAPPING[doc.doctype](doc).update_valuation_rate() @@ -291,6 +365,10 @@ def update_regional_gl_entries(gl_entries, doc): return gl_entries +def update_landed_cost_voucher_for_gst_expense(source, target): + BillOfEntry(source).update_landed_cost_voucher(target) + + def _get_asset_account(asset_category, company): fieldname = "fixed_asset_account" if is_cwip_accounting_enabled(asset_category): diff --git a/india_compliance/gst_india/overrides/test_ineligible_itc.py b/india_compliance/gst_india/overrides/test_ineligible_itc.py index b6901d6b7..4c289a228 100644 --- a/india_compliance/gst_india/overrides/test_ineligible_itc.py +++ b/india_compliance/gst_india/overrides/test_ineligible_itc.py @@ -721,93 +721,62 @@ def test_purchase_receipt_and_then_purchase_invoice_for_provisional_expense(self doc = create_transaction(**transaction_details) - # Check GL Entries - gl_entries = frappe.get_all( - "GL Entry", - filters={"voucher_no": doc.name}, - fields=["account", "debit", "credit"], - ) - - out_str = json.dumps(sorted(gl_entries, key=json.dumps)) - expected_out_str = json.dumps( - sorted( - [ - {"account": "GST Expense - _TIRC", "debit": 0.0, "credit": 190.08}, - { - "account": "Asset Received But Not Billed - _TIRC", - "debit": 0.0, - "credit": 1999.0, - }, - { - "account": "CWIP Account - _TIRC", - "debit": 2178.82, - "credit": 0.0, - }, - { - "account": "Administrative Expenses - _TIRC", - "debit": 998.0, - "credit": 0.0, - }, - { - "account": "Unsecured Loans - _TIRC", - "debit": 0.0, - "credit": 998.0, - }, - { - "account": "Administrative Expenses - _TIRC", - "debit": 1500.0, - "credit": 0.0, - }, - { - "account": "Unsecured Loans - _TIRC", - "debit": 0.0, - "credit": 1500.0, - }, - { - "account": "Stock Received But Not Billed - _TIRC", - "debit": 0.0, - "credit": 257.0, - }, - { - "account": "Stock In Hand - _TIRC", - "debit": 267.26, - "credit": 0.0, - }, - ], - key=json.dumps, - ) - ) - self.assertEqual(out_str, expected_out_str) - - # Check Stock Ledger Entries - incoming_rate = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": doc.name, "item_code": "Test Stock Item"}, - "incoming_rate", - ) - self.assertEqual(incoming_rate, 20) - - incoming_rate = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": doc.name, "item_code": "Test Ineligible Stock Item"}, - "incoming_rate", + self.assertGLEntry( + doc.name, + [ + {"account": "GST Expense - _TIRC", "debit": 0.0, "credit": 190.08}, + { + "account": "Asset Received But Not Billed - _TIRC", + "debit": 0.0, + "credit": 1999.0, + }, + { + "account": "CWIP Account - _TIRC", + "debit": 2178.82, + "credit": 0.0, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 998.0, + "credit": 0.0, + }, + { + "account": "Unsecured Loans - _TIRC", + "debit": 0.0, + "credit": 998.0, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 1500.0, + "credit": 0.0, + }, + { + "account": "Unsecured Loans - _TIRC", + "debit": 0.0, + "credit": 1500.0, + }, + { + "account": "Stock Received But Not Billed - _TIRC", + "debit": 0.0, + "credit": 257.0, + }, + { + "account": "Stock In Hand - _TIRC", + "debit": 267.26, + "credit": 0.0, + }, + ], ) - self.assertEqual(incoming_rate, 22.42) # 19 * 1.18 - # Check Asset Valuation Rate - asset_purchase_value = frappe.db.get_value( - "Asset", - {"purchase_receipt": doc.name, "item_code": "Test Fixed Asset"}, - "gross_purchase_amount", + self.assertStockValues( + doc.name, {"Test Stock Item": 20, "Test Ineligible Stock Item": 22.42} ) - self.assertEqual(asset_purchase_value, 1000) - asset_purchase_value = frappe.db.get_value( - "Asset", - {"purchase_receipt": doc.name, "item_code": "Test Ineligible Fixed Asset"}, - "gross_purchase_amount", + self.assertAssetValues( + "Purchase Receipt", + doc.name, + {"Test Fixed Asset": 1000, "Test Ineligible Fixed Asset": 1178.82}, ) - self.assertEqual(asset_purchase_value, 1178.82) # Create Purchase Invoice doc = make_purchase_invoice(doc.name) @@ -816,54 +785,44 @@ def test_purchase_receipt_and_then_purchase_invoice_for_provisional_expense(self self.assertEqual(doc.ineligibility_reason, "Ineligible As Per Section 17(5)") - gl_entries = frappe.get_all( - "GL Entry", - filters={"voucher_no": doc.name}, - fields=["account", "debit", "credit"], - ) - - out_str = json.dumps(sorted(gl_entries, key=json.dumps)) - expected_out_str = json.dumps( - sorted( - [ - {"account": "Round Off - _TIRC", "debit": 0.0, "credit": 0.32}, - { - "account": "GST Expense - _TIRC", - "debit": 369.72, - "credit": 179.64, - }, - {"account": "TDS Payable - _TIRC", "debit": 0.0, "credit": 475.4}, - { - "account": "Input Tax SGST - _TIRC", - "debit": 427.86, - "credit": 184.86, - }, - { - "account": "Input Tax CGST - _TIRC", - "debit": 427.86, - "credit": 184.86, - }, - { - "account": "Asset Received But Not Billed - _TIRC", - "debit": 1999.0, - "credit": 0.0, - }, - { - "account": "Administrative Expenses - _TIRC", - "debit": 2677.64, - "credit": 0.0, - }, - { - "account": "Stock Received But Not Billed - _TIRC", - "debit": 257.0, - "credit": 0.0, - }, - {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5134.0}, - ], - key=json.dumps, - ) + self.assertGLEntry( + doc.name, + [ + {"account": "Round Off - _TIRC", "debit": 0.0, "credit": 0.32}, + { + "account": "GST Expense - _TIRC", + "debit": 369.72, + "credit": 179.64, + }, + {"account": "TDS Payable - _TIRC", "debit": 0.0, "credit": 475.4}, + { + "account": "Input Tax SGST - _TIRC", + "debit": 427.86, + "credit": 184.86, + }, + { + "account": "Input Tax CGST - _TIRC", + "debit": 427.86, + "credit": 184.86, + }, + { + "account": "Asset Received But Not Billed - _TIRC", + "debit": 1999.0, + "credit": 0.0, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 2677.64, + "credit": 0.0, + }, + { + "account": "Stock Received But Not Billed - _TIRC", + "debit": 257.0, + "credit": 0.0, + }, + {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5134.0}, + ], ) - self.assertEqual(out_str, expected_out_str) # Disable Provisional Expense frappe.db.set_value( @@ -875,6 +834,39 @@ def test_purchase_receipt_and_then_purchase_invoice_for_provisional_expense(self }, ) + def test_purchase_invoice_with_bill_of_entry(self): + pass + + def assertGLEntry(self, docname, expected_gl_entry): + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_no": docname}, + fields=["account", "debit", "credit"], + ) + + out_str = json.dumps(sorted(gl_entries, key=json.dumps)) + expected_out_str = json.dumps(sorted(expected_gl_entry, key=json.dumps)) + + self.assertEqual(out_str, expected_out_str) + + def assertAssetValues(self, doctype, docname, asset_values): + for asset, value in asset_values.items(): + asset_purchase_value = frappe.db.get_value( + "Asset", + {f"{frappe.scrub(doctype)}": docname, "item_code": asset}, + "gross_purchase_amount", + ) + self.assertEqual(asset_purchase_value, value) + + def assertStockValues(self, docname, incoming_rates): + for item, value in incoming_rates.items(): + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": docname, "item_code": item}, + "incoming_rate", + ) + self.assertEqual(incoming_rate, value) + def create_test_items(): item_defaults = { diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py index ba1ade5a9..9d57f42ae 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -111,9 +111,9 @@ "before_validate": ( "india_compliance.gst_india.overrides.transaction.before_validate" ), - "before_submit": "india_compliance.gst_india.overrides.ineligible_itc.before_submit", - "before_gl_preview": "india_compliance.gst_india.overrides.ineligible_itc.before_submit", - "before_sl_preview": "india_compliance.gst_india.overrides.ineligible_itc.before_submit", + "before_submit": "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate", + "before_gl_preview": "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate", + "before_sl_preview": "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate", }, "Purchase Order": { "validate": ( @@ -130,9 +130,9 @@ "before_validate": ( "india_compliance.gst_india.overrides.transaction.before_validate" ), - "before_submit": "india_compliance.gst_india.overrides.ineligible_itc.before_submit", - "before_gl_preview": "india_compliance.gst_india.overrides.ineligible_itc.before_submit", - "before_sl_preview": "india_compliance.gst_india.overrides.ineligible_itc.before_submit", + "before_submit": "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate", + "before_gl_preview": "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate", + "before_sl_preview": "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate", }, "Sales Invoice": { "onload": "india_compliance.gst_india.overrides.sales_invoice.onload", From 26ac032b83a841c5f631202550273080739c3078 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 24 Oct 2023 09:18:46 +0530 Subject: [PATCH 3/7] test: refactor and make fixes in line with erpnext --- .../gst_india/overrides/ineligible_itc.py | 7 +- .../overrides/test_ineligible_itc.py | 934 +++++++----------- india_compliance/tests/__init__.py | 21 + 3 files changed, 402 insertions(+), 560 deletions(-) diff --git a/india_compliance/gst_india/overrides/ineligible_itc.py b/india_compliance/gst_india/overrides/ineligible_itc.py index 484dfc940..5528438bd 100644 --- a/india_compliance/gst_india/overrides/ineligible_itc.py +++ b/india_compliance/gst_india/overrides/ineligible_itc.py @@ -236,8 +236,8 @@ def update_item_gl_entries(self, item): self.reverse_input_taxes_entry(item) def is_debit_entry_required(self, item): - # For Stock Entry in PI, Additional Debit is accounted automatically from valuation rates - return self.is_expense_item(item) or item.is_fixed_asset + # For Stock Entry / Fixed Asset in PI, Additional Debit is accounted automatically from valuation rates + return self.is_expense_item(item) def is_expense_item(self, item): """ @@ -336,6 +336,9 @@ def update_landed_cost_voucher(self, landed_cost_voucher): total_gst_expense += gst_expense item.applicable_charges += gst_expense / item.qty + if total_gst_expense == 0: + return + landed_cost_voucher.append( "taxes", { diff --git a/india_compliance/gst_india/overrides/test_ineligible_itc.py b/india_compliance/gst_india/overrides/test_ineligible_itc.py index 4c289a228..dd21b5438 100644 --- a/india_compliance/gst_india/overrides/test_ineligible_itc.py +++ b/india_compliance/gst_india/overrides/test_ineligible_itc.py @@ -1,4 +1,5 @@ import json +from contextlib import contextmanager import frappe from frappe.tests.utils import FrappeTestCase @@ -38,6 +39,59 @@ # Ineligible Service Item = 499 * 2 * 18% = 179.64 or CGST + SGST = 89.82 + 89.82 = 179.64 +@contextmanager +def toggle_perpetual_inventory(): + frappe.db.set_value( + "Company", + "_Test Indian Registered Company", + "enable_perpetual_inventory", + 0, + ) + + if hasattr(frappe.local, "enable_perpetual_inventory"): + del frappe.local.enable_perpetual_inventory + + try: + yield + + finally: + frappe.db.set_value( + "Company", + "_Test Indian Registered Company", + "enable_perpetual_inventory", + 1, + ) + + if hasattr(frappe.local, "enable_perpetual_inventory"): + del frappe.local.enable_perpetual_inventory + + +@contextmanager +def toggle_provisional_accounting(): + # Enable Provisional Expense + frappe.db.set_value( + "Company", + "_Test Indian Registered Company", + { + "enable_provisional_accounting_for_non_stock_items": 1, + "default_provisional_account": "Unsecured Loans - _TIRC", + }, + ) + + try: + yield + + finally: + frappe.db.set_value( + "Company", + "_Test Indian Registered Company", + { + "enable_provisional_accounting_for_non_stock_items": 0, + "default_provisional_account": None, + }, + ) + + class TestIneligibleITC(FrappeTestCase): @classmethod def setUpClass(cls): @@ -57,85 +111,52 @@ def test_purchase_invoice_with_update_stock(self): self.assertEqual(doc.ineligibility_reason, "Ineligible As Per Section 17(5)") - # Check GL Entries - gl_entries = frappe.get_all( - "GL Entry", - filters={"voucher_no": doc.name}, - fields=["account", "debit", "credit"], - ) - - out_str = json.dumps(sorted(gl_entries, key=json.dumps)) - expected_out_str = json.dumps( - sorted( - [ - {"account": "Round Off - _TIRC", "debit": 0.28, "credit": 0.0}, - { - "account": "GST Expense - _TIRC", - "debit": 369.72, - "credit": 369.72, - }, # 179.64 + 179.82 + 10.26 - { - "account": "Input Tax SGST - _TIRC", - "debit": 427.86, - "credit": 184.86, # 369.72 / 2 - }, - { - "account": "Input Tax CGST - _TIRC", - "debit": 427.86, - "credit": 184.86, - }, - { - "account": "Administrative Expenses - _TIRC", - "debit": 2677.64, # 500 * 3 + 499 * 2 + 179.64 - "credit": 0.0, - }, - { - "account": "CWIP Account - _TIRC", - "debit": 2178.82, - "credit": 0.0, - }, # 1000 + 999 + 179.82 - { - "account": "Stock In Hand - _TIRC", - "debit": 267.26, - "credit": 0.0, - }, # 20 * 5 + 19 * 3 + 100 * 1 + 10.26 - {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5610.0}, - ], - key=json.dumps, - ) - ) - - self.assertEqual(out_str, expected_out_str) - - # Check Stock Ledger Entries - incoming_rate = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": doc.name, "item_code": "Test Stock Item"}, - "incoming_rate", - ) - self.assertEqual(incoming_rate, 20) - - incoming_rate = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": doc.name, "item_code": "Test Ineligible Stock Item"}, - "incoming_rate", - ) - self.assertEqual(incoming_rate, 22.42) # 19 * 1.18 - - # Check Asset Valuation Rate - asset_purchase_value = frappe.db.get_value( - "Asset", - {"purchase_invoice": doc.name, "item_code": "Test Fixed Asset"}, - "gross_purchase_amount", + self.assertGLEntry( + doc.name, + [ + {"account": "Round Off - _TIRC", "debit": 0.28, "credit": 0.0}, + { + "account": "GST Expense - _TIRC", + "debit": 369.72, + "credit": 369.72, + }, # 179.64 + 179.82 + 10.26 + { + "account": "Input Tax SGST - _TIRC", + "debit": 427.86, + "credit": 184.86, # 369.72 / 2 + }, + { + "account": "Input Tax CGST - _TIRC", + "debit": 427.86, + "credit": 184.86, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 2677.64, # 500 * 3 + 499 * 2 + 179.64 + "credit": 0.0, + }, + { + "account": "CWIP Account - _TIRC", + "debit": 2178.82, + "credit": 0.0, + }, # 1000 + 999 + 179.82 + { + "account": "Stock In Hand - _TIRC", + "debit": 267.26, + "credit": 0.0, + }, # 20 * 5 + 19 * 3 + 100 * 1 + 10.26 + {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5610.0}, + ], ) - self.assertEqual(asset_purchase_value, 1000) - asset_purchase_value = frappe.db.get_value( - "Asset", - {"purchase_invoice": doc.name, "item_code": "Test Ineligible Fixed Asset"}, - "gross_purchase_amount", + self.assertStockValues( + doc.name, {"Test Stock Item": 20, "Test Ineligible Stock Item": 22.42} ) - self.assertEqual(asset_purchase_value, 1178.82) # 999 + 179.82 + self.assertAssetValues( + "Purchase Invoice", + doc.name, + {"Test Fixed Asset": 1000, "Test Ineligible Fixed Asset": 1178.82}, + ) # 999 + 179.82 def test_purchase_invoice_with_ineligible_pos(self): transaction_details = { @@ -151,80 +172,47 @@ def test_purchase_invoice_with_ineligible_pos(self): self.assertEqual(doc.ineligibility_reason, "ITC restricted due to PoS rules") - # Check GL Entries - gl_entries = frappe.get_all( - "GL Entry", - filters={"voucher_no": doc.name}, - fields=["account", "debit", "credit"], - ) - - out_str = json.dumps(sorted(gl_entries, key=json.dumps)) - expected_out_str = json.dumps( - sorted( - [ - {"account": "Round Off - _TIRC", "debit": 0.28, "credit": 0.0}, - { - "account": "GST Expense - _TIRC", - "debit": 855.72, - "credit": 855.72, - }, # full taxes reversed - { - "account": "Input Tax IGST - _TIRC", - "debit": 855.72, - "credit": 855.72, - }, - { - "account": "Administrative Expenses - _TIRC", - "debit": 2947.64, - "credit": 0.0, - }, - { - "account": "CWIP Account - _TIRC", - "debit": 2358.82, - "credit": 0.0, - }, - { - "account": "Stock In Hand - _TIRC", - "debit": 303.26, - "credit": 0.0, - }, - {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5610.0}, - ], - key=json.dumps, - ) - ) - - self.assertEqual(out_str, expected_out_str) - - # Check Stock Ledger Entries - incoming_rate = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": doc.name, "item_code": "Test Stock Item"}, - "incoming_rate", - ) - self.assertEqual(incoming_rate, 23.60) - - incoming_rate = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": doc.name, "item_code": "Test Ineligible Stock Item"}, - "incoming_rate", + self.assertGLEntry( + doc.name, + [ + {"account": "Round Off - _TIRC", "debit": 0.28, "credit": 0.0}, + { + "account": "GST Expense - _TIRC", + "debit": 855.72, + "credit": 855.72, + }, # full taxes reversed + { + "account": "Input Tax IGST - _TIRC", + "debit": 855.72, + "credit": 855.72, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 2947.64, + "credit": 0.0, + }, + { + "account": "CWIP Account - _TIRC", + "debit": 2358.82, + "credit": 0.0, + }, + { + "account": "Stock In Hand - _TIRC", + "debit": 303.26, + "credit": 0.0, + }, + {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5610.0}, + ], ) - self.assertEqual(incoming_rate, 22.42) - # Check Asset Valuation Rate - asset_purchase_value = frappe.db.get_value( - "Asset", - {"purchase_invoice": doc.name, "item_code": "Test Fixed Asset"}, - "gross_purchase_amount", + self.assertStockValues( + doc.name, {"Test Stock Item": 23.6, "Test Ineligible Stock Item": 22.42} ) - self.assertEqual(asset_purchase_value, 1180) - - asset_purchase_value = frappe.db.get_value( - "Asset", - {"purchase_invoice": doc.name, "item_code": "Test Ineligible Fixed Asset"}, - "gross_purchase_amount", + self.assertAssetValues( + "Purchase Invoice", + doc.name, + {"Test Fixed Asset": 1180, "Test Ineligible Fixed Asset": 1178.82}, ) - self.assertEqual(asset_purchase_value, 1178.82) def test_purchase_receipt_and_then_purchase_invoice(self): transaction_details = { @@ -235,77 +223,45 @@ def test_purchase_receipt_and_then_purchase_invoice(self): doc = create_transaction(**transaction_details) - # Check GL Entries - gl_entries = frappe.get_all( - "GL Entry", - filters={"voucher_no": doc.name}, - fields=["account", "debit", "credit"], - ) - - out_str = json.dumps(sorted(gl_entries, key=json.dumps)) - expected_out_str = json.dumps( - sorted( - [ - { - "account": "GST Expense - _TIRC", - "debit": 0.0, - "credit": 190.08, - }, # 10.26 + 179.82 - { - "account": "Asset Received But Not Billed - _TIRC", - "debit": 0.0, - "credit": 1999.0, - }, - { - "account": "CWIP Account - _TIRC", - "debit": 2178.82, # 1999 + 179.82 - "credit": 0.0, - }, - { - "account": "Stock Received But Not Billed - _TIRC", - "debit": 0.0, - "credit": 257.0, - }, - { - "account": "Stock In Hand - _TIRC", - "debit": 267.26, # 257 + 10.26 - "credit": 0.0, - }, - ], - key=json.dumps, - ) - ) - self.assertEqual(out_str, expected_out_str) - - # Check Stock Ledger Entries - incoming_rate = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": doc.name, "item_code": "Test Stock Item"}, - "incoming_rate", - ) - self.assertEqual(incoming_rate, 20) - - incoming_rate = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": doc.name, "item_code": "Test Ineligible Stock Item"}, - "incoming_rate", + self.assertGLEntry( + doc.name, + [ + { + "account": "GST Expense - _TIRC", + "debit": 0.0, + "credit": 190.08, + }, # 10.26 + 179.82 + { + "account": "Asset Received But Not Billed - _TIRC", + "debit": 0.0, + "credit": 1999.0, + }, + { + "account": "CWIP Account - _TIRC", + "debit": 2178.82, # 1999 + 179.82 + "credit": 0.0, + }, + { + "account": "Stock Received But Not Billed - _TIRC", + "debit": 0.0, + "credit": 257.0, + }, + { + "account": "Stock In Hand - _TIRC", + "debit": 267.26, # 257 + 10.26 + "credit": 0.0, + }, + ], ) - self.assertEqual(incoming_rate, 22.42) # 19 * 1.18 - # Check Asset Valuation Rate - asset_purchase_value = frappe.db.get_value( - "Asset", - {"purchase_receipt": doc.name, "item_code": "Test Fixed Asset"}, - "gross_purchase_amount", + self.assertStockValues( + doc.name, {"Test Stock Item": 20, "Test Ineligible Stock Item": 22.42} ) - self.assertEqual(asset_purchase_value, 1000) - - asset_purchase_value = frappe.db.get_value( - "Asset", - {"purchase_receipt": doc.name, "item_code": "Test Ineligible Fixed Asset"}, - "gross_purchase_amount", + self.assertAssetValues( + "Purchase Receipt", + doc.name, + {"Test Fixed Asset": 1000, "Test Ineligible Fixed Asset": 1178.82}, ) - self.assertEqual(asset_purchase_value, 1178.82) # 999 + 179.82 # Create Purchase Invoice doc = make_purchase_invoice(doc.name) @@ -314,52 +270,42 @@ def test_purchase_receipt_and_then_purchase_invoice(self): self.assertEqual(doc.ineligibility_reason, "Ineligible As Per Section 17(5)") - gl_entries = frappe.get_all( - "GL Entry", - filters={"voucher_no": doc.name}, - fields=["account", "debit", "credit"], - ) - - out_str = json.dumps(sorted(gl_entries, key=json.dumps)) - expected_out_str = json.dumps( - sorted( - [ - {"account": "Round Off - _TIRC", "debit": 0.0, "credit": 0.32}, - { - "account": "GST Expense - _TIRC", - "debit": 369.72, # 179.82 + 179.64 + 10.26 - "credit": 179.64, # Only Expense - }, - {"account": "TDS Payable - _TIRC", "debit": 0.0, "credit": 475.4}, - { - "account": "Input Tax SGST - _TIRC", - "debit": 427.86, - "credit": 184.86, - }, - { - "account": "Input Tax CGST - _TIRC", - "debit": 427.86, - "credit": 184.86, # 369.72 / 2 - }, - { - "account": "Asset Received But Not Billed - _TIRC", - "debit": 1999.0, - "credit": 0.0, - }, - { - "account": "Administrative Expenses - _TIRC", - "debit": 2677.64, # 1500 + 998 + 179.64 - "credit": 0.0, - }, - { - "account": "Stock Received But Not Billed - _TIRC", - "debit": 257.0, - "credit": 0.0, - }, - {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5134.0}, - ], - key=json.dumps, - ) + self.assertGLEntry( + doc.name, + [ + {"account": "Round Off - _TIRC", "debit": 0.28, "credit": 0.0}, + { + "account": "GST Expense - _TIRC", + "debit": 369.72, # 179.82 + 179.64 + 10.26 + "credit": 179.64, # Only Expense + }, + { + "account": "Input Tax SGST - _TIRC", + "debit": 427.86, + "credit": 184.86, + }, + { + "account": "Input Tax CGST - _TIRC", + "debit": 427.86, + "credit": 184.86, # 369.72 / 2 + }, + { + "account": "Asset Received But Not Billed - _TIRC", + "debit": 1999.0, + "credit": 0.0, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 2677.64, # 1500 + 998 + 179.64 + "credit": 0.0, + }, + { + "account": "Stock Received But Not Billed - _TIRC", + "debit": 257.0, + "credit": 0.0, + }, + {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5610.0}, + ], ) def test_purchase_receipt_and_then_purchase_invoice_for_ineligible_pos(self): @@ -372,77 +318,49 @@ def test_purchase_receipt_and_then_purchase_invoice_for_ineligible_pos(self): doc = create_transaction(**transaction_details) - # Check GL Entries - gl_entries = frappe.get_all( - "GL Entry", - filters={"voucher_no": doc.name}, - fields=["account", "debit", "credit"], - ) - - out_str = json.dumps(sorted(gl_entries, key=json.dumps)) - expected_out_str = json.dumps( - sorted( - [ - { - "account": "GST Expense - _TIRC", - "debit": 0.0, - "credit": 406.08, - }, # 855.72 - 449.64 (reversal on expense) - { - "account": "Asset Received But Not Billed - _TIRC", - "debit": 0.0, - "credit": 1999.0, - }, - { - "account": "CWIP Account - _TIRC", - "debit": 2358.82, - "credit": 0.0, - }, - { - "account": "Stock Received But Not Billed - _TIRC", - "debit": 0.0, - "credit": 257.0, - }, - { - "account": "Stock In Hand - _TIRC", - "debit": 303.26, - "credit": 0.0, - }, - ], - key=json.dumps, - ) - ) - self.assertEqual(out_str, expected_out_str) - - # Check Stock Ledger Entries - incoming_rate = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": doc.name, "item_code": "Test Stock Item"}, - "incoming_rate", - ) - self.assertEqual(incoming_rate, 23.60) - - incoming_rate = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": doc.name, "item_code": "Test Ineligible Stock Item"}, - "incoming_rate", + self.assertGLEntry( + doc.name, + [ + { + "account": "GST Expense - _TIRC", + "debit": 0.0, + "credit": 406.08, + }, # 855.72 - 449.64 (reversal on expense) + { + "account": "Asset Received But Not Billed - _TIRC", + "debit": 0.0, + "credit": 1999.0, + }, + { + "account": "CWIP Account - _TIRC", + "debit": 2358.82, + "credit": 0.0, + }, + { + "account": "Stock Received But Not Billed - _TIRC", + "debit": 0.0, + "credit": 257.0, + }, + { + "account": "Stock In Hand - _TIRC", + "debit": 303.26, + "credit": 0.0, + }, + ], ) - self.assertEqual(incoming_rate, 22.42) - # Check Asset Valuation Rate - asset_purchase_value = frappe.db.get_value( - "Asset", - {"purchase_receipt": doc.name, "item_code": "Test Fixed Asset"}, - "gross_purchase_amount", + self.assertStockValues( + doc.name, + { + "Test Stock Item": 23.6, + "Test Ineligible Stock Item": 22.42, + }, ) - self.assertEqual(asset_purchase_value, 1180) - - asset_purchase_value = frappe.db.get_value( - "Asset", - {"purchase_receipt": doc.name, "item_code": "Test Ineligible Fixed Asset"}, - "gross_purchase_amount", + self.assertAssetValues( + "Purchase Receipt", + doc.name, + {"Test Fixed Asset": 1180, "Test Ineligible Fixed Asset": 1178.82}, ) - self.assertEqual(asset_purchase_value, 1178.82) # Create Purchase Invoice doc = make_purchase_invoice(doc.name) @@ -451,50 +369,38 @@ def test_purchase_receipt_and_then_purchase_invoice_for_ineligible_pos(self): self.assertEqual(doc.ineligibility_reason, "ITC restricted due to PoS rules") - # Check GL Entries - gl_entries = frappe.get_all( - "GL Entry", - filters={"voucher_no": doc.name}, - fields=["account", "debit", "credit"], - ) - - out_str = json.dumps(sorted(gl_entries, key=json.dumps)) - expected_out_str = json.dumps( - sorted( - [ - {"account": "Round Off - _TIRC", "debit": 0.0, "credit": 0.32}, - { - "account": "GST Expense - _TIRC", - "debit": 855.72, - "credit": 449.64, # expense reversal - }, - {"account": "TDS Payable - _TIRC", "debit": 0.0, "credit": 475.4}, - { - "account": "Input Tax IGST - _TIRC", - "debit": 855.72, - "credit": 855.72, - }, - { - "account": "Asset Received But Not Billed - _TIRC", - "debit": 1999.0, - "credit": 0.0, - }, - { - "account": "Administrative Expenses - _TIRC", - "debit": 2947.64, - "credit": 0.0, - }, - { - "account": "Stock Received But Not Billed - _TIRC", - "debit": 257.0, - "credit": 0.0, - }, - {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5134.0}, - ], - key=json.dumps, - ) + self.assertGLEntry( + doc.name, + [ + {"account": "Round Off - _TIRC", "debit": 0.28, "credit": 0.0}, + { + "account": "GST Expense - _TIRC", + "debit": 855.72, + "credit": 449.64, # expense reversal + }, + { + "account": "Input Tax IGST - _TIRC", + "debit": 855.72, + "credit": 855.72, + }, + { + "account": "Asset Received But Not Billed - _TIRC", + "debit": 1999.0, + "credit": 0.0, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 2947.64, + "credit": 0.0, + }, + { + "account": "Stock Received But Not Billed - _TIRC", + "debit": 257.0, + "credit": 0.0, + }, + {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5610.0}, + ], ) - self.assertEqual(out_str, expected_out_str) def test_purchase_returns_with_update_stock(self): transaction_details = { @@ -509,66 +415,46 @@ def test_purchase_returns_with_update_stock(self): doc = make_return_doc("Purchase Invoice", doc.name) doc.submit() - # Check GL Entries - gl_entries = frappe.get_all( - "GL Entry", - filters={"voucher_no": doc.name}, - fields=["account", "debit", "credit"], - ) - - out_str = json.dumps(sorted(gl_entries, key=json.dumps)) - expected_out_str = json.dumps( - sorted( - [ - {"account": "Round Off - _TIRC", "debit": 0.0, "credit": 0.28}, - { - "account": "GST Expense - _TIRC", - "debit": 369.72, - "credit": 369.72, - }, - { - "account": "Input Tax SGST - _TIRC", - "debit": 0.0, - "credit": 243.0, - }, - { - "account": "Input Tax CGST - _TIRC", - "debit": 0.0, - "credit": 243.0, - }, - { - "account": "CWIP Account - _TIRC", - "debit": 0.0, - "credit": 2178.82, - }, - { - "account": "Administrative Expenses - _TIRC", - "debit": 0.0, - "credit": 2677.64, - }, - { - "account": "Stock In Hand - _TIRC", - "debit": 0.0, - "credit": 267.26, - }, - {"account": "Creditors - _TIRC", "debit": 5610.0, "credit": 0.0}, - ], - key=json.dumps, - ) + self.assertGLEntry( + doc.name, + [ + {"account": "Round Off - _TIRC", "debit": 0.0, "credit": 0.28}, + { + "account": "GST Expense - _TIRC", + "debit": 369.72, + "credit": 369.72, + }, + { + "account": "Input Tax SGST - _TIRC", + "debit": 0.0, + "credit": 243.0, + }, + { + "account": "Input Tax CGST - _TIRC", + "debit": 0.0, + "credit": 243.0, + }, + { + "account": "CWIP Account - _TIRC", + "debit": 0.0, + "credit": 2178.82, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 0.0, + "credit": 2677.64, + }, + { + "account": "Stock In Hand - _TIRC", + "debit": 0.0, + "credit": 267.26, + }, + {"account": "Creditors - _TIRC", "debit": 5610.0, "credit": 0.0}, + ], ) - self.assertEqual(out_str, expected_out_str) + @toggle_perpetual_inventory() def test_purchase_receipt_and_then_purchase_invoice_for_non_perpetual_stock(self): - # Disable Perpetual Inventory - frappe.db.set_value( - "Company", - "_Test Indian Registered Company", - "enable_perpetual_inventory", - 0, - ) - # ERPNext uses erpnext.is_perpetual_inventory_enabled from local - del frappe.local.enable_perpetual_inventory - transaction_details = { "doctype": "Purchase Receipt", "items": SAMPLE_ITEM_LIST, @@ -576,53 +462,32 @@ def test_purchase_receipt_and_then_purchase_invoice_for_non_perpetual_stock(self } doc = create_transaction(**transaction_details) - - # Check GL Entries - gl_entries = frappe.get_all( - "GL Entry", - filters={"voucher_no": doc.name}, - fields=["account", "debit", "credit"], - ) - - out_str = json.dumps(sorted(gl_entries, key=json.dumps)) - expected_out_str = json.dumps( - sorted( - [ - { - "account": "GST Expense - _TIRC", - "debit": 0.0, - "credit": 179.82, - }, # only asset - { - "account": "Asset Received But Not Billed - _TIRC", - "debit": 0.0, - "credit": 1999.0, - }, - { - "account": "CWIP Account - _TIRC", - "debit": 2178.82, - "credit": 0.0, - }, - ], - key=json.dumps, - ) - ) - self.assertEqual(out_str, expected_out_str) - - # Check Asset Valuation Rate - asset_purchase_value = frappe.db.get_value( - "Asset", - {"purchase_receipt": doc.name, "item_code": "Test Fixed Asset"}, - "gross_purchase_amount", + self.assertGLEntry( + doc.name, + [ + { + "account": "GST Expense - _TIRC", + "debit": 0.0, + "credit": 179.82, + }, # only asset + { + "account": "Asset Received But Not Billed - _TIRC", + "debit": 0.0, + "credit": 1999.0, + }, + { + "account": "CWIP Account - _TIRC", + "debit": 2178.82, + "credit": 0.0, + }, + ], ) - self.assertEqual(asset_purchase_value, 1000) - asset_purchase_value = frappe.db.get_value( - "Asset", - {"purchase_receipt": doc.name, "item_code": "Test Ineligible Fixed Asset"}, - "gross_purchase_amount", + self.assertAssetValues( + doc.doctype, + doc.name, + {"Test Fixed Asset": 1000, "Test Ineligible Fixed Asset": 1178.82}, ) - self.assertEqual(asset_purchase_value, 1178.82) # Create Purchase Invoice doc = make_purchase_invoice(doc.name) @@ -631,88 +496,51 @@ def test_purchase_receipt_and_then_purchase_invoice_for_non_perpetual_stock(self self.assertEqual(doc.ineligibility_reason, "Ineligible As Per Section 17(5)") - # Check GL Entries - gl_entries = frappe.get_all( - "GL Entry", - filters={"voucher_no": doc.name}, - fields=["account", "debit", "credit"], - ) - - out_str = json.dumps(sorted(gl_entries, key=json.dumps)) - expected_out_str = json.dumps( - sorted( - [ - {"account": "Round Off - _TIRC", "debit": 0.0, "credit": 0.32}, - { - "account": "GST Expense - _TIRC", - "debit": 369.72, - "credit": 189.9, - }, - {"account": "TDS Payable - _TIRC", "debit": 0.0, "credit": 475.4}, - { - "account": "Input Tax SGST - _TIRC", - "debit": 427.86, - "credit": 184.86, - }, - { - "account": "Input Tax CGST - _TIRC", - "debit": 427.86, - "credit": 184.86, - }, - { - "account": "Asset Received But Not Billed - _TIRC", - "debit": 1999.0, - "credit": 0.0, - }, - { - "account": "Administrative Expenses - _TIRC", - "debit": 2677.64, - "credit": 0.0, - }, - { - "account": "Cost of Goods Sold - _TIRC", - "debit": 267.26, # stock with gst expense - "credit": 0.0, - }, - {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5134.0}, - ], - key=json.dumps, - ) - ) - - self.assertEqual(out_str, expected_out_str) - - # Check Stock Ledger Entries - incoming_rate = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": doc.name, "item_code": "Test Stock Item"}, - "incoming_rate", + self.assertGLEntry( + doc.name, + [ + {"account": "Round Off - _TIRC", "debit": 0.28, "credit": 0.0}, + { + "account": "GST Expense - _TIRC", + "debit": 369.72, + "credit": 189.9, + }, + { + "account": "Input Tax SGST - _TIRC", + "debit": 427.86, + "credit": 184.86, + }, + { + "account": "Input Tax CGST - _TIRC", + "debit": 427.86, + "credit": 184.86, + }, + { + "account": "Asset Received But Not Billed - _TIRC", + "debit": 1999.0, + "credit": 0.0, + }, + { + "account": "Administrative Expenses - _TIRC", + "debit": 2677.64, + "credit": 0.0, + }, + { + "account": "Cost of Goods Sold - _TIRC", + "debit": 267.26, # stock with gst expense + "credit": 0.0, + }, + {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5610.0}, + ], ) - self.assertEqual(incoming_rate, None) - # Enable Perpetual Inventory - frappe.db.set_value( - "Company", - "_Test Indian Registered Company", - "enable_perpetual_inventory", - 1, - ) - del frappe.local.enable_perpetual_inventory + self.assertStockValues(doc.name, {"Test Stock Item": None}) + @toggle_provisional_accounting() def test_purchase_receipt_and_then_purchase_invoice_for_provisional_expense(self): """ No change in accounting because of provisional accounting as it's reversed on purchase invoice """ - # Enable Provisional Expense - frappe.db.set_value( - "Company", - "_Test Indian Registered Company", - { - "enable_provisional_accounting_for_non_stock_items": 1, - "default_provisional_account": "Unsecured Loans - _TIRC", - }, - ) - transaction_details = { "doctype": "Purchase Receipt", "items": SAMPLE_ITEM_LIST, @@ -788,13 +616,12 @@ def test_purchase_receipt_and_then_purchase_invoice_for_provisional_expense(self self.assertGLEntry( doc.name, [ - {"account": "Round Off - _TIRC", "debit": 0.0, "credit": 0.32}, + {"account": "Round Off - _TIRC", "debit": 0.28, "credit": 0.0}, { "account": "GST Expense - _TIRC", "debit": 369.72, "credit": 179.64, }, - {"account": "TDS Payable - _TIRC", "debit": 0.0, "credit": 475.4}, { "account": "Input Tax SGST - _TIRC", "debit": 427.86, @@ -820,20 +647,10 @@ def test_purchase_receipt_and_then_purchase_invoice_for_provisional_expense(self "debit": 257.0, "credit": 0.0, }, - {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5134.0}, + {"account": "Creditors - _TIRC", "debit": 0.0, "credit": 5610.0}, ], ) - # Disable Provisional Expense - frappe.db.set_value( - "Company", - "_Test Indian Registered Company", - { - "enable_provisional_accounting_for_non_stock_items": 0, - "default_provisional_account": "", - }, - ) - def test_purchase_invoice_with_bill_of_entry(self): pass @@ -907,6 +724,7 @@ def create_test_items(): { "company_name": "_Test Indian Registered Company", "fixed_asset_account": asset_account.name, + "capital_work_in_progress_account": "CWIP Account - _TIRC", } ], } diff --git a/india_compliance/tests/__init__.py b/india_compliance/tests/__init__.py index c86cb8a0c..f3e444ab8 100644 --- a/india_compliance/tests/__init__.py +++ b/india_compliance/tests/__init__.py @@ -36,6 +36,7 @@ def before_tests(): set_default_settings_for_tests() create_test_records() + set_default_company_for_tests() frappe.db.commit() frappe.flags.country = "India" @@ -66,6 +67,26 @@ def create_test_records(): add_companies_to_fiscal_year(data) +def set_default_company_for_tests(): + # stock settings + frappe.db.set_value( + "Company", + "_Test Indian Registered Company", + { + "enable_perpetual_inventory": 1, + "default_inventory_account": "Stock In Hand - _TIRC", + "stock_adjustment_account": "Stock Adjustment - _TIRC", + "stock_received_but_not_billed": "Stock Received But Not Billed - _TIRC", + "expenses_included_in_valuation": "Expenses Included In Valuation - _TIRC", + }, + ) + + # set default company + global_defaults = frappe.get_single("Global Defaults") + global_defaults.default_company = "_Test Indian Registered Company" + global_defaults.save() + + def add_companies_to_fiscal_year(data): fy = get_fiscal_year(getdate(), as_dict=True) doc = frappe.get_doc("Fiscal Year", fy.name) From 99e875b30cb5d943c208ea86a0be4e14583fb1e8 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 24 Oct 2023 16:06:51 +0530 Subject: [PATCH 4/7] test: add bill of entry test case --- .../doctype/bill_of_entry/bill_of_entry.js | 9 ++- .../overrides/test_ineligible_itc.py | 57 ++++++++++++++++++- india_compliance/tests/__init__.py | 1 - 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.js b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.js index 85397837a..42fc80e43 100644 --- a/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.js +++ b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.js @@ -26,7 +26,14 @@ frappe.ui.form.on("Bill of Entry", { ); } - if (frm.doc.docstatus === 1 && frm.doc.total_customs_duty > 0) { + const has_ineligible_items = frm.doc.items.some( + item => item.is_ineligible_for_itc + ); + + if ( + (frm.doc.docstatus === 1 && frm.doc.total_customs_duty > 0) || + has_ineligible_items + ) { frm.add_custom_button( __("Landed Cost Voucher"), () => { diff --git a/india_compliance/gst_india/overrides/test_ineligible_itc.py b/india_compliance/gst_india/overrides/test_ineligible_itc.py index dd21b5438..96995c337 100644 --- a/india_compliance/gst_india/overrides/test_ineligible_itc.py +++ b/india_compliance/gst_india/overrides/test_ineligible_itc.py @@ -2,12 +2,17 @@ from contextlib import contextmanager import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import today from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( make_purchase_invoice, ) +from india_compliance.gst_india.doctype.bill_of_entry.bill_of_entry import ( + make_bill_of_entry, + make_landed_cost_voucher, +) from india_compliance.gst_india.utils.tests import create_transaction SAMPLE_ITEM_LIST = [ @@ -651,8 +656,56 @@ def test_purchase_receipt_and_then_purchase_invoice_for_provisional_expense(self ], ) + @change_settings("GST Settings", {"enable_overseas_transactions": 1}) def test_purchase_invoice_with_bill_of_entry(self): - pass + transaction_details = { + "doctype": "Purchase Invoice", + "supplier": "_Test Foreign Supplier", + "bill_no": "BILL-08", + "update_stock": 1, + "items": SAMPLE_ITEM_LIST, + } + doc = create_transaction(**transaction_details) + boe = make_bill_of_entry(doc.name) + boe.bill_of_entry_no = "BILL-09" + boe.bill_of_entry_date = today() + boe.submit() + + self.assertGLEntry( + boe.name, + [ + { + "account": "Administrative Expenses - _TIRC", + "debit": 179.64, + "credit": 0.0, + }, + { + "account": "Customs Duty Payable - _TIRC", + "debit": 0.0, + "credit": 855.72, + }, + {"account": "GST Expense - _TIRC", "debit": 369.72, "credit": 179.64}, + {"account": "Input Tax IGST - _TIRC", "debit": 0.0, "credit": 369.72}, + {"account": "Input Tax IGST - _TIRC", "debit": 855.72, "credit": 0.0}, + ], + ) + + lcv = make_landed_cost_voucher(boe.name) + lcv.save() + + for item in lcv.items: + if item.item_code == "Test Ineligible Stock Item": + self.assertEqual(item.applicable_charges, 3.42) # 10.26 / 3 Nos + elif item.item_code == "Test Ineligible Fixed Asset": + self.assertEqual(item.applicable_charges, 179.82) + else: + self.assertEqual(item.applicable_charges, 0.0) + + for row in lcv.taxes: + if row.expense_account == "GST Expense - _TIRC": + self.assertEqual(row.amount, 190.08) + else: + self.assertEqual(row.amount, 0.0) def assertGLEntry(self, docname, expected_gl_entry): gl_entries = frappe.get_all( diff --git a/india_compliance/tests/__init__.py b/india_compliance/tests/__init__.py index f3e444ab8..99d601b85 100644 --- a/india_compliance/tests/__init__.py +++ b/india_compliance/tests/__init__.py @@ -77,7 +77,6 @@ def set_default_company_for_tests(): "default_inventory_account": "Stock In Hand - _TIRC", "stock_adjustment_account": "Stock Adjustment - _TIRC", "stock_received_but_not_billed": "Stock Received But Not Billed - _TIRC", - "expenses_included_in_valuation": "Expenses Included In Valuation - _TIRC", }, ) From 7a35f9f3caa386e7e397865f33900c730dc2f53e Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 25 Oct 2023 20:45:37 +0530 Subject: [PATCH 5/7] fix: add support for gstr-3b and gst details report --- .../gstr_3b_report/gstr_3b_report.html | 6 +- .../doctype/gstr_3b_report/gstr_3b_report.py | 54 +++- .../gst_india/overrides/purchase_invoice.py | 5 +- .../report/gstr_3b_details/gstr_3b_details.py | 290 ++++++++++++++++-- 4 files changed, 320 insertions(+), 35 deletions(-) diff --git a/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.html b/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.html index f3fc60fdb..09397f0a9 100644 --- a/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.html +++ b/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.html @@ -221,7 +221,7 @@
4.   {{__("Eligible ITC")}}
-   (1) {{__("As per rules 42 & 43 of CGST Rules")}} +   (1) {{__("As per rules 42 & 43 of CGST Rules and section 17(5)")}} {{ flt(data.itc_elg.itc_rev[0].iamt, 2) }} {{ flt(data.itc_elg.itc_rev[0].camt, 2) }} {{ flt(data.itc_elg.itc_rev[0].samt, 2) }} @@ -249,14 +249,14 @@
4.   {{__("Eligible ITC")}}
-   (1) {{__("As per section 17(5)")}} +   (1) {{__("ITC reclaimed which was reversed under Table 4(B)(2) in earlier tax period")}} {{ flt(data.itc_elg.itc_inelg[0].iamt, 2) }} {{ flt(data.itc_elg.itc_inelg[0].camt, 2) }} {{ flt(data.itc_elg.itc_inelg[0].samt, 2) }} {{ flt(data.itc_elg.itc_inelg[0].csamt, 2) }} -   (2) {{__("Others")}} +   (2) {{__("Ineligible ITC under section 16(4) & ITC restricted due to PoS rules")}} {{ flt(data.itc_elg.itc_inelg[1].iamt, 2) }} {{ flt(data.itc_elg.itc_inelg[1].camt, 2) }} {{ flt(data.itc_elg.itc_inelg[1].samt, 2) }} diff --git a/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py b/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py index 50ee29045..d8c15a311 100644 --- a/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py +++ b/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py @@ -13,6 +13,9 @@ from frappe.utils import cstr, flt, get_date_str, get_first_day, get_last_day from india_compliance.gst_india.constants import INVOICE_DOCTYPES +from india_compliance.gst_india.report.gstr_3b_details.gstr_3b_details import ( + IneligibleITC, +) from india_compliance.gst_india.utils import ( get_gst_accounts_by_type, is_overseas_transaction, @@ -72,11 +75,6 @@ def set_itc_details(self, itc_details): "OTH": "All Other ITC", } - itc_ineligible_map = { - "RUL": "Ineligible As Per Section 17(5)", - "OTH": "Ineligible Others", - } - net_itc = self.report_dict["itc_elg"]["itc_net"] for d in self.report_dict["itc_elg"]["itc_avl"]: @@ -85,12 +83,48 @@ def set_itc_details(self, itc_details): d[key] = flt(itc_details.get(itc_type, {}).get(key)) net_itc[key] += flt(d[key], 2) - for d in self.report_dict["itc_elg"]["itc_inelg"]: - itc_type = itc_ineligible_map.get(d["ty"]) - for key in ["iamt", "camt", "samt", "csamt"]: - d[key] = flt(itc_details.get(itc_type, {}).get(key)) - def get_itc_reversal_entries(self): + self.update_itc_reversal_from_journal_entry() + self.update_itc_reversal_from_purchase_invoice() + self.update_itc_reversal_from_bill_of_entry() + + def update_itc_reversal_from_purchase_invoice(self): + ineligible_credit = IneligibleITC( + self.company, self.gst_details.get("gstin"), self.month_no, self.year + ).get_for_purchase_invoice(group_by="ineligibility_reason") + + return self.process_ineligible_credit(ineligible_credit) + + def update_itc_reversal_from_bill_of_entry(self): + ineligible_credit = IneligibleITC( + self.company, self.gst_details.get("gstin"), self.month_no, self.year + ).get_for_bill_of_entry() + + return self.process_ineligible_credit(ineligible_credit) + + def process_ineligible_credit(self, ineligible_credit): + if not ineligible_credit: + return + + tax_amounts = ["camt", "samt", "iamt", "csamt"] + + for row in ineligible_credit: + if row.itc_classification == "Ineligible As Per Section 17(5)": + for key in tax_amounts: + if key not in row: + continue + + self.report_dict["itc_elg"]["itc_rev"][0][key] += flt(row[key]) + self.report_dict["itc_elg"]["itc_net"][key] -= flt(row[key]) + + elif row.itc_classification == "ITC restricted due to PoS rules": + for key in tax_amounts: + if key not in row: + continue + + self.report_dict["itc_elg"]["itc_inelg"][1][key] += flt(row[key]) + + def update_itc_reversal_from_journal_entry(self): reversal_entries = frappe.db.sql( """ SELECT ja.account, j.ineligibility_reason, sum(credit_in_account_currency) as amount diff --git a/india_compliance/gst_india/overrides/purchase_invoice.py b/india_compliance/gst_india/overrides/purchase_invoice.py index 98a7ee1c2..54fbe00ae 100644 --- a/india_compliance/gst_india/overrides/purchase_invoice.py +++ b/india_compliance/gst_india/overrides/purchase_invoice.py @@ -43,11 +43,11 @@ def validate(doc, method=None): if validate_transaction(doc) is False: return + set_ineligibility_reason(doc) update_itc_totals(doc) validate_supplier_invoice_number(doc) validate_with_inward_supply(doc) set_reconciliation_status(doc) - set_ineligibility_reason(doc) def set_reconciliation_status(doc): @@ -80,6 +80,9 @@ def update_itc_totals(doc, method=None): doc.itc_central_tax = 0 doc.itc_cess_amount = 0 + if doc.ineligibility_reason == "ITC restricted due to PoS rules": + return + gst_accounts = get_gst_accounts_by_type(doc.company, "Input") for tax in doc.get("taxes"): diff --git a/india_compliance/gst_india/report/gstr_3b_details/gstr_3b_details.py b/india_compliance/gst_india/report/gstr_3b_details/gstr_3b_details.py index 75ae65a70..83e8adfad 100644 --- a/india_compliance/gst_india/report/gstr_3b_details/gstr_3b_details.py +++ b/india_compliance/gst_india/report/gstr_3b_details/gstr_3b_details.py @@ -3,10 +3,10 @@ import frappe from frappe import _ -from frappe.query_builder import Case +from frappe.query_builder import Case, DatePart from frappe.query_builder.custom import ConstantColumn -from frappe.query_builder.functions import Ifnull, LiteralValue, Sum -from frappe.utils import cint, get_first_day, get_last_day +from frappe.query_builder.functions import Extract, Ifnull, IfNull, LiteralValue, Sum +from frappe.utils import cint, flt, get_first_day, get_last_day from india_compliance.gst_india.utils import get_gst_accounts_by_type @@ -77,25 +77,25 @@ def extend_columns(self): self.columns.extend( [ { - "fieldname": "integrated_tax", + "fieldname": "iamt", "label": _("Integrated Tax"), "fieldtype": "Currency", "width": 100, }, { - "fieldname": "central_tax", + "fieldname": "camt", "label": _("Central Tax"), "fieldtype": "Currency", "width": 100, }, { - "fieldname": "state_tax", + "fieldname": "samt", "label": _("State/UT Tax"), "fieldtype": "Currency", "width": 100, }, { - "fieldname": "cess_amount", + "fieldname": "csamt", "label": _("Cess Tax"), "fieldtype": "Currency", "width": 100, @@ -113,8 +113,16 @@ def get_data(self): purchase_data = self.get_itc_from_purchase() boe_data = self.get_itc_from_boe() journal_entry_data = self.get_itc_from_journal_entry() - - data = purchase_data + boe_data + journal_entry_data + pi_ineligible_itc = self.get_ineligible_itc_from_purchase() + boe_ineligible_itc = self.get_ineligible_itc_from_boe() + + data = ( + purchase_data + + boe_data + + journal_entry_data + + pi_ineligible_itc + + boe_ineligible_itc + ) self.data = sorted( data, @@ -131,10 +139,10 @@ def get_itc_from_purchase(self): purchase_invoice.name.as_("voucher_no"), purchase_invoice.posting_date, purchase_invoice.itc_classification, - Sum(purchase_invoice.itc_integrated_tax).as_("integrated_tax"), - Sum(purchase_invoice.itc_central_tax).as_("central_tax"), - Sum(purchase_invoice.itc_state_tax).as_("state_tax"), - Sum(purchase_invoice.itc_cess_amount).as_("cess_amount"), + Sum(purchase_invoice.itc_integrated_tax).as_("iamt"), + Sum(purchase_invoice.itc_central_tax).as_("camt"), + Sum(purchase_invoice.itc_state_tax).as_("samt"), + Sum(purchase_invoice.itc_cess_amount).as_("csamt"), ) .where( (purchase_invoice.docstatus == 1) @@ -169,7 +177,7 @@ def get_itc_from_boe(self): boe_taxes.tax_amount, ) .else_(0) - ).as_("integrated_tax"), + ).as_("iamt"), Sum( Case() .when( @@ -177,9 +185,9 @@ def get_itc_from_boe(self): boe_taxes.tax_amount, ) .else_(0) - ).as_("cess_amount"), - LiteralValue(0).as_("central_tax"), - LiteralValue(0).as_("state_tax"), + ).as_("csamt"), + LiteralValue(0).as_("camt"), + LiteralValue(0).as_("samt"), ConstantColumn("Import of Goods").as_("itc_classification"), ) .where( @@ -212,7 +220,7 @@ def get_itc_from_journal_entry(self): (-1 * journal_entry_account.credit_in_account_currency), ) .else_(0) - ).as_("integrated_tax"), + ).as_("iamt"), Sum( Case() .when( @@ -220,7 +228,7 @@ def get_itc_from_journal_entry(self): (-1 * journal_entry_account.credit_in_account_currency), ) .else_(0) - ).as_("central_tax"), + ).as_("camt"), Sum( Case() .when( @@ -228,7 +236,7 @@ def get_itc_from_journal_entry(self): (-1 * journal_entry_account.credit_in_account_currency), ) .else_(0) - ).as_("state_tax"), + ).as_("samt"), Sum( Case() .when( @@ -236,7 +244,7 @@ def get_itc_from_journal_entry(self): (-1 * journal_entry_account.credit_in_account_currency), ) .else_(0) - ).as_("cess_amount"), + ).as_("csamt"), journal_entry.ineligibility_reason.as_("itc_classification"), ) .where( @@ -251,6 +259,34 @@ def get_itc_from_journal_entry(self): ) return query.run(as_dict=True) + def get_ineligible_itc_from_purchase(self): + ineligible_itc = IneligibleITC( + self.company, self.company_gstin, self.filters.month, self.filters.year + ).get_for_purchase_invoice() + + return self.process_ineligible_itc(ineligible_itc) + + def get_ineligible_itc_from_boe(self): + ineligible_itc = IneligibleITC( + self.company, self.company_gstin, self.filters.month, self.filters.year + ).get_for_bill_of_entry() + + return self.process_ineligible_itc(ineligible_itc) + + def process_ineligible_itc(self, ineligible_itc): + if not ineligible_itc: + return [] + + for row in ineligible_itc.copy(): + if row.itc_classification == "ITC restricted due to PoS rules": + ineligible_itc.remove(row) + continue + + for key in ["iamt", "camt", "samt", "csamt"]: + row[key] = row[key] * -1 + + return ineligible_itc + class GSTR3B_Inward_Nil_Exempt(BaseGSTR3BDetails): def extend_columns(self): @@ -370,3 +406,215 @@ def get_inward_nil_exempt(self): ) return query.run(as_dict=True) + + +class IneligibleITC: + def __init__(self, company, gstin, month, year) -> None: + self.gl_entry = frappe.qb.DocType("GL Entry") + self.company = company + self.gstin = gstin + self.month = month + self.year = year + self.gst_accounts = get_gst_accounts_by_type(company, "Input") + + def get_for_purchase_invoice(self, group_by="name"): + ineligible_transactions = self.get_vouchers_with_gst_expense("Purchase Invoice") + + if not ineligible_transactions: + return + + pi = frappe.qb.DocType("Purchase Invoice") + + credit_availed = ( + self.get_gl_entry_query("Purchase Invoice") + .inner_join(pi) + .on(pi.name == self.gl_entry.voucher_no) + .select(*self.select_net_gst_amount_from_gl_entry()) + .select( + pi.name.as_("voucher_no"), + pi.ineligibility_reason.as_("itc_classification"), + ) + .where(IfNull(pi.ineligibility_reason, "") != "") + .where(pi.name.isin(ineligible_transactions)) + .groupby(pi[group_by]) + .run(as_dict=1) + ) + + credit_available = ( + frappe.qb.from_(pi) + .select( + ConstantColumn("Purchase Invoice").as_("voucher_type"), + pi.name.as_("voucher_no"), + pi.posting_date, + pi.ineligibility_reason.as_("itc_classification"), + Sum(pi.itc_integrated_tax).as_("iamt"), + Sum(pi.itc_central_tax).as_("camt"), + Sum(pi.itc_state_tax).as_("samt"), + Sum(pi.itc_cess_amount).as_("csamt"), + ) + .where(IfNull(pi.ineligibility_reason, "") != "") + .where(pi.name.isin(ineligible_transactions)) + .groupby(pi[group_by]) + .run(as_dict=1) + ) + + return self.get_ineligible_credit(credit_availed, credit_available, group_by) + + def get_for_bill_of_entry(self, group_by="name"): + ineligible_transactions = self.get_vouchers_with_gst_expense("Bill of Entry") + + if not ineligible_transactions: + return + + boe = frappe.qb.DocType("Bill of Entry") + boe_taxes = frappe.qb.DocType("Bill of Entry Taxes") + + credit_availed = ( + self.get_gl_entry_query("Bill of Entry") + .inner_join(boe) + .on(boe.name == self.gl_entry.voucher_no) + .select(*self.select_net_gst_amount_from_gl_entry()) + .select( + boe.name.as_("voucher_no"), + ConstantColumn("Ineligible As Per Section 17(5)").as_( + "itc_classification" + ), + ) + .where(boe.name.isin(ineligible_transactions)) + .groupby(boe[group_by]) + .run(as_dict=1) + ) + + credit_available = ( + frappe.qb.from_(boe) + .join(boe_taxes) + .on(boe_taxes.parent == boe.name) + .select( + ConstantColumn("Bill of Entry").as_("voucher_type"), + boe.name.as_("voucher_no"), + boe.posting_date, + Sum( + Case() + .when( + boe_taxes.account_head == self.gst_accounts.igst_account, + boe_taxes.tax_amount, + ) + .else_(0) + ).as_("iamt"), + Sum( + Case() + .when( + boe_taxes.account_head == self.gst_accounts.cess_account, + boe_taxes.tax_amount, + ) + .else_(0) + ).as_("csamt"), + LiteralValue(0).as_("camt"), + LiteralValue(0).as_("samt"), + ConstantColumn("Ineligible As Per Section 17(5)").as_( + "itc_classification" + ), + ) + .where(boe.name.isin(ineligible_transactions)) + .groupby(boe[group_by]) + .run(as_dict=1) + ) + + return self.get_ineligible_credit(credit_availed, credit_available, group_by) + + def get_ineligible_credit(self, credit_availed, credit_available, group_by): + if group_by == "name": + group_by_field = "voucher_no" + elif group_by == "ineligibility_reason": + group_by_field = "itc_classification" + else: + group_by_field = group_by + + credit_availed_dict = frappe._dict( + {d[group_by_field]: d for d in credit_availed} + ) + ineligible_credit = [] + tax_amounts = ["camt", "samt", "iamt", "csamt"] + + for row in credit_available: + credit_availed = credit_availed_dict.get(row[group_by_field]) + if not credit_availed: + ineligible_credit.append(row) + continue + + for key in tax_amounts: + if key not in row: + continue + + row[key] -= flt(credit_availed.get(key, 0)) + + ineligible_credit.append(row) + + return ineligible_credit + + def get_vouchers_with_gst_expense(self, voucher_type): + gst_expense_account = frappe.get_cached_value( + "Company", self.company, "default_gst_expense_account" + ) + + data = ( + self.get_gl_entry_query(voucher_type) + .select(self.gl_entry.voucher_no) + .where(self.gl_entry.account == gst_expense_account) + .run(as_dict=1) + ) + + return set([d.voucher_no for d in data]) + + def select_net_gst_amount_from_gl_entry(self): + account_field_map = { + "cgst_account": "camt", + "sgst_account": "samt", + "igst_account": "iamt", + "cess_account": "csamt", + } + fields = [] + + for account_field, key in account_field_map.items(): + if ( + account_field not in self.gst_accounts + or not self.gst_accounts[account_field] + ): + continue + + fields.append( + ( + Sum( + Case() + .when( + self.gl_entry.account.eq(self.gst_accounts[account_field]), + self.gl_entry.debit_in_account_currency, + ) + .else_(0) + ) + - Sum( + Case() + .when( + self.gl_entry.account.eq(self.gst_accounts[account_field]), + self.gl_entry.credit_in_account_currency, + ) + .else_(0) + ) + ).as_(key) + ) + + return fields + + def get_gl_entry_query(self, voucher_type): + query = ( + frappe.qb.from_(self.gl_entry) + .where(self.gl_entry.docstatus == 1) + .where(self.gl_entry.is_opening == "No") + .where(self.gl_entry.voucher_type == voucher_type) + .where(self.gl_entry.is_cancelled == 0) + .where(self.gl_entry.company_gstin == self.gstin) + .where(Extract(DatePart.month, self.gl_entry.posting_date).eq(self.month)) + .where(Extract(DatePart.year, self.gl_entry.posting_date).eq(self.year)) + ) + + return query From bd84c7ccfdce434d7bb4abbc08a40fc102aa1d05 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 25 Oct 2023 20:50:08 +0530 Subject: [PATCH 6/7] fix: set fetch_if_empty as 0 in client side --- india_compliance/public/js/transaction.js | 24 +++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/india_compliance/public/js/transaction.js b/india_compliance/public/js/transaction.js index 8483ed97c..6bc5db212 100644 --- a/india_compliance/public/js/transaction.js +++ b/india_compliance/public/js/transaction.js @@ -22,16 +22,36 @@ for (const doctype of ["Sales Invoice", "Delivery Note"]) { ignore_port_code_validation(doctype); } -for (const doctype of [...TRANSACTION_DOCTYPES, "Material Request", "Supplier Quotation", "POS Invoice"]) { +for (const doctype of [ + ...TRANSACTION_DOCTYPES, + "Material Request", + "Supplier Quotation", + "POS Invoice", +]) { set_fetch_if_empty_for_gst_hsn_code(doctype); } function set_fetch_if_empty_for_gst_hsn_code(doctype) { - frappe.ui.form.on(doctype, "setup", function(frm) { + frappe.ui.form.on(doctype, "setup", function (frm) { frm.get_docfield("items", "gst_hsn_code").fetch_if_empty = 0; }); } +for (const doctype of [ + "Supplier Quotation Item", + "Purchase Order Item", + "Purchase Receipt Item", + "Purchase Invoice Item", +]) { + set_fetch_if_empty_for_is_ineligible_for_itc(doctype); +} + +function set_fetch_if_empty_for_gst_hsn_code(doctype) { + frappe.ui.form.on(doctype, "setup", function (frm) { + frm.get_docfield("items", "is_ineligible_for_itc").fetch_if_empty = 0; + }); +} + function fetch_gst_details(doctype) { const event_fields = [ "tax_category", From 61c94cc982e05ca76f909ab6a43ed7390d3d8f57 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 25 Oct 2023 20:55:25 +0530 Subject: [PATCH 7/7] chore: remove unused hooks --- india_compliance/hooks.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py index 9adab0f04..29af32b3e 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -206,9 +206,6 @@ "erpnext.stock.doctype.purchase_receipt.purchase_receipt.update_regional_gl_entries": ( "india_compliance.gst_india.overrides.ineligible_itc.update_regional_gl_entries" ), - "erpnext.controllers.stock_controller.update_regional_gl_entries": ( - "india_compliance.gst_india.overrides.ineligible_itc.update_regional_gl_entries" - ), "erpnext.accounts.party.get_regional_address_details": ( "india_compliance.gst_india.overrides.transaction.update_party_details" ),