Skip to content
Draft
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
1 change: 1 addition & 0 deletions india_compliance/gst_india/constants/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
)

TAXABLE_GST_TREATMENTS = ("Taxable", "Zero-Rated")
IGNORED_GST_TREATMENT = "Ignored for GST"


STATE_NUMBERS = {
Expand Down
4 changes: 2 additions & 2 deletions india_compliance/gst_india/constants/custom_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,7 @@
"fieldname": "gst_treatment",
"label": "GST Treatment",
"fieldtype": "Autocomplete",
"options": "Taxable\nZero-Rated\nNil-Rated\nExempted\nNon-GST",
"options": "Taxable\nZero-Rated\nNil-Rated\nExempted\nNon-GST\nIgnored for GST",
"fetch_from": "item_tax_template.gst_treatment",
"fetch_if_empty": 1,
"insert_after": "item_tax_template",
Expand Down Expand Up @@ -1293,7 +1293,7 @@
"label": "GST Treatment",
"fieldtype": "Autocomplete",
"default": "Taxable",
"options": "Taxable\nNil-Rated\nExempted\nNon-GST",
"options": "Taxable\nNil-Rated\nExempted\nNon-GST\nIgnored for GST",
"insert_after": "column_break_3",
"translatable": 0,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,40 @@ def test_itc_reversal_journal_entry_is_included_in_gstr_3b(self):
self.assertEqual(output["itc_elg"]["itc_net"]["camt"], -9.0)
self.assertEqual(output["itc_elg"]["itc_net"]["samt"], -9.0)

def test_ignored_items_excluded_from_itc(self):
"""
Test that Purchase Invoice items marked as 'Ignored for GST' are excluded
from GSTR-3B ITC calculations.
"""
# Create a purchase invoice with mixed items
pi = create_purchase_invoice(
is_in_state=True,
do_not_submit=True,
items=[
{
"item_code": "_Test Trading Goods 1",
"qty": 1.0,
"rate": 100.0,
},
{
"item_code": "_Test Trading Goods 1",
"qty": 1.0,
"rate": 200.0,
},
],
)

# Mark second item as ignored for GST
pi.items[1].gst_treatment = "Ignored for GST"
pi.save()
pi.submit()

# The ignored item should not contribute to ITC
# Only first item's GST (9 CGST + 9 SGST) should be eligible
self.assertEqual(pi.items[0].cgst_amount, 9.0)
self.assertEqual(pi.items[1].cgst_amount, 0.0) # Ignored item
self.assertEqual(pi.items[1].taxable_value, 0.0) # Ignored item


def create_sales_invoices():
create_sales_invoice(is_in_state=True)
Expand Down
119 changes: 119 additions & 0 deletions india_compliance/gst_india/overrides/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,125 @@ def test_non_taxable_items_with_tax(self):
doc,
)

def test_ignored_gst_treatment_resets_values(self):
"""
Test that items marked as 'Ignored for GST' have their taxable value,
GST rates, and amounts reset to zero.
"""
doc = create_transaction(
**self.transaction_details,
is_in_state=True,
do_not_submit=True,
)

# Mark item as ignored for GST
doc.items[0].gst_treatment = "Ignored for GST"
doc.save()

# All GST values should be reset to zero
self.assertDocumentEqual(
{
"gst_treatment": "Ignored for GST",
"taxable_value": 0,
"igst_rate": 0,
"cgst_rate": 0,
"sgst_rate": 0,
"igst_amount": 0,
"cgst_amount": 0,
"sgst_amount": 0,
},
doc.items[0],
)

def test_ignored_gst_treatment_skips_validation(self):
"""
Test that items with 'Ignored for GST' treatment do not trigger
the validation error for non-taxable items with tax.
"""
doc = create_transaction(
**self.transaction_details,
is_in_state=True,
item_tax_template="GST 28% - _TIRC",
do_not_submit=True,
)

# Mark item as ignored for GST - should not throw validation error
doc.items[0].gst_treatment = "Ignored for GST"

# This should NOT raise an error unlike Nil-Rated
validate_item_tax_template(doc)

def test_transaction_with_mixed_taxable_and_ignored_items(self):
"""
Test workflow with both taxable items and ignored items in the same transaction.
"""
doc = create_transaction(
**self.transaction_details,
is_in_state=True,
do_not_save=True,
)

# Add a second item
append_item(doc, frappe._dict(rate=200))
doc.insert()

# Mark the second item as ignored and save again
doc.items[1].gst_treatment = "Ignored for GST"
doc.save()

# First item should have normal GST calculation
self.assertEqual(doc.items[0].gst_treatment, "Taxable")
self.assertEqual(doc.items[0].taxable_value, 100)
self.assertEqual(doc.items[0].cgst_rate, 9)
self.assertEqual(doc.items[0].sgst_rate, 9)

# Second item should have all values as zero
self.assertDocumentEqual(
{
"gst_treatment": "Ignored for GST",
"taxable_value": 0,
"cgst_rate": 0,
"sgst_rate": 0,
"cgst_amount": 0,
"sgst_amount": 0,
},
doc.items[1],
)

def test_taxable_value_hook(self):
"""
Test that the india_compliance_get_item_taxable_value hook allows
custom apps to override taxable value calculation.
"""
custom_taxable_value = 50.0

def custom_hook(doc, item):
# Override taxable value for testing
return custom_taxable_value

# Register the hook temporarily
frappe.get_hooks.clear()

# Add custom hook
frappe.local.hooks = frappe._dict()
frappe.local.hooks["india_compliance_get_item_taxable_value"] = [custom_hook]

try:
doc = create_transaction(
**self.transaction_details,
is_in_state=True,
do_not_submit=True,
)
doc.save()

# The hook should have overridden the taxable value
self.assertEqual(doc.items[0].taxable_value, custom_taxable_value)
finally:
# Restore original hooks
if hasattr(frappe.local, "hooks"):
del frappe.local.hooks
frappe.get_hooks.clear()

def test_validate_item_tax_template(self):
item_tax_template = frappe.get_doc("Item Tax Template", "GST 28% - _TIRC")
tax_accounts = item_tax_template.get("taxes")
Expand Down
36 changes: 36 additions & 0 deletions india_compliance/gst_india/overrides/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
GST_RCM_TAX_TYPES,
GST_REFUND_TAX_TYPES,
GST_TAX_TYPES,
IGNORED_GST_TREATMENT,
SALES_DOCTYPES,
STATE_NUMBERS,
SUBCONTRACTING_DOCTYPES,
Expand Down Expand Up @@ -134,6 +135,14 @@ def update_taxable_values(doc):
if apportioned_charges != total_charges:
item.taxable_value += total_charges - apportioned_charges

# Allow custom apps to override taxable value calculation
for item in doc.items:
for method in frappe.get_hooks("india_compliance_get_item_taxable_value"):
custom_value = frappe.call(method, doc=doc, item=item)
if custom_value is not None:
item.taxable_value = custom_value
break


def validate_item_wise_tax_detail(doc):
if doc.doctype not in DOCTYPES_WITH_GST_DETAIL:
Expand Down Expand Up @@ -693,6 +702,10 @@ def _validate_hsn_codes(doc, valid_hsn_length, throw=False, message=None):
rows_with_invalid_hsn = []

for item in doc.items:
# Skip HSN validation for items marked as ignored for GST
if item.gst_treatment == IGNORED_GST_TREATMENT:
continue

item.gst_hsn_code = (item.gst_hsn_code or "").replace(" ", "")

if not (hsn_code := item.get("gst_hsn_code")):
Expand Down Expand Up @@ -1178,8 +1191,23 @@ def update(self, doc):
self.set_temp_item_wise_tax_detail_object()

self.set_item_name_wise_tax_details()
self.reset_ignored_items()
self.validate_item_gst_details()

def reset_ignored_items(self):
"""
Reset taxable_value, tax rates, and tax amounts to zero for items marked as 'Ignored for GST'.
This ensures these items don't contribute to GST totals in statutory reports.
"""
for item in self.doc.get("items"):
if item.gst_treatment != IGNORED_GST_TREATMENT:
continue

item.taxable_value = 0
for tax_type in GST_TAX_TYPES:
item.set(f"{tax_type}_rate", 0)
item.set(f"{tax_type}_amount", 0)

def get_item_defaults(self):
item_defaults = frappe._dict(count=0)

Expand Down Expand Up @@ -1475,6 +1503,10 @@ def set_default_treatment(self):
default_treatment = self.get_default_treatment()

for item in self.doc.items:
# Preserve "Ignored for GST" treatment if already set
if item.gst_treatment == IGNORED_GST_TREATMENT:
continue

item.gst_treatment = self.gst_treatment_map.get(item.item_tax_template)

if not item.gst_treatment or not item.item_tax_template:
Expand Down Expand Up @@ -1726,6 +1758,10 @@ def validate_item_tax_template(doc):
if item.taxable_value == 0:
continue

# Skip items marked as ignored for GST
if item.gst_treatment == IGNORED_GST_TREATMENT:
continue

if item.gst_treatment == "Zero-Rated" and not doc.get("is_export_with_gst"):
continue

Expand Down
7 changes: 6 additions & 1 deletion india_compliance/gst_india/utils/gstr3b/gstr3b_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import IfNull, Sum

from india_compliance.gst_india.constants import GST_TAX_TYPES
from india_compliance.gst_india.constants import (
GST_TAX_TYPES,
IGNORED_GST_TREATMENT,
)
from india_compliance.gst_india.overrides.transaction import is_inter_state_supply
from india_compliance.gst_india.utils import get_full_gst_uom
from india_compliance.gst_india.utils.gstr_1 import GSTR1_SubCategory
Expand Down Expand Up @@ -204,6 +207,8 @@ def get_base_purchase_query(self):
.where((self.PI.is_opening == "No"))
.where(self.PI.company_gstin != IfNull(self.PI.supplier_gstin, ""))
.where(IfNull(self.PI.itc_classification, "") != "Import Of Goods")
# Exclude items marked as ignored for GST
.where(IfNull(self.PI_ITEM.gst_treatment, "") != IGNORED_GST_TREATMENT)
)

return self.get_query_with_common_filters(query, self.PI)
Expand Down
7 changes: 6 additions & 1 deletion india_compliance/gst_india/utils/gstr_1/gstr_1_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
from frappe.query_builder.functions import Date, IfNull, Sum
from frappe.utils import cint, flt, getdate

from india_compliance.gst_india.constants import GST_REFUND_TAX_TYPES
from india_compliance.gst_india.constants import (
GST_REFUND_TAX_TYPES,
IGNORED_GST_TREATMENT,
)
from india_compliance.gst_india.utils import (
get_escaped_name,
get_full_gst_uom,
Expand Down Expand Up @@ -139,6 +142,8 @@ def get_base_query(self):
.where(self.si.docstatus == 1)
.where(self.si.is_opening != "Yes")
.where(IfNull(self.si.billing_address_gstin, "") != self.si.company_gstin)
# Exclude items marked as ignored for GST
.where(IfNull(self.si_item.gst_treatment, "") != IGNORED_GST_TREATMENT)
.orderby(
self.si.posting_date,
self.si.name,
Expand Down
55 changes: 55 additions & 0 deletions india_compliance/gst_india/utils/gstr_1/test_gstr_1_books_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,61 @@ def test_transaction_split_b2b_nil(self):
][0],
)

def test_transaction_with_ignored_item(self):
"""
Test that items marked as 'Ignored for GST' are excluded from GSTR-1 report.
Creates an invoice with one taxable item and one ignored item,
verifies only the taxable item appears in the report.
"""
si = create_sales_invoice(
customer="_Test Registered Customer",
is_in_state=True,
do_not_submit=True,
items=[
{
"item_code": "_Test Trading Goods 1",
"qty": 1.0,
"rate": 100.0,
},
{
"item_code": "_Test Trading Goods 1",
"qty": 1.0,
"rate": 200.0,
},
],
)

# Mark second item as ignored for GST
si.items[1].gst_treatment = "Ignored for GST"
si.save()
si.submit()

data = GSTR1BooksData(filters=FILTERS).prepare_mapped_data()

# Only the taxable item (100.0) should appear in report
# The ignored item (200.0) should be excluded
self.assertDictEq(
{
"transaction_type": "Invoice",
"total_taxable_value": 100.0, # Only first item
"total_igst_amount": 0.0,
"total_cgst_amount": 9.0,
"total_sgst_amount": 9.0,
"total_cess_amount": 0.0,
"items": [
{
"taxable_value": 100.0,
"igst_amount": 0.0,
"cgst_amount": 9.0,
"sgst_amount": 9.0,
"cess_amount": 0.0,
"tax_rate": 18.0,
}
],
},
data[GSTR1_SubCategory.B2B_REGULAR.value][si.name],
)

def test_hsn_summary_with_bifurcation(self):
si = create_sales_invoice(
customer="_Test Registered Customer",
Expand Down
5 changes: 5 additions & 0 deletions india_compliance/gst_india/utils/transaction_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
GST_REFUND_TAX_TYPES,
GST_TAX_RATES,
GST_TAX_TYPES,
IGNORED_GST_TREATMENT,
TAXABLE_GST_TREATMENTS,
VALID_HSN_LENGTHS,
)
Expand Down Expand Up @@ -325,6 +326,10 @@ def get_all_item_details(self):
items = self.group_same_items()

for row in items:
# Skip items marked as ignored for GST
if row.gst_treatment == IGNORED_GST_TREATMENT:
continue

item_details = frappe._dict(
{
"item_no": row.idx,
Expand Down
Loading
Loading