Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
31 changes: 7 additions & 24 deletions india_compliance/gst_india/overrides/test_purchase_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,39 +272,22 @@ 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",
is_reverse_charge=True,
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")
Comment on lines +288 to +290

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use a realistic alternate claim period here.

Line 288 currently asserts that an arbitrary far-future MMYYYY value is valid, which is broader than this feature and could conflict with any later deadline-based validation. A next-month period still proves the cross-month behavior without locking in 012099 as acceptable.

Suggested test adjustment
-        pinv.itc_claim_period = "012099"
+        next_period = format_period(add_months(pinv.posting_date, 1))
+        pinv.itc_claim_period = next_period
         pinv.save()
-        self.assertEqual(pinv.itc_claim_period, "012099")
+        self.assertEqual(pinv.itc_claim_period, next_period)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pinv.itc_claim_period = "012099"
pinv.save()
self.assertEqual(pinv.itc_claim_period, "012099")
next_period = format_period(add_months(pinv.posting_date, 1))
pinv.itc_claim_period = next_period
pinv.save()
self.assertEqual(pinv.itc_claim_period, next_period)


def test_itc_claim_period_deferred(self):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
21 changes: 0 additions & 21 deletions india_compliance/gst_india/utils/itc_claim.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
# =============================================================================
Expand Down
5 changes: 4 additions & 1 deletion india_compliance/gst_india/utils/test_itc_claim.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"))
Expand Down
Loading