diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index cb45c35d4f..bccc61cea7 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -16,16 +16,11 @@ echo "Setting Up System Dependencies..."
sudo apt update
sudo apt remove mysql-server mysql-client
-sudo apt install libcups2-dev redis-server mariadb-client-10.6
+sudo apt install libcups2-dev redis-server mariadb-client
install_whktml() {
- if [ "$(lsb_release -rs)" = "22.04" ]; then
- wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
- sudo apt install /tmp/wkhtmltox.deb
- else
- echo "Please update this script to support wkhtmltopdf for $(lsb_release -ds)"
- exit 1
- fi
+ wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
+ sudo apt install /tmp/wkhtmltox.deb
}
install_whktml &
wkpid=$!
diff --git a/india_compliance/__init__.py b/india_compliance/__init__.py
index 384aaaec78..3cc51b948c 100644
--- a/india_compliance/__init__.py
+++ b/india_compliance/__init__.py
@@ -1 +1,14 @@
+import frappe
+from frappe.utils.user import is_website_user
+
__version__ = "16.0.0-dev"
+
+
+def check_app_permission():
+ if frappe.session.user == "Administrator":
+ return True
+
+ if is_website_user():
+ return False
+
+ return True
diff --git a/india_compliance/gst_india/api_classes/e_invoice.py b/india_compliance/gst_india/api_classes/e_invoice.py
index c59b6c7543..e337623014 100644
--- a/india_compliance/gst_india/api_classes/e_invoice.py
+++ b/india_compliance/gst_india/api_classes/e_invoice.py
@@ -21,6 +21,8 @@ class EInvoiceAPI(BaseAPI):
# Cancel IRN errors
"9999": "Invoice is not active",
"4002": "EwayBill is already generated for this IRN",
+ # IRN Generated in different Portal
+ "2148": "Requested IRN data is not available",
# Invalid GSTIN error
"3028": "GSTIN is invalid",
"3029": "GSTIN is not active",
diff --git a/india_compliance/gst_india/api_classes/taxpayer_base.py b/india_compliance/gst_india/api_classes/taxpayer_base.py
index 44f7173f83..81ceee94ff 100644
--- a/india_compliance/gst_india/api_classes/taxpayer_base.py
+++ b/india_compliance/gst_india/api_classes/taxpayer_base.py
@@ -466,13 +466,12 @@ def generate_app_key(self):
return app_key
- def get_files(self, return_period, token, action, endpoint, otp=None):
+ def get_files(self, return_period, token, action, endpoint):
response = self.get(
action=action,
return_period=return_period,
params={"ret_period": return_period, "token": token},
endpoint=endpoint,
- otp=otp,
)
if response.error_type == "queued":
diff --git a/india_compliance/gst_india/api_classes/taxpayer_returns.py b/india_compliance/gst_india/api_classes/taxpayer_returns.py
index fb0df061bb..ac7e605b4e 100644
--- a/india_compliance/gst_india/api_classes/taxpayer_returns.py
+++ b/india_compliance/gst_india/api_classes/taxpayer_returns.py
@@ -1,7 +1,10 @@
import frappe
from frappe import _
-from india_compliance.gst_india.api_classes.taxpayer_base import TaxpayerBaseAPI
+from india_compliance.gst_india.api_classes.taxpayer_base import (
+ FilesAPI,
+ TaxpayerBaseAPI,
+)
class ReturnsAPI(TaxpayerBaseAPI):
@@ -23,9 +26,9 @@ class ReturnsAPI(TaxpayerBaseAPI):
"RET2B1010": "authorization_failed", # API Authorization Failed for 2B
}
- def download_files(self, return_period, token, otp=None):
+ def download_files(self, return_period, token):
return super().get_files(
- return_period, token, action="FILEDET", endpoint="returns", otp=otp
+ return_period, token, action="FILEDET", endpoint="returns"
)
def get_return_status(self, return_period, reference_id, otp=None):
@@ -37,17 +40,19 @@ def get_return_status(self, return_period, reference_id, otp=None):
otp=otp,
)
- def proceed_to_file(self, return_type, return_period, otp=None):
+ def proceed_to_file(self, return_type, return_period, is_nil_return, otp=None):
+ data = {
+ "gstin": self.company_gstin,
+ "ret_period": return_period,
+ }
+
+ if is_nil_return:
+ data["isnil"] = "Y"
+
return self.post(
return_type=return_type,
return_period=return_period,
- json={
- "action": "RETNEWPTF",
- "data": {
- "gstin": self.company_gstin,
- "ret_period": return_period,
- }, # "isnil": "N" / "Y"
- },
+ json={"action": "RETNEWPTF", "data": data},
endpoint="returns/gstrptf",
otp=otp,
)
@@ -55,6 +60,7 @@ def proceed_to_file(self, return_type, return_period, otp=None):
class GSTR2bAPI(ReturnsAPI):
API_NAME = "GSTR-2B"
+ END_POINT = "returns/gstr2b"
def get_data(self, return_period, otp=None, file_num=None):
params = {"rtnprd": return_period}
@@ -65,10 +71,29 @@ def get_data(self, return_period, otp=None, file_num=None):
action="GET2B",
return_period=return_period,
params=params,
- endpoint="returns/gstr2b",
+ endpoint=self.END_POINT,
otp=otp,
)
+ def regenerate_2b(self, return_period):
+ return self.put(
+ json={
+ "action": "GEN2B",
+ "data": {"rtin": self.company_gstin, "itcprd": return_period},
+ },
+ endpoint=self.END_POINT,
+ )
+
+ def get_2b_gen_status(self, transaction_id):
+ return self.get(
+ action="GENSTS2B",
+ params={
+ "gstin": self.company_gstin,
+ "int_tran_id": transaction_id,
+ },
+ endpoint=self.END_POINT,
+ )
+
class GSTR2aAPI(ReturnsAPI):
API_NAME = "GSTR-2A"
@@ -151,3 +176,215 @@ def file_gstr_1(self, return_period, summary_data, pan, evc_otp):
},
endpoint="returns/gstr1",
)
+
+
+class GSTR3bAPI(ReturnsAPI):
+ END_POINT = "returns/gstr3b"
+
+ def setup(self, company_gstin, return_period):
+ self.return_period = return_period
+ super().setup(company_gstin=company_gstin)
+
+ def get_data(self):
+ return self.get(
+ action="RETSUM",
+ return_period=self.return_period,
+ params={"gstin": self.company_gstin, "ret_period": self.return_period},
+ endpoint=self.END_POINT,
+ )
+
+ def save_gstr3b(self, data):
+ return self.put(
+ return_period=self.return_period,
+ json={
+ "action": "RETSAVE",
+ "data": data,
+ },
+ endpoint=self.END_POINT,
+ )
+
+ def submit_gstr3b(self, data):
+ return self.post(
+ return_period=self.return_period,
+ json={
+ "action": "RETSUBMIT",
+ "data": data,
+ },
+ endpoint=self.END_POINT,
+ )
+
+ def save_offset_liability_gstr3b(self, data):
+ return self.put(
+ return_period=self.return_period,
+ json={
+ "action": "RETOFFSET",
+ "data": data,
+ },
+ endpoint=self.END_POINT,
+ )
+
+ def file_gstr_3b(self, data, pan, evc_otp):
+ return self.post(
+ return_period=self.return_period,
+ json={
+ "action": "RETFILE",
+ "data": data,
+ "st": "EVC",
+ "sid": f"{pan}|{evc_otp}",
+ },
+ endpoint=self.END_POINT,
+ )
+
+ def get_itc_liab_data(self):
+ return self.get(
+ action="AUTOLIAB",
+ return_period=self.return_period,
+ params={"gstin": self.company_gstin, "ret_period": self.return_period},
+ endpoint=self.END_POINT,
+ )
+
+ def validate_3b_against_auto_calc(self, data):
+ return self.post(
+ return_period=self.return_period,
+ json={
+ "action": "VALID",
+ "data": data,
+ },
+ endpoint=self.END_POINT,
+ )
+
+ def get_system_calc_interest(self):
+ return self.get(
+ action="RETINT",
+ return_period=self.return_period,
+ params={"gstin": self.company_gstin, "ret_period": self.return_period},
+ endpoint=self.END_POINT,
+ )
+
+ def recompute_interest(self):
+ return self.post(
+ return_period=self.return_period,
+ json={
+ "action": "CMPINT",
+ "data": {"gstn": self.company_gstin, "ret_period": self.return_period},
+ },
+ endpoint=self.END_POINT,
+ )
+
+ def save_past_liab(self, data):
+ return self.put(
+ return_period=self.return_period,
+ json={"action": "RETBKP", "data": data},
+ endpoint=self.END_POINT,
+ )
+
+ def get_itc_reversal_bal(self):
+ return self.get(
+ action="CLOSINGBAL",
+ return_period=self.return_period,
+ params={"gstin": self.company_gstin},
+ endpoint=self.END_POINT,
+ )
+
+ def get_rcm_bal(self):
+ return self.get(
+ action="RCMCLOSINGBAL",
+ return_period=self.return_period,
+ params={"gstin": self.company_gstin},
+ endpoint=self.END_POINT,
+ )
+
+ def get_opening_bal(self):
+ return self.get(
+ action="OPENINGBAL",
+ return_period=self.return_period,
+ params={"gstin": self.company_gstin},
+ endpoint=self.END_POINT,
+ )
+
+ def get_rcm_opening_bal(self):
+ return self.get(
+ action="RCMOPNBAL",
+ return_period=self.return_period,
+ params={"gstin": self.company_gstin},
+ endpoint=self.END_POINT,
+ )
+
+ def save_opening_bal(self, data):
+ return self.post(
+ return_period=self.return_period,
+ json={"action": "SAVEOB", "data": data},
+ endpoint=self.END_POINT,
+ )
+
+ def submit_rcm_opening_bal(self, data):
+ return self.post(
+ return_period=self.return_period,
+ json={
+ "action": "SAVERCMOPNBAL",
+ "data": data,
+ },
+ endpoint=self.END_POINT,
+ )
+
+
+class IMSAPI(ReturnsAPI):
+ API_NAME = "IMS"
+ END_POINT = "returns/ims"
+
+ def get_data(self, section):
+ return self.get(
+ action="GETINV",
+ params={
+ "gstin": self.company_gstin,
+ "section": section,
+ },
+ endpoint=self.END_POINT,
+ )
+
+ def download_files(self, return_period, token):
+ return self.get_files(
+ return_period, token, action="FILEDET", endpoint=self.END_POINT
+ )
+
+ def get_files(self, return_period, token, action, endpoint):
+ response = self.get(
+ action=action,
+ return_period=return_period,
+ params={"gstin": self.company_gstin, "token": token},
+ endpoint=endpoint,
+ )
+
+ if response.error_type == "queued":
+ return response
+
+ return FilesAPI().get_all(response)
+
+ def save(self, data):
+ return self.put(
+ endpoint=self.END_POINT,
+ json={
+ "action": "SAVE",
+ "data": {"rtin": self.company_gstin, "reqtyp": "SAVE", "invdata": data},
+ },
+ )
+
+ def reset(self, data):
+ return self.put(
+ endpoint=self.END_POINT,
+ json={
+ "action": "RESETIMS",
+ "data": {
+ "rtin": self.company_gstin,
+ "reqtyp": "RESET",
+ "invdata": data,
+ },
+ },
+ )
+
+ def get_request_status(self, transaction_id):
+ return self.get(
+ action="REQSTS",
+ endpoint=self.END_POINT,
+ params={"gstin": self.company_gstin, "int_tran_id": transaction_id},
+ )
diff --git a/india_compliance/gst_india/client_scripts/purchase_invoice.js b/india_compliance/gst_india/client_scripts/purchase_invoice.js
index dae0d69e32..bb4948ed16 100644
--- a/india_compliance/gst_india/client_scripts/purchase_invoice.js
+++ b/india_compliance/gst_india/client_scripts/purchase_invoice.js
@@ -74,20 +74,19 @@ frappe.ui.form.on(DOCTYPE, {
on_submit: function (frm) {
if (!frm._inward_supply) return;
-
// go back to previous page and match the invoice with the inward supply
setTimeout(() => {
- frappe.route_hooks.after_load = reco_frm => {
- if (!reco_frm.purchase_reconciliation_tool) return;
- purchase_reconciliation_tool.link_documents(
- reco_frm,
+ frappe.route_hooks.after_load = source_frm => {
+ if (!source_frm.reconciliation_tabs) return;
+ reconciliation.link_documents(
+ source_frm,
frm.doc.name,
frm._inward_supply.name,
"Purchase Invoice",
false
);
};
- frappe.set_route("Form", "Purchase Reconciliation Tool");
+ frappe.set_route("Form", frm._inward_supply.source_doc);
}, 2000);
},
});
diff --git a/india_compliance/gst_india/constants/__init__.py b/india_compliance/gst_india/constants/__init__.py
index 39f0996658..6086191907 100644
--- a/india_compliance/gst_india/constants/__init__.py
+++ b/india_compliance/gst_india/constants/__init__.py
@@ -36,6 +36,23 @@
"Input Service Distributor": "B2B",
}
+GST_CATEGORY_MAP = {
+ "R": "Regular",
+ "SEZWP": "SEZ supplies with payment of tax",
+ "SEZWOP": "SEZ supplies with out payment of tax",
+ "DE": "Deemed exports",
+ "CBW": "Intra-State Supplies attracting IGST",
+}
+
+ACTION_MAP = {"A": "Accepted", "R": "Rejected", "P": "Pending", "N": "No Action"}
+
+STATUS_CODE_MAP = {
+ "P": "Processed",
+ "PE": "Processed with Errors",
+ "ER": "Error",
+ "IP": "In Progress",
+}
+
EXPORT_TYPES = (
"WOP", # Without Payment of Tax [0]
"WP", # With Payment of Tax [1]
diff --git a/india_compliance/gst_india/data/test_ims.json b/india_compliance/gst_india/data/test_ims.json
new file mode 100644
index 0000000000..214fee0d8e
--- /dev/null
+++ b/india_compliance/gst_india/data/test_ims.json
@@ -0,0 +1,134 @@
+{
+ "b2b": [
+ {
+ "stin": "24MAYAS0100J1JD",
+ "inum": "b1",
+ "inv_typ": "R",
+ "action": "A",
+ "ispendactblocked": "N",
+ "srcform": "R1",
+ "rtnprd": "012023",
+ "srcfilstatus": "Not Filed",
+ "idt": "23-01-2023",
+ "val": 1000,
+ "pos": "24",
+ "txval": 100,
+ "iamt": 20,
+ "camt": 20,
+ "samt": 20,
+ "cess": 0,
+ "hash": "1f5fb5de9e491c24dcdee9ac01d3fa5f7889f20d3a8c923bfd2e3c7f0d0125f1"
+ }
+ ],
+ "b2ba": [
+ {
+ "oinum": "ab2",
+ "oidt": "24-02-2023",
+ "stin": "24MAYAS0100J1JD",
+ "rtnprd": "012023",
+ "inum": "b1a",
+ "action": "A",
+ "ispendactblocked": "N",
+ "inv_typ": "R",
+ "srcform": "R1",
+ "idt": "23-01-2023",
+ "val": 1000,
+ "pos": "07",
+ "txval": 100,
+ "iamt": 20,
+ "camt": 20,
+ "samt": 20,
+ "cess": 0,
+ "srcfilstatus": "Not Filed",
+ "hash": "1f5fb5de9e491c24dcdee9ac01d3fa5f7889f20d3a8c923bfd2e3c7f0d0125f1"
+ }
+ ],
+ "b2bdn": [
+ {
+ "stin": "24MAYAS0100J1JD",
+ "nt_num": "dn2",
+ "action": "A",
+ "inv_typ": "R",
+ "srcform": "R1",
+ "ispendactblocked": "N",
+ "rtnprd": "012023",
+ "nt_dt": "24-02-2023",
+ "val": 1000.1,
+ "pos": "07",
+ "txval": 1000.1,
+ "iamt": 20,
+ "camt": 20,
+ "samt": 20,
+ "cess": 0,
+ "srcfilstatus": "Filed",
+ "hash": "1f5fb5de9e491c24dcdee9ac01d3fa5f7889f20d3a8c923bfd2e3c7f0d0125f1"
+ }
+ ],
+ "b2bdna": [
+ {
+ "stin": "24MAYAS0100J1JD",
+ "ont_num": "dn2",
+ "ont_dt": "24-02-2023",
+ "nt_num": "dna2",
+ "action": "A",
+ "inv_typ": "R",
+ "srcform": "R1",
+ "ispendactblocked": "N",
+ "rtnprd": "012023",
+ "nt_dt": "24-02-2023",
+ "val": 1000.1,
+ "pos": "07",
+ "txval": 1000.1,
+ "iamt": 20,
+ "camt": 20,
+ "samt": 20,
+ "cess": 0,
+ "srcfilstatus": "Filed",
+ "hash": "1f5fb5de9e491c24dcdee9ac01d3fa5f7889f20d3a8c923bfd2e3c7f0d0125f1"
+ }
+ ],
+ "b2bcn": [
+ {
+ "stin": "24MAYAS0100J1JD",
+ "nt_num": "cn2",
+ "action": "A",
+ "inv_typ": "R",
+ "srcform": "R1",
+ "rtnprd": "012023",
+ "ispendactblocked": "N",
+ "nt_dt": "24-02-2023",
+ "val": 1000.1,
+ "pos": "07",
+ "txval": 1000.1,
+ "iamt": 20,
+ "camt": 20,
+ "samt": 20,
+ "cess": 0,
+ "srcfilstatus": "Not Filed",
+ "hash": "1f5fb5de9e491c24dcdee9ac01d3fa5f7889f20d3a8c923bfd2e3c7f0d0125f1"
+ }
+ ],
+ "b2bcna": [
+ {
+ "stin": "24MAYAS0100J1JD",
+ "ont_num": "cn2",
+ "ont_dt": "24-02-2023",
+ "nt_num": "cna2",
+ "action": "A",
+ "inv_typ": "R",
+ "srcform": "R1",
+ "ispendactblocked": "N",
+ "rtnprd": "012023",
+ "nt_dt": "24-02-2023",
+ "val": 1000.1,
+ "pos": "07",
+ "txval": 1000.1,
+ "iamt": 20,
+ "camt": 20,
+ "samt": 20,
+ "cess": 0,
+ "srcfilstatus": "Not Filed",
+ "hash": "1f5fb5de9e491c24dcdee9ac01d3fa5f7889f20d3a8c923bfd2e3c7f0d0125f1"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/india_compliance/gst_india/doctype/bill_of_entry/test_bill_of_entry.py b/india_compliance/gst_india/doctype/bill_of_entry/test_bill_of_entry.py
index 0fd56b4797..03161896a0 100644
--- a/india_compliance/gst_india/doctype/bill_of_entry/test_bill_of_entry.py
+++ b/india_compliance/gst_india/doctype/bill_of_entry/test_bill_of_entry.py
@@ -17,13 +17,23 @@
from india_compliance.gst_india.utils import get_gst_accounts_by_type
from india_compliance.gst_india.utils.tests import create_purchase_invoice
+IGNORE_TEST_RECORD_DEPENDENCIES = [
+ "Bill of Entry",
+ "Purchase Invoice",
+ "Cost Center",
+ # "Tax Category",
+ "Item",
+ # "UOM",
+ "Item Tax Template",
+ "Project",
+ "Company",
+ "Account",
+]
+
class TestBillofEntry(IntegrationTestCase):
@classmethod
def setUpClass(cls):
- # don't create test objects
- frappe.local.test_objects["Bill of Entry"] = []
-
super().setUpClass()
frappe.db.set_single_value("GST Settings", "enable_overseas_transactions", 1)
diff --git a/india_compliance/gst_india/doctype/gst_hsn_code/test_gst_hsn_code.py b/india_compliance/gst_india/doctype/gst_hsn_code/test_gst_hsn_code.py
index 115ece33d7..60f2d109aa 100644
--- a/india_compliance/gst_india/doctype/gst_hsn_code/test_gst_hsn_code.py
+++ b/india_compliance/gst_india/doctype/gst_hsn_code/test_gst_hsn_code.py
@@ -9,13 +9,12 @@
update_taxes_in_item_master,
)
+IGNORE_TEST_RECORD_DEPENDENCIES = ["Item Tax Template"]
+
class TestGSTHSNCode(IntegrationTestCase):
@classmethod
def setUpClass(cls):
- # don't create test objects
- frappe.local.test_objects["GST HSN Code"] = []
-
super().setUpClass()
@change_settings("GST Settings", {"validate_hsn_code": 0})
diff --git a/india_compliance/gst_india/doctype/gst_invoice_management_system/__init__.py b/india_compliance/gst_india/doctype/gst_invoice_management_system/__init__.py
new file mode 100644
index 0000000000..dde5bb557a
--- /dev/null
+++ b/india_compliance/gst_india/doctype/gst_invoice_management_system/__init__.py
@@ -0,0 +1,246 @@
+import frappe
+from frappe.query_builder import Case
+from frappe.query_builder.custom import ConstantColumn
+from frappe.query_builder.functions import Abs, IfNull, Sum
+
+from india_compliance.gst_india.constants import GST_TAX_TYPES
+from india_compliance.gst_india.doctype.purchase_reconciliation_tool import (
+ GSTIN_RULES,
+ PAN_RULES,
+ BaseUtil,
+ Reconciler,
+)
+
+
+class IMSReconciler(Reconciler):
+ CATEGORIES = (
+ {"doc_type": "Invoice", "category": "B2B"},
+ {"doc_type": "Debit Note", "category": "CDNR"},
+ {"doc_type": "Credit Note", "category": "CDNR"},
+ )
+
+ def reconcile(self, filters):
+ """
+ Reconcile purchases and inward supplies.
+ """
+ for row in self.CATEGORIES:
+ filters["doc_type"], self.category = row.values()
+
+ purchases = PurchaseInvoice().get_unmatched(filters)
+ inward_supplies = InwardSupply().get_unmatched(filters)
+
+ # GSTIN Level matching
+ self.reconcile_for_rules(GSTIN_RULES, purchases, inward_supplies)
+
+ # PAN Level matching
+ purchases = self.get_pan_level_data(purchases)
+ inward_supplies = self.get_pan_level_data(inward_supplies)
+ self.reconcile_for_rules(PAN_RULES, purchases, inward_supplies)
+
+
+class InwardSupply:
+ def __init__(self):
+ self.IMS = frappe.qb.DocType("GST Inward Supply")
+
+ def get_all(self, company_gstin, names=None):
+ query = self.get_query(company_gstin, ["action", "doc_type"])
+
+ if names:
+ query = query.where(self.IMS.name.isin(names))
+
+ return query.run(as_dict=True)
+
+ def get_for_save(self, company_gstin):
+ return (
+ self.get_query_for_upload(company_gstin)
+ .where(self.IMS.ims_action != "No Action")
+ .run(as_dict=True)
+ )
+
+ def get_for_reset(self, company_gstin):
+ return (
+ self.get_query_for_upload(company_gstin)
+ .where(self.IMS.ims_action == "No Action")
+ .run(as_dict=True)
+ )
+
+ def get_query_for_upload(self, company_gstin):
+ return self.get_query(
+ company_gstin,
+ additional_fields=[
+ "doc_type",
+ "is_amended",
+ "sup_return_period",
+ "document_value",
+ ],
+ ).where(self.IMS.ims_action != self.IMS.previous_ims_action)
+
+ def get_unmatched(self, filters):
+ query = self.get_query(filters.company_gstin)
+ data = (
+ query.where(IfNull(self.IMS.match_status, "") == "")
+ .where(self.IMS.doc_type == filters.doc_type)
+ .run(as_dict=True)
+ )
+
+ for doc in data:
+ doc.fy = BaseUtil.get_fy(doc.bill_date)
+
+ return BaseUtil.get_dict_for_key("supplier_gstin", data)
+
+ def get_query(self, company_gstin, additional_fields=None):
+ fields = self.get_fields(additional_fields=additional_fields)
+
+ return (
+ frappe.qb.from_(self.IMS)
+ .select(
+ *fields,
+ ConstantColumn("GST Inward Supply").as_("doctype"),
+ Case()
+ .when(
+ (self.IMS.ims_action == self.IMS.previous_ims_action),
+ False,
+ )
+ .else_(True)
+ .as_("pending_upload"),
+ )
+ .where(IfNull(self.IMS.previous_ims_action, "") != "")
+ .where(self.IMS.company_gstin == company_gstin)
+ )
+
+ def get_fields(self, additional_fields=None):
+ fields = [
+ "supplier_gstin",
+ "supplier_name",
+ "company_gstin",
+ "bill_no",
+ "bill_date",
+ "name",
+ "is_reverse_charge",
+ "place_of_supply",
+ "link_name",
+ "link_doctype",
+ "match_status",
+ "ims_action",
+ "previous_ims_action",
+ "supply_type",
+ "classification",
+ "is_pending_action_allowed",
+ "supplier_return_form",
+ "is_supplier_return_filed",
+ ]
+
+ if additional_fields:
+ fields += additional_fields
+
+ fields = [self.IMS[field] for field in fields]
+ fields += self.get_tax_fields()
+
+ return fields
+
+ def get_tax_fields(self):
+ fields = GST_TAX_TYPES[:-1] + ("taxable_value",)
+ return [self.IMS[field] for field in fields]
+
+
+class PurchaseInvoice:
+ def __init__(self):
+ self.PI = frappe.qb.DocType("Purchase Invoice")
+ self.PI_ITEM = frappe.qb.DocType("Purchase Invoice Item")
+
+ def get_all(self, names=None, filters=None):
+ query = self.get_query(filters=filters)
+
+ if names:
+ query = query.where(self.PI.name.isin(names))
+
+ purchases = query.run(as_dict=True)
+
+ return {doc.name: doc for doc in purchases}
+
+ def get_unmatched(self, filters):
+ gst_category = (
+ "Registered Regular",
+ "Tax Deductor",
+ "Tax Collector",
+ "Input Service Distributor",
+ )
+ is_return = 1 if filters.doc_type == "Credit Note" else 0
+
+ data = (
+ self.get_query(filters=filters)
+ .where(self.PI.gst_category.isin(gst_category))
+ .where(self.PI.reconciliation_status == "Unreconciled")
+ .where(self.PI.is_return == is_return)
+ .where(self.PI.ineligibility_reason != "ITC restricted due to PoS rules")
+ .run(as_dict=True)
+ )
+
+ for doc in data:
+ doc.fy = BaseUtil.get_fy(doc.bill_date)
+
+ return BaseUtil.get_dict_for_key("supplier_gstin", data)
+
+ def get_query(self, filters=None, additional_fields=None):
+ fields = self.get_fields(additional_fields)
+
+ query = (
+ frappe.qb.from_(self.PI)
+ .left_join(self.PI_ITEM)
+ .on(self.PI_ITEM.parent == self.PI.name)
+ .select(
+ Abs(Sum(self.PI_ITEM.taxable_value)).as_("taxable_value"),
+ *fields,
+ ConstantColumn("Purchase Invoice").as_("doctype"),
+ )
+ .where(self.PI.docstatus == 1)
+ .where(IfNull(self.PI.reconciliation_status, "") != "Not Applicable")
+ .where(self.PI.is_opening == "No")
+ .where(self.PI_ITEM.parenttype == "Purchase Invoice")
+ .where(self.PI.is_reverse_charge == 0) # for IMS
+ .groupby(self.PI.name)
+ )
+
+ if filters:
+ query = self.apply_filters(query, filters)
+
+ return query
+
+ def apply_filters(self, query, filters):
+ if filters.get("company"):
+ query = query.where(self.PI.company == filters.company)
+
+ if filters.get("company_gstin"):
+ query = query.where(self.PI.company_gstin == filters.company_gstin)
+
+ return query
+
+ def get_fields(self, additional_fields=None):
+ fields = [
+ "supplier_gstin",
+ "supplier_name",
+ "bill_no",
+ "bill_date",
+ "name",
+ "company",
+ "company_gstin",
+ "is_reverse_charge",
+ "place_of_supply",
+ ]
+
+ if additional_fields:
+ fields += additional_fields
+
+ fields = [self.PI[field] for field in fields]
+ fields += self.get_tax_fields()
+
+ return fields
+
+ def get_tax_fields(self):
+ return [
+ self.query_tax_amount(f"{tax_type}_amount").as_(tax_type)
+ for tax_type in GST_TAX_TYPES
+ ]
+
+ def query_tax_amount(self, field):
+ return Abs(Sum(getattr(self.PI_ITEM, field)))
diff --git a/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.css b/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.css
new file mode 100644
index 0000000000..003527b27c
--- /dev/null
+++ b/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.css
@@ -0,0 +1,109 @@
+div[data-page-route="GST Invoice Management System"] {
+ --dt-row-height: 34px;
+}
+
+div[data-page-route="GST Invoice Management System"] .section-body {
+ max-width: 100% !important;
+}
+
+div[data-page-route="GST Invoice Management System"]
+ [data-fieldname="invoice_html"]
+ .section-body {
+ margin-top: 0;
+}
+
+div[data-page-route="GST Invoice Management System"]
+ [data-fieldname="invoice_html"]
+ .form-tabs-list {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 0 solid black;
+ padding-right: var(--padding-lg);
+ position: inherit;
+}
+
+div[data-page-route="GST Invoice Management System"]
+ [data-fieldname="invoice_html"]
+ .form-tabs-list
+ .custom-button-group {
+ display: flex;
+}
+
+div[data-page-route="GST Invoice Management System"]
+ [data-fieldname="invoice_html"]
+ .form-tabs-list
+ .inner-group-button,
+.filter-selector {
+ margin-bottom: 8px;
+ margin-left: 8px;
+}
+
+div[data-page-route="GST Invoice Management System"]
+ [data-fieldname="invoice_html"]
+ .form-tabs-list
+ .custom-button-group
+ .btn {
+ padding: 5px 10px;
+}
+
+div[data-page-route="GST Invoice Management System"] .title-area .indicator-pill {
+ display: none;
+}
+
+div[data-page-route="GST Invoice Management System"] .datatable .dt-scrollable {
+ overflow-y: auto !important;
+ margin-bottom: 2em;
+ min-height: calc(100vh - 450px);
+}
+
+div[data-page-route="GST Invoice Management System"] .datatable .dt-row {
+ height: unset;
+}
+
+div[data-page-route="GST Invoice Management System"] .datatable .dt-row-filter {
+ height: var(--dt-row-height);
+}
+
+div[data-page-route="GST Invoice Management System"]
+ .datatable
+ .dt-row-filter
+ .dt-cell {
+ max-height: var(--dt-row-height);
+}
+
+div[data-page-route="GST Invoice Management System"] [data-fieldname="no_invoice_data"],
+div[data-page-route="GST Invoice Management System"]
+ [data-fieldname="invoice_empty_state"] {
+ min-height: 320px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+}
+
+div[data-page-route="GST Invoice Management System"]
+ [data-fieldname="no_invoice_data"]
+ > img,
+div[data-page-route="GST Invoice Management System"]
+ [data-fieldname="invoice_empty_state"]
+ > img {
+ margin-bottom: var(--margin-md);
+ max-height: 100px;
+}
+
+div[data-page-route="GST Invoice Management System"] .dropdown-divider {
+ height: 0;
+ margin: 5px 0;
+ border-top: 1px solid var(--border-color);
+ padding: 0;
+}
+
+.modal-dialog div[data-fieldname="detail_table"] .table > tbody > tr > td {
+ text-align: center;
+ width: 30%;
+}
+
+div[data-page-route="GST Invoice Management System"] .action-summary {
+ text-decoration: none;
+}
diff --git a/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.js b/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.js
new file mode 100644
index 0000000000..d693e43489
--- /dev/null
+++ b/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.js
@@ -0,0 +1,990 @@
+// Copyright (c) 2024, Resilient Tech and contributors
+// For license information, please see license.txt
+
+const api_enabled = india_compliance.is_api_enabled();
+const DOCTYPE = "GST Invoice Management System";
+const DOC_PATH =
+ "india_compliance.gst_india.doctype.gst_invoice_management_system.gst_invoice_management_system";
+
+const category_map = {
+ "B2B-Invoices": "Invoice",
+ "B2B-Credit Notes": "Credit Note",
+ "B2B-Debit Notes": "Debit Note",
+};
+
+const ACTION_MAP = {
+ "No Action": "No Action",
+ Accept: "Accepted",
+ Pending: "Pending",
+ Reject: "Rejected",
+};
+
+frappe.ui.form.on(DOCTYPE, {
+ async setup(frm) {
+ await frappe.require("ims.bundle.js");
+
+ frm.reconciliation_tabs = new IMS(
+ frm,
+ ["invoice", "match_summary", "action_summary"],
+ "invoice_html"
+ );
+
+ frm.trigger("company");
+
+ // Setup Listeners
+
+ // Download Queued
+ frappe.realtime.on("ims_download_queued", message => {
+ frappe.msgprint(message["message"]);
+ });
+
+ // Downloaded and Reconciled Invoices
+ frappe.realtime.on("ims_download_completed", message => {
+ frm.ims_actions.get_ims_data();
+ frappe.show_alert({ message: message["message"], indicator: "green" });
+ });
+
+ // Upload and Check Status
+ frappe.realtime.on("upload_data_and_check_status", async message => {
+ await frm.ims_actions.get_ims_data();
+ frm.ims_actions.upload_ims_data();
+ });
+ },
+
+ async company(frm) {
+ render_empty_state(frm);
+ if (!frm.doc.company) return;
+ const options = await india_compliance.set_gstin_options(frm);
+
+ frm.set_value("company_gstin", options[0]);
+ },
+
+ company_gstin: render_empty_state,
+
+ refresh(frm) {
+ show_download_invoices_message(frm);
+
+ frm.ims_actions = new IMSAction(frm);
+ frm.ims_actions.setup_actions();
+ },
+});
+
+class IMS extends reconciliation.reconciliation_tabs {
+ refresh(data) {
+ super.refresh(data);
+ this.set_actions_summary();
+ }
+
+ get_tab_group_fields() {
+ return [
+ {
+ //hack: for the FieldGroup(Layout) to avoid rendering default "details" tab
+ fieldtype: "Section Break",
+ },
+ {
+ label: "Match Summary",
+ fieldtype: "Tab Break",
+ fieldname: "match_summary_tab",
+ active: 1,
+ },
+ {
+ fieldtype: "HTML",
+ fieldname: "match_summary_data",
+ },
+ {
+ label: "Actions Summary",
+ fieldtype: "Tab Break",
+ fieldname: "action_summary_tab",
+ },
+ {
+ fieldtype: "HTML",
+ fieldname: "action_summary_data",
+ },
+ {
+ label: "Document View",
+ fieldtype: "Tab Break",
+ fieldname: "invoice_tab",
+ },
+ {
+ fieldtype: "HTML",
+ fieldname: "invoice_data",
+ },
+ ];
+ }
+
+ get_filter_fields() {
+ const fields = [
+ {
+ label: "Supplier Name",
+ fieldname: "supplier_name",
+ fieldtype: "Autocomplete",
+ options: this.get_autocomplete_options("supplier_name"),
+ },
+ {
+ label: "Supplier GSTIN",
+ fieldname: "supplier_gstin",
+ fieldtype: "Autocomplete",
+ options: this.get_autocomplete_options("supplier_gstin"),
+ },
+ {
+ label: "Match Status",
+ fieldname: "match_status",
+ fieldtype: "Select",
+ options: [
+ "Exact Match",
+ "Suggested Match",
+ "Mismatch",
+ "Manual Match",
+ "Missing in PI",
+ ],
+ },
+ {
+ label: "Action",
+ fieldname: "ims_action",
+ fieldtype: "Select",
+ options: ["No Action", "Accepted", "Rejected", "Pending"],
+ },
+ {
+ label: "Document Type",
+ fieldname: "doc_type",
+ fieldtype: "Select",
+ options: ["Invoice", "Credit Note", "Debit Note"],
+ },
+ {
+ label: "Upload Pending",
+ fieldname: "pending_upload",
+ fieldtype: "Check",
+ },
+ {
+ label: "Is Pending Action Allowed",
+ fieldname: "is_pending_action_allowed",
+ fieldtype: "Check",
+ },
+ {
+ label: "Classification",
+ fieldname: "classification",
+ fieldtype: "Select",
+ options: ["B2B", "B2BA", "CDNR", "CDNRA"],
+ },
+ {
+ label: "Is Supplier Return Filed",
+ fieldname: "is_supplier_return_filed",
+ fieldtype: "Check",
+ },
+ ];
+
+ fields.forEach(field => (field.parent = DOCTYPE));
+ return fields;
+ }
+
+ set_listeners() {
+ const me = this;
+
+ // TODO: Refactor like purchase_reconciliation.js
+
+ this.tabs.invoice_tab.datatable.$datatable.on(
+ "click",
+ ".supplier-gstin",
+ function (e) {
+ me.update_filter(e, "supplier_gstin", $(this).text().trim(), me);
+ }
+ );
+
+ this.tabs.invoice_tab.datatable.$datatable.on(
+ "click",
+ ".match-status",
+ function (e) {
+ me.update_filter(e, "match_status", $(this).text(), me);
+ }
+ );
+
+ this.tabs.match_summary_tab.datatable.$datatable.on(
+ "click",
+ ".match-status",
+ function (e) {
+ me.update_filter(e, "match_status", $(this).text(), me);
+ }
+ );
+
+ this.tabs.invoice_tab.datatable.$datatable.on(
+ "click",
+ ".ims-action",
+ function (e) {
+ me.update_filter(e, "ims_action", $(this).text(), me);
+ }
+ );
+
+ this.tabs.action_summary_tab.datatable.$datatable.on(
+ "click",
+ ".invoice-category",
+ function (e) {
+ me.update_filter(e, "doc_type", category_map[$(this).text()], me);
+ }
+ );
+
+ this.tabs.invoice_tab.datatable.$datatable.on(
+ "click",
+ ".classification",
+ function (e) {
+ me.update_filter(e, "classification", $(this).text(), me);
+ }
+ );
+
+ this.tabs.invoice_tab.datatable.$datatable.on(
+ "click",
+ ".btn.eye",
+ function (e) {
+ const row = me.mapped_invoice_data[$(this).attr("data-name")];
+ me.dm = new DetailViewDialog(me.frm, row);
+ }
+ );
+ }
+
+ async update_filter(e, field, field_value, me) {
+ e.preventDefault();
+
+ await me.filter_group.add_or_remove_filter([DOCTYPE, field, "=", field_value]);
+ me.filter_group.apply();
+ }
+
+ get_match_summary_columns() {
+ return [
+ {
+ label: "Match Status",
+ fieldname: "match_status",
+ width: 200,
+ _value: (...args) => `${args[0]}`,
+ },
+ {
+ label: "Count
2A/2B Docs",
+ fieldname: "inward_supply_count",
+ width: 120,
+ align: "center",
+ },
+ {
+ label: "Count
Purchase Docs",
+ fieldname: "purchase_count",
+ width: 120,
+ align: "center",
+ },
+ {
+ label: "Taxable Amount Diff
2A/2B - Purchase",
+ fieldname: "taxable_value_difference",
+ width: 180,
+ align: "center",
+ _value: (...args) => format_number(args[0]),
+ },
+ {
+ label: "Tax Difference
2A/2B - Purchase",
+ fieldname: "tax_difference",
+ width: 180,
+ align: "center",
+ _value: (...args) => format_number(args[0]),
+ },
+ {
+ label: "% Action Taken",
+ fieldname: "action_taken",
+ width: 120,
+ align: "center",
+ _value: (...args) => {
+ return (
+ roundNumber(
+ (args[2].action_taken_count / args[2].total_docs) * 100,
+ 2
+ ) + " %"
+ );
+ },
+ },
+ ];
+ }
+
+ get_match_summary_data() {
+ if (!this.data.length) return [];
+
+ const data = {};
+ this.filtered_data.forEach(row => {
+ let new_row = data[row.match_status];
+ if (!new_row) {
+ new_row = data[row.match_status] = {
+ match_status: row.match_status,
+ inward_supply_count: 0,
+ purchase_count: 0,
+ action_taken_count: 0,
+ total_docs: 0,
+ tax_difference: 0,
+ taxable_value_difference: 0,
+ };
+ }
+ if (row.inward_supply_name) new_row.inward_supply_count += 1;
+ if (row.purchase_invoice_name) new_row.purchase_count += 1;
+ if (row.ims_action != "No Action") new_row.action_taken_count += 1;
+ new_row.total_docs += 1;
+ new_row.tax_difference += row.tax_difference || 0;
+ new_row.taxable_value_difference += row.taxable_value_difference || 0;
+ });
+
+ return Object.values(data);
+ }
+
+ get_invoice_columns() {
+ return [
+ {
+ fieldname: "view",
+ fieldtype: "html",
+ width: 60,
+ align: "center",
+ _value: (...args) => get_icon(...args),
+ },
+ {
+ label: "Supplier Name",
+ fieldname: "supplier_name_gstin",
+ align: "center",
+ width: 200,
+ },
+ {
+ label: "Bill No.",
+ fieldname: "bill_no",
+ align: "center",
+ width: 120,
+ },
+ {
+ label: "Match Status",
+ fieldname: "match_status",
+ align: "center",
+ width: 120,
+ _value: (...args) => `${args[0]}`,
+ },
+ {
+ label: "Action",
+ fieldname: "ims_action",
+ align: "center",
+ width: 100,
+ _value: (...args) => `${args[0]}`,
+ },
+ {
+ label: "GST Inward
Supply",
+ fieldname: "inward_supply_name",
+ align: "center",
+ fieldtype: "Link",
+ options: "GST Inward Supply",
+ width: 150,
+ _after_format: (...args) => get_value_with_indicator(...args),
+ },
+ {
+ label: "Linked Voucher",
+ fieldname: "linked_doc",
+ align: "center",
+ width: 150,
+ fieldtype: "Dynamic Link",
+ options: "linked_voucher_type",
+ },
+ {
+ label: "Tax Difference
2A/2B - Purchase",
+ fieldname: "tax_difference",
+ align: "center",
+ width: 150,
+ _value: (...args) => format_number(args[0]),
+ },
+ {
+ label: "Taxable Amount Diff
2A/2B - Purchase",
+ fieldname: "taxable_value_difference",
+ align: "center",
+ width: 160,
+ _value: (...args) => format_number(args[0]),
+ },
+ {
+ label: "Classification",
+ fieldname: "classification",
+ align: "center",
+ width: 100,
+ _value: (...args) =>
+ `${args[0]}`,
+ },
+ ];
+ }
+
+ get_invoice_data() {
+ if (!this.data.length) return [];
+
+ const data = [];
+ this.mapped_invoice_data = {};
+
+ this.filtered_data.forEach(row => {
+ this.mapped_invoice_data[row.inward_supply_name] = row;
+
+ data.push({
+ supplier_name_gstin: this.get_supplier_name_gstin(row),
+ bill_no: row.bill_no,
+ classification: row._inward_supply.classification,
+ ims_action: row.ims_action || "",
+ match_status: row.match_status,
+ linked_doc: row.purchase_invoice_name,
+ tax_difference: row.tax_difference,
+ taxable_value_difference: row.taxable_value_difference,
+ inward_supply_name: row.inward_supply_name,
+ pending_upload: row.pending_upload,
+ is_supplier_return_filed: row.is_supplier_return_filed,
+ });
+ });
+
+ return data;
+ }
+
+ get_action_summary_columns() {
+ return [
+ {
+ label: "Category",
+ fieldname: "category",
+ width: 200,
+ _value: (...args) =>
+ `${args[0]}`,
+ },
+ {
+ label: "No Action",
+ fieldname: "no_action",
+ width: 200,
+ },
+ {
+ label: "Accepted",
+ fieldname: "accepted",
+ width: 200,
+ },
+ {
+ label: "Pending",
+ fieldname: "pending",
+ width: 200,
+ },
+ {
+ label: "Rejected",
+ fieldname: "rejected",
+ width: 200,
+ },
+ ];
+ }
+
+ get_action_summary_data(data) {
+ const category_map = {
+ Invoice: "B2B-Invoices",
+ "Credit Note": "B2B-Credit Notes",
+ "Debit Note": "B2B-Debit Notes",
+ };
+ let summary_data = {};
+ if (!data) data = this.filtered_data;
+
+ data.forEach(row => {
+ const action = frappe.scrub(row.ims_action);
+ const category = category_map[row.doc_type];
+ if (!summary_data[category]) {
+ summary_data[category] = {
+ category,
+ no_action: 0,
+ accepted: 0,
+ rejected: 0,
+ pending: 0,
+ };
+ }
+ summary_data[category][action] += 1;
+ });
+
+ return Object.values(summary_data);
+ }
+
+ async set_actions_summary() {
+ const actions_data = this.get_action_summary_data(this.data);
+
+ if ($(".action-performed-summary").length) {
+ $(".action-performed-summary").remove();
+ }
+
+ $(function () {
+ $('[data-toggle="tooltip"]').tooltip();
+ });
+
+ const actions_summary = {
+ no_action: { count: 0, color: "#7c7c7c" },
+ accepted: { count: 0, color: "#28a745" },
+ pending: { count: 0, color: "#ffc107" },
+ rejected: { count: 0, color: "#e03636" },
+ };
+
+ actions_data.forEach(row => {
+ actions_summary.accepted.count += row.accepted;
+ actions_summary.pending.count += row.pending;
+ actions_summary.rejected.count += row.rejected;
+ actions_summary.no_action.count += row.no_action;
+ });
+
+ const action_performed_cards = Object.entries(actions_summary)
+ .map(([value, data]) => {
+ const action = frappe.unscrub(value);
+ return `
{{ __(\"Generate to view the data\") }}
" + }, + { + "depends_on": "eval: doc.data_state === \"unavailable\"", + "fieldname": "no_invoice_data", + "fieldtype": "HTML", + "options": "{{ __(\"No data available for selected filters.\") }}
\n{{ __(\"Download Invoices\") }}" + }, + { + "fieldname": "data_section", + "fieldtype": "Section Break" + } + ], + "hide_toolbar": 1, + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2025-01-17 12:19:24.001794", + "modified_by": "Administrator", + "module": "GST India", + "name": "GST Invoice Management System", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "role": "Accounts User", + "write": 1 + } + ], + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.py b/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.py new file mode 100644 index 0000000000..ed8bc1ad9c --- /dev/null +++ b/india_compliance/gst_india/doctype/gst_invoice_management_system/gst_invoice_management_system.py @@ -0,0 +1,420 @@ +# Copyright (c) 2024, Resilient Tech and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + +from india_compliance.gst_india.api_classes.taxpayer_base import ( + TaxpayerBaseAPI, + otp_handler, +) +from india_compliance.gst_india.api_classes.taxpayer_returns import IMSAPI +from india_compliance.gst_india.constants import STATUS_CODE_MAP +from india_compliance.gst_india.doctype.gst_invoice_management_system import ( + IMSReconciler, + InwardSupply, + PurchaseInvoice, +) +from india_compliance.gst_india.doctype.gst_return_log.generate_gstr_1 import ( + verify_request_in_progress, +) +from india_compliance.gst_india.doctype.gstr_action.gstr_action import set_gstr_actions +from india_compliance.gst_india.doctype.purchase_reconciliation_tool import ( + ReconciledData, +) +from india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_utils import ( + get_formatted_options, +) +from india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_utils import ( + link_documents as _link_documents, +) +from india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_utils import ( + unlink_documents as _unlink_documents, +) +from india_compliance.gst_india.utils.gstr_2 import ( + GSTRCategory, + ReturnType, + download_ims_invoices, + get_data_handler, +) +from india_compliance.gst_india.utils.gstr_utils import ( + publish_action_status_notification, +) + +CATEGORY_MAP = { + "Invoice_0": GSTRCategory.B2B.value, + "Invoice_1": GSTRCategory.B2BA.value, + "Debit Note_0": GSTRCategory.B2BDN.value, + "Debit Note_1": GSTRCategory.B2BDNA.value, + "Credit Note_0": GSTRCategory.B2BCN.value, + "Credit Note_1": GSTRCategory.B2BCNA.value, +} + + +class GSTInvoiceManagementSystem(Document): + @frappe.whitelist() + def autoreconcile_and_get_data(self): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + + filters = frappe._dict( + { + "company": self.company, + "company_gstin": self.company_gstin, + } + ) + + # Auto-Reconcile invoices + IMSReconciler().reconcile(filters) + + return { + "invoice_data": self.get_invoice_data(filters=filters), + "pending_actions": self.get_pending_actions(), + } + + def get_invoice_data(self, inward_supply=None, purchase=None, filters=None): + if not filters: + filters = frappe._dict( + { + "company": self.company, + "company_gstin": self.company_gstin, + } + ) + + inward_supplies = InwardSupply().get_all( + company_gstin=self.company_gstin, names=inward_supply + ) + + if not purchase: + purchase = [doc.link_name for doc in inward_supplies] + + purchases = PurchaseInvoice().get_all(names=purchase, filters=filters) + + invoice_data = [] + for doc in inward_supplies: + invoice_data.append( + frappe._dict( + { + "ims_action": doc.ims_action, + "pending_upload": doc.pending_upload, + "previous_ims_action": doc.previous_ims_action, + "is_pending_action_allowed": doc.is_pending_action_allowed, + "is_supplier_return_filed": doc.is_supplier_return_filed, + "doc_type": doc.doc_type, + "_inward_supply": doc, + "_purchase_invoice": purchases.pop( + doc.link_name, frappe._dict() + ), + } + ) + ) + + # Missing in 2A/2B is ignored for IMS + + ReconciledData().process_data(invoice_data, retain_doc=True) + + return invoice_data + + def get_pending_actions(self): + return frappe.get_all( + "GSTR Action", + { + "parent": f"IMS-ALL-{self.company_gstin}", + "parenttype": "GST Return Log", + "status": ["is", "not set"], + "token": ["is", "set"], + }, + pluck="request_type", + ) + + @frappe.whitelist() + def update_action(self, invoice_names, action): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + + invoice_names = frappe.parse_json(invoice_names) + + frappe.db.set_value( + "GST Inward Supply", + {"name": ("in", invoice_names)}, + "ims_action", + action, + ) + + @frappe.whitelist() + def get_invoice_details(self, purchase_name, inward_supply_name): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + + inward_supply = InwardSupply().get_all( + self.company_gstin, names=[inward_supply_name] + ) + purchases = PurchaseInvoice().get_all(names=[purchase_name]) + + reconciliation_data = [ + frappe._dict( + { + "_inward_supply": ( + inward_supply[0] if inward_supply else frappe._dict() + ), + "_purchase_invoice": purchases.get(purchase_name, frappe._dict()), + } + ) + ] + + ReconciledData().process_data(reconciliation_data, retain_doc=True) + + return reconciliation_data[0] + + @frappe.whitelist() + def link_documents(self, purchase_invoice_name, inward_supply_name, link_doctype): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + + purchases, inward_supplies = _link_documents( + purchase_invoice_name, inward_supply_name, link_doctype + ) + + return self.get_invoice_data(inward_supplies, purchases) + + @frappe.whitelist() + def unlink_documents(self, data): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + + purchases, inward_supplies = _unlink_documents(data) + + return self.get_invoice_data(inward_supplies, purchases) + + @frappe.whitelist() + def get_link_options(self, doctype, filters): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + + if isinstance(filters, dict): + filters = frappe._dict(filters) + + PI = frappe.qb.DocType("Purchase Invoice") + query = ( + PurchaseInvoice() + .get_query(additional_fields=["gst_category", "is_return"]) + .where(PI.supplier_gstin.like(f"%{filters.supplier_gstin}%")) + .where(PI.bill_date[filters.bill_from_date : filters.bill_to_date]) + ) + + if not filters.show_matched: + query = query.where(PI.reconciliation_status == "Unreconciled") + + return get_formatted_options(query.run(as_dict=True)) + + +@frappe.whitelist() +@otp_handler +def download_invoices(company_gstin): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + + TaxpayerBaseAPI(company_gstin).validate_auth_token() + + frappe.enqueue(download_ims_invoices, queue="long", gstin=company_gstin) + + +@frappe.whitelist() +@otp_handler +def save_invoices(company_gstin): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + frappe.has_permission("GST Return Log", "write", throw=True) + + return save_ims_invoices(company_gstin) + + +@frappe.whitelist() +@otp_handler +def reset_invoices(company_gstin): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + frappe.has_permission("GST Return Log", "write", throw=True) + + return reset_ims_invoices(company_gstin) + + +@frappe.whitelist() +@otp_handler +def sync_with_gstn_and_reupload(company_gstin): + frappe.has_permission("GST Invoice Management System", "write", throw=True) + frappe.has_permission("GST Return Log", "write", throw=True) + + TaxpayerBaseAPI(company_gstin).validate_auth_token() + + frappe.enqueue( + download_and_upload_ims_invoices, + queue="long", + company_gstin=company_gstin, + ) + + +@frappe.whitelist() +@otp_handler +def check_action_status(company_gstin, action): + frappe.has_permission("GST Return Log", "write", throw=True) + + ims_log = frappe.get_doc( + "GST Return Log", + f"IMS-ALL-{company_gstin}", + ) + + return process_save_or_reset_ims(ims_log, action) + + +def download_and_upload_ims_invoices(company_gstin): + """ + 1. This function will download invoices from GST Portal, + and if there are some queued invoices then upload will be skipped. + + 2. If there are no queued invoices, then it will upload the invoices to GST Portal. + + 3. It will check the status regardless of whether any data was uploaded or not. + (To notify user that process is completed successfully). + """ + + has_queued_invoices = download_ims_invoices(company_gstin, for_upload=True) + + # TODO: flag for pending upload and cron job for queued invoices + if has_queued_invoices: + return + + frappe.publish_realtime( + "upload_data_and_check_status", + user=frappe.session.user, + ) + + +def save_ims_invoices(company_gstin): + if not frappe.db.exists("GST Return Log", f"IMS-ALL-{company_gstin}"): + frappe.throw(_("Please download invoices before uploading")) + + ims_log = frappe.get_doc( + "GST Return Log", + f"IMS-ALL-{company_gstin}", + ) + + save_data = get_data_for_upload(company_gstin, "save") + + if not save_data: + return + + verify_request_in_progress(ims_log, False) + + api = IMSAPI(company_gstin) + + # Upload invoices where action in ["Accepted", "Rejected", "Pending"] + response = api.save(save_data) + set_gstr_actions(ims_log, "save", response.get("reference_id"), api.request_id) + + +def reset_ims_invoices(company_gstin): + if not frappe.db.exists("GST Return Log", f"IMS-ALL-{company_gstin}"): + frappe.throw(_("Please download invoices before uploading")) + + ims_log = frappe.get_doc( + "GST Return Log", + f"IMS-ALL-{company_gstin}", + ) + + reset_data = get_data_for_upload(company_gstin, "reset") + + if not reset_data: + return + + verify_request_in_progress(ims_log, False) + + api = IMSAPI(company_gstin) + + # Reset invoices where action is "No Action" + response = api.reset(reset_data) + set_gstr_actions(ims_log, "reset", response.get("reference_id"), api.request_id) + + +def get_data_for_upload(company_gstin, request_type): + upload_data = {} + key_invoice_map = {} + + if request_type == "save": + gst_inward_supply_list = InwardSupply().get_for_save(company_gstin) + else: + gst_inward_supply_list = InwardSupply().get_for_reset(company_gstin) + + for invoice in gst_inward_supply_list: + key = f"{invoice.doc_type}_{invoice.is_amended}" + key_invoice_map.setdefault(key, []).append(invoice) + + for key, invoices in key_invoice_map.items(): + category = CATEGORY_MAP[key] + _class = get_data_handler(ReturnType.IMS.value, category)() + upload_invoices = [] + + for invoice in invoices: + upload_invoices.append( + { + **_class.convert_data_to_gov_format(invoice), + **_class.get_category_details(invoice), + } + ) + + if upload_invoices: + upload_data[category.lower()] = upload_invoices + + return upload_data + + +def process_save_or_reset_ims(return_log, action): + response = {"status_cd": "P"} # dummy_response + doc = return_log.get_unprocessed_action(action) + if not doc: + return response + + api = IMSAPI(return_log.gstin) + response = api.get_request_status(doc.token) + + status_cd = response.get("status_cd") + + if status_cd != "IP": + doc.db_set({"status": STATUS_CODE_MAP.get(status_cd)}) + publish_action_status_notification( + "IMS", + return_log.return_period, + doc.request_type, + status_cd, + return_log.gstin, + api.request_id if status_cd == "ER" else None, + ) + + if status_cd in ["P", "PE"]: + # Exclude erroneous invoices from previous IMS action update + # This is enqueued because linking of integration request is enqueued + # TODO: flag for re-upload? + frappe.enqueue( + update_previous_ims_action, + queue="long", + integration_request=doc.integration_request, + error_report=response.get("error_report") or dict(), + ) + + return response + + +def update_previous_ims_action(integration_request, error_report=None): + uploaded_invoices = get_uploaded_invoices(integration_request) + + for category, invoices in uploaded_invoices.items(): + _class = get_data_handler(ReturnType.IMS.value, category.upper()) + _class().update_previous_ims_action(invoices, error_report.get(category, [])) + + +def get_uploaded_invoices(integration_request): + request_data = frappe.parse_json( + frappe.db.get_value( + "Integration Request", {"name": integration_request}, "data" + ) + ) + + if not request_data: + return {} + + if isinstance(request_data, str): + request_data = frappe.parse_json(request_data) + + return request_data["body"]["data"]["invdata"] diff --git a/india_compliance/gst_india/doctype/gst_invoice_management_system/invoice_detail_comparison.html b/india_compliance/gst_india/doctype/gst_invoice_management_system/invoice_detail_comparison.html new file mode 100644 index 0000000000..6735501a0d --- /dev/null +++ b/india_compliance/gst_india/doctype/gst_invoice_management_system/invoice_detail_comparison.html @@ -0,0 +1,91 @@ ++ | 2A / 2B | +Purchase | +||
---|---|---|---|---|
Company GSTIN | +{{ inward_supply.company_gstin || '-' }} | +{{ purchase.company_gstin || '-' }} | +||
Document Links + {% if inward_supply.name %} + | {{ frappe.utils.get_form_link("GST Inward Supply", + inward_supply.name, true) }} | + {% else %} +- | + {% endif %} + + {% if purchase.name %} +{{ frappe.utils.get_form_link(purchase.doctype, + purchase.name, true)}} | + {% else %} +- | + {% endif %} +
Bill No + | {{ inward_supply.bill_no || '-' }} | +{{ purchase.bill_no || '-' }} | +||
Bill Date + | + {{ frappe.format(inward_supply.bill_date, {'fieldtype': 'Date'}) || '-' }} + | ++ {{ frappe.format(purchase.bill_date, {'fieldtype': 'Date'}) || '-' }} + | +||
Place of Supply | +{{ inward_supply.place_of_supply || '-' }} | +{{ purchase.place_of_supply || '-' }} | +||
Reverse Charge | +{{ inward_supply.is_reverse_charge || '-' }} | +{{ purchase.is_reverse_charge || '-' }} | +||
CGST + | {{ inward_supply.cgst || '-' }} | +{{ purchase.cgst || '-' }} | +||
SGST + | {{ inward_supply.sgst || '-' }} | +{{ purchase.sgst || '-' }} | +||
IGST + | {{ inward_supply.igst || '-' }} | +{{ purchase.igst || '-' }} | +||
CESS + | {{ inward_supply.cess || '-' }} | +{{ purchase.cess || '-' }} | +||
Taxable Amount + | {{ inward_supply.taxable_value || '-' }} | +{{ purchase.taxable_value || '-' }} | +
{{ __(\"No data available for selected filters\") }}
" @@ -119,27 +116,10 @@ "options": "GSTR 2B\nBoth GSTR 2A & 2B" }, { - "depends_on": "eval: !doc.reconciliation_data", + "depends_on": "eval: !doc.data_state", "fieldname": "not_reconciled", "fieldtype": "HTML", - "options": "{{ __(\"Reconcile to view the data\") }}
" - }, - { - "fieldname": "section_break_cmfa", - "fieldtype": "Section Break", - "hidden": 1 - }, - { - "fieldname": "reconciliation_data", - "fieldtype": "JSON", - "label": "Reconciliation Data" - }, - { - "default": "0", - "fieldname": "is_modified", - "fieldtype": "Check", - "hidden": 1, - "label": "Is Modified" + "options": "{{ __(\"Generate to view the data\") }}
" }, { "default": "0", @@ -152,7 +132,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-08-09 17:57:55.801686", + "modified": "2025-01-16 11:43:52.167256", "modified_by": "Administrator", "module": "GST India", "name": "Purchase Reconciliation Tool", diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py index e9dc84c188..aa10e5b76d 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.py @@ -1,15 +1,14 @@ # Copyright (c) 2022, Resilient Tech and contributors # For license information, please see license.txt -import json import re from collections import defaultdict from typing import List import frappe +from frappe import _ from frappe.model.document import Document from frappe.query_builder.functions import IfNull from frappe.utils import add_to_date, cint, now_datetime -from frappe.utils.response import json_handler from india_compliance.gst_india.api_classes.taxpayer_base import ( TaxpayerBaseAPI, @@ -23,6 +22,18 @@ ReconciledData, Reconciler, ) +from india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_utils import ( + get_formatted_options, +) +from india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_utils import ( + link_documents as _link_documents, +) +from india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_utils import ( + set_reconciliation_status, +) +from india_compliance.gst_india.doctype.purchase_reconciliation_tool.purchase_reconciliation_utils import ( + unlink_documents as _unlink_documents, +) from india_compliance.gst_india.utils import ( get_gstin_list, get_json_from_file, @@ -31,7 +42,7 @@ ) from india_compliance.gst_india.utils.exporter import ExcelExporter from india_compliance.gst_india.utils.gstr_2 import ( - ACTIONS, + GSTR_2A_ACTIONS, IMPORT_CATEGORY, ReturnType, download_gstr_2a, @@ -78,7 +89,10 @@ def onload(self): ), ) - def validate(self): + @frappe.whitelist() + def reconcile_and_generate_data(self): + frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True) + # reconcile purchases and inward supplies if frappe.flags.in_install or frappe.flags.in_migrate: return @@ -88,11 +102,8 @@ def validate(self): _Reconciler.reconcile(row["original"], row["amended"]) self.ReconciledData = ReconciledData(**self.get_reco_doc()) - self.reconciliation_data = json.dumps( - self.ReconciledData.get(), default=json_handler - ) - self.db_set("is_modified", 0) + return self.ReconciledData.get() @frappe.whitelist() def upload_gstr(self, return_type, period, file_path): @@ -232,44 +243,11 @@ def get_invoice_details(self, purchase_name, inward_supply_name): def link_documents(self, purchase_invoice_name, inward_supply_name, link_doctype): frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True) - if not purchase_invoice_name or not inward_supply_name: - return - - purchases = [] - inward_supplies = [] - - # silently handle existing links - if isup_linked_with := frappe.db.get_value( - "GST Inward Supply", inward_supply_name, "link_name" - ): - self._unlink_documents((inward_supply_name,)) - purchases.append(isup_linked_with) - - link_doc = { - "link_doctype": link_doctype, - "link_name": purchase_invoice_name, - } - if pur_linked_with := frappe.db.get_all( - "GST Inward Supply", link_doc, pluck="name" - ): - self._unlink_documents((pur_linked_with)) - inward_supplies.extend(pur_linked_with) - - link_doc["match_status"] = "Manual Match" - - # link documents - frappe.db.set_value( - "GST Inward Supply", - inward_supply_name, - link_doc, + purchases, inward_supplies = _link_documents( + purchase_invoice_name, inward_supply_name, link_doctype ) - purchases.append(purchase_invoice_name) - inward_supplies.append(inward_supply_name) - self.db_set("is_modified", 1) - self.set_reconciliation_status( - link_doctype, [purchase_invoice_name], "Match Found" - ) + set_reconciliation_status(link_doctype, (purchase_invoice_name,), "Match Found") return self.ReconciledData.get(purchases, inward_supplies) @@ -277,59 +255,9 @@ def link_documents(self, purchase_invoice_name, inward_supply_name, link_doctype def unlink_documents(self, data): frappe.has_permission("Purchase Reconciliation Tool", "write", throw=True) - data = frappe.parse_json(data) - inward_supplies = set() - purchases = set() - boe = set() - - for doc in data: - inward_supplies.add(doc.get("inward_supply_name")) - - purchase_doctype = doc.get("purchase_doctype") - if purchase_doctype == "Purchase Invoice": - purchases.add(doc.get("purchase_invoice_name")) + purchases, inward_supplies = _unlink_documents(data) - elif purchase_doctype == "Bill of Entry": - boe.add(doc.get("purchase_invoice_name")) - - self.set_reconciliation_status("Purchase Invoice", purchases, "Unreconciled") - self.set_reconciliation_status("Bill of Entry", boe, "Unreconciled") - self._unlink_documents(inward_supplies) - - self.db_set("is_modified", 1) - - return self.ReconciledData.get(purchases.union(boe), inward_supplies) - - def set_reconciliation_status(self, doctype, names, status): - if not names: - return - - frappe.db.set_value( - doctype, {"name": ("in", names)}, "reconciliation_status", status - ) - - def _unlink_documents(self, inward_supplies): - if not inward_supplies: - return - - GSTR2 = frappe.qb.DocType("GST Inward Supply") - ( - frappe.qb.update(GSTR2) - .set("link_doctype", "") - .set("link_name", "") - .set("match_status", "Unlinked") - .where(GSTR2.name.isin(inward_supplies)) - .run() - ) - - # Revert action performed - ( - frappe.qb.update(GSTR2) - .set("action", "No Action") - .where(GSTR2.name.isin(inward_supplies)) - .where(GSTR2.action.notin(("Ignore", "Pending"))) - .run() - ) + return self.ReconciledData.get(purchases, inward_supplies) @frappe.whitelist() def apply_action(self, data, action): @@ -364,10 +292,8 @@ def apply_action(self, data, action): "GST Inward Supply", {"name": ("in", inward_supplies)}, "action", action ) - self.set_reconciliation_status("Purchase Invoice", purchases, status) - self.set_reconciliation_status("Bill of Entry", boe, status) - - self.db_set("is_modified", 1) + set_reconciliation_status("Purchase Invoice", purchases, status) + set_reconciliation_status("Bill of Entry", boe, status) @frappe.whitelist() def get_link_options(self, doctype, filters): @@ -398,7 +324,7 @@ def get_purchase_invoice_options(self, filters): PI.name.notin(PurchaseInvoice.query_matched_purchase_invoice()) ) - return self._get_link_options(query.run(as_dict=True)) + return get_formatted_options(query.run(as_dict=True)) def get_inward_supply_options(self, filters): GSTR2 = frappe.qb.DocType("GST Inward Supply") @@ -416,7 +342,7 @@ def get_inward_supply_options(self, filters): if not filters.show_matched: query = query.where(IfNull(GSTR2.link_name, "") == "") - return self._get_link_options(query.run(as_dict=True)) + return get_formatted_options(query.run(as_dict=True)) def get_bill_of_entry_options(self, filters): BOE = frappe.qb.DocType("Bill of Entry") @@ -429,22 +355,7 @@ def get_bill_of_entry_options(self, filters): BOE.name.notin(BillOfEntry.query_matched_bill_of_entry()) ) - return self._get_link_options(query.run(as_dict=True)) - - def _get_link_options(self, data): - for row in data: - row.value = row.label = row.name - if not row.get("classification"): - row.classification = self.ReconciledData.guess_classification(row) - - row.description = ( - f"{row.bill_no}, {row.bill_date}, Taxable Amount: {row.taxable_value}" - ) - row.description += ( - f", Tax Amount: {BaseUtil.get_total_tax(row)}, {row.classification}" - ) - - return data + return get_formatted_options(query.run(as_dict=True)) def download_gstr( @@ -472,10 +383,13 @@ def download_gstr( except Exception as e: frappe.publish_realtime( - "gstr_2a_2b_download_failed", - {"error": str(e)}, + "gstr_2a_2b_download_message", + { + "title": _("2A/2B Download Failed"), + "message": str(e), + "indicator": "red", + }, user=frappe.session.user, - doctype="Purchase Reconciliation Tool", ) @@ -653,7 +567,7 @@ def download_gst_returns(self): def get_gst_categories(self): return [ category.value - for category in ACTIONS.values() + for category in GSTR_2A_ACTIONS.values() if getattr(self.gst_settings, "reconcile_for_" + category.value.lower()) ] diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_utils.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_utils.py new file mode 100644 index 0000000000..fca48ae699 --- /dev/null +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_utils.py @@ -0,0 +1,124 @@ +import frappe + +from india_compliance.gst_india.doctype.purchase_reconciliation_tool import ( + BaseUtil, + ReconciledData, +) + + +def link_documents(purchase_invoice_name, inward_supply_name, link_doctype): + purchases = [] + inward_supplies = [] + + if not purchase_invoice_name or not inward_supply_name: + return purchases, inward_supplies + + # silently handle existing links + if isup_linked_with := frappe.db.get_value( + "GST Inward Supply", inward_supply_name, "link_name" + ): + set_reconciliation_status(link_doctype, (isup_linked_with,), "Unreconciled") + _unlink_documents((inward_supply_name,)) + purchases.append(isup_linked_with) + + link_doc = { + "link_doctype": link_doctype, + "link_name": purchase_invoice_name, + } + if pur_linked_with := frappe.db.get_all( + "GST Inward Supply", link_doc, pluck="name" + ): + _unlink_documents((pur_linked_with)) + inward_supplies.extend(pur_linked_with) + + link_doc["match_status"] = "Manual Match" + + # link documents + frappe.db.set_value("GST Inward Supply", inward_supply_name, link_doc) + set_reconciliation_status(link_doctype, (purchase_invoice_name,), "Match Found") + + purchases.append(purchase_invoice_name) + inward_supplies.append(inward_supply_name) + + return purchases, inward_supplies + + +def unlink_documents(data): + data = frappe.parse_json(data) + inward_supplies = set() + purchases = set() + boe = set() + + for row in data: + inward_supplies.add(row.get("inward_supply_name")) + + purchase_doctype = row.get("purchase_doctype") + if purchase_doctype == "Purchase Invoice": + purchases.add(row.get("purchase_invoice_name")) + + elif purchase_doctype == "Bill of Entry": + boe.add(row.get("purchase_invoice_name")) + + set_reconciliation_status("Purchase Invoice", purchases, "Unreconciled") + set_reconciliation_status("Bill of Entry", boe, "Unreconciled") + _unlink_documents(inward_supplies) + + return purchases.union(boe), inward_supplies + + +def _unlink_documents(inward_supplies): + if not inward_supplies: + return + + GSTR2 = frappe.qb.DocType("GST Inward Supply") + ( + frappe.qb.update(GSTR2) + .set("link_doctype", "") + .set("link_name", "") + .set("match_status", "Unlinked") + .where(GSTR2.name.isin(inward_supplies)) + .run() + ) + + # Revert Purchase Reconciliation action performed + ( + frappe.qb.update(GSTR2) + .set("action", "No Action") + .where(GSTR2.name.isin(inward_supplies)) + .where(GSTR2.action.notin(("Ignore", "Pending"))) + .run() + ) + + # Revert IMS action performed + ( + frappe.qb.update(GSTR2) + .set("ims_action", "No Action") + .where(GSTR2.name.isin(inward_supplies)) + .where(GSTR2.ims_action == "Accepted") + .run() + ) + + +def get_formatted_options(data): + for row in data: + row.value = row.label = row.name + if not row.get("classification"): + row.classification = ReconciledData.guess_classification(row) + + row.description = ( + f"{row.bill_no}, {row.bill_date}, Taxable Amount: {row.taxable_value}" + ) + row.description += ( + f", Tax Amount: {BaseUtil.get_total_tax(row)}, {row.classification}" + ) + + return data + + +def set_reconciliation_status(doctype, names, status): + if not names: + return + + frappe.db.set_value( + doctype, {"name": ("in", names)}, "reconciliation_status", status + ) diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/test_purchase_reconciliation_tool.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/test_purchase_reconciliation_tool.py index 04417fde86..ada502782d 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/test_purchase_reconciliation_tool.py +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/test_purchase_reconciliation_tool.py @@ -14,6 +14,8 @@ create_purchase_invoice as _create_purchase_invoice, ) +IGNORE_TEST_RECORD_DEPENDENCIES = ["Company"] + PURCHASE_INVOICE_DEFAULT_ARGS = { "bill_no": "BILL-23-00001", "bill_date": "2023-12-11", @@ -53,9 +55,6 @@ class TestPurchaseReconciliationTool(IntegrationTestCase): @classmethod def setUpClass(cls): - # don't create test objects - frappe.local.test_objects["Purchase Reconciliation Tool"] = [] - super().setUpClass() # create 2023-2024 fiscal year diff --git a/india_compliance/gst_india/overrides/test_purchase_invoice.py b/india_compliance/gst_india/overrides/test_purchase_invoice.py index cc887c16b5..54b2f56fb9 100644 --- a/india_compliance/gst_india/overrides/test_purchase_invoice.py +++ b/india_compliance/gst_india/overrides/test_purchase_invoice.py @@ -77,11 +77,12 @@ def test_validate_invoice_length(self): do_not_save=True, ) setattr(pinv, "__newname", "INV/2022/00001/asdfsadg") # NOQA - pinv.meta.autoname = "prompt" - pinv.save() self.assertEqual( frappe.parse_json(frappe.message_log[-1]).get("message"), "Transaction Name must be 16 characters or fewer to meet GST requirements", ) + + # Reset autoname (as it's cached) + pinv.meta.autoname = "naming_series:" diff --git a/india_compliance/gst_india/overrides/transaction.py b/india_compliance/gst_india/overrides/transaction.py index 06eeba91bf..2238db3275 100644 --- a/india_compliance/gst_india/overrides/transaction.py +++ b/india_compliance/gst_india/overrides/transaction.py @@ -898,11 +898,12 @@ def get_gst_details(party_details, doctype, company, *, update_place_of_supply=F gst_details.update(party_gst_details) # POS - gst_details.place_of_supply = ( - party_details.place_of_supply - if (not update_place_of_supply and party_details.place_of_supply) - else get_place_of_supply(party_details, doctype) - ) + if not update_place_of_supply and party_details.place_of_supply: + gst_details.place_of_supply = party_details.place_of_supply + else: + place_of_supply = get_place_of_supply(party_details, doctype) + gst_details.place_of_supply = place_of_supply + party_details.place_of_supply = place_of_supply # set is_reverse_charge as per party_gst_details if not set if not is_sales_transaction and "is_reverse_charge" not in party_details: @@ -977,11 +978,7 @@ def get_gst_details(party_details, doctype, company, *, update_place_of_supply=F if default_tax := get_tax_template( master_doctype, company, - is_inter_state_supply( - party_details.copy().update( - doctype=doctype, place_of_supply=gst_details.place_of_supply - ), - ), + is_inter_state_supply(frappe._dict({**party_details, "doctype": doctype})), party_details.get(company_gstin_field)[:2], party_details.is_reverse_charge, ): diff --git a/india_compliance/gst_india/print_format/e_waybill/e_waybill.css b/india_compliance/gst_india/print_format/e_waybill/e_waybill.css index 3bf9be7803..3d03f5766c 100644 --- a/india_compliance/gst_india/print_format/e_waybill/e_waybill.css +++ b/india_compliance/gst_india/print_format/e_waybill/e_waybill.css @@ -1,8 +1,30 @@ + +.table-element { + font-size: 12px; +} + +.bold { + font-weight: bold; +} + +.no-border { + border: none !important; +} + +.no-border > * { + border: none; +} + .print-format { padding: 0.25in; color: black; } +table, th, td { + border: 1px solid #d1d8dd; + border-collapse: collapse; +} + @media print { .print-format { padding: 0; @@ -53,10 +75,6 @@ border-top: 1px solid black !important; } -.print-format .ewb-no-span { - font-size: 13px; -} - .print-format .section-separator { border-top: 2px solid #bbb; margin: 8px 0 16px; diff --git a/india_compliance/gst_india/print_format/e_waybill/e_waybill.html b/india_compliance/gst_india/print_format/e_waybill/e_waybill.html index 78a3abf4da..452b7533d8 100644 --- a/india_compliance/gst_india/print_format/e_waybill/e_waybill.html +++ b/india_compliance/gst_india/print_format/e_waybill/e_waybill.html @@ -6,281 +6,268 @@ {%- set irn = frappe.db.get_value("Sales Invoice", {"ewaybill": data.ewbNo}, "irn") -%} {% if data.supplyType == "O" %} - {% set generated_by = data.fromTrdName %} +{% set generated_by = data.fromTrdName %} {% else %} - {% set generated_by = data.toTrdName %} +{% set generated_by = data.toTrdName %} {% endif %} - -
- e-Waybill- |
- |
- |
-
+ e-Waybill+ |
+ |
+ |
+
- e-Waybill No: + e-Waybill No | - - {{ add_spacing(data.ewbNo, 4) }} + + {{ add_spacing(data.ewbNo, 4) }} |
- e-Waybill Date: + e-Waybill Date | - - {{ data.ewayBillDate | replace(":00 ", "") }} + + {{ data.ewayBillDate | replace("00 ", "") }} |
- Generated By: + Generated By | - - - {{ add_spacing(data.userGstin, 5) }} - {{ generated_by }} - + + {{ add_spacing(data.userGstin, 5) }} - {{ generated_by }} |
- Valid From: + Valid From | - - {{ data.ewayBillDate | replace(":00 ", " ") }} - {{ " [" }}{{ data.actualDist }}{{ "Kms]" }} + + {{ data.ewayBillDate | replace(":00 ", " ") }} {{ " [" }} {{ data.actualDist }}{{ "Kms]" }} |
- Valid Until: + Valid Until | - - {{ data.validUpto[:10] }} + + {{ data.validUpto[:10] }} |
- IRN: + IRN | - - {{ irn }} + + {{ irn }} |
- - Part - A - | -||
GSTIN of Supplier | +GSTIN of Supplier | - - {{ add_spacing(data.fromGstin, 5) }}-{{ data.fromTrdName }} + + {{ add_spacing(data.fromGstin, 5) }} - {{ data.fromTrdName }} |
Place of Dispatch | - - {{ data.fromPlace }}, - {{ get_state(data.actFromStateCode) }} - - {{ data.fromPincode }} + + {{ data.fromPlace }}, {{ get_state(data.actFromStateCode) }} - {{ data.fromPincode }} | |
GSTIN of Recipient | - - {{ add_spacing(data.toGstin, 5) }}-{{ data.toTrdName }} - + + {{ add_spacing(data.toGstin, 5) }} - {{ data.toTrdName }} | |
Place of Delivery | - - {{ data.toPlace }}, - {{ get_state(data.actToStateCode) }} -{{ data.toPincode }} - + + {{ data.toPlace }}, {{ get_state(data.actToStateCode) }} -{{ data.toPincode }} | |
Document No. | - + {{ data.docNo }} - | |
Document Date | - + {{ data.docDate }} - | |
- Transaction Type: + Transaction Type | - {{ get_transport_type(data.transactionType) }} + + {{ get_transport_type(data.transactionType) }} | |
Value of Goods | - - - {{ frappe.utils.fmt_money(data.totInvValue, currency="INR") }} + + {{ frappe.utils.fmt_money(data.totInvValue, currency="INR") }} | |
HSN Code | - - {{ data.itemList[0].hsnCode }} - {%if data.itemList[0].productDesc%} - {{ " - " ~data.itemList[0].productDesc }} - {%endif%} - + + {{ data.itemList[0].hsnCode }} + {%if data.itemList[0].productDesc%} + {{ " - " ~data.itemList[0].productDesc }} + {%endif%} | |
Reason for Transportation | - - {{ get_supply_type(data.supplyType) }} - {{ get_sub_supply_type(data.subSupplyType) }} + + {{ get_supply_type(data.supplyType) }} - {{ get_sub_supply_type(data.subSupplyType) }} | |
Transporter | - - {{ add_spacing(data.transporterId, 5) }}- - {{ data.transporterName }} + + {{ add_spacing(data.transporterId, 5) }} - {{ data.transporterName }} | |
- - Part - B + |
Mode | +
+ Vehicle / Trans + Doc No & Dt. |
- ||||||||||||||||||
-
-
+
| From | +Entered Date | +Entered By | +
+ Cdata No. + (If any) |
- |||||||||||||||
- |
+ Multi Veh.Info + (If any) |
||||||||||||||||||
- + | + {{ get_transport_mode(detail.transMode) }} + | ++ {{ detail.vehicleNo }} / + {{ detail.transDocNo }} Dt + {{ detail.transDocDate }} + | +{{ detail.fromPlace }} | ++ {{ detail.enteredDate | + replace(":00 ", " ") }} | +{{ detail.userGSTINTransin }} | +- | +- |
+
+
+ |
+