diff --git a/india_compliance/gst_india/api_classes/base.py b/india_compliance/gst_india/api_classes/base.py index d4671f9559..c12e3d7c4e 100644 --- a/india_compliance/gst_india/api_classes/base.py +++ b/india_compliance/gst_india/api_classes/base.py @@ -161,7 +161,9 @@ def _make_request( raise e finally: - log.output = response_json.copy() + if response_json: + log.output = response_json.copy() + self.mask_sensitive_info(log) enqueue_integration_request(**log) @@ -252,21 +254,26 @@ def generate_request_id(self, length=12): return frappe.generate_hash(length=length) def mask_sensitive_info(self, log): + request_headers = log.request_headers + output = log.output + data = log.data + request_body = data and data.get("body") + for key in self.SENSITIVE_INFO: - if key in log.request_headers: - log.request_headers[key] = "*****" + if key in request_headers: + request_headers[key] = "*****" - if key in log.output: - log.output[key] = "*****" + if output and key in output: + output[key] = "*****" - if not log.data: - return + if not data: + continue - if key in log.get("data", {}): - log.data[key] = "*****" + if key in data: + data[key] = "*****" - if key in log.get("data", {}).get("body", {}): - log.data["body"][key] = "*****" + if request_body and key in request_body: + request_body[key] = "*****" def get_public_ip(): 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 fec3779c26..c761dc82b1 100644 --- a/india_compliance/gst_india/client_scripts/e_invoice_actions.js +++ b/india_compliance/gst_india/client_scripts/e_invoice_actions.js @@ -304,7 +304,7 @@ function is_e_invoice_applicable(frm, show_message = false) { ) { is_einv_applicable = false; message_list.push( - "At least one item must be taxable or transaction is categorized as export." + "All items are either Nil-Rated/Exempted/Non-GST. At least one item must be taxable or the transaction should be categorised as export." ); } diff --git a/india_compliance/gst_india/client_scripts/expense_claim.js b/india_compliance/gst_india/client_scripts/expense_claim.js index abe58138cb..3c0d4167b3 100644 --- a/india_compliance/gst_india/client_scripts/expense_claim.js +++ b/india_compliance/gst_india/client_scripts/expense_claim.js @@ -8,5 +8,5 @@ frappe.ui.form.on("Expense Taxes and Charges", { }); function toggle_gstin_for_expense_claim(frm) { - toggle_company_gstin(frm, taxes_table="taxes", account_field="account_head"); + toggle_company_gstin(frm, "taxes", "account_head"); } \ No newline at end of file diff --git a/india_compliance/gst_india/client_scripts/journal_entry.js b/india_compliance/gst_india/client_scripts/journal_entry.js index 48b1d3c004..235184a4e3 100644 --- a/india_compliance/gst_india/client_scripts/journal_entry.js +++ b/india_compliance/gst_india/client_scripts/journal_entry.js @@ -15,7 +15,7 @@ frappe.ui.form.on("Journal Entry Account", { }); function toggle_gstin_for_journal_entry(frm) { - toggle_company_gstin(frm, taxes_table="accounts", account_head="account"); + toggle_company_gstin(frm, "accounts", "account"); } async function toggle_company_gstin(frm, taxes_table, account_head) { 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 0c9dc5172c..87252dcfcb 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 @@ -55,6 +55,16 @@ class TestPurchaseReconciliationTool(FrappeTestCase): def setUpClass(cls): super().setUpClass() + # create 2023-2024 fiscal year + fiscal_year = frappe.new_doc("Fiscal Year") + fiscal_year.update( + { + "year_start_date": "2023-04-01", + "year_end_date": "2024-03-31", + "year": "2023-2024", + } + ).insert(ignore_if_duplicate=True) + cls.test_data = frappe.get_file_json( frappe.get_app_path( "india_compliance", diff --git a/india_compliance/gst_india/number_card/invoice_cancelled_but_not_e_invoice/invoice_cancelled_but_not_e_invoice.json b/india_compliance/gst_india/number_card/invoice_cancelled_but_not_e_invoice/invoice_cancelled_but_not_e_invoice.json index be44b5b929..073056b8b3 100644 --- a/india_compliance/gst_india/number_card/invoice_cancelled_but_not_e_invoice/invoice_cancelled_but_not_e_invoice.json +++ b/india_compliance/gst_india/number_card/invoice_cancelled_but_not_e_invoice/invoice_cancelled_but_not_e_invoice.json @@ -1,5 +1,6 @@ { "aggregate_function_based_on": "", + "color": "#EC864B", "creation": "2023-07-25 18:33:55.452360", "docstatus": 0, "doctype": "Number Card", @@ -12,7 +13,7 @@ "is_standard": 1, "label": "Active e-Invoice, Cancelled Invoice", "method": "", - "modified": "2024-02-23 11:37:11.634469", + "modified": "2024-04-09 11:46:37.126752", "modified_by": "Administrator", "module": "GST India", "name": "Invoice Cancelled But Not e-Invoice", diff --git a/india_compliance/gst_india/number_card/pending_e_invoices/pending_e_invoices.json b/india_compliance/gst_india/number_card/pending_e_invoices/pending_e_invoices.json index 3604e87897..3363e0f91e 100644 --- a/india_compliance/gst_india/number_card/pending_e_invoices/pending_e_invoices.json +++ b/india_compliance/gst_india/number_card/pending_e_invoices/pending_e_invoices.json @@ -1,6 +1,6 @@ { "aggregate_function_based_on": "", - "color": "#ECAD4B", + "color": "#EC864B", "creation": "2023-07-25 15:10:39.976867", "docstatus": 0, "doctype": "Number Card", @@ -14,7 +14,7 @@ "is_standard": 1, "label": "Pending e-Invoices", "method": "", - "modified": "2024-02-23 11:37:43.073988", + "modified": "2024-04-09 11:45:32.550901", "modified_by": "Administrator", "module": "GST India", "name": "Pending e-Invoices", diff --git a/india_compliance/gst_india/number_card/pending_e_waybill/pending_e_waybill.json b/india_compliance/gst_india/number_card/pending_e_waybill/pending_e_waybill.json index 84428bc064..f806e3f67f 100644 --- a/india_compliance/gst_india/number_card/pending_e_waybill/pending_e_waybill.json +++ b/india_compliance/gst_india/number_card/pending_e_waybill/pending_e_waybill.json @@ -1,6 +1,6 @@ { "aggregate_function_based_on": "", - "color": "#e02f2f", + "color": "#EC864B", "creation": "2023-08-04 11:30:55.485885", "docstatus": 0, "doctype": "Number Card", @@ -12,7 +12,7 @@ "is_public": 1, "is_standard": 1, "label": "Pending e-Waybills", - "modified": "2023-10-12 17:16:39.286029", + "modified": "2024-04-09 11:46:27.074636", "modified_by": "Administrator", "module": "GST India", "name": "Pending e-Waybill", diff --git a/india_compliance/gst_india/overrides/test_transaction.py b/india_compliance/gst_india/overrides/test_transaction.py index 8a84f91d60..c280188f9f 100644 --- a/india_compliance/gst_india/overrides/test_transaction.py +++ b/india_compliance/gst_india/overrides/test_transaction.py @@ -8,6 +8,7 @@ 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.controllers.accounts_controller import update_child_qty_rate from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice from india_compliance.gst_india.constants import SALES_DOCTYPES @@ -905,3 +906,77 @@ def create_tax_accounts(account_name): **defaults, } ).insert(ignore_if_duplicate=True) + + +class TestItemUpdate(FrappeTestCase): + DATA = { + "customer": "_Test Unregistered Customer", + "item_code": "_Test Trading Goods 1", + "qty": 1, + "rate": 100, + "is_in_state": 1, + } + + @classmethod + def setUpClass(cls): + super().setUpClass() + + def create_order(self, doctype): + self.DATA["doctype"] = doctype + doc = create_transaction(**self.DATA) + return doc + + def test_so_and_po_after_item_update(self): + for doctype in ["Sales Order", "Purchase Order"]: + doc = self.create_order(doctype) + + self.assertDocumentEqual( + { + "taxable_value": 100, + "cgst_amount": 9, + "sgst_amount": 9, + }, + doc.items[0], + ) + + # Update Item Rate + item = doc.items[0] + item_to_update = [ + { + "item_code": item.item_code, + "qty": item.qty, + "rate": 200, + "docname": item.name, + "name": item.name, + "idx": item.idx, + } + ] + + update_child_qty_rate(doctype, json.dumps(item_to_update), doc.name) + doc = frappe.get_doc(doctype, doc.name) + + self.assertDocumentEqual( + { + "taxable_value": 200, + "cgst_amount": 18, + "sgst_amount": 18, + }, + doc.items[0], + ) + + # Insert New Item + item_to_update.append( + {"item_code": "_Test Trading Goods 1", "qty": 1, "rate": 50, "idx": 2} + ) + + update_child_qty_rate(doctype, json.dumps(item_to_update), doc.name) + doc = frappe.get_doc(doctype, doc.name) + + self.assertDocumentEqual( + { + "taxable_value": 50, + "cgst_amount": 4.5, + "sgst_amount": 4.5, + }, + doc.items[1], + ) diff --git a/india_compliance/gst_india/overrides/transaction.py b/india_compliance/gst_india/overrides/transaction.py index dc145a678d..1601a02ecc 100644 --- a/india_compliance/gst_india/overrides/transaction.py +++ b/india_compliance/gst_india/overrides/transaction.py @@ -1433,9 +1433,6 @@ def before_print(doc, method=None, print_settings=None): ): return - if doc.get("group_same_items"): - ItemGSTDetails().update(doc) - set_gst_breakup(doc) @@ -1490,8 +1487,22 @@ def ignore_gst_validations(doc, throw=True): return True -def before_update_after_submit_item(doc, method=None): - frappe.flags.through_update_item = True +def on_change_item(doc, method=None): + """ + Objective: + Child item is saved before trying to update parent doc. + Hence we can't verify has_value_changed for items in the parent doc. + + Solution: + - Set a flag in on_change of item + - Runs for both insert and save (update after submit) + - Set flag only if `ignore_validate_update_after_submit` is set + + Reference: + erpnext.controllers.accounts_controller.update_child_qty_rate + """ + if doc.flags.ignore_validate_update_after_submit: + frappe.flags.through_update_item = True def before_update_after_submit(doc, method=None): diff --git a/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.js b/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.js index c61fe49a58..d788b3c0ed 100644 --- a/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.js +++ b/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.js @@ -1,17 +1,24 @@ // Copyright (c) 2024, Resilient Tech and contributors // For license information, please see license.txt const INVOICE_TYPE = { - "B2B,SEZ,DE": ["B2B Regular", "B2B Reverse Charge", "SEZWP", "SEZWOP", "Deemed Exports"], + "B2B, SEZ, DE": [ + "B2B Regular", + "B2B Reverse Charge", + "SEZWP", + "SEZWOP", + "Deemed Exports", + ], "B2C (Large)": ["B2C (Large)"], - "Exports": ["EXPWP", "EXPWOP"], + Exports: ["EXPWP", "EXPWOP"], "B2C (Others)": ["B2C (Others)"], - "Nil-Rated,Exempted,Non-GST": ["Nil-Rated", "Exempted", "Non-GST"], + "Nil-Rated, Exempted, Non-GST": ["Nil-Rated", "Exempted", "Non-GST"], "Credit/Debit Notes (Registered)": ["CDNR"], "Credit/Debit Notes (Unregistered)": ["CDNUR"], -} +}; frappe.query_reports["GST Sales Register Beta"] = { onload: set_sub_category_options, + filters: [ { fieldname: "company", @@ -46,42 +53,80 @@ frappe.query_reports["GST Sales Register Beta"] = { fieldname: "date_range", label: __("Date Range"), fieldtype: "DateRange", - default: [india_compliance.last_month_start(), india_compliance.last_month_end()], - width: "80" + default: [ + india_compliance.last_month_start(), + india_compliance.last_month_end(), + ], + width: "80", }, { fieldtype: "Select", fieldname: "summary_by", label: __("Summary By"), options: "Overview\nSummary by HSN\nSummary by Item", - default: "Summary by Item" + default: "Summary by Item", }, { fieldtype: "Autocomplete", fieldname: "invoice_category", label: __("Invoice Category"), - options: "B2B,SEZ,DE\nB2C (Large)\nExports\nB2C (Others)\nNil-Rated,Exempted,Non-GST\nCredit/Debit Notes (Registered)\nCredit/Debit Notes (Unregistered)", + options: + "B2B, SEZ, DE\nB2C (Large)\nExports\nB2C (Others)\nNil-Rated, Exempted, Non-GST\nCredit/Debit Notes (Registered)\nCredit/Debit Notes (Unregistered)", on_change(report) { - report.set_filter_value('invoice_sub_category', ""); + report.set_filter_value("invoice_sub_category", ""); set_sub_category_options(report); }, - depends_on: 'eval:doc.summary_by=="Summary by HSN" || doc.summary_by=="Summary by Item"' + depends_on: + 'eval:doc.summary_by=="Summary by HSN" || doc.summary_by=="Summary by Item"', }, { fieldtype: "Autocomplete", fieldname: "invoice_sub_category", label: __("Invoice Sub Category"), - depends_on: 'eval:doc.summary_by=="Summary by HSN" || doc.summary_by=="Summary by Item"' + depends_on: + 'eval:doc.summary_by=="Summary by HSN" || doc.summary_by=="Summary by Item"', + }, + ], + + formatter: (value, row, column, data, default_formatter) => { + value = default_formatter(value, row, column, data); + if (data && data.indent === 0) { + let $value = $(`${value}`).css("font-weight", "bold"); + value = $value.wrap("

").parent().html(); } - ] + + return value; + }, }; function set_sub_category_options(report) { const invoice_category = frappe.query_report.get_filter_value("invoice_category"); - report.get_filter('invoice_sub_category').set_data(INVOICE_TYPE[invoice_category] || []); + report + .get_filter("invoice_sub_category") + .set_data(INVOICE_TYPE[invoice_category] || []); if (invoice_category && INVOICE_TYPE[invoice_category].length === 1) { - report.set_filter_value("invoice_sub_category", INVOICE_TYPE[invoice_category][0]) + report.set_filter_value( + "invoice_sub_category", + INVOICE_TYPE[invoice_category][0] + ); } } +frappe_report_column_total = frappe.utils.report_column_total; + +// Override datatable hook for column total calculation +frappe.utils.report_column_total = function (...args) { + const summary_by = frappe.query_report.get_filter_value("summary_by"); + if (summary_by !== "Overview") return frappe_report_column_total.apply(this, args); + + const column_field = args[1].column.fieldname; + if (column_field === "description") return; + + const total = this.datamanager.data.reduce((acc, row) => { + if (row.indent !== 1) acc += row[column_field] || 0; + return acc; + }, 0); + + return total; +}; diff --git a/india_compliance/gst_india/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/india_compliance/gst_india/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index 5d6a52b297..b2f2691a50 100644 --- a/india_compliance/gst_india/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/india_compliance/gst_india/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.model.meta import get_field_precision -from frappe.utils import cstr, flt, getdate +from frappe.utils import flt, getdate import erpnext from india_compliance.gst_india.utils import get_gst_accounts_by_type, get_gst_uom @@ -349,7 +349,6 @@ def download_json_file(): def get_hsn_wise_json_data(filters, report_data): filters = frappe._dict(filters) - gst_accounts = get_gst_accounts_by_type(filters.company, "Output") data = [] count = 1 @@ -372,21 +371,10 @@ def get_hsn_wise_json_data(filters, report_data): if hsn_description := hsn.get("description"): row["desc"] = hsn_description[:30] - row["iamt"] += flt( - hsn.get(frappe.scrub(cstr(gst_accounts.get("igst_account"))), 0.0), 2 - ) - - row["camt"] += flt( - hsn.get(frappe.scrub(cstr(gst_accounts.get("cgst_account"))), 0.0), 2 - ) - - row["samt"] += flt( - hsn.get(frappe.scrub(cstr(gst_accounts.get("sgst_account"))), 0.0), 2 - ) - - row["csamt"] += flt( - hsn.get(frappe.scrub(cstr(gst_accounts.get("cess_account"))), 0.0), 2 - ) + row["iamt"] += flt(hsn.get("igst_account"), 2) + row["camt"] += flt(hsn.get("cgst_account"), 2) + row["samt"] += flt(hsn.get("sgst_account"), 2) + row["csamt"] += flt(hsn.get("cess_account"), 2) data.append(row) count += 1 diff --git a/india_compliance/gst_india/utils/gstr/gstr_1.py b/india_compliance/gst_india/utils/gstr/gstr_1.py index b4ea6e8eac..9ee9c6293a 100644 --- a/india_compliance/gst_india/utils/gstr/gstr_1.py +++ b/india_compliance/gst_india/utils/gstr/gstr_1.py @@ -11,6 +11,8 @@ B2C_LIMIT = 2_50_000 +# TODO: Enum for Invoice Type + class GSTR1_Categories(Enum): """ @@ -18,7 +20,7 @@ class GSTR1_Categories(Enum): """ # Invoice Items Bifurcation - B2B = "B2B,SEZ,DE" + B2B = "B2B, SEZ, DE" B2CL = "B2C (Large)" EXP = "Exports" B2CS = "B2C (Others)" @@ -59,29 +61,61 @@ class GSTR1_SubCategories(Enum): # DOC_ISSUE = "Document Issued" +CATEGORY_SUB_CATEGORY_MAPPING = { + GSTR1_Categories.B2B: ( + GSTR1_SubCategories.B2B_REGULAR, + GSTR1_SubCategories.B2B_REVERSE_CHARGE, + GSTR1_SubCategories.SEZWP, + GSTR1_SubCategories.SEZWOP, + GSTR1_SubCategories.DE, + ), + GSTR1_Categories.B2CL: (GSTR1_SubCategories.B2CL,), + GSTR1_Categories.EXP: (GSTR1_SubCategories.EXPWP, GSTR1_SubCategories.EXPWOP), + GSTR1_Categories.B2CS: (GSTR1_SubCategories.B2CS,), + GSTR1_Categories.NIL_EXEMPT: ( + GSTR1_SubCategories.NIL_RATED, + GSTR1_SubCategories.EXEMPTED, + GSTR1_SubCategories.NON_GST, + ), + GSTR1_Categories.CDNR: (GSTR1_SubCategories.CDNR,), + GSTR1_Categories.CDNUR: (GSTR1_SubCategories.CDNUR,), +} + SUB_CATEGORIES_DESCRIPTION = { - "SEZWP": "SEZ with payment", - "SEZWOP": "SEZ without payment", - "EXPWP": "Exports with payment", - "EXPWOP": "Exports without payment", - "CDNR": "Credit/Debit Notes (Registered)", - "CDNUR": "Credit/Debit Notes (Unregistered)", + GSTR1_SubCategories.SEZWP: "SEZ with payment", + GSTR1_SubCategories.SEZWOP: "SEZ without payment", + GSTR1_SubCategories.EXPWP: "Exports with payment", + GSTR1_SubCategories.EXPWOP: "Exports without payment", + GSTR1_SubCategories.CDNR: "Credit/Debit Notes (Registered)", + GSTR1_SubCategories.CDNUR: "Credit/Debit Notes (Unregistered)", } CATEGORY_CONDITIONS = { - "B2B,SEZ,DE": {"category": "is_b2b_invoice", "sub_category": "set_for_b2b"}, - "B2C (Large)": {"category": "is_b2cl_invoice", "sub_category": "set_for_b2cl"}, - "Exports": {"category": "is_export_invoice", "sub_category": "set_for_exports"}, - "B2C (Others)": {"category": "is_b2cs_invoice", "sub_category": "set_for_b2cs"}, - "Nil-Rated,Exempted,Non-GST": { + GSTR1_Categories.B2B.value: { + "category": "is_b2b_invoice", + "sub_category": "set_for_b2b", + }, + GSTR1_Categories.B2CL.value: { + "category": "is_b2cl_invoice", + "sub_category": "set_for_b2cl", + }, + GSTR1_Categories.EXP.value: { + "category": "is_export_invoice", + "sub_category": "set_for_exports", + }, + GSTR1_Categories.B2CS.value: { + "category": "is_b2cs_invoice", + "sub_category": "set_for_b2cs", + }, + GSTR1_Categories.NIL_EXEMPT.value: { "category": "is_nil_rated_exempted_non_gst_invoice", "sub_category": "set_for_nil_exp_non_gst", }, - "Credit/Debit Notes (Registered)": { + GSTR1_Categories.CDNR.value: { "category": "is_cdnr_invoice", "sub_category": "set_for_cdnr", }, - "Credit/Debit Notes (Unregistered)": { + GSTR1_Categories.CDNUR.value: { "category": "is_cdnur_invoice", "sub_category": "set_for_cdnur", }, @@ -259,7 +293,7 @@ def is_b2cl_cn_dn(self, invoice): return (abs(invoice_total) > B2C_LIMIT) and self.is_inter_state(invoice) @cache_invoice_condition - def is_b2cl_invoice(self, invoice): + def is_b2cl_inv(self, invoice): return abs(invoice.total_amount) > B2C_LIMIT and self.is_inter_state(invoice) @@ -291,6 +325,7 @@ def is_b2cl_invoice(self, invoice): and not self.is_cn_dn(invoice) and not self.has_gstin_and_is_not_export(invoice) and not self.is_export(invoice) + and self.is_b2cl_inv(invoice) ) def is_b2cs_invoice(self, invoice): @@ -298,7 +333,7 @@ def is_b2cs_invoice(self, invoice): not self.is_nil_rated_exempted_or_non_gst(invoice) and not self.has_gstin_and_is_not_export(invoice) and not self.is_export(invoice) - and (not self.is_b2cl_cn_dn(invoice) or not self.is_b2cl_invoice(invoice)) + and (not self.is_b2cl_cn_dn(invoice) or not self.is_b2cl_inv(invoice)) ) def is_cdnr_invoice(self, invoice): @@ -324,20 +359,20 @@ def set_for_b2b(self, invoice): def set_for_b2cl(self, invoice): # NO INVOICE VALUE - invoice.invoice_sub_category = "B2C (Large)" + invoice.invoice_sub_category = GSTR1_SubCategories.B2CL.value def set_for_exports(self, invoice): if invoice.is_export_with_gst: - invoice.invoice_sub_category = "EXPWP" + invoice.invoice_sub_category = GSTR1_SubCategories.EXPWP.value invoice.invoice_type = "WPAY" else: - invoice.invoice_sub_category = "EXPWOP" + invoice.invoice_sub_category = GSTR1_SubCategories.EXPWOP.value invoice.invoice_type = "WOPAY" def set_for_b2cs(self, invoice): # NO INVOICE VALUE - invoice.invoice_sub_category = "B2C (Others)" + invoice.invoice_sub_category = GSTR1_SubCategories.B2CS.value def set_for_nil_exp_non_gst(self, invoice): # INVOICE TYPE @@ -351,20 +386,20 @@ def set_for_nil_exp_non_gst(self, invoice): # INVOICE SUB CATEGORY if self.is_nil_rated(invoice): - invoice.invoice_sub_category = "Nil-Rated" + invoice.invoice_sub_category = GSTR1_SubCategories.NIL_RATED.value elif self.is_exempted(invoice): - invoice.invoice_sub_category = "Exempted" + invoice.invoice_sub_category = GSTR1_SubCategories.EXEMPTED.value elif self.is_non_gst(invoice): - invoice.invoice_sub_category = "Non-GST" + invoice.invoice_sub_category = GSTR1_SubCategories.NON_GST.value def set_for_cdnr(self, invoice): self._set_invoice_type_for_b2b_and_cdnr(invoice) - invoice.invoice_sub_category = "CDNR" + invoice.invoice_sub_category = GSTR1_SubCategories.CDNR.value def set_for_cdnur(self, invoice): - invoice.invoice_sub_category = "CDNUR" + invoice.invoice_sub_category = GSTR1_SubCategories.CDNUR.value if self.is_export(invoice): if invoice.is_export_with_gst: invoice.invoice_type = "EXPWP" @@ -379,27 +414,35 @@ def set_for_cdnur(self, invoice): def _set_invoice_type_for_b2b_and_cdnr(self, invoice): if invoice.gst_category == "Deemed Export": invoice.invoice_type = "Deemed Exp" - invoice.invoice_sub_category = "Deemed Export" + invoice.invoice_sub_category = GSTR1_SubCategories.DE.value elif invoice.gst_category == "SEZ": if invoice.is_export_with_gst: invoice.invoice_type = "SEZ supplies with payment" - invoice.invoice_sub_category = "SEZWP" + invoice.invoice_sub_category = GSTR1_SubCategories.SEZWP.value else: invoice.invoice_type = "SEZ supplies without payment" - invoice.invoice_sub_category = "SEZWOP" + invoice.invoice_sub_category = GSTR1_SubCategories.SEZWOP.value elif invoice.is_reverese_charge: invoice.invoice_type = "Regular B2B" - invoice.invoice_sub_category = "B2B Reverse Charge" + invoice.invoice_sub_category = GSTR1_SubCategories.B2B_REVERSE_CHARGE.value else: invoice.invoice_type = "Regular B2B" - invoice.invoice_sub_category = "B2B Regular" + invoice.invoice_sub_category = GSTR1_SubCategories.B2B_REGULAR.value class GSTR1Invoices(GSTR1Query, GSTR1Subcategory): + AMOUNT_FIELDS = { + "taxable_value": 0, + "igst_amount": 0, + "cgst_amount": 0, + "sgst_amount": 0, + "total_cess_amount": 0, + } + def __init__(self, filters=None): super().__init__(filters) @@ -481,39 +524,89 @@ def get_filtered_invoices( return filtered_invoices def get_overview(self): + final_summary = [] + sub_category_summary = self.get_sub_category_summary() + + for category, sub_categories in CATEGORY_SUB_CATEGORY_MAPPING.items(): + category_summary = { + "description": category.value, + "no_of_records": 0, + "indent": 0, + **self.AMOUNT_FIELDS, + } + final_summary.append(category_summary) + + for sub_category in sub_categories: + sub_category_row = sub_category_summary[sub_category.value] + category_summary["no_of_records"] += sub_category_row["no_of_records"] + + for key in self.AMOUNT_FIELDS: + category_summary[key] += sub_category_row[key] + + final_summary.append(sub_category_row) + + self.update_overlaping_invoice_summary(sub_category_summary, final_summary) + + return final_summary + + def get_sub_category_summary(self): invoices = self.get_invoices_for_item_wise_summary() self.process_invoices(invoices) - amount_fields = { - "taxable_value": 0, - "igst_amount": 0, - "cgst_amount": 0, - "sgst_amount": 0, - "total_cess_amount": 0, - } - summary = {} - subcategories = [category.value for category in GSTR1_SubCategories] - for category in subcategories: - summary[category] = { - "description": SUB_CATEGORIES_DESCRIPTION.get(category, category), + for category in GSTR1_SubCategories: + summary[category.value] = { + "description": SUB_CATEGORIES_DESCRIPTION.get(category, category.value), "no_of_records": 0, + "indent": 1, "unique_records": set(), - **amount_fields, + **self.AMOUNT_FIELDS, } for row in invoices: - category_key = summary[ + summary_row = summary[ row.get("invoice_sub_category", row["invoice_category"]) ] - for key in amount_fields: - category_key[key] += row[key] + for key in self.AMOUNT_FIELDS: + summary_row[key] += row[key] + + summary_row["unique_records"].add(row.invoice_no) + + for summary_row in summary.values(): + summary_row["no_of_records"] = len(summary_row["unique_records"]) - category_key["unique_records"].add(row.invoice_no) + return summary - for category_key in summary.values(): - category_key["no_of_records"] = len(category_key["unique_records"]) + def update_overlaping_invoice_summary(self, sub_category_summary, final_summary): + nil_exempt_non_gst = ( + GSTR1_SubCategories.NIL_RATED.value, + GSTR1_SubCategories.EXEMPTED.value, + GSTR1_SubCategories.NON_GST.value, + ) + + # Get Unique Taxable Invoices + unique_invoices = set() + for category, row in sub_category_summary.items(): + if category in nil_exempt_non_gst: + continue + + unique_invoices.update(row["unique_records"]) - return list(summary.values()) + # Get Overlaping Invoices + overlaping_invoices = set() + for category in nil_exempt_non_gst: + category_invoices = sub_category_summary[category]["unique_records"] + + overlaping_invoices.update(category_invoices.intersection(unique_invoices)) + unique_invoices.update(category_invoices) + + # Update Summary + if overlaping_invoices: + final_summary.append( + { + "description": "Overlaping Invoices in Nil-Rated/Exempt/Non-GST", + "no_of_records": -len(overlaping_invoices), + } + ) diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py index e72041c87b..6c25714349 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -157,7 +157,7 @@ "before_update_after_submit": "india_compliance.gst_india.overrides.transaction.before_update_after_submit", }, "Purchase Order Item": { - "before_update_after_submit": "india_compliance.gst_india.overrides.transaction.before_update_after_submit_item", + "on_change": "india_compliance.gst_india.overrides.transaction.on_change_item", }, "Purchase Receipt": { "onload": [ @@ -206,7 +206,7 @@ "before_update_after_submit": "india_compliance.gst_india.overrides.transaction.before_update_after_submit", }, "Sales Order Item": { - "before_update_after_submit": "india_compliance.gst_india.overrides.transaction.before_update_after_submit_item", + "on_change": "india_compliance.gst_india.overrides.transaction.on_change_item", }, "Supplier": { "validate": [ @@ -406,6 +406,15 @@ } } +fields_for_group_similar_items = [ + "taxable_value", + "cgst_amount", + "sgst_amount", + "igst_amount", + "cess_amount", + "cess_non_advol_amount", +] + # Includes in # ------------------ diff --git a/india_compliance/patches/check_version_compatibility.py b/india_compliance/patches/check_version_compatibility.py index 4cd0e323d2..deede46cbe 100644 --- a/india_compliance/patches/check_version_compatibility.py +++ b/india_compliance/patches/check_version_compatibility.py @@ -18,7 +18,7 @@ { "app_name": "ERPNext", "current_version": version.parse(erpnext.__version__), - "required_versions": {"version-14": "14.64.0", "version-15": "15.15.0"}, + "required_versions": {"version-14": "14.66.5", "version-15": "15.19.3"}, }, ] diff --git a/india_compliance/public/js/custom_number_card.js b/india_compliance/public/js/custom_number_card.js new file mode 100644 index 0000000000..ee444d1d72 --- /dev/null +++ b/india_compliance/public/js/custom_number_card.js @@ -0,0 +1,19 @@ +let FrappeNumberCard = frappe.widget.widget_factory.number_card; + +class CustomNumberCard extends FrappeNumberCard { + render_number() { + if ( + [ + "Pending e-Waybill", + "Pending e-Invoices", + "Invoice Cancelled But Not e-Invoice", + ].includes(this.card_doc.name) && + !this.formatted_number + ) + this.card_doc.color = null; + + super.render_number(); + } +} + +frappe.widget.widget_factory.number_card = CustomNumberCard; diff --git a/india_compliance/public/js/india_compliance.bundle.js b/india_compliance/public/js/india_compliance.bundle.js index e384acdca2..8ea391d433 100644 --- a/india_compliance/public/js/india_compliance.bundle.js +++ b/india_compliance/public/js/india_compliance.bundle.js @@ -4,3 +4,4 @@ import "./transaction"; import "./audit_trail_notification"; import "./item_tax_template_notification"; import "./quick_info_popover"; +import "./custom_number_card";