diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 8bf768db2..532961297 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -47,7 +47,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v2
+ uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,4 +56,4 @@ jobs:
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
+ uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml
index 9feb95a17..915748cd7 100644
--- a/.github/workflows/server-tests.yml
+++ b/.github/workflows/server-tests.yml
@@ -106,7 +106,7 @@ jobs:
run: cat ~/frappe-bench/bench_start.log || true
- name: Upload coverage data
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: coverage
path: /home/runner/frappe-bench/sites/coverage.xml
@@ -120,7 +120,7 @@ jobs:
uses: actions/checkout@v4
- name: Download artifacts
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
- name: Upload coverage data
uses: codecov/codecov-action@v3
diff --git a/india_compliance/boot.py b/india_compliance/boot.py
index 4cf699477..3b28dba71 100644
--- a/india_compliance/boot.py
+++ b/india_compliance/boot.py
@@ -26,6 +26,7 @@ def set_bootinfo(bootinfo):
bootinfo["ic_api_enabled_from_conf"] = bool(frappe.conf.ic_api_secret)
set_trigger_for_audit_trail_notification(bootinfo)
+ set_trigger_for_item_tax_template_notification(bootinfo)
def set_trigger_for_audit_trail_notification(bootinfo):
@@ -39,3 +40,12 @@ def set_trigger_for_audit_trail_notification(bootinfo):
return
bootinfo["needs_audit_trail_notification"] = True
+
+
+def set_trigger_for_item_tax_template_notification(bootinfo):
+ if not bootinfo.sysdefaults or not cint(
+ bootinfo.sysdefaults.get("needs_item_tax_template_notification", 0)
+ ):
+ return
+
+ bootinfo["needs_item_tax_template_notification"] = True
diff --git a/india_compliance/exceptions.py b/india_compliance/exceptions.py
index a403d6121..9f1ae8315 100644
--- a/india_compliance/exceptions.py
+++ b/india_compliance/exceptions.py
@@ -1,4 +1,8 @@
-class GatewayTimeoutError(Exception):
+class GSPServerError(Exception):
+ def __init__(self, message="GSP/GST server is down", *args, **kwargs):
+ super().__init__(message, *args, **kwargs)
+
+
+class GatewayTimeoutError(GSPServerError):
def __init__(self, message="The server took too long to respond", *args, **kwargs):
- self.message = message
- super().__init__(self.message, *args, **kwargs)
+ super().__init__(message, *args, **kwargs)
diff --git a/india_compliance/gst_india/api_classes/base.py b/india_compliance/gst_india/api_classes/base.py
index fb1b6c90c..f0e19205c 100644
--- a/india_compliance/gst_india/api_classes/base.py
+++ b/india_compliance/gst_india/api_classes/base.py
@@ -6,7 +6,7 @@
from frappe import _
from frappe.utils import sbool
-from india_compliance.exceptions import GatewayTimeoutError
+from india_compliance.exceptions import GatewayTimeoutError, GSPServerError
from india_compliance.gst_india.utils import is_api_enabled
from india_compliance.gst_india.utils.api import enqueue_integration_request
@@ -187,6 +187,9 @@ def handle_error_response(self, response_json):
if isinstance(success_value, str):
success_value = sbool(success_value)
+ if not success_value:
+ self.handle_server_error(response_json)
+
if not success_value and not self.is_ignored_error(response_json):
frappe.throw(
response_json.get("message")
@@ -195,6 +198,18 @@ def handle_error_response(self, response_json):
title=_("API Request Failed"),
)
+ def handle_server_error(self, response_json):
+ error_message_list = [
+ "GSPGSTDOWN",
+ "GSPERR300",
+ "Connection reset",
+ "No route to host",
+ ]
+
+ for error in error_message_list:
+ if error in response_json.get("message"):
+ raise GSPServerError
+
def is_ignored_error(self, response_json):
# Override in subclass, return truthy value to stop frappe.throw
pass
diff --git a/india_compliance/gst_india/client_scripts/company.js b/india_compliance/gst_india/client_scripts/company.js
index cff619929..25663d16b 100644
--- a/india_compliance/gst_india/client_scripts/company.js
+++ b/india_compliance/gst_india/client_scripts/company.js
@@ -26,7 +26,7 @@ frappe.ui.form.on(DOCTYPE, {
frappe.call({
method: "india_compliance.gst_india.overrides.company.make_default_tax_templates",
- args: { company: frm.doc.name },
+ args: { company: frm.doc.name, gst_rate: frm.doc.default_gst_rate},
callback: function () {
frappe.msgprint(__("Default Tax Templates created"));
},
diff --git a/india_compliance/gst_india/client_scripts/e_invoice_actions.js b/india_compliance/gst_india/client_scripts/e_invoice_actions.js
index 5412f92e1..96f70eb2e 100644
--- a/india_compliance/gst_india/client_scripts/e_invoice_actions.js
+++ b/india_compliance/gst_india/client_scripts/e_invoice_actions.js
@@ -222,7 +222,9 @@ function is_e_invoice_applicable(frm) {
frm.doc.company_gstin != frm.doc.billing_address_gstin &&
(frm.doc.place_of_supply === "96-Other Countries" ||
frm.doc.billing_address_gstin) &&
- !frm.doc.items[0].is_non_gst &&
+ frm.doc.items.some(item =>
+ ["Taxable", "Zero-Rated"].includes(item.gst_treatment)
+ ) &&
is_valid_e_invoice_applicability_date(frm)
);
}
diff --git a/india_compliance/gst_india/client_scripts/item_tax_template.js b/india_compliance/gst_india/client_scripts/item_tax_template.js
new file mode 100644
index 000000000..12528d87f
--- /dev/null
+++ b/india_compliance/gst_india/client_scripts/item_tax_template.js
@@ -0,0 +1,95 @@
+frappe.ui.form.on("Item Tax Template", {
+ refresh: show_missing_accounts_banner,
+ fetch_gst_accounts: fetch_and_update_missing_gst_accounts,
+ async gst_rate(frm) {
+ if (frm.doc.gst_rate === null) return;
+
+ await Promise.all(
+ frm.doc.taxes.map(async row => {
+ const tax_rate = await get_tax_rate_for_account(frm, row.tax_type);
+ if (tax_rate == null) return;
+
+ row.tax_rate = tax_rate;
+ })
+ );
+
+ frm.refresh_field("taxes");
+ },
+});
+
+async function show_missing_accounts_banner(frm) {
+ if (frm.doc.gst_treatment === "Non-GST" || frm.doc.__islocal) return;
+
+ const missing_accounts = await get_missing_gst_accounts(frm);
+ if (!missing_accounts) return;
+
+ // show banner
+ frm.dashboard.add_comment(
+ __(`Missing GST Accounts: {0}`, [missing_accounts.join(", ")]),
+ "orange",
+ true
+ );
+}
+
+async function fetch_and_update_missing_gst_accounts(frm) {
+ const missing_accounts = await get_missing_gst_accounts(frm);
+ if (!missing_accounts) return;
+
+ // cleanup existing empty rows
+ frm.doc.taxes = frm.doc.taxes.filter(row => row.tax_type);
+
+ // add missing rows
+ await Promise.all(
+ missing_accounts.map(async account => {
+ const tax_rate = await get_tax_rate_for_account(frm, account);
+ frm.add_child("taxes", { tax_type: account, tax_rate: tax_rate });
+ })
+ );
+
+ frm.refresh_field("taxes");
+}
+
+async function get_tax_rate_for_account(frm, account) {
+ const gst_rate = frm.doc.gst_rate;
+ if (!gst_rate) return 0;
+
+ const gst_accounts = await get_gst_accounts(frm);
+ if (!gst_accounts) return null;
+
+ const [_, intra_state_accounts, inter_state_accounts] = gst_accounts;
+
+ if (intra_state_accounts.includes(account)) return gst_rate / 2;
+ else if (inter_state_accounts.includes(account)) return gst_rate;
+ else return null;
+}
+
+async function get_missing_gst_accounts(frm) {
+ let gst_accounts = await get_gst_accounts(frm);
+ if (!gst_accounts) return;
+
+ const all_gst_accounts = gst_accounts[0];
+ const template_accounts = frm.doc.taxes.map(t => t.tax_type);
+ const missing_accounts = all_gst_accounts.filter(
+ a => a && !template_accounts.includes(a)
+ );
+
+ if (missing_accounts.length) return missing_accounts;
+}
+
+async function get_gst_accounts(frm) {
+ const company = frm.doc.company;
+
+ // cache company gst accounts
+ if (!frm._company_gst_accounts?.[company]) {
+ frm._company_gst_accounts = frm._company_gst_accounts || {};
+ const { message } = await frappe.call({
+ method: "india_compliance.gst_india.overrides.transaction.get_valid_gst_accounts",
+ args: { company: company },
+ });
+
+ frm._company_gst_accounts[company] = message;
+ }
+
+ if (!frm._company_gst_accounts[company]) return;
+ return frm._company_gst_accounts[company];
+}
diff --git a/india_compliance/gst_india/client_scripts/sales_invoice.js b/india_compliance/gst_india/client_scripts/sales_invoice.js
index d0c155138..d05296f79 100644
--- a/india_compliance/gst_india/client_scripts/sales_invoice.js
+++ b/india_compliance/gst_india/client_scripts/sales_invoice.js
@@ -83,8 +83,11 @@ function is_gst_invoice(frm) {
frm.doc.is_opening != "Yes" &&
frm.doc.company_gstin &&
frm.doc.company_gstin != frm.doc.billing_address_gstin &&
- !frm.doc.items.some(item => item.is_non_gst) &&
- !frm.doc.items.every(item => item.is_nil_exempt);
+ !frm.doc.items.some(item => item.gst_treatment == "Non-GST") &&
+ !frm.doc.items.every(
+ item =>
+ item.gst_treatment == "Nil-Rated" || item.gst_treatment == "Exempted"
+ );
if (frm.doc.place_of_supply === "96-Other Countries") {
return gst_invoice_conditions && frm.doc.is_export_with_gst;
diff --git a/india_compliance/gst_india/constants/custom_fields.py b/india_compliance/gst_india/constants/custom_fields.py
index ec075efe7..f3d075138 100644
--- a/india_compliance/gst_india/constants/custom_fields.py
+++ b/india_compliance/gst_india/constants/custom_fields.py
@@ -2,6 +2,7 @@
from india_compliance.gst_india.constants import (
GST_CATEGORIES,
+ GST_TAX_RATES,
PORT_CODES,
STATE_NUMBERS,
)
@@ -53,6 +54,17 @@
"insert_after": "parent_company",
},
*party_fields[1:],
+ {
+ "fieldname": "default_gst_rate",
+ "label": "Default GST Rate",
+ "fieldtype": "Select",
+ "options": "\n".join(str(f) for f in GST_TAX_RATES),
+ "description": "Sales / Purchase Taxes and Charges Template will be created based on this GST Rate",
+ "default": "18.0",
+ "depends_on": "eval:doc.country == 'India' && doc.__islocal",
+ "insert_after": "country",
+ "translatable": 0,
+ },
{
"fieldname": "default_customs_expense_account",
"label": "Default Customs Duty Expense Account",
@@ -268,9 +280,22 @@
"translatable": 0,
}
],
- # Transaction Item Fields
+ # Transaction Item: Tax Fields
+ "Material Request Item": [
+ {
+ "fieldname": "gst_hsn_code",
+ "label": "HSN/SAC",
+ "fieldtype": "Data",
+ "fetch_from": "item_code.gst_hsn_code",
+ "insert_after": "description",
+ "allow_on_submit": 1,
+ "print_hide": 1,
+ "fetch_if_empty": 1,
+ "translatable": 0,
+ },
+ ],
+ # Taxable Value and GST Details
(
- "Material Request Item",
"Supplier Quotation Item",
"Purchase Order Item",
"Purchase Receipt Item",
@@ -293,38 +318,135 @@
"translatable": 0,
},
{
- "fieldname": "is_nil_exempt",
- "label": "Is Nil Rated or Exempted",
- "fieldtype": "Check",
- "fetch_from": "item_code.is_nil_exempt",
- "insert_after": "gst_hsn_code",
- "print_hide": 1,
- },
- {
- "fieldname": "is_non_gst",
- "label": "Is Non GST",
- "fieldtype": "Check",
- "fetch_from": "item_code.is_non_gst",
- "insert_after": "is_nil_exempt",
+ "fieldname": "gst_treatment",
+ "label": "GST Treatment",
+ "fieldtype": "Autocomplete",
+ "options": "Taxable\nZero-Rated\nNil-Rated\nExempted\nNon-GST",
+ "fetch_from": "item_tax_template.gst_treatment",
+ "fetch_if_empty": 1,
+ "insert_after": "item_tax_template",
"print_hide": 1,
+ "read_only": 1,
+ "translatable": 0,
+ "no_copy": 1,
},
- ],
- # Taxable Value
- (
- "Delivery Note Item",
- "Sales Invoice Item",
- "POS Invoice Item",
- "Purchase Invoice Item",
- "Purchase Receipt Item",
- ): [
{
"fieldname": "taxable_value",
"label": "Taxable Value",
"fieldtype": "Currency",
"insert_after": "base_net_amount",
- "hidden": 1,
"options": "Company:company:default_currency",
+ "read_only": 1,
+ "translatable": 0,
+ "no_copy": 1,
"print_hide": 1,
+ "hidden": 0,
+ },
+ {
+ "fieldtype": "Section Break",
+ "label": "GST Details",
+ "insert_after": "taxable_value",
+ "fieldname": "gst_details_section",
+ "collapsible": 1,
+ },
+ {
+ "fieldname": "igst_rate",
+ "label": "IGST Rate",
+ "fieldtype": "Float",
+ "insert_after": "gst_details_section",
+ "read_only": 1,
+ "translatable": 0,
+ "no_copy": 1,
+ },
+ {
+ "fieldname": "cgst_rate",
+ "label": "CGST Rate",
+ "fieldtype": "Float",
+ "insert_after": "igst_rate",
+ "read_only": 1,
+ "translatable": 0,
+ "no_copy": 1,
+ },
+ {
+ "fieldname": "sgst_rate",
+ "label": "SGST Rate",
+ "fieldtype": "Float",
+ "insert_after": "cgst_rate",
+ "read_only": 1,
+ "translatable": 0,
+ "no_copy": 1,
+ },
+ {
+ "fieldname": "cess_rate",
+ "label": "CESS Rate",
+ "fieldtype": "Float",
+ "insert_after": "sgst_rate",
+ "read_only": 1,
+ "translatable": 0,
+ "no_copy": 1,
+ },
+ {
+ "fieldname": "cess_non_advol_rate",
+ "label": "CESS Non Advol Rate",
+ "fieldtype": "Float",
+ "insert_after": "cess_rate",
+ "read_only": 1,
+ "translatable": 0,
+ "no_copy": 1,
+ },
+ {
+ "fieldtype": "Column Break",
+ "insert_after": "cess_non_advol_rate",
+ "fieldname": "cb_gst_details",
+ },
+ {
+ "fieldname": "igst_amount",
+ "label": "IGST Amount",
+ "fieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "insert_after": "cb_gst_details",
+ "read_only": 1,
+ "translatable": 0,
+ "no_copy": 1,
+ },
+ {
+ "fieldname": "cgst_amount",
+ "label": "CGST Amount",
+ "fieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "insert_after": "igst_amount",
+ "read_only": 1,
+ "translatable": 0,
+ "no_copy": 1,
+ },
+ {
+ "fieldname": "sgst_amount",
+ "label": "SGST Amount",
+ "fieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "insert_after": "cgst_amount",
+ "read_only": 1,
+ "translatable": 0,
+ "no_copy": 1,
+ },
+ {
+ "fieldname": "cess_amount",
+ "label": "CESS Amount",
+ "fieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "insert_after": "sgst_amount",
+ "read_only": 1,
+ "translatable": 0,
+ "no_copy": 1,
+ },
+ {
+ "fieldname": "cess_non_advol_amount",
+ "label": "CESS Non Advol Amount",
+ "fieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "insert_after": "cess_amount",
+ "read_only": 1,
+ "translatable": 0,
"no_copy": 1,
},
],
@@ -339,7 +461,7 @@
"label": "Is Ineligible for Input Tax Credit",
"fieldtype": "Check",
"fetch_from": "item_code.is_ineligible_for_itc",
- "insert_after": "is_nil_exempt",
+ "insert_after": "gst_hsn_code",
"fetch_if_empty": 1,
"print_hide": 1,
},
@@ -674,22 +796,35 @@
"description": "You can search code by the description of the category.",
},
{
- "fieldname": "is_nil_exempt",
- "label": "Is Nil Rated or Exempted",
+ "fieldname": "is_ineligible_for_itc",
+ "label": "Is Ineligible for Input Tax Credit",
"fieldtype": "Check",
- "insert_after": "gst_hsn_code",
+ "insert_after": "item_tax_section_break",
},
+ ],
+ "Item Tax Template": [
{
- "fieldname": "is_non_gst",
- "label": "Is Non GST",
- "fieldtype": "Check",
- "insert_after": "is_nil_exempt",
+ "fieldname": "gst_treatment",
+ "label": "GST Treatment",
+ "fieldtype": "Autocomplete",
+ "default": "Taxable",
+ "options": "Taxable\nNil-Rated\nExempted\nNon-GST",
+ "insert_after": "column_break_3",
+ "translatable": 0,
},
{
- "fieldname": "is_ineligible_for_itc",
- "label": "Is Ineligible for Input Tax Credit",
- "fieldtype": "Check",
- "insert_after": "item_tax_section_break",
+ "fieldname": "gst_rate",
+ "label": "GST Rate",
+ "fieldtype": "Float",
+ "insert_after": "gst_treatment",
+ "depends_on": "eval:doc.gst_treatment == 'Taxable'",
+ "translatable": 0,
+ },
+ {
+ "fieldname": "fetch_gst_accounts",
+ "label": "Fetch GST Accounts",
+ "fieldtype": "Button",
+ "insert_after": "section_break_5",
},
],
}
diff --git a/india_compliance/gst_india/data/tax_defaults.json b/india_compliance/gst_india/data/tax_defaults.json
index 31f12c846..c8f9c263b 100644
--- a/india_compliance/gst_india/data/tax_defaults.json
+++ b/india_compliance/gst_india/data/tax_defaults.json
@@ -33,6 +33,8 @@
"item_tax_templates": [
{
"title": "GST 18%",
+ "gst_rate": 18,
+ "gst_treatment": "Taxable",
"taxes": [
{
"tax_type": {
@@ -95,6 +97,8 @@
},
{
"title": "GST 5%",
+ "gst_rate": 5,
+ "gst_treatment": "Taxable",
"taxes": [
{
"tax_type": {
@@ -157,6 +161,8 @@
},
{
"title": "GST 12%",
+ "gst_rate": 12,
+ "gst_treatment": "Taxable",
"taxes": [
{
"tax_type": {
@@ -219,6 +225,8 @@
},
{
"title": "GST 28%",
+ "gst_rate": 28,
+ "gst_treatment": "Taxable",
"taxes": [
{
"tax_type": {
@@ -278,6 +286,198 @@
}
}
]
+ },
+ {
+ "title": "Nil-Rated",
+ "gst_rate": 0,
+ "gst_treatment": "Nil-Rated",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Output Tax SGST",
+ "tax_rate": 0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Output Tax CGST",
+ "tax_rate": 0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Output Tax IGST",
+ "tax_rate": 0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax SGST",
+ "tax_rate": 0,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax CGST",
+ "tax_rate": 0,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax IGST",
+ "tax_rate": 0,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax SGST RCM",
+ "tax_rate": 0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax CGST RCM",
+ "tax_rate": 0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax IGST RCM",
+ "tax_rate": 0
+ }
+ }
+ ]
+ },
+ {
+ "title": "Exempted",
+ "gst_rate": 0,
+ "gst_treatment": "Exempted",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Output Tax SGST",
+ "tax_rate": 0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Output Tax CGST",
+ "tax_rate": 0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Output Tax IGST",
+ "tax_rate": 0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax SGST",
+ "tax_rate": 0,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax CGST",
+ "tax_rate": 0,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax IGST",
+ "tax_rate": 0,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax SGST RCM",
+ "tax_rate": 0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax CGST RCM",
+ "tax_rate": 0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax IGST RCM",
+ "tax_rate": 0
+ }
+ }
+ ]
+ },
+ {
+ "title": "Non-GST",
+ "gst_rate": 0,
+ "gst_treatment": "Non-GST",
+ "taxes": [
+ {
+ "tax_type": {
+ "account_name": "Output Tax SGST",
+ "tax_rate": 0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Output Tax CGST",
+ "tax_rate": 0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Output Tax IGST",
+ "tax_rate": 0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax SGST",
+ "tax_rate": 0,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax CGST",
+ "tax_rate": 0,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax IGST",
+ "tax_rate": 0,
+ "root_type": "Asset"
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax SGST RCM",
+ "tax_rate": 0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax CGST RCM",
+ "tax_rate": 0
+ }
+ },
+ {
+ "tax_type": {
+ "account_name": "Input Tax IGST RCM",
+ "tax_rate": 0
+ }
+ }
+ ]
}
],
"sales_tax_templates": [
diff --git a/india_compliance/gst_india/data/test_e_invoice.json b/india_compliance/gst_india/data/test_e_invoice.json
index 61089bbec..256b7aef1 100644
--- a/india_compliance/gst_india/data/test_e_invoice.json
+++ b/india_compliance/gst_india/data/test_e_invoice.json
@@ -207,6 +207,111 @@
]
}
},
+ "nil_exempted_item": {
+ "kwargs": {
+ "item_code": "_Test Nil Rated Item",
+ "customer_address": "_Test Registered Customer-Billing",
+ "shipping_address_name": "_Test Registered Customer-Billing",
+ "rate":100
+ },
+ "request_data": {
+ "BuyerDtls": {
+ "Addr1": "Test Address - 3",
+ "Gstin": "36AMBPG7773M002",
+ "LglNm": "_Test Registered Customer",
+ "Loc": "Test City",
+ "Pin": 500055,
+ "Pos": "02",
+ "Stcd": "36",
+ "TrdNm": "_Test Registered Customer"
+ },
+ "DocDtls": {
+ "Dt": "18/12/2023",
+ "No": "test_invoice_no",
+ "Typ": "INV"
+ },
+ "ItemList": [
+ {
+ "SlNo": "2",
+ "PrdDesc": "Test Trading Goods 1",
+ "IsServc": "N",
+ "HsnCd": "61149090",
+ "Unit": "NOS",
+ "Qty": 1.0,
+ "UnitPrice": 10.0,
+ "TotAmt": 10.0,
+ "Discount": 0,
+ "AssAmt": 10,
+ "GstRt": 12.0,
+ "IgstAmt": 0,
+ "CgstAmt": 0.6,
+ "SgstAmt": 0.6,
+ "CesRt": 0,
+ "CesAmt": 0,
+ "CesNonAdvlAmt": 0,
+ "TotItemVal": 11.2
+ }
+ ],
+ "PayDtls": {
+ "CrDay": 0,
+ "PaidAmt": 0,
+ "PaymtDue": 111.0
+ },
+ "SellerDtls": {
+ "Addr1": "Test Address - 1",
+ "Gstin": "02AMBPG7773M002",
+ "LglNm": "_Test Indian Registered Company",
+ "Loc": "Test City",
+ "Pin": 171302,
+ "Stcd": "02",
+ "TrdNm": "_Test Indian Registered Company"
+ },
+ "TranDtls": {
+ "RegRev": "N",
+ "SupTyp": "B2B",
+ "TaxSch": "GST"
+ },
+ "ValDtls": {
+ "AssVal": 10.0,
+ "CesVal": 0,
+ "CgstVal": 0.6,
+ "Discount": 0.0,
+ "IgstVal": 0,
+ "OthChrg": 100.0,
+ "RndOffAmt": -0.2,
+ "SgstVal": 0.6,
+ "TotInvVal": 111.0
+ },
+ "Version": "1.1"
+ },
+ "response_data": {
+ "success": true,
+ "message": "IRN generated successfully",
+ "result": {
+ "AckNo": 232210036754863,
+ "AckDt": "2022-09-17 16:26:00",
+ "Irn": "68fb4fab44aee99fb23292478c4bd838e664837c9f1b04e3d9134ffed0b40b60",
+ "SignedInvoice": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiQWNrTm9cIjoyMzIyMTAwMzY3NTQ4NjMsXCJBY2tEdFwiOlwiMjAyMi0wOS0xNyAxNjoyNjowMFwiLFwiSXJuXCI6XCI2OGZiNGZhYjQ0YWVlOTlmYjIzMjkyNDc4YzRiZDgzOGU2NjQ4MzdjOWYxYjA0ZTNkOTEzNGZmZWQwYjQwYjYwXCIsXCJWZXJzaW9uXCI6XCIxLjFcIixcIlRyYW5EdGxzXCI6e1wiVGF4U2NoXCI6XCJHU1RcIixcIlN1cFR5cFwiOlwiQjJCXCIsXCJSZWdSZXZcIjpcIk5cIn0sXCJEb2NEdGxzXCI6e1wiVHlwXCI6XCJJTlZcIixcIk5vXCI6XCJnMnF4aFlcIixcIkR0XCI6XCIxNy8wOS8yMDIyXCJ9LFwiU2VsbGVyRHRsc1wiOntcIkdzdGluXCI6XCIwMUFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJfVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJUcmRObVwiOlwiVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJBZGRyMVwiOlwiVGVzdCBBZGRyZXNzIC0gMVwiLFwiTG9jXCI6XCJUZXN0IENpdHlcIixcIlBpblwiOjE5MzUwMSxcIlN0Y2RcIjpcIjAxXCJ9LFwiQnV5ZXJEdGxzXCI6e1wiR3N0aW5cIjpcIjM2QU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIl9UZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlBvc1wiOlwiMDFcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJEaXNwRHRsc1wiOntcIk5tXCI6XCJUZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAxXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6MTkzNTAxLFwiU3RjZFwiOlwiMDFcIn0sXCJTaGlwRHRsc1wiOntcIkdzdGluXCI6XCIzNkFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJJdGVtTGlzdFwiOlt7XCJJdGVtTm9cIjowLFwiU2xOb1wiOlwiMVwiLFwiSXNTZXJ2Y1wiOlwiWVwiLFwiUHJkRGVzY1wiOlwiVGVzdCBTZXJ2aWNlIEl0ZW1cIixcIkhzbkNkXCI6XCI5OTU0MTFcIixcIlF0eVwiOjEuMCxcIlVuaXRcIjpcIk5PU1wiLFwiVW5pdFByaWNlXCI6MTAwLjAsXCJUb3RBbXRcIjoxMDAuMCxcIkRpc2NvdW50XCI6MCxcIkFzc0FtdFwiOjEwMC4wLFwiR3N0UnRcIjowLjAsXCJJZ3N0QW10XCI6MCxcIkNnc3RBbXRcIjowLFwiU2dzdEFtdFwiOjAsXCJDZXNSdFwiOjAsXCJDZXNBbXRcIjowLFwiQ2VzTm9uQWR2bEFtdFwiOjAsXCJUb3RJdGVtVmFsXCI6MTAwLjB9XSxcIlZhbER0bHNcIjp7XCJBc3NWYWxcIjoxMDAuMCxcIkNnc3RWYWxcIjowLFwiU2dzdFZhbFwiOjAsXCJJZ3N0VmFsXCI6MCxcIkNlc1ZhbFwiOjAsXCJEaXNjb3VudFwiOjAsXCJPdGhDaHJnXCI6MC4wLFwiUm5kT2ZmQW10XCI6MC4wLFwiVG90SW52VmFsXCI6MTAwLjB9LFwiUGF5RHRsc1wiOntcIkNyRGF5XCI6MCxcIlBhaWRBbXRcIjowLFwiUGF5bXREdWVcIjoxMDAuMH0sXCJFd2JEdGxzXCI6e1wiRGlzdGFuY2VcIjowfX0iLCJpc3MiOiJOSUMifQ.rcfXkciqDJypX-xCqaUU3xAk2gccHK_qBD_FBIUsEr-SyWVs4LStgXwQEWUhTYnEfcGGm_sWX15ewC0jn9iWVFCNNFnjKc8vsFQqnbzvi-bnr6CWkjRXOxVqQTfis6PbtTrblojBq2hhBaT1B_ZlgLePi5qFNWnxxaHItjYtBEeBzW5JxzXWQTqESrBy02iLQgQMOexmQ6jKGdUR3tRRG5MVB7QfXbL9BpQr-DXbHhbDGllT2S_xyj9SBsN6ICeluCG-ZJdHE_kCoIxc8iY_nXneb2PsBciij14cHb96h4ddNZtapxTPTh4CumVDmAgLBSVsBTugp8vm0L-jd7n3dg",
+ "SignedQRCode": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiU2VsbGVyR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiQnV5ZXJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJEb2NOb1wiOlwiZzJxeGhZXCIsXCJEb2NUeXBcIjpcIklOVlwiLFwiRG9jRHRcIjpcIjE3LzA5LzIwMjJcIixcIlRvdEludlZhbFwiOjEwMC4wLFwiSXRlbUNudFwiOjEsXCJNYWluSHNuQ29kZVwiOlwiOTk1NDExXCIsXCJJcm5cIjpcIjY4ZmI0ZmFiNDRhZWU5OWZiMjMyOTI0NzhjNGJkODM4ZTY2NDgzN2M5ZjFiMDRlM2Q5MTM0ZmZlZDBiNDBiNjBcIixcIklybkR0XCI6XCIyMDIyLTA5LTE3IDE2OjI2OjAwXCJ9IiwiaXNzIjoiTklDIn0.A99BPXfKiGSNjnEcqmxc7RGutWakaeW0NMan9oC5yMw6zAoTNcVc34GtQKV7iajBZQhyiFrNwn5n6QtYOXafpitHcI_yrUWSojQBPPpPlslqj4hbnbCy7kmOZZ8mOKISHJrsIZJxpjRlSquIzfDN4aP1aT_qHDqwFqyA8RyJM-id5EpDaTrUFK12HwjKfAXHn4shEUBBgEWrHYOKK6VdpCNi6F_5I5bbJRivrvUJxMLjk3Ux9fyylqnEeyE2NThs9hFuV9EgoVzGE3FhfPZsooToAuG_npYEv3f6Q9KbOw3pNQ3NkqFwvmFjfNJLXdbxZIPe9fe9F1c-CRrIoNo_9w",
+ "Status": "ACT",
+ "EwbNo": null,
+ "EwbDt": null,
+ "EwbValidTill": null,
+ "Remarks": null
+ },
+ "info": [
+ {
+ "InfCd": "EWBERR",
+ "Desc": [
+ {
+ "ErrorCode": "4019",
+ "ErrorMessage": "Provide Transporter ID in order to generate Part A of e-Way Bill"
+ }
+ ]
+ }
+ ]
+ }
+ },
"return_invoice": {
"kwargs": {
"qty": -1,
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 d8c15a311..ff6840605 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
@@ -220,11 +220,12 @@ def _get_tax_amount(account_type):
def get_inward_nil_exempt(self, state):
inward_nil_exempt = frappe.db.sql(
"""
- SELECT p.place_of_supply, p.supplier_address,i.taxable_value, i.is_nil_exempt, i.is_non_gst
+ SELECT p.place_of_supply, p.supplier_address,
+ i.taxable_value, i.gst_treatment
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
WHERE p.docstatus = 1 and p.name = i.parent
and p.is_opening = 'No'
- and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and
+ and (i.gst_treatment != 'Taxable' or p.gst_category = 'Registered Composition') and
month(p.posting_date) = %s and year(p.posting_date) = %s
and p.company = %s and p.company_gstin = %s
""",
@@ -244,26 +245,21 @@ def get_inward_nil_exempt(self, state):
d.place_of_supply = "00-" + cstr(state)
supplier_state = address_state_map.get(d.supplier_address) or state
+ is_intra_state = cstr(supplier_state) == cstr(
+ d.place_of_supply.split("-")[1]
+ )
amount = flt(d.taxable_value, 2)
- if (
- d.is_nil_exempt == 1
- or d.get("gst_category") == "Registered Composition"
- ) and cstr(supplier_state) == cstr(d.place_of_supply.split("-")[1]):
- inward_nil_exempt_details["gst"]["intra"] += amount
- elif (
- d.is_nil_exempt == 1
- or d.get("gst_category") == "Registered Composition"
- ) and cstr(supplier_state) != cstr(d.place_of_supply.split("-")[1]):
- inward_nil_exempt_details["gst"]["inter"] += amount
- elif d.is_non_gst == 1 and cstr(supplier_state) == cstr(
- d.place_of_supply.split("-")[1]
- ):
- inward_nil_exempt_details["non_gst"]["intra"] += amount
- elif d.is_non_gst == 1 and cstr(supplier_state) != cstr(
- d.place_of_supply.split("-")[1]
- ):
- inward_nil_exempt_details["non_gst"]["inter"] += amount
+ if d.gst_treatment != "Non-GST":
+ if is_intra_state:
+ inward_nil_exempt_details["gst"]["intra"] += amount
+ else:
+ inward_nil_exempt_details["gst"]["inter"] += amount
+ else:
+ if is_intra_state:
+ inward_nil_exempt_details["non_gst"]["intra"] += amount
+ else:
+ inward_nil_exempt_details["non_gst"]["inter"] += amount
return inward_nil_exempt_details
@@ -300,7 +296,7 @@ def get_outward_tax_invoices(self, doctype, reverse_charge=None):
def get_outward_items(self, doctype):
self.invoice_items = frappe._dict()
- self.is_nil_exempt = []
+ self.is_nil_or_exempt = []
self.is_non_gst = []
if not self.invoice_map:
@@ -310,7 +306,7 @@ def get_outward_items(self, doctype):
f"""
SELECT
item_code, parent, taxable_value, item_tax_rate,
- is_nil_exempt, is_non_gst
+ gst_treatment
FROM
`tab{doctype} Item`
WHERE parent in ({", ".join(["%s"] * len(self.invoice_map))})
@@ -323,10 +319,16 @@ def get_outward_items(self, doctype):
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0)
- if d.is_nil_exempt and d.item_code not in self.is_nil_exempt:
- self.is_nil_exempt.append(d.item_code)
+ is_nil_rated = d.gst_treatment == "Nil-Rated"
+ is_exempted = d.gst_treatment == "Exempted"
+ is_non_gst = d.gst_treatment == "Non-GST"
- if d.is_non_gst and d.item_code not in self.is_non_gst:
+ if (
+ is_nil_rated or is_exempted
+ ) and d.item_code not in self.is_nil_or_exempt:
+ self.is_nil_or_exempt.append(d.item_code)
+
+ if is_non_gst and d.item_code not in self.is_non_gst:
self.is_non_gst.append(d.item_code)
def get_outward_tax_details(self, doctype):
@@ -372,7 +374,9 @@ def get_outward_tax_details(self, doctype):
if not (
cgst_or_sgst
or account in self.account_heads.get("iamt")
- or (item_code in self.is_non_gst + self.is_nil_exempt)
+ or (
+ item_code in self.is_non_gst + self.is_nil_or_exempt
+ )
):
continue
@@ -411,7 +415,7 @@ def get_outward_tax_details(self, doctype):
else:
for item in items.keys():
if (
- item in self.is_nil_exempt + self.is_non_gst
+ item in self.is_nil_or_exempt + self.is_non_gst
and item
not in self.items_based_on_tax_rate.get(invoice, {}).get(0, [])
):
@@ -433,7 +437,7 @@ def set_outward_taxable_supplies(self):
for rate, items in items_based_on_rate.items():
for item_code, taxable_value in self.invoice_items.get(inv).items():
if item_code in items:
- if item_code in self.is_nil_exempt:
+ if item_code in self.is_nil_or_exempt:
self.report_dict["sup_details"]["osup_nil_exmp"][
"txval"
] += taxable_value
diff --git a/india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py b/india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py
index 1f425c164..73b01f2ca 100644
--- a/india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py
+++ b/india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py
@@ -55,7 +55,6 @@ def test_gstr_3b_report(self):
).insert()
output = json.loads(report.json_output)
-
self.assertEqual(output["sup_details"]["osup_det"]["iamt"], 18)
self.assertEqual(output["sup_details"]["osup_det"]["txval"], 300)
self.assertEqual(output["sup_details"]["isup_rev"]["txval"], 100)
@@ -82,7 +81,7 @@ def test_gst_rounding(self):
def create_sales_invoices():
create_sales_invoice(is_in_state=True)
- create_sales_invoice(is_nil_exempt=True)
+ create_sales_invoice(item_code="_Test Nil Rated Item")
create_sales_invoice(
customer="_Test Registered Composition Customer",
is_out_state=True,
diff --git a/india_compliance/gst_india/overrides/company.py b/india_compliance/gst_india/overrides/company.py
index 63a267d58..321109dd2 100644
--- a/india_compliance/gst_india/overrides/company.py
+++ b/india_compliance/gst_india/overrides/company.py
@@ -1,4 +1,5 @@
import frappe
+from frappe.utils import flt
from erpnext.setup.setup_wizard.operations.taxes_setup import from_detailed_data
from india_compliance.gst_india.utils import get_data_file_path
@@ -22,11 +23,14 @@ def make_company_fixtures(doc, method=None):
if not frappe.flags.country_change or doc.country != "India":
return
- create_company_fixtures(doc.name)
+ create_company_fixtures(doc.name, doc.default_gst_rate)
-def create_company_fixtures(company):
- make_default_tax_templates(company)
+def create_company_fixtures(company, gst_rate=None):
+ if not frappe.flags.in_setup_wizard:
+ # Manual Trigger in Setup Wizard with custom rate
+ make_default_tax_templates(company, gst_rate)
+
make_default_customs_accounts(company)
make_default_gst_expense_accounts(company)
@@ -57,14 +61,44 @@ def make_default_gst_expense_accounts(company):
@frappe.whitelist()
-def make_default_tax_templates(company: str):
+def make_default_tax_templates(company: str, gst_rate=None):
frappe.has_permission("Company", ptype="write", doc=company, throw=True)
- default_taxes = frappe.get_file_json(get_data_file_path("tax_defaults.json"))
+ default_taxes = get_tax_defaults(gst_rate)
from_detailed_data(company, default_taxes)
update_gst_settings(company)
+def get_tax_defaults(gst_rate=None):
+ if not gst_rate:
+ gst_rate = 18
+
+ default_taxes = frappe.get_file_json(get_data_file_path("tax_defaults.json"))
+
+ gst_rate = flt(gst_rate, 3)
+ if gst_rate == 18:
+ return default_taxes
+
+ return modify_tax_defaults(default_taxes, gst_rate)
+
+
+def modify_tax_defaults(default_taxes, gst_rate):
+ # Identifying new_rate based on existing rate
+ for template_type in ("sales_tax_templates", "purchase_tax_templates"):
+ template = default_taxes["chart_of_accounts"]["*"][template_type]
+ for tax in template:
+ for row in tax.get("taxes"):
+ rate = (
+ gst_rate
+ if row["account_head"]["tax_rate"] == 18
+ else flt(gst_rate / 2, 3)
+ )
+
+ row["account_head"]["tax_rate"] = rate
+
+ return default_taxes
+
+
def update_gst_settings(company):
# Will only add default GST accounts if present
input_account_names = ["Input Tax CGST", "Input Tax SGST", "Input Tax IGST"]
diff --git a/india_compliance/gst_india/overrides/item_tax_template.py b/india_compliance/gst_india/overrides/item_tax_template.py
new file mode 100644
index 000000000..37d55cd33
--- /dev/null
+++ b/india_compliance/gst_india/overrides/item_tax_template.py
@@ -0,0 +1,63 @@
+import frappe
+from frappe import _
+from frappe.utils import rounded
+
+from india_compliance.gst_india.overrides.transaction import get_valid_accounts
+
+
+def validate(doc, method=None):
+ validate_zero_tax_options(doc)
+ validate_tax_rates(doc)
+
+
+def validate_zero_tax_options(doc):
+ if doc.gst_treatment != "Taxable":
+ doc.gst_rate = 0
+ return
+
+ if doc.gst_rate == 0:
+ frappe.throw(
+ _("GST Rate cannot be zero for Taxable GST Treatment"),
+ title=_("Invalid GST Rate"),
+ )
+
+
+def validate_tax_rates(doc):
+ if doc.gst_rate < 0 or doc.gst_rate > 100:
+ frappe.throw(
+ _("GST Rate should be between 0 and 100"), title=_("Invalid GST Rate")
+ )
+
+ if not doc.taxes:
+ return
+
+ __, intra_state_accounts, inter_state_accounts = get_valid_accounts(
+ doc.company, for_sales=True, for_purchase=True, throw=False
+ )
+
+ if not intra_state_accounts and not inter_state_accounts:
+ return
+
+ invalid_tax_rates = {}
+ for row in doc.taxes:
+ # check intra state
+ if row.tax_type in intra_state_accounts and doc.gst_rate != row.tax_rate * 2:
+ invalid_tax_rates[row.idx] = doc.gst_rate / 2
+
+ # check inter state
+ elif row.tax_type in inter_state_accounts and doc.gst_rate != row.tax_rate:
+ invalid_tax_rates[row.idx] = doc.gst_rate
+
+ if not invalid_tax_rates:
+ return
+
+ # throw
+ message = (
+ "Plese make sure account tax rates are in sync with GST rate mentioned."
+ " Following rows have inconsistant tax rates:
"
+ )
+
+ for idx, tax_rate in invalid_tax_rates.items():
+ message += f"Row #{idx} - should be {rounded(tax_rate, 2)}%
"
+
+ frappe.throw(_(message), title=_("Invalid Tax Rates"))
diff --git a/india_compliance/gst_india/overrides/purchase_invoice.py b/india_compliance/gst_india/overrides/purchase_invoice.py
index d85ae04fd..62ef22905 100644
--- a/india_compliance/gst_india/overrides/purchase_invoice.py
+++ b/india_compliance/gst_india/overrides/purchase_invoice.py
@@ -65,7 +65,7 @@ def is_b2b_invoice(doc):
or doc.gst_category in ["Registered Composition", "Unregistered", "Overseas"]
or doc.supplier_gstin == doc.company_gstin
or doc.is_opening == "Yes"
- or any(row for row in doc.items if row.is_non_gst == 1)
+ or any(row for row in doc.items if row.gst_treatment == "Non-GST")
)
diff --git a/india_compliance/gst_india/overrides/test_company.py b/india_compliance/gst_india/overrides/test_company.py
index 72a5e04cf..c2fe84047 100644
--- a/india_compliance/gst_india/overrides/test_company.py
+++ b/india_compliance/gst_india/overrides/test_company.py
@@ -1,6 +1,8 @@
import frappe
from frappe.tests.utils import FrappeTestCase
+from india_compliance.gst_india.overrides.company import get_tax_defaults
+
class TestCompanyFixtures(FrappeTestCase):
@classmethod
@@ -31,3 +33,18 @@ def tearDownClass(cls):
def test_tax_defaults_setup(self):
# Check for tax category creations.
self.assertTrue(frappe.db.exists("Tax Category", "Reverse Charge In-State"))
+
+ def test_get_tax_defaults(self):
+ gst_rate = 12
+ default_taxes = get_tax_defaults(gst_rate)
+
+ for template_type in ("sales_tax_templates", "purchase_tax_templates"):
+ template = default_taxes["chart_of_accounts"]["*"][template_type]
+ for tax in template:
+ for row in tax.get("taxes"):
+ expected_rate = (
+ gst_rate
+ if "IGST" in row["account_head"]["account_name"]
+ else gst_rate / 2
+ )
+ self.assertEqual(row["account_head"]["tax_rate"], expected_rate)
diff --git a/india_compliance/gst_india/overrides/test_item_tax_template.py b/india_compliance/gst_india/overrides/test_item_tax_template.py
new file mode 100644
index 000000000..5bab7593c
--- /dev/null
+++ b/india_compliance/gst_india/overrides/test_item_tax_template.py
@@ -0,0 +1,109 @@
+import re
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+from india_compliance.gst_india.overrides.transaction import get_valid_accounts
+
+# Creation of Item tax template for indian and foreign company
+# Validation of GST Rate
+
+
+class TestTransaction(FrappeTestCase):
+ def test_item_tax_template_for_foreign_company(self):
+ doc = create_item_tax_template(
+ company="_Test Foreign Company", gst_rate=0, gst_treatment="Exempt"
+ )
+ self.assertTrue(doc.gst_rate == 0)
+ self.assertTrue(doc.gst_treatment == "Exempt")
+
+ def test_item_tax_template_for_indian_company(self):
+ doc = create_item_tax_template()
+ self.assertTrue(doc.gst_rate == 18)
+ self.assertTrue(doc.gst_treatment == "Taxable")
+
+ def test_validate_zero_tax_options(self):
+ doc = create_item_tax_template(gst_rate=0, do_not_save=True)
+ self.assertRaisesRegex(
+ frappe.exceptions.ValidationError,
+ re.compile(r"^(GST Rate cannot be zero for.*)$"),
+ doc.insert,
+ )
+
+ def test_validate_tax_rates(self):
+ doc = create_item_tax_template(do_not_save=True)
+ doc.gst_rate = 110
+
+ self.assertRaisesRegex(
+ frappe.exceptions.ValidationError,
+ re.compile(r"^(GST Rate should be between 0 and 100)$"),
+ doc.insert,
+ )
+
+ doc.gst_rate = 18
+ doc.taxes[1].tax_rate = 2
+
+ self.assertRaisesRegex(
+ frappe.exceptions.ValidationError,
+ re.compile(r"^(Plese make sure account tax rates.*)$"),
+ doc.save,
+ )
+
+
+def create_item_tax_template(**data):
+ doc = frappe.new_doc("Item Tax Template")
+ gst_rate = data.get("gst_rate") if data.get("gst_rate") is not None else 18
+ doc.update(
+ {
+ "company": data.get("company") or "_Test Indian Registered Company",
+ "title": frappe.generate_hash("", 10),
+ "gst_treatment": data.get("gst_treatment") or "Taxable",
+ "gst_rate": gst_rate,
+ }
+ )
+
+ if data.get("taxes"):
+ doc.extend("taxes", data.get("taxes"))
+
+ return save_item_tax_template(doc, data)
+
+ __, intra_state_accounts, inter_state_accounts = get_valid_accounts(
+ doc.company, for_sales=True, for_purchase=True, throw=False
+ )
+
+ if not intra_state_accounts and not inter_state_accounts:
+ intra_state_accounts = frappe.get_all(
+ "Account",
+ filters={
+ "company": doc.company,
+ "account_type": "Tax",
+ },
+ pluck="name",
+ )
+
+ for account in intra_state_accounts:
+ doc.append(
+ "taxes",
+ {
+ "tax_type": account,
+ "tax_rate": gst_rate / 2,
+ },
+ )
+
+ for account in inter_state_accounts:
+ doc.append(
+ "taxes",
+ {
+ "tax_type": account,
+ "tax_rate": gst_rate,
+ },
+ )
+
+ return save_item_tax_template(doc, data)
+
+
+def save_item_tax_template(doc, data):
+ if data.get("do_not_save"):
+ return doc
+
+ return doc.insert()
diff --git a/india_compliance/gst_india/overrides/test_transaction.py b/india_compliance/gst_india/overrides/test_transaction.py
index d169fb862..cb48f7320 100644
--- a/india_compliance/gst_india/overrides/test_transaction.py
+++ b/india_compliance/gst_india/overrides/test_transaction.py
@@ -3,12 +3,14 @@
from parameterized import parameterized_class
import frappe
-from frappe.tests.utils import FrappeTestCase
+from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import today
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
+from erpnext.accounts.party import _get_party_details
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
from india_compliance.gst_india.constants import SALES_DOCTYPES
-from india_compliance.gst_india.overrides.transaction import DOCTYPES_WITH_TAXABLE_VALUE
+from india_compliance.gst_india.overrides.transaction import DOCTYPES_WITH_GST_DETAIL
from india_compliance.gst_india.utils.tests import (
_append_taxes,
append_item,
@@ -36,6 +38,7 @@ class TestTransaction(FrappeTestCase):
def setUpClass(cls):
frappe.db.savepoint("before_test_transaction")
cls.is_sales_doctype = cls.doctype in SALES_DOCTYPES
+ create_cess_accounts()
@classmethod
def tearDownClass(cls):
@@ -301,7 +304,7 @@ def test_gst_category_without_gstin(self):
frappe.db.set_value("Address", address, "gstin", gstin)
def test_taxable_value_with_charges(self):
- if self.doctype not in DOCTYPES_WITH_TAXABLE_VALUE:
+ if self.doctype not in DOCTYPES_WITH_GST_DETAIL:
return
doc = create_transaction(**self.transaction_details, do_not_save=True)
@@ -327,7 +330,7 @@ def test_taxable_value_with_charges(self):
self.assertDocumentEqual({"taxable_value": 120}, doc.items[0]) # 100 + 20
def test_taxable_value_with_charges_after_tax(self):
- if self.doctype not in DOCTYPES_WITH_TAXABLE_VALUE:
+ if self.doctype not in DOCTYPES_WITH_GST_DETAIL:
return
doc = create_transaction(
@@ -469,6 +472,105 @@ def test_purchase_from_unregistered_supplier(self):
doc.insert,
)
+ def test_invalid_charge_type_as_actual(self):
+ doc = create_transaction(**self.transaction_details, do_not_save=True)
+ _append_taxes(doc, ["CGST", "SGST"], charge_type="Actual", tax_amount=9)
+
+ self.assertRaisesRegex(
+ frappe.exceptions.ValidationError,
+ re.compile(
+ r"^(.*Charge Type is set to Actual. However, this would not compute item taxes.*)$"
+ ),
+ doc.save,
+ )
+
+ def test_invalid_charge_type_for_cess_non_advol(self):
+ doc = create_transaction(**self.transaction_details, do_not_save=True)
+ _append_taxes(doc, ["CGST", "SGST"], charge_type="On Item Quantity")
+
+ self.assertRaisesRegex(
+ frappe.exceptions.ValidationError,
+ re.compile(r"^(.*as it is not a Cess Non Advol Account.*)$"),
+ doc.save,
+ )
+
+ doc = create_transaction(**self.transaction_details, do_not_save=True)
+ _append_taxes(doc, ["CGST", "SGST", "Cess Non Advol"])
+
+ self.assertRaisesRegex(
+ frappe.exceptions.ValidationError,
+ re.compile(r"^(.*as it is a Cess Non Advol Account.*)$"),
+ doc.save,
+ )
+
+ def test_gst_details_set_correctly(self):
+ doc = create_transaction(
+ **self.transaction_details, rate=200, is_in_state=True, do_not_save=True
+ )
+ _append_taxes(doc, "Cess Non Advol", charge_type="On Item Quantity", rate=20)
+ doc.insert()
+ self.assertDocumentEqual(
+ {
+ "gst_treatment": "Taxable",
+ "igst_rate": 0,
+ "cgst_rate": 9,
+ "sgst_rate": 9,
+ "cess_non_advol_rate": 20,
+ "igst_amount": 0,
+ "cgst_amount": 18,
+ "sgst_amount": 18,
+ "cess_non_advol_amount": 20,
+ },
+ doc.items[0],
+ )
+
+ # test non gst treatment
+ doc = create_transaction(
+ **self.transaction_details, item_code="_Test Non GST Item"
+ )
+ self.assertDocumentEqual(
+ {"gst_treatment": "Non-GST"},
+ doc.items[0],
+ )
+
+ @change_settings("GST Settings", {"enable_overseas_transactions": 1})
+ def test_gst_treatment_for_exports(self):
+ if not self.is_sales_doctype:
+ return
+
+ doc = create_transaction(
+ **self.transaction_details,
+ is_in_state=True,
+ )
+ self.assertEqual(doc.items[0].gst_treatment, "Taxable")
+
+ # Update Customer after it's already set
+ doc_details = {
+ **self.transaction_details,
+ "customer": "_Test Foreign Customer",
+ "party_name": "_Test Foreign Customer",
+ }
+ doc = create_transaction(**doc_details, do_not_submit=True)
+ self.assertEqual(doc.items[0].gst_treatment, "Zero-Rated")
+
+ party_field = "party_name" if self.doctype == "Quotation" else "customer"
+
+ customer = "_Test Registered Customer"
+ doc.update(
+ {
+ party_field: customer,
+ **_get_party_details(
+ party=customer,
+ company=doc.company,
+ posting_date=today(),
+ doctype=doc.doctype,
+ ),
+ }
+ )
+ doc.selling_price_list = "Standard Selling"
+ doc.save()
+ self.assertEqual(doc.items[0].gst_treatment, "Taxable")
+
def test_purchase_with_different_place_of_supply(self):
if self.is_sales_doctype:
return
@@ -649,3 +751,47 @@ def test_copy_e_waybill_fields_from_si_to_return(self):
si_return = make_sales_return(si.name)
self.assertEqual(si_return.vehicle_no, None)
+
+
+def create_cess_accounts():
+ input_cess_non_advol_account = create_tax_accounts("Input Tax Cess Non Advol")
+ output_cess_non_advol_account = create_tax_accounts("Output Tax Cess Non Advol")
+ input_cess_account = create_tax_accounts("Input Tax Cess")
+ output_cess_account = create_tax_accounts("Output Tax Cess")
+
+ settings = frappe.get_doc("GST Settings")
+ for row in settings.gst_accounts:
+ if row.company != "_Test Indian Registered Company":
+ continue
+
+ if row.account_type == "Input":
+ row.cess_account = input_cess_account.name
+ row.cess_non_advol_account = input_cess_non_advol_account.name
+
+ if row.account_type == "Output":
+ row.cess_account = output_cess_account.name
+ row.cess_non_advol_account = output_cess_non_advol_account.name
+
+ settings.save()
+
+
+def create_tax_accounts(account_name):
+ defaults = {
+ "company": "_Test Indian Registered Company",
+ "doctype": "Account",
+ "account_type": "Tax",
+ "is_group": 0,
+ }
+
+ if "Input" in account_name:
+ parent_account = "Tax Assets - _TIRC"
+ else:
+ parent_account = "Duties and Taxes - _TIRC"
+
+ return frappe.get_doc(
+ {
+ "account_name": account_name,
+ "parent_account": parent_account,
+ **defaults,
+ }
+ ).save()
diff --git a/india_compliance/gst_india/overrides/test_transaction_data.py b/india_compliance/gst_india/overrides/test_transaction_data.py
index 2f0ba5b64..bafc3de3c 100644
--- a/india_compliance/gst_india/overrides/test_transaction_data.py
+++ b/india_compliance/gst_india/overrides/test_transaction_data.py
@@ -152,6 +152,8 @@ def test_set_transaction_details(self):
"party_name": "_Test Registered Customer",
"date": format_date(frappe.utils.today(), "dd/mm/yyyy"),
"total": 100.0,
+ "total_taxable_value": 100.0,
+ "total_non_taxable_value": 0.0,
"rounding_adjustment": 0.0,
"grand_total": 100.0,
"grand_total_in_foreign_currency": "",
@@ -193,6 +195,8 @@ def test_set_transaction_details_with_other_charges(self):
"party_name": "_Test Registered Customer",
"date": format_date(frappe.utils.today(), "dd/mm/yyyy"),
"total": 100.0,
+ "total_taxable_value": 100.0,
+ "total_non_taxable_value": 0.0,
"rounding_adjustment": -0.18,
"grand_total": 119.0,
"grand_total_in_foreign_currency": "",
@@ -255,6 +259,7 @@ def test_get_all_item_details(self):
"cess_non_advol_rate": 0,
"tax_rate": 0.0,
"total_value": 100.0,
+ "gst_treatment": "Taxable",
}
],
)
@@ -287,6 +292,7 @@ def test_get_all_item_details(self):
"cess_non_advol_rate": 0,
"tax_rate": 18.0,
"total_value": 236.0,
+ "gst_treatment": "Taxable",
}
],
)
diff --git a/india_compliance/gst_india/overrides/transaction.py b/india_compliance/gst_india/overrides/transaction.py
index 2069b27e5..5f95e865e 100644
--- a/india_compliance/gst_india/overrides/transaction.py
+++ b/india_compliance/gst_india/overrides/transaction.py
@@ -10,7 +10,11 @@
get_itemised_taxable_amount,
)
-from india_compliance.gst_india.constants import SALES_DOCTYPES, STATE_NUMBERS
+from india_compliance.gst_india.constants import (
+ GST_TAX_TYPES,
+ SALES_DOCTYPES,
+ STATE_NUMBERS,
+)
from india_compliance.gst_india.constants.custom_fields import E_WAYBILL_INV_FIELDS
from india_compliance.gst_india.doctype.gstin.gstin import (
_validate_gstin_info,
@@ -18,6 +22,7 @@
)
from india_compliance.gst_india.utils import (
get_all_gst_accounts,
+ get_gst_accounts_by_tax_type,
get_gst_accounts_by_type,
get_hsn_settings,
get_place_of_supply,
@@ -30,9 +35,13 @@
get_tax_withholding_accounts,
)
-DOCTYPES_WITH_TAXABLE_VALUE = {
+DOCTYPES_WITH_GST_DETAIL = {
+ "Supplier Quotation",
+ "Purchase Order",
"Purchase Receipt",
"Purchase Invoice",
+ "Quotation",
+ "Sales Order",
"Delivery Note",
"Sales Invoice",
"POS Invoice",
@@ -40,7 +49,7 @@
def update_taxable_values(doc, valid_accounts):
- if doc.doctype not in DOCTYPES_WITH_TAXABLE_VALUE:
+ if doc.doctype not in DOCTYPES_WITH_GST_DETAIL:
return
total_charges = 0
@@ -119,7 +128,7 @@ def get_tds_amount(doc):
def is_indian_registered_company(doc):
- if not doc.company_gstin:
+ if not doc.get("company_gstin"):
country, gst_category = frappe.get_cached_value(
"Company", doc.company, ("country", "gst_category")
)
@@ -152,24 +161,34 @@ def validate_mandatory_fields(doc, fields, error_message=None):
)
-def get_valid_accounts(company, is_sales_transaction=False):
+@frappe.whitelist()
+def get_valid_gst_accounts(company):
+ frappe.has_permission("Item Tax Template", "read", throw=True)
+ return get_valid_accounts(company, for_sales=True, for_purchase=True, throw=False)
+
+
+def get_valid_accounts(company, *, for_sales=False, for_purchase=False, throw=True):
all_valid_accounts = []
intra_state_accounts = []
inter_state_accounts = []
- def add_to_valid_accounts(account_type):
- accounts = get_gst_accounts_by_type(company, account_type)
+ account_types = []
+ if for_sales:
+ account_types.append("Output")
+
+ if for_purchase:
+ account_types.extend(["Input", "Reverse Charge"])
+
+ for account_type in account_types:
+ accounts = get_gst_accounts_by_type(company, account_type, throw=throw)
+ if not accounts:
+ continue
+
all_valid_accounts.extend(accounts.values())
intra_state_accounts.append(accounts.cgst_account)
intra_state_accounts.append(accounts.sgst_account)
inter_state_accounts.append(accounts.igst_account)
- if is_sales_transaction:
- add_to_valid_accounts("Output")
- else:
- add_to_valid_accounts("Input")
- add_to_valid_accounts("Reverse Charge")
-
return all_valid_accounts, intra_state_accounts, inter_state_accounts
@@ -182,7 +201,6 @@ def validate_gst_accounts(doc, is_sales_transaction=False):
- SEZ / Inter-State supplies should not have CGST or SGST account
- Intra-State supplies should not have IGST account
"""
-
if not doc.taxes:
return
@@ -211,7 +229,12 @@ def _throw(message, title=None):
frappe.throw(message, title=title or _("Invalid GST Account"))
all_valid_accounts, intra_state_accounts, inter_state_accounts = get_valid_accounts(
- doc.company, is_sales_transaction
+ doc.company,
+ for_sales=is_sales_transaction,
+ for_purchase=not is_sales_transaction,
+ )
+ cess_non_advol_accounts = get_gst_accounts_by_tax_type(
+ doc.company, "cess_non_advol"
)
# Company GSTIN = Party GSTIN
@@ -231,7 +254,6 @@ def _throw(message, title=None):
)
# Sales / Purchase Validations
-
if is_sales_transaction:
if is_export_without_payment_of_gst(doc) and (
idx := _get_matched_idx(rows_to_validate, all_valid_accounts)
@@ -290,6 +312,18 @@ def _throw(message, title=None):
for row in rows_to_validate:
account_head = row.account_head
+ if row.charge_type == "Actual":
+ item_tax_detail = frappe.parse_json(row.item_wise_tax_detail)
+ for tax_rate, tax_amount in item_tax_detail.values():
+ if tax_amount and not tax_rate:
+ _throw(
+ _(
+ "Tax Row #{0}: Charge Type is set to Actual. However, this would"
+ " not compute item taxes, and your further reporting will be affected."
+ ).format(row.idx),
+ title=_("Invalid Charge Type"),
+ )
+
if account_head not in all_valid_accounts:
_throw(
_("{0} is not a valid GST account for this transaction").format(
@@ -326,8 +360,32 @@ def _throw(message, title=None):
if row.charge_type == "On Previous Row Total":
previous_row_references.add(row.row_id)
+ if (
+ row.charge_type == "On Item Quantity"
+ and account_head not in cess_non_advol_accounts
+ ):
+ _throw(
+ _(
+ "Row #{0}: Charge Type cannot be On Item Quantity"
+ " as it is not a Cess Non Advol Account"
+ ).format(row.idx),
+ title=_("Invalid Charge Type"),
+ )
+
+ if (
+ row.charge_type != "On Item Quantity"
+ and account_head in cess_non_advol_accounts
+ ):
+ _throw(
+ _(
+ "Row #{0}: Charge Type must be On Item Quantity"
+ " as it is a Cess Non Advol Account"
+ ).format(row.idx),
+ title=_("Invalid Charge Type"),
+ )
+
+ used_accounts = set(row.account_head for row in rows_to_validate)
if not is_inter_state:
- used_accounts = set(row.account_head for row in rows_to_validate)
if used_accounts and not set(intra_state_accounts[:2]).issubset(used_accounts):
_throw(
_(
@@ -346,6 +404,22 @@ def _throw(message, title=None):
title=_("Invalid Reference Row"),
)
+ for row in doc.get("items") or []:
+ if not row.item_tax_template:
+ continue
+
+ for account in used_accounts:
+ if account in row.item_tax_rate:
+ continue
+
+ frappe.msgprint(
+ _(
+ "Item Row #{0}: GST Account {1} is missing in Item Tax Template {2}"
+ ).format(row.idx, bold(account), bold(row.item_tax_template)),
+ title=_("Invalid Item Tax Template"),
+ indicator="orange",
+ )
+
return all_valid_accounts
@@ -376,7 +450,7 @@ def validate_items(doc):
for row in doc.items:
# Collect data to validate that non-GST items are not used with GST items
- if row.is_non_gst:
+ if row.gst_treatment == "Non-GST":
non_gst_items.append(row.idx)
continue
@@ -875,7 +949,231 @@ def is_export_without_payment_of_gst(doc):
return is_overseas_doc(doc) and not doc.is_export_with_gst
+class ItemGSTDetails:
+ def get(self, docs, doctype, company):
+ """
+ Return Item GST Details for a list of documents
+ """
+ self.set_gst_accounts(doctype, company)
+ response = frappe._dict()
+ for doc in docs:
+ self.doc = doc
+ if not doc.get("items") or not doc.get("taxes"):
+ continue
+
+ self.update_item_count()
+ self.set_item_wise_tax_details()
+ self.set_tax_amount_precisions(doctype)
+
+ for item in doc.get("items"):
+ response.setdefault(item.name, frappe._dict()).update(
+ self.get_item_tax_detail(item)
+ )
+
+ return response
+
+ def update(self, doc):
+ """
+ Update Item GST Details for a single document
+ """
+ self.doc = doc
+ if not self.doc.get("items") or not self.doc.get("taxes"):
+ return
+
+ self.set_gst_accounts(doc.doctype, doc.company)
+ self.update_item_count()
+ self.set_item_wise_tax_details()
+ self.set_tax_amount_precisions(doc.doctype)
+ self.update_item_tax_details()
+
+ def set_gst_accounts(self, doctype, company):
+ if doctype in SALES_DOCTYPES:
+ account_type = "Output"
+ else:
+ account_type = "Input"
+
+ gst_account_map = get_gst_accounts_by_type(company, account_type, throw=False)
+ self.gst_account_map = {v: k for k, v in gst_account_map.items()}
+
+ def update_item_count(self):
+ self.item_count = frappe._dict()
+ for item in self.doc.get("items"):
+ key = item.item_code or item.item_name
+ self.item_count.setdefault(key, 0)
+ self.item_count[key] += 1
+
+ def set_item_wise_tax_details(self):
+ """
+ Item Tax Details complied
+ Example:
+ {
+ "Item Code 1": {
+ "count": 2,
+ "cgst_rate": 9,
+ "cgst_amount": 18,
+ "sgst_rate": 9,
+ "sgst_amount": 18,
+ ...
+ },
+ ...
+ }
+
+ Possible Exceptions Handled:
+ - There could be more than one row for same account
+ - Item count added to handle rounding errors
+ """
+ tax_details = frappe._dict()
+ item_defaults = frappe._dict(count=0)
+
+ for row in GST_TAX_TYPES:
+ item_defaults.update({f"{row}_rate": 0, f"{row}_amount": 0})
+
+ for row in self.doc.taxes:
+ if (
+ not row.tax_amount
+ or not row.item_wise_tax_detail
+ or row.account_head not in self.gst_account_map
+ ):
+ continue
+
+ account_type = self.gst_account_map[row.account_head]
+ tax = account_type[:-8]
+
+ old = frappe.parse_json(row.item_wise_tax_detail)
+
+ # update item taxes
+ for item_name in set(old.keys()):
+ item_taxes = tax_details.setdefault(item_name, item_defaults.copy())
+
+ item_taxes["count"] = self.item_count[item_name]
+
+ tax_rate, tax_amount = old[item_name]
+
+ # cases when charge type == "Actual"
+ if tax_amount and not tax_rate:
+ continue
+
+ item_taxes[f"{tax}_rate"] = tax_rate
+ item_taxes[f"{tax}_amount"] += tax_amount
+
+ self.item_tax_details = tax_details
+
+ def update_item_tax_details(self):
+ for item in self.doc.get("items"):
+ item.update(self.get_item_tax_detail(item))
+
+ def get_item_tax_detail(self, item):
+ """
+ - get item_tax_detail as it is if
+ - only one row exists for same item
+ - it is the last item
+
+ - If count is greater than 1,
+ - Manually calculate tax_amount for item
+ - Reduce item_tax_detail with
+ - tax_amount
+ - count
+ """
+ item_key = item.item_code or item.item_name
+ item_tax_detail = self.item_tax_details.get(item_key)
+ if not item_tax_detail:
+ return {}
+
+ if item_tax_detail.count == 1:
+ return item_tax_detail
+
+ # Handle rounding errors
+ response = item_tax_detail.copy()
+ for tax in GST_TAX_TYPES:
+ if (tax_rate := item_tax_detail[f"{tax}_rate"]) == 0:
+ continue
+
+ tax_amount_field = f"{tax}_amount"
+ precision = self.precision.get(tax_amount_field)
+
+ multiplier = item.qty if tax == "cess_non_advol" else item.taxable_value
+ tax_amount = flt(tax_rate * multiplier, precision)
+ tax_amount = max(tax_amount, item_tax_detail[tax_amount_field])
+
+ item_tax_detail[tax_amount_field] -= tax_amount
+ item_tax_detail["count"] -= 1
+
+ response.update({tax_amount_field: tax_amount})
+
+ return response
+
+ def set_tax_amount_precisions(self, doctype):
+ item_doctype = f"{doctype} Item"
+ meta = frappe.get_meta(item_doctype)
+
+ self.precision = frappe._dict()
+
+ for tax_type in GST_TAX_TYPES:
+ field = f"{tax_type}_amount"
+ if not meta.has_field(field):
+ continue
+
+ precision = meta.get_field(field).precision
+ self.precision.update({field: precision})
+
+
+def set_gst_treatment_for_item(doc):
+ is_overseas = is_overseas_doc(doc)
+ is_sales_transaction = doc.doctype in SALES_DOCTYPES
+
+ default_gst_treatment = "Taxable"
+ gst_accounts = get_all_gst_accounts(doc.company)
+
+ for row in doc.taxes:
+ if row.charge_type in ("Actual", "On Item Quantity"):
+ continue
+
+ if row.account_head not in gst_accounts:
+ continue
+
+ if row.rate == 0:
+ default_gst_treatment = "Nil-Rated"
+
+ break
+
+ item_templates = set()
+ gst_treatments = set()
+ gst_treatment_map = {}
+
+ for item in doc.items:
+ item_templates.add(item.item_tax_template)
+ gst_treatments.add(item.gst_treatment)
+
+ if "Zero-Rated" in gst_treatments and not is_overseas:
+ # doc changed from overseas to local sale post validate
+ _gst_treatments = frappe.get_all(
+ "Item Tax Template",
+ filters={"name": ("in", item_templates)},
+ fields=["name", "gst_treatment"],
+ )
+ gst_treatment_map = {row.name: row.gst_treatment for row in _gst_treatments}
+
+ for item in doc.items:
+ if not item.gst_treatment or not item.item_tax_template:
+ item.gst_treatment = default_gst_treatment
+
+ if not is_sales_transaction:
+ continue
+
+ if is_overseas:
+ # IGST sec 16(2) - ITC can be claimed for exempt supply
+ item.gst_treatment = "Zero-Rated"
+
+ elif item.gst_treatment == "Zero-Rated":
+ item.gst_treatment = (
+ gst_treatment_map.get(item.item_tax_template) or default_gst_treatment
+ )
+
+
def set_reverse_charge_as_per_gst_settings(doc):
+ if doc.doctype in SALES_DOCTYPES:
+ return
+
gst_settings = frappe.get_cached_value(
"GST Settings",
"GST Settings",
@@ -980,6 +1278,13 @@ def before_validate(doc, method=None):
set_reverse_charge_as_per_gst_settings(doc)
+def update_gst_details(doc, method=None):
+ if doc.doctype in DOCTYPES_WITH_GST_DETAIL:
+ ItemGSTDetails().update(doc)
+
+ set_gst_treatment_for_item(doc)
+
+
def after_mapping(target_doc, method=None, source_doc=None):
# Copy e-Waybill fields only from DN to SI
if not source_doc or source_doc.doctype not in (
diff --git a/india_compliance/gst_india/report/e_invoice_summary/e_invoice_summary.py b/india_compliance/gst_india/report/e_invoice_summary/e_invoice_summary.py
index 1d260843b..b90f4d78d 100644
--- a/india_compliance/gst_india/report/e_invoice_summary/e_invoice_summary.py
+++ b/india_compliance/gst_india/report/e_invoice_summary/e_invoice_summary.py
@@ -177,7 +177,7 @@ def get_cancelled_active_e_invoice_query(filters, sales_invoice, query):
def e_invoice_conditions(e_invoice_applicability_date):
sales_invoice = frappe.qb.DocType("Sales Invoice")
- sub_query = validate_sales_invoice_item()
+ taxable_invoices = validate_sales_invoice_item()
conditions = []
conditions.append(sales_invoice.posting_date >= e_invoice_applicability_date)
@@ -190,7 +190,7 @@ def e_invoice_conditions(e_invoice_applicability_date):
| (Coalesce(sales_invoice.billing_address_gstin, "") != "")
)
)
- conditions.append(sales_invoice.name.notin(sub_query))
+ conditions.append(sales_invoice.name.isin(taxable_invoices))
return reduce(lambda a, b: a & b, conditions)
@@ -198,14 +198,15 @@ def e_invoice_conditions(e_invoice_applicability_date):
def validate_sales_invoice_item():
sales_invoice_item = frappe.qb.DocType("Sales Invoice Item")
- sub_query = (
+ taxable_invoices = (
frappe.qb.from_(sales_invoice_item)
.select(sales_invoice_item.parent)
- .where(sales_invoice_item.is_non_gst == 1)
.where(sales_invoice_item.parenttype == "Sales Invoice")
+ .where(sales_invoice_item.gst_treatment.isin(["Taxable", "Zero-Rated"]))
.distinct()
)
- return sub_query
+
+ return taxable_invoices
def get_columns(filters=None):
diff --git a/india_compliance/gst_india/report/gstr_1/gstr_1.py b/india_compliance/gst_india/report/gstr_1/gstr_1.py
index 4593972ca..e2956b5b7 100644
--- a/india_compliance/gst_india/report/gstr_1/gstr_1.py
+++ b/india_compliance/gst_india/report/gstr_1/gstr_1.py
@@ -400,8 +400,8 @@ def get_invoice_items(self):
items = frappe.db.sql(
"""
- select item_code, item_name, parent, taxable_value, item_tax_rate, is_nil_exempt,
- is_non_gst from `tab%s Item`
+ select item_code, item_name, parent, taxable_value, item_tax_rate, gst_treatment
+ from `tab%s Item`
where parent in (%s)
"""
% (self.doctype, ", ".join(["%s"] * len(self.invoices))),
@@ -414,14 +414,16 @@ def get_invoice_items(self):
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0)
- if d.is_nil_exempt:
- self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0])
- if d.item_tax_rate:
- self.nil_exempt_non_gst[d.parent][0] += d.get("taxable_value", 0)
- else:
- self.nil_exempt_non_gst[d.parent][1] += d.get("taxable_value", 0)
- elif d.is_non_gst:
- self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0])
+ is_nil_rated = d.gst_treatment == "Nil-Rated"
+ is_exempted = d.gst_treatment == "Exempted"
+ is_non_gst = d.gst_treatment == "Non-GST"
+
+ self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0])
+ if is_nil_rated:
+ self.nil_exempt_non_gst[d.parent][0] += d.get("taxable_value", 0)
+ elif is_exempted:
+ self.nil_exempt_non_gst[d.parent][1] += d.get("taxable_value", 0)
+ elif is_non_gst:
self.nil_exempt_non_gst[d.parent][2] += d.get("taxable_value", 0)
def get_items_based_on_tax_rate(self):
@@ -1165,7 +1167,7 @@ def get_query(self):
.else_(0)
.as_("same_gstin_billing"),
self.sales_invoice.is_opening,
- self.sales_invoice_item.is_non_gst,
+ self.sales_invoice_item.gst_treatment,
)
.where(self.sales_invoice.company == self.filters.company)
.where(
@@ -1301,7 +1303,7 @@ def seperate_data_by_nature_of_document(self, data):
nature_of_document["Excluded from Report (Same GSTIN Billing)"].append(
doc
)
- elif doc.is_non_gst:
+ elif doc.gst_treatment == "Non-GST":
nature_of_document["Excluded from Report (Has Non GST Item)"].append(
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 ca317aa77..d1f7dab00 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
@@ -344,12 +344,12 @@ def get_data(self):
taxable_value = invoice.taxable_value
if (
- invoice.is_nil_exempt == 1
+ invoice.gst_treatment in ["Nil-Rated", "Exempted"]
or invoice.get("gst_category") == "Registered Composition"
):
nature_of_supply = "Composition Scheme, Exempted, Nil Rated"
- elif invoice.is_non_gst == 1:
+ elif invoice.gst_treatment == "Non-GST":
nature_of_supply = "Non GST Supply"
if supplier_state == place_of_supply:
@@ -390,8 +390,7 @@ def get_inward_nil_exempt(self):
purchase_invoice.place_of_supply,
purchase_invoice.supplier_address,
Sum(purchase_invoice_item.taxable_value).as_("taxable_value"),
- purchase_invoice_item.is_nil_exempt,
- purchase_invoice_item.is_non_gst,
+ purchase_invoice_item.gst_treatment,
purchase_invoice.supplier_gstin,
purchase_invoice.supplier_address,
)
@@ -400,8 +399,7 @@ def get_inward_nil_exempt(self):
& (purchase_invoice.is_opening == "No")
& (purchase_invoice.name == purchase_invoice_item.parent)
& (
- (purchase_invoice_item.is_nil_exempt == 1)
- | (purchase_invoice_item.is_non_gst == 1)
+ (purchase_invoice_item.gst_treatment != "Taxable")
| (purchase_invoice.gst_category == "Registered Composition")
)
& (purchase_invoice.posting_date[self.from_date : self.to_date])
diff --git a/india_compliance/gst_india/setup/__init__.py b/india_compliance/gst_india/setup/__init__.py
index 332279c16..bedf0b5a2 100644
--- a/india_compliance/gst_india/setup/__init__.py
+++ b/india_compliance/gst_india/setup/__init__.py
@@ -21,7 +21,7 @@
from india_compliance.gst_india.utils import get_data_file_path
from india_compliance.gst_india.utils.custom_fields import toggle_custom_fields
-ITEM_VARIANT_FIELDNAMES = frozenset(("gst_hsn_code", "is_nil_exempt", "is_non_gst"))
+ITEM_VARIANT_FIELDNAMES = frozenset(("gst_hsn_code",))
def after_install():
diff --git a/india_compliance/gst_india/utils/__init__.py b/india_compliance/gst_india/utils/__init__.py
index 509f92b17..108de7cc9 100644
--- a/india_compliance/gst_india/utils/__init__.py
+++ b/india_compliance/gst_india/utils/__init__.py
@@ -471,6 +471,48 @@ def get_gst_accounts_by_type(company, account_type, throw=True):
)
+def get_gst_accounts_by_tax_type(company, tax_type, throw=True):
+ """
+ :param company: Company to get GST Accounts for
+ :param tax_type: Tax Type to get GST Accounts for eg: "cgst"
+
+ Returns a list of accounts:
+ """
+ if not company:
+ frappe.throw(_("Please set Company first"))
+
+ tax_type = tax_type.lower()
+ field = f"{tax_type}_account"
+
+ if field not in GST_ACCOUNT_FIELDS:
+ frappe.throw(_("Invalid Tax Type"))
+
+ settings = frappe.get_cached_doc("GST Settings", "GST Settings")
+ accounts_list = []
+
+ has_account_settings = False
+ for row in settings.gst_accounts:
+ if row.company != company:
+ continue
+
+ has_account_settings = True
+ if gst_account := row.get(field):
+ accounts_list.append(gst_account)
+
+ if accounts_list:
+ return accounts_list
+
+ if has_account_settings or not throw:
+ return accounts_list
+
+ frappe.throw(
+ _(
+ "Could not retrieve GST Accounts of type {0} from GST Settings for"
+ " Company {1}"
+ ).format(frappe.bold(tax_type), frappe.bold(company)),
+ )
+
+
@frappe.whitelist()
def get_all_gst_accounts(company):
"""
@@ -759,3 +801,8 @@ def tar_gz_bytes_to_data(tar_gz_bytes: bytes) -> str | None:
break
return data
+
+
+@frappe.whitelist(methods=["POST"])
+def disable_item_tax_template_notification():
+ frappe.defaults.clear_user_default("needs_item_tax_template_notification")
diff --git a/india_compliance/gst_india/utils/e_invoice.py b/india_compliance/gst_india/utils/e_invoice.py
index 71b08de00..25674ce3c 100644
--- a/india_compliance/gst_india/utils/e_invoice.py
+++ b/india_compliance/gst_india/utils/e_invoice.py
@@ -13,7 +13,7 @@
random_string,
)
-from india_compliance.exceptions import GatewayTimeoutError
+from india_compliance.exceptions import GatewayTimeoutError, GSPServerError
from india_compliance.gst_india.api_classes.e_invoice import EInvoiceAPI
from india_compliance.gst_india.constants import (
CURRENCY_CODES,
@@ -43,10 +43,7 @@
_cancel_e_waybill,
log_and_process_e_waybill_generation,
)
-from india_compliance.gst_india.utils.transaction_data import (
- GSTTransactionData,
- validate_non_gst_items,
-)
+from india_compliance.gst_india.utils.transaction_data import GSTTransactionData
@frappe.whitelist()
@@ -90,7 +87,7 @@ def log_error():
try:
generate_e_invoice(docname, throw=False, force=force)
- except GatewayTimeoutError:
+ except GSPServerError:
frappe.db.set_value(
"Sales Invoice",
{"name": ("in", docnames), "irn": ("is", "not set")},
@@ -165,27 +162,9 @@ def generate_e_invoice(docname, throw=True, force=False):
result = api.generate_irn(data)
- except GatewayTimeoutError as e:
- einvoice_status = "Failed"
-
- if settings.enable_retry_e_invoice_generation:
- einvoice_status = "Auto-Retry"
- settings.db_set(
- "is_retry_e_invoice_generation_pending", 1, update_modified=False
- )
-
- doc.db_set({"einvoice_status": einvoice_status}, commit=True)
-
- frappe.msgprint(
- _(
- "Government services are currently slow, resulting in a Gateway Timeout error."
- " We apologize for the inconvenience caused. Your e-invoice generation will be automatically retried every 5 minutes."
- ),
- _("Warning"),
- indicator="yellow",
- )
-
- raise e
+ except GSPServerError as e:
+ handle_server_errors(settings, doc, e)
+ return
except frappe.ValidationError as e:
doc.db_set({"einvoice_status": "Failed"})
@@ -370,7 +349,8 @@ def _throw(error):
)
)
- if not validate_non_gst_items(doc, throw=throw):
+ if not validate_taxable_item(doc, throw=throw):
+ # e-Invoice not required for invoice wih all nill-rated/exempted items.
return
if not (doc.place_of_supply == "96-Other Countries" or doc.billing_address_gstin):
@@ -408,6 +388,26 @@ def validate_hsn_codes_for_e_invoice(doc):
)
+def validate_taxable_item(doc, throw=True):
+ """
+ Validates that the document contains at least one GST taxable item.
+
+ If all items are Nil-Rated or Exempted and throw is True, it raises an exception.
+ Otherwise, it simply returns False.
+
+ """
+ # Check if there is at least one taxable item in the document
+ if any(item.gst_treatment in ("Taxable", "Zero-Rated") for item in doc.items):
+ return True
+
+ if not throw:
+ return
+
+ frappe.throw(
+ _("e-Invoice is not applicable for invoice with only Nil-Rated/Exempted items"),
+ )
+
+
def get_e_invoice_applicability_date(doc, settings=None, throw=True):
if not settings:
settings = frappe.get_cached_doc("GST Settings")
@@ -471,15 +471,63 @@ def get_e_invoice_info(doc):
)
+def handle_server_errors(settings, doc, error):
+ error_message = "Government services are currently slow/down. We apologize for the inconvenience caused."
+
+ error_message_title = {
+ GatewayTimeoutError: _("Gateway Timeout Error"),
+ GSPServerError: _("GSP/GST Server Down"),
+ }
+
+ einvoice_status = "Failed"
+
+ if settings.enable_retry_e_invoice_generation:
+ einvoice_status = "Auto-Retry"
+ settings.db_set(
+ "is_retry_e_invoice_generation_pending", 1, update_modified=False
+ )
+ error_message += (
+ " Your e-invoice generation will be automatically retried every 5 minutes."
+ )
+ else:
+ error_message += " Please try again after some time."
+
+ doc.db_set({"einvoice_status": einvoice_status}, commit=True)
+
+ frappe.msgprint(
+ msg=_(error_message),
+ title=error_message_title.get(type(error)),
+ indicator="yellow",
+ )
+
+
class EInvoiceData(GSTTransactionData):
def get_data(self):
self.validate_transaction()
self.set_transaction_details()
self.set_item_list()
+ self.update_other_charges()
self.set_transporter_details()
self.set_party_address_details()
return self.sanitize_data(self.get_invoice_data())
+ def set_item_list(self):
+ self.item_list = []
+
+ for item_details in self.get_all_item_details():
+ if item_details.get("gst_treatment") not in ("Taxable", "Zero-Rated"):
+ continue
+
+ self.item_list.append(self.get_item_data(item_details))
+
+ def update_other_charges(self):
+ """
+ Non Taxable Value should be added to other charges.
+ """
+ self.transaction_details.other_charges += (
+ self.transaction_details.total_non_taxable_value
+ )
+
def validate_transaction(self):
super().validate_transaction()
validate_e_invoice_applicability(self.doc, self.settings)
@@ -734,7 +782,7 @@ def get_invoice_data(self):
},
"ItemList": self.item_list,
"ValDtls": {
- "AssVal": self.transaction_details.total,
+ "AssVal": self.transaction_details.total_taxable_value,
"CgstVal": self.transaction_details.total_cgst_amount,
"SgstVal": self.transaction_details.total_sgst_amount,
"IgstVal": self.transaction_details.total_igst_amount,
diff --git a/india_compliance/gst_india/utils/test_e_invoice.py b/india_compliance/gst_india/utils/test_e_invoice.py
index ef35c9ba7..6c24d43b7 100644
--- a/india_compliance/gst_india/utils/test_e_invoice.py
+++ b/india_compliance/gst_india/utils/test_e_invoice.py
@@ -294,6 +294,65 @@ def test_generate_e_invoice_with_service_item(self):
frappe.db.get_value("e-Waybill Log", {"reference_name": si.name}, "name")
)
+ @responses.activate
+ def test_generate_e_invoice_with_nil_exempted_item(self):
+ """Generate test e-Invoice for nil/exempted items Item"""
+
+ test_data = self.e_invoice_test_data.get("nil_exempted_item")
+ si = create_sales_invoice(
+ **test_data.get("kwargs"), do_not_submit=True, is_in_state=True
+ )
+
+ append_item(
+ si,
+ frappe._dict(
+ rate=10,
+ item_tax_template="GST 12% - _TIRC",
+ uom="Nos",
+ gst_hsn_code="61149090",
+ gst_treatment="Taxable",
+ ),
+ )
+ si.save()
+ si.submit()
+
+ # Assert if request data given in Json
+ self.assertDictEqual(test_data.get("request_data"), EInvoiceData(si).get_data())
+
+ # Mock response for generating irn
+ self._mock_e_invoice_response(data=test_data)
+
+ generate_e_invoice(si.name)
+
+ # Assert if Integration Request Log generated
+ self.assertDocumentEqual(
+ {
+ "output": frappe.as_json(test_data.get("response_data"), indent=4),
+ },
+ frappe.get_doc(
+ "Integration Request",
+ {"reference_doctype": "Sales Invoice", "reference_docname": si.name},
+ ),
+ )
+
+ # Assert if Sales Doc updated
+ self.assertDocumentEqual(
+ {
+ "irn": test_data.get("response_data").get("result").get("Irn"),
+ "einvoice_status": "Generated",
+ },
+ frappe.get_doc("Sales Invoice", si.name),
+ )
+
+ self.assertDocumentEqual(
+ {"name": test_data.get("response_data").get("result").get("Irn")},
+ frappe.get_doc("e-Invoice Log", {"sales_invoice": si.name}),
+ )
+
+ self.assertFalse(
+ frappe.db.get_value("e-Waybill Log", {"reference_name": si.name}, "name")
+ )
+
@responses.activate
def test_credit_note_e_invoice_with_goods_item(self):
"""Generate test e-Invoice for returned Sales Invoices"""
@@ -557,6 +616,36 @@ def test_validate_e_invoice_applicability(self):
)
si.irn = ""
+
+ si.items = []
+ append_item(
+ si,
+ frappe._dict(
+ item_code="_Test Nil Rated Item",
+ item_name="_Test Nil Rated Item",
+ gst_hsn_code="61149090",
+ gst_treatment="Nil-Rated",
+ ),
+ )
+ self.assertRaisesRegex(
+ frappe.exceptions.ValidationError,
+ re.compile(
+ r"^(e-Invoice is not applicable for invoice with only Nil-Rated/Exempted items*)$"
+ ),
+ validate_e_invoice_applicability,
+ si,
+ )
+
+ append_item(
+ si,
+ frappe._dict(
+ rate=10,
+ item_tax_template="GST 12% - _TIRC",
+ uom="Nos",
+ gst_hsn_code="61149090",
+ gst_treatment="Taxable",
+ ),
+ )
frappe.db.set_single_value("GST Settings", "enable_e_invoice", 0)
self.assertRaisesRegex(
diff --git a/india_compliance/gst_india/utils/test_e_waybill.py b/india_compliance/gst_india/utils/test_e_waybill.py
index 8269cfa8f..c5eacb47c 100644
--- a/india_compliance/gst_india/utils/test_e_waybill.py
+++ b/india_compliance/gst_india/utils/test_e_waybill.py
@@ -389,6 +389,7 @@ def test_get_all_item_details(self):
"cess_non_advol_rate": 0,
"tax_rate": 0.0,
"total_value": 100.0,
+ "gst_treatment": "Taxable",
}
],
EWaybillData(si).get_all_item_details(),
@@ -480,7 +481,7 @@ def test_validate_applicability(self):
{"gst_transporter_id": "05AAACG2140A1ZL", "mode_of_transport": "Road"}
)
- si.items[0].is_non_gst = 1
+ si.items[0].gst_treatment = "Non-GST"
self.assertRaisesRegex(
frappe.exceptions.ValidationError,
@@ -488,7 +489,7 @@ def test_validate_applicability(self):
EWaybillData(si).validate_applicability,
)
- si.items[0].is_non_gst = 0
+ si.items[0].gst_treatment = "Taxable"
si.update(
{
"company_gstin": "05AAACG2115R1ZN",
diff --git a/india_compliance/gst_india/utils/tests.py b/india_compliance/gst_india/utils/tests.py
index ea4d73d7a..25ea522a1 100644
--- a/india_compliance/gst_india/utils/tests.py
+++ b/india_compliance/gst_india/utils/tests.py
@@ -106,9 +106,8 @@ def append_item(transaction, data=None, company_abbr="_TIRC"):
"uom": data.uom,
"rate": data.rate or 100,
"cost_center": f"Main - {company_abbr}",
- "is_nil_exempt": data.is_nil_exempt,
- "is_non_gst": data.is_non_gst,
"item_tax_template": data.item_tax_template,
+ "gst_treatment": data.gst_treatment,
"gst_hsn_code": data.gst_hsn_code,
"warehouse": f"Stores - {company_abbr}",
"expense_account": f"Cost of Goods Sold - {company_abbr}",
@@ -123,6 +122,7 @@ def _append_taxes(
rate=9,
charge_type="On Net Total",
row_id=None,
+ tax_amount=None,
):
if isinstance(accounts, str):
accounts = [accounts]
@@ -145,6 +145,9 @@ def _append_taxes(
"cost_center": f"Main - {company_abbr}",
}
+ if tax_amount:
+ tax["tax_amount"] = tax_amount
+
if account.endswith("RCM"):
tax["add_deduct_tax"] = "Deduct"
diff --git a/india_compliance/gst_india/utils/transaction_data.py b/india_compliance/gst_india/utils/transaction_data.py
index ce6f7b8f6..4633c1aaa 100644
--- a/india_compliance/gst_india/utils/transaction_data.py
+++ b/india_compliance/gst_india/utils/transaction_data.py
@@ -65,6 +65,15 @@ def set_transaction_details(self):
else "base_rounded_total"
)
+ total = 0
+ total_taxable_value = 0
+
+ for row in self.doc.items:
+ total += row.taxable_value
+
+ if row.gst_treatment in ("Taxable", "Zero-Rated"):
+ total_taxable_value += row.taxable_value
+
self.transaction_details.update(
{
"company_name": self.sanitize_value(self.doc.company),
@@ -75,8 +84,10 @@ def set_transaction_details(self):
)
),
"date": format_date(self.doc.posting_date, self.DATE_FORMAT),
- "total": abs(
- self.rounded(sum(row.taxable_value for row in self.doc.items))
+ "total": abs(self.rounded(total)),
+ "total_taxable_value": abs(self.rounded(total_taxable_value)),
+ "total_non_taxable_value": abs(
+ self.rounded(total - total_taxable_value)
),
"rounding_adjustment": rounding_adjustment,
"grand_total": abs(self.rounded(self.doc.get(grand_total_fieldname))),
@@ -270,6 +281,7 @@ def get_all_item_details(self):
row.item_name, regex=3, max_length=300
),
"uom": get_gst_uom(row.uom, self.settings),
+ "gst_treatment": row.gst_treatment,
}
)
self.update_item_details(item_details, row)
@@ -581,7 +593,7 @@ def _throw(message, **format_args):
def validate_non_gst_items(doc, throw=True):
- if doc.items[0].is_non_gst:
+ if doc.items[0].gst_treatment == "Non-GST":
if not throw:
return
diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py
index 7d297a2e0..56cb60a42 100644
--- a/india_compliance/hooks.py
+++ b/india_compliance/hooks.py
@@ -37,6 +37,7 @@
"gst_india/client_scripts/delivery_note.js",
],
"Item": "gst_india/client_scripts/item.js",
+ "Item Tax Template": "gst_india/client_scripts/item_tax_template.js",
"Expense Claim": [
"gst_india/client_scripts/journal_entry.js",
"gst_india/client_scripts/expense_claim.js",
@@ -94,6 +95,8 @@
"india_compliance.gst_india.overrides.transaction.ignore_logs_on_trash"
),
"onload": "india_compliance.gst_india.overrides.delivery_note.onload",
+ "before_save": "india_compliance.gst_india.overrides.transaction.update_gst_details",
+ "before_submit": "india_compliance.gst_india.overrides.transaction.update_gst_details",
"validate": (
"india_compliance.gst_india.overrides.transaction.validate_transaction"
),
@@ -106,6 +109,9 @@
"validate": "india_compliance.gst_india.overrides.gl_entry.validate",
},
"Item": {"validate": "india_compliance.gst_india.overrides.item.validate"},
+ "Item Tax Template": {
+ "validate": "india_compliance.gst_india.overrides.item_tax_template.validate"
+ },
"Journal Entry": {
"validate": "india_compliance.gst_india.overrides.journal_entry.validate",
},
@@ -121,7 +127,11 @@
"before_validate": (
"india_compliance.gst_india.overrides.transaction.before_validate"
),
- "before_submit": "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate",
+ "before_save": "india_compliance.gst_india.overrides.transaction.update_gst_details",
+ "before_submit": [
+ "india_compliance.gst_india.overrides.transaction.update_gst_details",
+ "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate",
+ ],
"before_gl_preview": "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate",
"before_sl_preview": "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate",
"after_mapping": "india_compliance.gst_india.overrides.transaction.after_mapping",
@@ -133,6 +143,8 @@
"before_validate": (
"india_compliance.gst_india.overrides.transaction.before_validate"
),
+ "before_save": "india_compliance.gst_india.overrides.transaction.update_gst_details",
+ "before_submit": "india_compliance.gst_india.overrides.transaction.update_gst_details",
},
"Purchase Receipt": {
"validate": (
@@ -141,7 +153,11 @@
"before_validate": (
"india_compliance.gst_india.overrides.transaction.before_validate"
),
- "before_submit": "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate",
+ "before_save": "india_compliance.gst_india.overrides.transaction.update_gst_details",
+ "before_submit": [
+ "india_compliance.gst_india.overrides.transaction.update_gst_details",
+ "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate",
+ ],
"before_gl_preview": "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate",
"before_sl_preview": "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate",
},
@@ -151,6 +167,8 @@
),
"onload": "india_compliance.gst_india.overrides.sales_invoice.onload",
"validate": "india_compliance.gst_india.overrides.sales_invoice.validate",
+ "before_save": "india_compliance.gst_india.overrides.transaction.update_gst_details",
+ "before_submit": "india_compliance.gst_india.overrides.transaction.update_gst_details",
"on_submit": "india_compliance.gst_india.overrides.sales_invoice.on_submit",
"on_update_after_submit": (
"india_compliance.gst_india.overrides.sales_invoice.on_update_after_submit"
@@ -162,6 +180,8 @@
"validate": (
"india_compliance.gst_india.overrides.transaction.validate_transaction"
),
+ "before_save": "india_compliance.gst_india.overrides.transaction.update_gst_details",
+ "before_submit": "india_compliance.gst_india.overrides.transaction.update_gst_details",
},
"Supplier": {
"validate": [
@@ -185,16 +205,25 @@
"validate": (
"india_compliance.gst_india.overrides.transaction.validate_transaction"
),
+ "before_save": "india_compliance.gst_india.overrides.transaction.update_gst_details",
+ "before_submit": "india_compliance.gst_india.overrides.transaction.update_gst_details",
},
"Quotation": {
"validate": (
"india_compliance.gst_india.overrides.transaction.validate_transaction"
),
+ "before_save": "india_compliance.gst_india.overrides.transaction.update_gst_details",
+ "before_submit": "india_compliance.gst_india.overrides.transaction.update_gst_details",
},
"Supplier Quotation": {
+ "before_validate": (
+ "india_compliance.gst_india.overrides.transaction.before_validate"
+ ),
"validate": (
"india_compliance.gst_india.overrides.transaction.validate_transaction"
),
+ "before_save": "india_compliance.gst_india.overrides.transaction.update_gst_details",
+ "before_submit": "india_compliance.gst_india.overrides.transaction.update_gst_details",
},
"Accounts Settings": {
"validate": "india_compliance.audit_trail.overrides.accounts_settings.validate"
diff --git a/india_compliance/install.py b/india_compliance/install.py
index f42686b0d..e8dcdcd71 100644
--- a/india_compliance/install.py
+++ b/india_compliance/install.py
@@ -39,6 +39,7 @@
"update_company_gstin",
"update_payment_entry_fields",
"update_itc_classification_field",
+ "improve_item_tax_template",
"update_vehicle_no_field_in_purchase_receipt",
)
diff --git a/india_compliance/patches.txt b/india_compliance/patches.txt
index 1d52624b9..1f2fd77b9 100644
--- a/india_compliance/patches.txt
+++ b/india_compliance/patches.txt
@@ -3,8 +3,8 @@ 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() #38
-execute:from india_compliance.gst_india.setup import create_property_setters; create_property_setters() #5
+execute:from india_compliance.gst_india.setup import create_custom_fields; create_custom_fields() #40
+execute:from india_compliance.gst_india.setup import create_property_setters; create_property_setters() #6
india_compliance.patches.post_install.remove_old_fields
india_compliance.patches.post_install.update_company_gstin
india_compliance.patches.post_install.update_custom_role_for_e_invoice_summary
@@ -34,4 +34,5 @@ india_compliance.patches.post_install.update_company_fixtures #1
india_compliance.patches.post_install.update_itc_classification_field
india_compliance.patches.v14.update_purchase_reco_email_template
india_compliance.patches.v14.delete_purchase_receipt_standard_custom_fields
-india_compliance.patches.post_install.update_vehicle_no_field_in_purchase_receipt
\ No newline at end of file
+india_compliance.patches.post_install.improve_item_tax_template
+india_compliance.patches.post_install.update_vehicle_no_field_in_purchase_receipt
diff --git a/india_compliance/patches/post_install/improve_item_tax_template.py b/india_compliance/patches/post_install/improve_item_tax_template.py
new file mode 100644
index 000000000..60d96caf1
--- /dev/null
+++ b/india_compliance/patches/post_install/improve_item_tax_template.py
@@ -0,0 +1,440 @@
+import click
+
+import frappe
+import frappe.defaults
+from frappe.query_builder import Case
+from frappe.query_builder.functions import IfNull
+from frappe.utils import get_datetime, random_string
+from frappe.utils.user import get_users_with_role
+
+from india_compliance.gst_india.constants import GST_TAX_TYPES, SALES_DOCTYPES
+from india_compliance.gst_india.overrides.transaction import (
+ ItemGSTDetails,
+ get_valid_accounts,
+)
+from india_compliance.gst_india.utils import (
+ get_all_gst_accounts,
+ get_gst_accounts_by_type,
+)
+from india_compliance.patches.post_install.update_e_invoice_fields_and_logs import (
+ delete_custom_fields,
+)
+
+TRANSACTION_DOCTYPES = (
+ "Material Request Item",
+ "Supplier Quotation Item",
+ "Purchase Order Item",
+ "Purchase Receipt Item",
+ "Purchase Invoice Item",
+ "Quotation Item",
+ "Sales Order Item",
+ "Delivery Note Item",
+ "Sales Invoice Item",
+ "POS Invoice Item",
+)
+
+FIELDS_TO_DELETE = {
+ "Item": [
+ {"fieldname": "is_nil_exempt"},
+ {"fieldname": "is_non_gst"},
+ ],
+ TRANSACTION_DOCTYPES: [
+ {"fieldname": "is_nil_exempt"},
+ {"fieldname": "is_non_gst"},
+ ],
+}
+
+NEW_TEMPLATES = {
+ "is_nil_rated": "Nil-Rated",
+ "is_exempted": "Exempted",
+ "is_non_gst": "Non-GST",
+}
+
+
+def execute():
+ companies = get_indian_companies()
+ templates = create_or_update_item_tax_templates(companies)
+ update_items_with_templates(templates)
+
+ update_gst_treatment_for_transactions()
+ update_gst_details_for_transactions(companies)
+
+ remove_old_item_variant_settings()
+ delete_custom_fields(FIELDS_TO_DELETE)
+
+
+def get_indian_companies():
+ return frappe.get_all("Company", filters={"country": "India"}, pluck="name")
+
+
+def create_or_update_item_tax_templates(companies):
+ if not companies:
+ return {}
+
+ DOCTYPE = "Item Tax Template"
+ item_templates = frappe.get_all(DOCTYPE, pluck="name")
+ companies_with_templates = set()
+ companies_gst_accounts = frappe._dict()
+
+ # update tax rates
+ for template_name in item_templates:
+ doc = frappe.get_doc(DOCTYPE, template_name)
+ if doc.company not in companies:
+ continue
+
+ gst_accounts = get_all_gst_accounts(doc.company)
+ if not gst_accounts or not doc.taxes:
+ continue
+
+ gst_rates = set()
+ companies_with_templates.add(doc.company)
+ companies_gst_accounts[doc.company] = gst_accounts
+ _, intra_state_accounts, inter_state_accounts = get_valid_accounts(
+ doc.company, for_sales=True, for_purchase=True
+ )
+
+ for row in doc.taxes:
+ if row.tax_type in intra_state_accounts:
+ gst_rates.add(row.tax_rate * 2)
+ elif row.tax_type in inter_state_accounts:
+ gst_rates.add(row.tax_rate)
+
+ if len(gst_rates) != 1:
+ continue
+
+ doc.gst_rate = next(iter(gst_rates))
+
+ if doc.gst_treatment != "Taxable":
+ # Cases where patch is run again
+ continue
+
+ elif doc.gst_rate > 0:
+ doc.gst_treatment = "Taxable"
+
+ elif doc.gst_rate == 0:
+ doc.gst_treatment = "Nil-Rated"
+
+ doc.save()
+
+ # create new templates for nil rated, exempted, non gst
+ templates = {}
+ for company in companies_with_templates:
+ gst_accounts = [
+ {"tax_type": account, "tax_rate": 0}
+ for account in companies_gst_accounts[company]
+ ]
+
+ for new_template in NEW_TEMPLATES.values():
+ if template_name := frappe.db.get_value(
+ DOCTYPE, {"company": company, "gst_treatment": new_template}
+ ):
+ templates.setdefault(new_template, []).append(template_name)
+ continue
+
+ doc = frappe.get_doc(
+ {
+ "doctype": DOCTYPE,
+ "title": new_template,
+ "company": company,
+ "gst_treatment": new_template,
+ "tax_rate": 0,
+ }
+ )
+
+ doc.extend("taxes", gst_accounts)
+ doc.insert(ignore_if_duplicate=True)
+ templates.setdefault(new_template, []).append(doc.name)
+
+ return templates
+
+
+def update_items_with_templates(templates):
+ "Disclaimer: No specific way to differentate between nil and exempted. Hence all transactions are updated to nil"
+
+ if not templates:
+ return
+
+ table = frappe.qb.DocType("Item")
+ item_list = (
+ frappe.qb.from_(table)
+ .select("name", "is_nil_exempt", "is_non_gst")
+ .where((table.is_nil_exempt == 1) | (table.is_non_gst == 1))
+ .run(as_dict=True)
+ )
+
+ items = [item.name for item in item_list]
+ all_templates = []
+ for category_templates in templates.values():
+ all_templates.extend(category_templates)
+
+ # Don't update for existing templates
+ item_templates = frappe.get_all(
+ "Item Tax",
+ fields=["parent as item", "item_tax_template"],
+ filters={
+ "parenttype": "Item",
+ "parent": ["in", items],
+ "item_tax_template": ["in", all_templates],
+ },
+ )
+
+ item_wise_templates = frappe._dict()
+ for item in item_templates:
+ item_wise_templates.setdefault(item.item, set()).add(item.item_tax_template)
+
+ fields = (
+ "name",
+ "parent",
+ "parentfield",
+ "parenttype",
+ "item_tax_template",
+ "owner",
+ "modified_by",
+ "creation",
+ "modified",
+ )
+
+ values = []
+ time = get_datetime()
+
+ def extend_tax_template(item, templates):
+ for template in templates:
+ if template in item_wise_templates.get(item.name, []):
+ continue
+
+ values.append(
+ [
+ random_string(10),
+ item.name,
+ "taxes",
+ "Item",
+ template,
+ "Administrator",
+ "Administrator",
+ time,
+ time,
+ ]
+ )
+
+ for item in item_list:
+ if item.is_nil_exempt:
+ extend_tax_template(item, templates["Nil-Rated"])
+ continue
+
+ if item.is_non_gst:
+ extend_tax_template(item, templates["Non-GST"])
+
+ frappe.db.bulk_insert("Item Tax", fields=fields, values=values)
+
+
+def remove_old_item_variant_settings():
+ item_variant = frappe.get_single("Item Variant Settings")
+ for field in reversed(item_variant.fields):
+ if field.field_name in ("is_nil_exempt", "is_non_gst"):
+ item_variant.fields.remove(field)
+
+ item_variant.save()
+
+
+###### Updating Transactions ##############################################
+def update_gst_treatment_for_transactions():
+ "Disclaimer: No specific way to differentate between nil and exempted. Hence all transactions are updated to nil"
+
+ for item_doctype in TRANSACTION_DOCTYPES:
+ # GST Treatment is not required in Material Request Item
+ if item_doctype == "Material Request Item":
+ continue
+
+ table = frappe.qb.DocType(item_doctype)
+ query = frappe.qb.update(table)
+
+ (
+ query.set(
+ table.gst_treatment,
+ Case()
+ .when(table.is_nil_exempt == 1, "Nil-Rated")
+ .when(table.is_non_gst == 1, "Non-GST")
+ .else_("Taxable"),
+ )
+ .where(IfNull(table.gst_treatment, "") == "")
+ .run()
+ )
+
+ doctype = item_doctype.replace(" Item", "")
+ if doctype not in SALES_DOCTYPES:
+ continue
+
+ doc = frappe.qb.DocType(doctype)
+
+ (
+ query.join(doc)
+ .on(doc.name == table.parent)
+ .set(table.gst_treatment, "Zero-Rated")
+ .where(
+ (doc.gst_category == "SEZ")
+ | (
+ (doc.gst_category == "Overseas")
+ & (doc.place_of_supply == "96-Other Countries")
+ )
+ )
+ .run()
+ )
+
+ click.secho(
+ "Nil Rated items are differentiated from Exempted for GST (configrable from Item Tax Template).",
+ color="yellow",
+ )
+ click.secho(
+ "All transactions that were marked as Nil or Exempt, are now marked as Nil Rated.",
+ color="red",
+ )
+
+ for user in get_users_with_role("Accounts Manager"):
+ frappe.defaults.set_user_default(
+ "needs_item_tax_template_notification", 1, user=user
+ )
+
+
+def update_gst_details_for_transactions(companies):
+ for company in companies:
+ gst_accounts = []
+ for account_type in ["Input", "Output"]:
+ gst_accounts.extend(
+ get_gst_accounts_by_type(company, account_type).values()
+ )
+
+ if not gst_accounts:
+ continue
+
+ for doctype in ("Sales Invoice", "Purchase Invoice"):
+ is_sales_doctype = doctype in SALES_DOCTYPES
+ docs = get_docs_with_gst_accounts(doctype, gst_accounts)
+ if not docs:
+ continue
+
+ chunk_size = 5000
+ total_docs = len(docs)
+
+ for i in range(0, total_docs, chunk_size):
+ chunk = docs[i : i + chunk_size]
+
+ taxes = get_taxes_for_docs(chunk, doctype, is_sales_doctype)
+ items = get_items_for_docs(chunk, doctype)
+ complied_docs = compile_docs(taxes, items)
+
+ if not complied_docs:
+ continue
+
+ gst_details = ItemGSTDetails().get(
+ complied_docs.values(), doctype, company
+ )
+
+ build_query_and_update_gst_details(gst_details, doctype)
+
+
+def get_docs_with_gst_accounts(doctype, gst_accounts):
+ gl_entry = frappe.qb.DocType("GL Entry")
+
+ return (
+ frappe.qb.from_(gl_entry)
+ .select("voucher_no")
+ .where(gl_entry.voucher_type == doctype)
+ .where(gl_entry.account.isin(gst_accounts))
+ .where(gl_entry.is_cancelled == 0)
+ .groupby("voucher_no")
+ .run(pluck=True)
+ )
+
+
+def get_taxes_for_docs(docs, doctype, is_sales_doctype):
+ taxes_doctype = (
+ "Sales Taxes and Charges" if is_sales_doctype else "Purchase Taxes and Charges"
+ )
+ taxes = frappe.qb.DocType(taxes_doctype)
+ return (
+ frappe.qb.from_(taxes)
+ .select(
+ taxes.tax_amount,
+ taxes.account_head,
+ taxes.parent,
+ taxes.item_wise_tax_detail,
+ )
+ .where(taxes.parenttype == doctype)
+ .where(taxes.parent.isin(docs))
+ .run(as_dict=True)
+ )
+
+
+def get_items_for_docs(docs, doctype):
+ item_doctype = f"{doctype} Item"
+ item = frappe.qb.DocType(item_doctype)
+ return (
+ frappe.qb.from_(item)
+ .select(
+ item.name,
+ item.parent,
+ item.item_code,
+ item.item_name,
+ item.qty,
+ item.taxable_value,
+ )
+ .where(item.parenttype == doctype)
+ .where(item.parent.isin(docs))
+ .run(as_dict=True)
+ )
+
+
+def compile_docs(taxes, items):
+ """
+ Complie docs, so that each one could be accessed as if it's a single doc.
+ """
+ response = frappe._dict()
+
+ for tax in taxes:
+ doc = response.setdefault(tax.parent, frappe._dict({"taxes": [], "items": []}))
+ doc.get("taxes").append(tax)
+
+ for item in items:
+ doc = response.setdefault(item.parent, frappe._dict({"taxes": [], "items": []}))
+ doc.get("items").append(item)
+
+ return response
+
+
+def build_query_and_update_gst_details(gst_details, doctype):
+ transaction_item = frappe.qb.DocType(f"{doctype} Item")
+
+ update_query = frappe.qb.update(transaction_item)
+
+ # Initialize CASE queries
+ conditions = frappe._dict()
+ conditions_available_for = set()
+
+ for tax in GST_TAX_TYPES:
+ for field in (f"{tax}_rate", f"{tax}_amount"):
+ conditions[field] = Case()
+
+ # Update item conditions (WHEN)
+ for item_name, row in gst_details.items():
+ for field in conditions:
+ if not row[field]:
+ continue
+
+ conditions[field] = conditions[field].when(
+ transaction_item.name == item_name, row[field]
+ )
+ conditions_available_for.add(field)
+
+ # Update queries
+ for field in conditions:
+ # Atleast one WHEN condition required for Case queries
+ if field not in conditions_available_for:
+ continue
+
+ # ELSE
+ conditions[field] = conditions[field].else_(transaction_item[field])
+ update_query = update_query.set(transaction_item[field], conditions[field])
+
+ update_query = update_query.where(
+ transaction_item.name.isin(list(gst_details.keys()))
+ ).run()
diff --git a/india_compliance/public/js/india_compliance.bundle.js b/india_compliance/public/js/india_compliance.bundle.js
index f4e34791c..e384acdca 100644
--- a/india_compliance/public/js/india_compliance.bundle.js
+++ b/india_compliance/public/js/india_compliance.bundle.js
@@ -2,4 +2,5 @@ import "./utils";
import "./quick_entry";
import "./transaction";
import "./audit_trail_notification";
+import "./item_tax_template_notification";
import "./quick_info_popover";
diff --git a/india_compliance/public/js/item_tax_template_notification.js b/india_compliance/public/js/item_tax_template_notification.js
new file mode 100644
index 000000000..a48247f7d
--- /dev/null
+++ b/india_compliance/public/js/item_tax_template_notification.js
@@ -0,0 +1,45 @@
+// TODO: Update documentation links
+$(document).on("app_ready", async function () {
+ if (!frappe.boot.needs_item_tax_template_notification) return;
+
+ // let other processes finish
+ await new Promise(resolve => setTimeout(resolve, 700));
+ const d = frappe.msgprint({
+ title: __("🚨 Important: Changes to Item Tax Template"),
+ indicator: "orange",
+ message: __(
+ `Dear India Compliance User,
+
+
+ We are pleased to inform you about a recent update on how Item Tax Templates are
+ maintained in India Compliance App.
+
+
+ Migration Guide:
+ Migrating Item Tax Template
+
+
+ Breaking Change:
+