diff --git a/india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py b/india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py index f2cd7717e6..3964ce0b5b 100644 --- a/india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py +++ b/india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py @@ -5,7 +5,7 @@ import frappe from frappe.tests import IntegrationTestCase, change_settings -from frappe.utils import get_month, getdate +from frappe.utils import add_months, get_month, getdate from india_compliance.gst_india.doctype.bill_of_entry.bill_of_entry import ( make_bill_of_entry, @@ -18,6 +18,7 @@ GSTR3B_Inward_Nil_Exempt, ) from india_compliance.gst_india.utils import get_gst_accounts_by_type +from india_compliance.gst_india.utils.itc_claim import format_period from india_compliance.gst_india.utils.tests import ( append_item, create_purchase_invoice, @@ -752,6 +753,63 @@ def test_inter_state_advance_payment_entry(self): self.assertEqual(output["sup_details"]["osup_det"]["camt"], 0.0) self.assertEqual(output["sup_details"]["osup_det"]["samt"], 0.0) + def test_rcm_outward_liability(self): + """RCM outward liability uses posting date while ITC uses claim period.""" + today = getdate() + next_month = add_months(today, 1) + next_period = format_period(next_month) + + pi = create_purchase_invoice( + supplier="_Test Unregistered Supplier", + is_reverse_charge=True, + is_in_state_rcm=True, + posting_date=today, + do_not_submit=True, + ) + pi.itc_claim_period = next_period + pi.save() + pi.submit() + + # -- Report for THIS month (filter_by ITC Claim Period) -- + report_this = frappe.get_doc( + { + "doctype": "GSTR 3B Report", + "company": "_Test Indian Registered Company", + "company_gstin": "24AAQCA8719H1ZC", + "year": today.year, + "month_or_quarter": get_month(today), + "filter_by": "ITC Claim Period", + } + ).insert() + output = json.loads(report_this.json_output) + + # Outward RCM liability always by posting date → invoice IS included + self.assertEqual(output["sup_details"]["isup_rev"]["txval"], 100.0) + # ITC by claim period → invoice is NOT included (deferred to next month) + itc_section = {r["ty"]: r for r in output["itc_elg"]["itc_avl"]} + self.assertEqual(itc_section.get("ISRC", {}).get("camt", 0.0), 0.0) + self.assertEqual(itc_section.get("ISRC", {}).get("samt", 0.0), 0.0) + + # -- Report for NEXT month (filter_by ITC Claim Period) -- + report_next = frappe.get_doc( + { + "doctype": "GSTR 3B Report", + "company": "_Test Indian Registered Company", + "company_gstin": "24AAQCA8719H1ZC", + "year": next_month.year, + "month_or_quarter": get_month(next_month), + "filter_by": "ITC Claim Period", + } + ).insert() + output = json.loads(report_next.json_output) + + # Outward RCM by posting date → invoice is NOT in next month's liability + self.assertEqual(output["sup_details"]["isup_rev"]["txval"], 0.0) + # ITC by claim period → invoice IS in next month's ITC + itc_section = {r["ty"]: r for r in output["itc_elg"]["itc_avl"]} + self.assertEqual(itc_section.get("ISRC", {}).get("camt", 0.0), 9.0) + self.assertEqual(itc_section.get("ISRC", {}).get("samt", 0.0), 9.0) + def create_sales_invoices(): create_sales_invoice(is_in_state=True) diff --git a/india_compliance/gst_india/overrides/test_purchase_invoice.py b/india_compliance/gst_india/overrides/test_purchase_invoice.py index e7616974c9..99f1102660 100644 --- a/india_compliance/gst_india/overrides/test_purchase_invoice.py +++ b/india_compliance/gst_india/overrides/test_purchase_invoice.py @@ -272,7 +272,8 @@ def test_itc_claim_period_invalid_format(self): def test_itc_claim_period_for_unregistered_rcm(self): """ - For Unregistered supplier RCM, ITC Claim Period must match the posting period + Unregistered RCM ITC Claim Period can be any valid period — + not restricted to the posting period. """ pinv = create_purchase_invoice( supplier="_Test Unregistered Supplier", @@ -280,31 +281,13 @@ def test_itc_claim_period_for_unregistered_rcm(self): do_not_submit=True, ) - posting_period = format_period(pinv.posting_date) - self.assertEqual(pinv.itc_claim_period, posting_period) - - # Try to change itc_claim_period to a different period - should fail - pinv.itc_claim_period = "012099" # Different period - - self.assertRaisesRegex( - frappe.exceptions.ValidationError, - re.compile( - r"ITC Claim Period must be .* for purchases from Unregistered suppliers under Reverse Charge" - ), - pinv.save, - ) - - # Try to set to "Deferred" - should also fail for Unregistered RCM - pinv.reload() pinv.itc_claim_period = ITC_CLAIM_PERIOD_DEFERRED + pinv.save() + self.assertEqual(pinv.itc_claim_period, ITC_CLAIM_PERIOD_DEFERRED) - self.assertRaisesRegex( - frappe.exceptions.ValidationError, - re.compile( - r"ITC Claim Period must be .* for purchases from Unregistered suppliers under Reverse Charge" - ), - pinv.save, - ) + pinv.itc_claim_period = "012099" + pinv.save() + self.assertEqual(pinv.itc_claim_period, "012099") def test_itc_claim_period_deferred(self): """ diff --git a/india_compliance/gst_india/utils/gstr3b/gstr3b_outward_data.py b/india_compliance/gst_india/utils/gstr3b/gstr3b_outward_data.py index 2fe9c1c432..d45bfaab57 100644 --- a/india_compliance/gst_india/utils/gstr3b/gstr3b_outward_data.py +++ b/india_compliance/gst_india/utils/gstr3b/gstr3b_outward_data.py @@ -124,6 +124,7 @@ def is_inward_reverse_charge(self, invoice): class GSTR3BOutwardInvoices(GSTR3BCategoryConditions): def __init__(self, filters): self.filters = filters + self.filters.filter_by = "Posting Date" self.inward_query = GSTR3BInwardQuery(filters) self.gstr1_query = GSTR1Query(filters) diff --git a/india_compliance/gst_india/utils/itc_claim.py b/india_compliance/gst_india/utils/itc_claim.py index 14d5f35277..68145bb9a3 100644 --- a/india_compliance/gst_india/utils/itc_claim.py +++ b/india_compliance/gst_india/utils/itc_claim.py @@ -337,23 +337,18 @@ def _calculate_itc_claim_period( if inward_supply and inward_supply.get("return_period_2b"): default_period = _max_period(posting_period, inward_supply.return_period_2b) - if doc.get("gst_category") == "Unregistered" and doc.get("is_reverse_charge"): - return posting_period - return _get_next_unfiled_period(doc.company_gstin, default_period, doc.posting_date, filed) def validate_itc_claim_period(doc) -> None: validate_mandatory_fields(doc, "itc_claim_period") _validate_period_format(doc.itc_claim_period) - _validate_itc_claim_period_for_rcm_invoice(doc) _validate_itc_claim_period_as_per_filing(doc) def validate_itc_claim_period_on_update_after_submit(doc) -> None: validate_mandatory_fields(doc, "itc_claim_period") _validate_period_format(doc.itc_claim_period) - _validate_itc_claim_period_for_rcm_invoice(doc) # On update-after-submit, period checks are needed only if period changed. previous = doc.get_doc_before_save() @@ -386,22 +381,6 @@ def _validate_itc_claim_period_as_per_filing(doc) -> None: ) -def _validate_itc_claim_period_for_rcm_invoice(doc) -> None: - """For Unregistered RCM, ITC must be claimed in the same period as posting.""" - if ( - doc.doctype == "Purchase Invoice" - and doc.gst_category == "Unregistered" - and doc.is_reverse_charge - and doc.itc_claim_period != format_period(doc.posting_date) - ): - frappe.throw( - _( - "ITC Claim Period must be {0} (same as posting date) for purchases from" - " Unregistered suppliers under Reverse Charge." - ).format(format_period(doc.posting_date)) - ) - - # ============================================================================= # Bulk Processing # ============================================================================= diff --git a/india_compliance/gst_india/utils/test_itc_claim.py b/india_compliance/gst_india/utils/test_itc_claim.py index 1b84125908..df237de8d8 100644 --- a/india_compliance/gst_india/utils/test_itc_claim.py +++ b/india_compliance/gst_india/utils/test_itc_claim.py @@ -358,7 +358,7 @@ def test_calc_2b_period_earlier_than_posting(self): self.assertEqual(result, "032024") def test_calc_unregistered_rcm(self): - """Unregistered RCM always returns posting period.""" + """Unregistered RCM follows same logic as regular invoices (next unfiled period).""" doc = self._make_doc( gst_category="Unregistered", is_reverse_charge=1, @@ -367,6 +367,9 @@ def test_calc_unregistered_rcm(self): result = _calculate_itc_claim_period(doc, filed=set()) self.assertEqual(result, "012024") + result = _calculate_itc_claim_period(doc, filed={"012024"}) + self.assertEqual(result, "022024") + def test_calc_no_inward_supply(self): """No inward supply → uses posting period as start.""" doc = self._make_doc(posting_date=getdate("2024-01-15"))