From ef8921ccd7cd6a60c6b19a009411db4622b9e2e0 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 28 Feb 2024 08:04:57 +0530 Subject: [PATCH] fix: add support for GL Repost for Purchase Invoice ITC Reversal (#1756) * fix: add support for gl repost for Purchase Invoice ITC Reversal * fix: process futher to update stock ledger only if required * fix: revert simplification of ineligible itc to fix repost item stock * test: repost accounting ledger correctly with itc ineligibility * refactor: check if debit entry is required before getting expense account * test: enable repost settings for purchase invoice * fix: repost purchases item valuation and its test cases --- .../doctype/bill_of_entry/bill_of_entry.py | 2 + .../gst_india/overrides/ineligible_itc.py | 96 +++++++++------ .../overrides/repost_accounting_ledger.py | 5 + .../overrides/test_ineligible_itc.py | 115 ++++++++++++++++-- india_compliance/hooks.py | 9 ++ india_compliance/patches.txt | 1 + 6 files changed, 178 insertions(+), 50 deletions(-) create mode 100644 india_compliance/gst_india/overrides/repost_accounting_ledger.py 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 7f587879c..ab62444b5 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 @@ -16,6 +16,7 @@ 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 @@ -50,6 +51,7 @@ def validate(self): self.validate_purchase_invoice() self.validate_taxes() self.reconciliation_status = "Unreconciled" + update_valuation_rate(self) def on_submit(self): gl_entries = self.get_gl_entries() diff --git a/india_compliance/gst_india/overrides/ineligible_itc.py b/india_compliance/gst_india/overrides/ineligible_itc.py index cdf49a993..430101c53 100644 --- a/india_compliance/gst_india/overrides/ineligible_itc.py +++ b/india_compliance/gst_india/overrides/ineligible_itc.py @@ -58,25 +58,16 @@ def update_valuation_rate(self): # TODO: handle rounding off of gst amount from gst settings self.update_item_valuation_rate(item, ineligible_tax_amount) - def update_stock_ledger_entries(self): - """ - Cancels and re-creates Stock Ledger Entries - This is done with new valuation rate - - Reference: erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher.LandedCostVoucher.update_landed_cost - """ - self.doc.docstatus = 2 - self.doc.update_stock_ledger() - - self.doc.docstatus = 1 - self.doc.update_stock_ledger() - def update_gl_entries(self, gl_entries): - self.update_valuation_rate() - self.update_stock_ledger_entries() - self.gl_entries = gl_entries + if ( + frappe.flags.through_repost_accounting_ledger + or frappe.flags.through_repost_item_valuation + ): + self.doc.update_valuation_rate() + self.update_valuation_rate() + if not self.doc.get("_has_ineligible_itc_items"): return gl_entries @@ -151,7 +142,13 @@ def make_gst_expense_entry(self, item): ) ) + if not self.is_debit_entry_required(item): + return + expense_account = self.get_item_expense_account(item) + if not expense_account: + return + against_account = self.get_against_account(item) remarks = item.get("_remarks") @@ -184,24 +181,40 @@ def reverse_stock_adjustment_entry(self, item): This method reverses the Stock Adjustment Entry """ stock_account = self.get_item_expense_account(item) - cogs_account = self.doc.get_company_default("default_expense_account") + cogs_account = self.company.default_expense_account ineligible_item_tax_amount = item.get("_ineligible_tax_amount", 0) - self.gl_entries.append( - self.doc.get_gl_dict( - { - "account": stock_account, - self.cr_or_dr: ineligible_item_tax_amount, - f"{self.cr_or_dr}_in_account_currency": ineligible_item_tax_amount, - "cost_center": item.cost_center or self.cost_center, - "remarks": item.get("_remarks"), - } + for entry in self.gl_entries: + if ( + entry.get("account") != stock_account + or entry.get("cost_center") != item.cost_center + ): + continue + + entry[self.dr_or_cr] -= ineligible_item_tax_amount + entry[f"{self.dr_or_cr}_in_account_currency"] -= ineligible_item_tax_amount + break + + else: + # FALLBACK: If Stock Account not found in GL Entries + self.gl_entries.append( + self.doc.get_gl_dict( + { + "account": stock_account, + self.cr_or_dr: ineligible_item_tax_amount, + f"{self.cr_or_dr}_in_account_currency": ineligible_item_tax_amount, + "cost_center": item.cost_center or self.cost_center, + "remarks": item.get("_remarks"), + } + ) ) - ) for entry in self.gl_entries: - if entry.get("account") != cogs_account: + if ( + entry.get("account") != cogs_account + or entry.get("cost_center") != item.cost_center + ): continue entry[self.cr_or_dr] -= ineligible_item_tax_amount @@ -290,6 +303,9 @@ def get_item_tax_amount(self, item, tax): return abs(tax_amount) + def is_debit_entry_required(self, item): + return True + def update_asset_valuation_rate(self, item): frappe.db.set_value( "Asset", @@ -321,7 +337,7 @@ def update_item_gl_entries(self, item): if (item.get("_is_stock_item")) or item.get("is_fixed_asset"): self.make_gst_expense_entry(item) - if self.doc.is_return and item.get("_is_stock_item"): + if item.get("_is_stock_item"): self.reverse_stock_adjustment_entry(item) def is_eligibility_restricted_due_to_pos(self): @@ -348,20 +364,15 @@ def update_valuation_rate(self): super().update_valuation_rate() - def update_stock_ledger_entries(self): - if not self.doc.update_stock: - return - - super().update_stock_ledger_entries() - 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) - if self.doc.is_return and item.get("_is_stock_item"): - self.reverse_stock_adjustment_entry(item) + def is_debit_entry_required(self, item): + # 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): """ @@ -408,9 +419,6 @@ def update_valuation_rate(self): super().update_valuation_rate() - def update_stock_ledger_entries(self): - return - 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: @@ -469,6 +477,14 @@ def update_landed_cost_voucher(self, landed_cost_voucher): } +def update_valuation_rate(doc, method=None): + if doc.get("is_opening") == "Yes" or not is_indian_registered_company(doc): + return + + if doc.doctype in DOCTYPE_MAPPING: + DOCTYPE_MAPPING[doc.doctype](doc).update_valuation_rate() + + def update_regional_gl_entries(gl_entries, doc): if doc.get("is_opening") == "Yes" or not is_indian_registered_company(doc): return gl_entries diff --git a/india_compliance/gst_india/overrides/repost_accounting_ledger.py b/india_compliance/gst_india/overrides/repost_accounting_ledger.py new file mode 100644 index 000000000..0db94663c --- /dev/null +++ b/india_compliance/gst_india/overrides/repost_accounting_ledger.py @@ -0,0 +1,5 @@ +import frappe + + +def before_submit(doc, method=None): + frappe.flags.through_repost_accounting_ledger = True diff --git a/india_compliance/gst_india/overrides/test_ineligible_itc.py b/india_compliance/gst_india/overrides/test_ineligible_itc.py index ada914c34..987433632 100644 --- a/india_compliance/gst_india/overrides/test_ineligible_itc.py +++ b/india_compliance/gst_india/overrides/test_ineligible_itc.py @@ -8,6 +8,9 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( make_purchase_invoice, ) +from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import ( + repost_entries, +) from india_compliance.gst_india.doctype.bill_of_entry.bill_of_entry import ( make_bill_of_entry, @@ -163,6 +166,81 @@ def test_purchase_invoice_with_update_stock(self): {"Test Fixed Asset": 1000, "Test Ineligible Fixed Asset": 1178.82}, ) # 999 + 179.82 + # Repost Accounting Ledger + if not frappe.db.exists( + "Repost Allowed Types", + { + "document_type": "Purchase Invoice", + "parent": "Repost Accounting Ledger Settings", + }, + ): + settings = frappe.get_single("Repost Accounting Ledger Settings") + settings.append( + "allowed_types", {"document_type": "Purchase Invoice", "allowed": 1} + ) + settings.save() + + doc.items[4].expense_account = "Office Rent - _TIRC" + doc.items[5].expense_account = "Office Rent - _TIRC" + + doc.save() + doc.repost_accounting_entries() + + expected_entries = [ + {"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": "Office Rent - _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.assertGLEntry(doc.name, expected_entries) + + # Repost Item Valuation + repost_doc = frappe.get_doc( + { + "doctype": "Repost Item Valuation", + "voucher_type": "Purchase Invoice", + "voucher_no": doc.name, + "status": "Queued", + } + ) + repost_doc.save() + repost_doc.submit() + + repost_entries() + + status = frappe.db.get_value("Repost Item Valuation", repost_doc.name, "status") + self.assertEqual(status, "Completed") + self.assertGLEntry(doc.name, expected_entries) + def test_purchase_invoice_with_ineligible_pos(self): transaction_details = { "doctype": "Purchase Invoice", @@ -272,6 +350,21 @@ def test_purchase_receipt_and_then_purchase_invoice(self): {"Test Fixed Asset": 1000, "Test Ineligible Fixed Asset": 1178.82}, ) + repost_doc = frappe.get_doc( + { + "doctype": "Repost Item Valuation", + "voucher_type": "Purchase Receipt", + "voucher_no": doc.name, + } + ) + repost_doc.save() + repost_doc.submit() + + repost_entries() + + status = frappe.db.get_value("Repost Item Valuation", repost_doc.name, "status") + self.assertEqual(status, "Completed") + # Create Purchase Invoice doc = make_purchase_invoice(doc.name) doc.bill_no = "BILL-03" @@ -768,7 +861,7 @@ def test_purchase_invoice_with_bill_of_entry(self): def assertGLEntry(self, docname, expected_gl_entry): gl_entries = frappe.get_all( "GL Entry", - filters={"voucher_no": docname}, + filters={"voucher_no": docname, "is_cancelled": 0}, fields=["account", "debit", "credit"], ) @@ -833,7 +926,7 @@ def create_test_items(): "account_type": "Fixed Asset", } ) - asset_account.insert() + asset_account.insert(ignore_if_duplicate=True) asset_category = frappe.get_doc( { @@ -850,9 +943,11 @@ def create_test_items(): ], } ) - asset_category.insert() + asset_category.insert(ignore_if_duplicate=True) - frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() + frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert( + ignore_if_duplicate=True + ) asset_item = { "doctype": "Item", @@ -879,31 +974,31 @@ def create_test_items(): } # Stock Item - frappe.get_doc(stock_item).insert() + frappe.get_doc(stock_item).insert(ignore_if_duplicate=True) frappe.get_doc( { **stock_item, "item_code": "Test Ineligible Stock Item", "is_ineligible_for_itc": 1, } - ).insert() + ).insert(ignore_if_duplicate=True) # Fixed Asset - frappe.get_doc(asset_item).insert() + frappe.get_doc(asset_item).insert(ignore_if_duplicate=True) frappe.get_doc( { **asset_item, "item_code": "Test Ineligible Fixed Asset", "is_ineligible_for_itc": 1, } - ).insert() + ).insert(ignore_if_duplicate=True) # Service Item - frappe.get_doc(service_item).insert() + frappe.get_doc(service_item).insert(ignore_if_duplicate=True) frappe.get_doc( { **service_item, "item_code": "Test Ineligible Service Item", "is_ineligible_for_itc": 1, } - ).insert() + ).insert(ignore_if_duplicate=True) diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py index c1d121a2e..b6a2578c5 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -137,7 +137,10 @@ "before_save": "india_compliance.gst_india.overrides.transaction.update_gst_details", "before_submit": [ "india_compliance.gst_india.overrides.transaction.update_gst_details", + "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", "after_mapping": "india_compliance.gst_india.overrides.transaction.after_mapping", }, "Purchase Order": { @@ -164,7 +167,13 @@ "before_save": "india_compliance.gst_india.overrides.transaction.update_gst_details", "before_submit": [ "india_compliance.gst_india.overrides.transaction.update_gst_details", + "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", + }, + "Repost Accounting Ledger": { + "before_submit": "india_compliance.gst_india.overrides.repost_accounting_ledger.before_submit", }, "Sales Invoice": { "onload": [ diff --git a/india_compliance/patches.txt b/india_compliance/patches.txt index b386c447d..8998320d3 100644 --- a/india_compliance/patches.txt +++ b/india_compliance/patches.txt @@ -42,3 +42,4 @@ india_compliance.patches.post_install.update_vehicle_no_field_in_purchase_receip india_compliance.patches.post_install.update_gst_treatment_for_taxable_nil_transaction_item india_compliance.patches.post_install.update_default_gstr3b_status india_compliance.patches.v14.update_gst_details_for_invoices_with_same_item_multiple_times +execute:from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import execute_repost_item_valuation; execute_repost_item_valuation() \ No newline at end of file