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)