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/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/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/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 9bdf2c166..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,15 +83,51 @@ 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.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 +135,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 +143,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 +158,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 +167,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 +176,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..5528438bd --- /dev/null +++ b/india_compliance/gst_india/overrides/ineligible_itc.py @@ -0,0 +1,380 @@ +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.overrides.transaction import ( + is_indian_registered_company, +) +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.get("is_return") else "debit" + self.cr_or_dr = "debit" if doc.get("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 + """ + 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() + + 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.get("is_fixed_asset"): + ineligible_tax_amount = item._ineligible_tax_amount + if self.doc.get("is_return"): + ineligible_tax_amount = -ineligible_tax_amount + + # TODO: handle rounding off + self.update_item_valuation_rate(item, ineligible_tax_amount) + + def update_gl_entries(self, gl_entries): + self.gl_entries = gl_entries + + if 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.get("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 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 + """ + 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 / Fixed Asset in PI, Additional Debit is accounted automatically from valuation rates + return self.is_expense_item(item) + + 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): + 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 + + if total_gst_expense == 0: + return + + 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, +} + + +def update_valuation_rate(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 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): + 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..54fbe00ae 100644 --- a/india_compliance/gst_india/overrides/purchase_invoice.py +++ b/india_compliance/gst_india/overrides/purchase_invoice.py @@ -43,6 +43,7 @@ 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) @@ -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 @@ -79,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"): @@ -200,3 +204,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..96995c337 --- /dev/null +++ b/india_compliance/gst_india/overrides/test_ineligible_itc.py @@ -0,0 +1,841 @@ +import json +from contextlib import contextmanager + +import frappe +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 = [ + {"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 + + +@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): + 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)") + + 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.assertStockValues( + doc.name, {"Test Stock Item": 20, "Test Ineligible Stock Item": 22.42} + ) + 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 = { + "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") + + 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.assertStockValues( + doc.name, {"Test Stock Item": 23.6, "Test Ineligible Stock Item": 22.42} + ) + self.assertAssetValues( + "Purchase Invoice", + doc.name, + {"Test Fixed Asset": 1180, "Test Ineligible Fixed Asset": 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) + + 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.assertStockValues( + doc.name, {"Test Stock Item": 20, "Test Ineligible Stock Item": 22.42} + ) + self.assertAssetValues( + "Purchase Receipt", + doc.name, + {"Test Fixed Asset": 1000, "Test Ineligible Fixed Asset": 1178.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)") + + 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): + transaction_details = { + "doctype": "Purchase Receipt", + "items": SAMPLE_ITEM_LIST, + "place_of_supply": "27-Maharashtra", + "is_out_state": 1, + } + + doc = create_transaction(**transaction_details) + + 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.assertStockValues( + doc.name, + { + "Test Stock Item": 23.6, + "Test Ineligible Stock Item": 22.42, + }, + ) + self.assertAssetValues( + "Purchase Receipt", + doc.name, + {"Test Fixed Asset": 1180, "Test Ineligible Fixed Asset": 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") + + 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}, + ], + ) + + 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() + + 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}, + ], + ) + + @toggle_perpetual_inventory() + def test_purchase_receipt_and_then_purchase_invoice_for_non_perpetual_stock(self): + transaction_details = { + "doctype": "Purchase Receipt", + "items": SAMPLE_ITEM_LIST, + "is_in_state": 1, + } + + doc = create_transaction(**transaction_details) + 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.assertAssetValues( + doc.doctype, + doc.name, + {"Test Fixed Asset": 1000, "Test Ineligible Fixed Asset": 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)") + + 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.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 + """ + transaction_details = { + "doctype": "Purchase Receipt", + "items": SAMPLE_ITEM_LIST, + "is_in_state": 1, + } + + doc = create_transaction(**transaction_details) + + 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.assertStockValues( + doc.name, {"Test Stock Item": 20, "Test Ineligible Stock Item": 22.42} + ) + + self.assertAssetValues( + "Purchase Receipt", + doc.name, + {"Test Fixed Asset": 1000, "Test Ineligible Fixed Asset": 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)") + + self.assertGLEntry( + doc.name, + [ + {"account": "Round Off - _TIRC", "debit": 0.28, "credit": 0.0}, + { + "account": "GST Expense - _TIRC", + "debit": 369.72, + "credit": 179.64, + }, + { + "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": 5610.0}, + ], + ) + + @change_settings("GST Settings", {"enable_overseas_transactions": 1}) + def test_purchase_invoice_with_bill_of_entry(self): + 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( + "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 = { + "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, + "capital_work_in_progress_account": "CWIP Account - _TIRC", + } + ], + } + ) + 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..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,31 +77,31 @@ 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, }, { - "fieldname": "eligibility_for_itc", + "fieldname": "itc_classification", "label": _("Eligibility for ITC"), "fieldtype": "Data", "width": 100, @@ -113,12 +113,20 @@ 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, - 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,11 +138,11 @@ 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, - 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"), + purchase_invoice.itc_classification, + 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) @@ -142,7 +150,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) ) @@ -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,10 +185,10 @@ 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"), - ConstantColumn("Import of Goods").as_("eligibility_for_itc"), + ).as_("csamt"), + LiteralValue(0).as_("camt"), + LiteralValue(0).as_("samt"), + ConstantColumn("Import of Goods").as_("itc_classification"), ) .where( (boe.docstatus == 1) @@ -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,8 +244,8 @@ def get_itc_from_journal_entry(self): (-1 * journal_entry_account.credit_in_account_currency), ) .else_(0) - ).as_("cess_amount"), - journal_entry.reversal_type.as_("eligibility_for_itc"), + ).as_("csamt"), + journal_entry.ineligibility_reason.as_("itc_classification"), ) .where( (journal_entry.docstatus == 1) @@ -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 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 ec4860899..29af32b3e 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -109,6 +109,9 @@ "before_validate": ( "india_compliance.gst_india.overrides.transaction.before_validate" ), + "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": ( @@ -125,6 +128,9 @@ "before_validate": ( "india_compliance.gst_india.overrides.transaction.before_validate" ), + "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", @@ -194,6 +200,12 @@ "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.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 d396377fc..0396e698f 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() #33 +execute:from india_compliance.gst_india.setup import create_custom_fields; create_custom_fields() #34 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 8c4bf2a7c..e2b25a3fd 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() + ) 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", diff --git a/india_compliance/tests/__init__.py b/india_compliance/tests/__init__.py index c86cb8a0c..99d601b85 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,25 @@ 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", + }, + ) + + # 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)