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 d85388a5f..956a3d5b8 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 @@ -120,24 +120,33 @@ def get_itc_reversal_entries(self): self.update_itc_reversal_from_bill_of_entry() def update_itc_reversal_from_purchase_invoice(self): + self.update_itc_reversal_for_purchase_us_17_4() + self.update_itc_reversal_for_purchase_due_to_pos() + + def update_itc_reversal_for_purchase_due_to_pos(self): ineligible_credit = IneligibleITC( self.company, self.gst_details.get("gstin"), self.month_no, self.year - ).get_ineligible_itc_us_17_5_for_purchase(group_by="ineligibility_reason") + ).get_for_purchase( + "ITC restricted due to PoS rules", group_by="ineligibility_reason" + ) - ineligible_credit_due_to_pos = IneligibleITC( - self.company, self.gst_details.get("gstin"), self.month_no, self.year - ).get_ineligible_itc_due_to_pos_for_purchase(group_by="ineligibility_reason") + self.process_ineligible_credit(ineligible_credit) - ineligible_credit.extend(ineligible_credit_due_to_pos) + def update_itc_reversal_for_purchase_us_17_4(self): + ineligible_credit = IneligibleITC( + self.company, self.gst_details.get("gstin"), self.month_no, self.year + ).get_for_purchase( + "Ineligible As Per Section 17(5)", group_by="ineligibility_reason" + ) - return self.process_ineligible_credit(ineligible_credit) + 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) + self.process_ineligible_credit(ineligible_credit) def process_ineligible_credit(self, ineligible_credit): if not ineligible_credit: diff --git a/india_compliance/gst_india/overrides/ineligible_itc.py b/india_compliance/gst_india/overrides/ineligible_itc.py index 3c332d806..7c1a9b957 100644 --- a/india_compliance/gst_india/overrides/ineligible_itc.py +++ b/india_compliance/gst_india/overrides/ineligible_itc.py @@ -1,6 +1,8 @@ +from collections import defaultdict + import frappe from frappe import _ -from frappe.utils import flt, get_link_to_form, rounded +from frappe.utils import flt, get_link_to_form from erpnext.assets.doctype.asset.asset import ( get_asset_account, is_cwip_accounting_enabled, @@ -32,41 +34,27 @@ def update_valuation_rate(self): - 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) + self.update_item_ineligibility() - if item._ineligible_tax_amount: - self.doc._has_ineligible_itc_items = True + if not self.doc.get("_has_ineligible_itc_items"): + return - if item.item_code in stock_items and self.is_perpetual: - item._is_stock_item = True + for item in self.doc.items: + if not item.get("_ineligible_tax_amount"): + continue 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 of gst amount from gst settings self.update_item_valuation_rate(item, ineligible_tax_amount) def update_gl_entries(self, gl_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() + self.update_item_ineligibility() if not self.doc.get("_has_ineligible_itc_items"): return gl_entries @@ -84,6 +72,34 @@ def update_gl_entries(self, gl_entries): self.update_item_gl_entries(item) + def update_item_ineligibility(self): + self.doc._has_ineligible_itc_items = False + stock_items = self.doc.get_stock_items() + + self.tax_account_dict = { + row.gst_tax_type: row.account_head + for row in self.doc.taxes + if row.gst_tax_type + } + + if not self.tax_account_dict: + return + + 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 and self.is_perpetual: + item._is_stock_item = True + def update_item_gl_entries(self, item): return @@ -272,39 +288,25 @@ def update_ineligible_taxes(self, item): "Input SGST - FC": 50, } """ - ineligible_taxes = frappe._dict() + ineligible_taxes = defaultdict(float) + ineligible_tax_amount = 0 - for tax in self.doc.taxes: - if tax.gst_tax_type not in GST_TAX_TYPES: + for tax_type in GST_TAX_TYPES: + tax_amount = abs(flt(item.get(f"{tax_type}_amount"))) + tax_account = self.tax_account_dict.get(tax_type) + + if not tax_amount: continue - ineligible_taxes[tax.account_head] = self.get_item_tax_amount(item, tax) + ineligible_taxes[tax_account] += tax_amount + ineligible_tax_amount += tax_amount item._ineligible_taxes = ineligible_taxes - item._ineligible_tax_amount = sum(ineligible_taxes.values()) + item._ineligible_tax_amount = ineligible_tax_amount 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 @@ -416,16 +418,6 @@ def update_valuation_rate(self): 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 diff --git a/india_compliance/gst_india/overrides/purchase_receipt.py b/india_compliance/gst_india/overrides/purchase_receipt.py index e062f4d08..e5de8e4f8 100644 --- a/india_compliance/gst_india/overrides/purchase_receipt.py +++ b/india_compliance/gst_india/overrides/purchase_receipt.py @@ -24,17 +24,14 @@ def onload(doc, method=None): if ignore_gst_validations(doc, throw=False): return - doc.flags.ignore_mandatory = True if ( validate_mandatory_fields( - doc, ("company_gstin", "place_of_supply", "gst_category") + doc, ("company_gstin", "place_of_supply", "gst_category"), throw=False ) is False ): return - doc.flags.ignore_mandatory = False - set_ineligibility_reason(doc, show_alert=False) diff --git a/india_compliance/gst_india/overrides/test_ineligible_itc.py b/india_compliance/gst_india/overrides/test_ineligible_itc.py index d9df89581..c8f749fd8 100644 --- a/india_compliance/gst_india/overrides/test_ineligible_itc.py +++ b/india_compliance/gst_india/overrides/test_ineligible_itc.py @@ -518,6 +518,7 @@ def test_purchase_returns_with_update_stock(self): doc = create_transaction(**transaction_details) doc = make_return_doc("Purchase Invoice", doc.name) + doc.save() doc.submit() self.assertGLEntry( diff --git a/india_compliance/gst_india/overrides/transaction.py b/india_compliance/gst_india/overrides/transaction.py index 540b4e174..5805c3193 100644 --- a/india_compliance/gst_india/overrides/transaction.py +++ b/india_compliance/gst_india/overrides/transaction.py @@ -210,7 +210,7 @@ def is_indian_registered_company(doc): return True -def validate_mandatory_fields(doc, fields, error_message=None): +def validate_mandatory_fields(doc, fields, error_message=None, throw=True): if isinstance(fields, str): fields = (fields,) @@ -224,6 +224,9 @@ def validate_mandatory_fields(doc, fields, error_message=None): if doc.flags.ignore_mandatory: return False + if not throw: + return False + frappe.throw( error_message.format(bold(_(doc.meta.get_label(field)))), title=_("Missing Required Field"), 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 69180e4a8..7cdff286f 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 @@ -6,9 +6,8 @@ from frappe.query_builder import Case, DatePart from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.functions import Extract, Ifnull, IfNull, LiteralValue, Sum -from frappe.utils import cint, flt, get_first_day, get_last_day +from frappe.utils import cint, get_first_day, get_last_day -from india_compliance.gst_india.constants.__init__ import GST_TAX_TYPES from india_compliance.gst_india.utils import get_escaped_gst_accounts @@ -271,7 +270,7 @@ def get_itc_from_journal_entry(self): def get_ineligible_itc_from_purchase(self): ineligible_itc = IneligibleITC( self.company, self.company_gstin, self.filters.month, self.filters.year - ).get_ineligible_itc_us_17_5_for_purchase() + ).get_for_purchase("Ineligible As Per Section 17(5)") return self.process_ineligible_itc(ineligible_itc) @@ -419,275 +418,61 @@ def get_inward_nil_exempt(self): 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_escaped_gst_accounts(company, "Input") - def get_ineligible_itc_us_17_5_for_purchase(self, group_by="name"): - """ - - Ineligible As Per Section 17(5) - - ITC restricted due to ineligible items in purchase invoice - """ - ineligible_transactions = self.get_vouchers_with_gst_expense("Purchase Invoice") + def get_for_purchase(self, ineligibility_reason, group_by="name"): + doctype = "Purchase Invoice" + dt = frappe.qb.DocType(doctype) + dt_item = frappe.qb.DocType(f"{doctype} Item") - 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, "") == "Ineligible As Per Section 17(5)" - ) - .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, "") == "Ineligible As Per Section 17(5)" - ) - .where(pi.name.isin(ineligible_transactions)) - .groupby(pi[group_by]) - .run(as_dict=1) + query = ( + self.get_common_query(doctype, dt, dt_item) + .select((dt.ineligibility_reason).as_("itc_classification")) + .where((dt.is_opening == "No")) + .where(IfNull(dt.ineligibility_reason, "") == ineligibility_reason) ) - return self.get_ineligible_credit(credit_availed, credit_available, group_by) - - def get_ineligible_itc_due_to_pos_for_purchase(self, group_by="name"): - """ - - ITC restricted due to PoS rules - """ - ineligible_transactions = self.get_vouchers_with_gst_expense("Purchase Invoice") - - if not ineligible_transactions: - return [] + if ineligibility_reason == "Ineligible As Per Section 17(5)": + query = query.where(dt_item.is_ineligible_for_itc == 1) - pi = frappe.qb.DocType("Purchase Invoice") - taxes = frappe.qb.DocType("Purchase Taxes and Charges") - - # utility function - def get_tax_case_statement(gst_tax_types, alias): - return Sum( - Case() - .when( - taxes.gst_tax_type.isin(gst_tax_types), - taxes.base_tax_amount_after_discount_amount, - ) - .else_(0) - ).as_(alias) - - # Credit availed is not required as it will be always 0 for pos - - ineligible_credit = ( - frappe.qb.from_(pi) - .inner_join(taxes) - .on(pi.name == taxes.parent) - .select( - pi.name.as_("voucher_no"), - pi.posting_date, - pi.ineligibility_reason.as_("itc_classification"), - get_tax_case_statement(["igst"], "iamt"), - get_tax_case_statement(["cgst"], "camt"), - get_tax_case_statement(["sgst"], "samt"), - get_tax_case_statement( - [ - "cess", - "cess_non_advol", - ], - "csamt", - ), - ) - .where(taxes.gst_tax_type.isin(GST_TAX_TYPES)) - .where( - IfNull(pi.ineligibility_reason, "") == "ITC restricted due to PoS rules" - ) - .where(pi.name.isin(ineligible_transactions)) - .where(taxes.parenttype == "Purchase Invoice") - .groupby(pi[group_by]) - .run(as_dict=True) - ) - - return ineligible_credit + return query.groupby(dt[group_by]).run(as_dict=True) 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) + doctype = "Bill of Entry" + dt = frappe.qb.DocType(doctype) + dt_item = frappe.qb.DocType(f"{doctype} Item") + query = ( + self.get_common_query(doctype, dt, dt_item) .select( - ConstantColumn("Bill of Entry").as_("voucher_type"), - boe.name.as_("voucher_no"), - boe.posting_date, - Sum( - Case() - .when( - boe_taxes.gst_tax_type == "igst", - boe_taxes.tax_amount, - ) - .else_(0) - ).as_("iamt"), - Sum( - Case() - .when( - boe_taxes.gst_tax_type == "cess", - 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) + .where(dt_item.is_ineligible_for_itc == 1) ) - return self.get_ineligible_credit(credit_availed, credit_available, group_by) + return query.groupby(dt[group_by]).run(as_dict=True) - 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) + def get_common_query(self, doctype, dt, dt_item): + return ( + frappe.qb.from_(dt) + .join(dt_item) + .on(dt.name == dt_item.parent) + .select( + ConstantColumn(doctype).as_("voucher_type"), + dt.name.as_("voucher_no"), + dt.posting_date, + Sum(dt_item.igst_amount).as_("iamt"), + Sum(dt_item.cgst_amount).as_("camt"), + Sum(dt_item.sgst_amount).as_("samt"), + Sum(dt_item.cess_amount + dt_item.cess_non_advol_amount).as_("csamt"), ) - - 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)) + .where(dt.docstatus == 1) + .where(dt.company_gstin == self.gstin) + .where(dt.company == self.company) + .where(Extract(DatePart.month, dt.posting_date).eq(self.month)) + .where(Extract(DatePart.year, dt.posting_date).eq(self.year)) ) - - return query diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py index 05f7f4957..c380e6585 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -144,10 +144,7 @@ "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", "on_cancel": "india_compliance.gst_india.overrides.purchase_invoice.on_cancel", }, @@ -182,10 +179,7 @@ "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", }, "Sales Invoice": { "onload": [ @@ -302,6 +296,9 @@ "erpnext.controllers.accounts_controller.get_advance_payment_entries_for_regional": ( "india_compliance.gst_india.overrides.payment_entry.get_advance_payment_entries_for_regional" ), + "erpnext.controllers.buying_controller.update_regional_item_valuation_rate": ( + "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate" + ), "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" ), diff --git a/india_compliance/patches/check_version_compatibility.py b/india_compliance/patches/check_version_compatibility.py index b0e31b094..2067317b3 100644 --- a/india_compliance/patches/check_version_compatibility.py +++ b/india_compliance/patches/check_version_compatibility.py @@ -18,7 +18,7 @@ { "app_name": "ERPNext", "current_version": version.parse(erpnext.__version__), - "required_versions": {"version-14": "14.66.5", "version-15": "15.23.2"}, + "required_versions": {"version-14": "14.70.7", "version-15": "15.27.7"}, }, ]