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/constants/__init__.py b/india_compliance/gst_india/constants/__init__.py index 903688f7c2..52dab78965 100644 --- a/india_compliance/gst_india/constants/__init__.py +++ b/india_compliance/gst_india/constants/__init__.py @@ -167,7 +167,7 @@ "Lakshadweep Islands": (682, 682), "Kerala": (670, 695), "Tamil Nadu": (600, 643), - "Puducherry": ((533, 533), (605, 605), (607, 607), (609, 609)), + "Puducherry": ((533, 533), (605, 605), (607, 607), (609, 609), (673, 673)), "Andaman and Nicobar Islands": (744, 744), "Andhra Pradesh": (500, 535), } diff --git a/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.py b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.py index a4b7ebe07e..9241025e09 100644 --- a/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.py +++ b/india_compliance/gst_india/doctype/bill_of_entry/bill_of_entry.py @@ -254,9 +254,11 @@ def validate_purchase_invoice(self): def validate_taxes(self): input_accounts = get_gst_accounts_by_type(self.company, "Input", throw=True) taxable_value_map = {} + item_qty_map = {} for row in self.get("items"): taxable_value_map[row.name] = row.taxable_value + item_qty_map[row.name] = row.qty for tax in self.taxes: if not tax.tax_amount: @@ -278,33 +280,43 @@ def validate_taxes(self): [input_accounts.cess_non_advol_account], tax ) - if tax.charge_type == "Actual": + if tax.charge_type != "Actual": + continue - item_wise_tax_rates = json.loads(tax.item_wise_tax_rates) - if not item_wise_tax_rates: - frappe.throw( - _( - "Tax Row #{0}: Charge Type is set to Actual. However, this would" - " not compute item taxes, and your further reporting will be affected." - ).format(tax.idx), - title=_("Invalid Charge Type"), - ) + item_wise_tax_rates = json.loads(tax.item_wise_tax_rates) + if not item_wise_tax_rates: + frappe.throw( + _( + "Tax Row #{0}: Charge Type is set to Actual. However, this would" + " not compute item taxes, and your further reporting will be affected." + ).format(tax.idx), + title=_("Invalid Charge Type"), + ) + + # validating total tax + total_tax = 0 + is_non_cess_advol = ( + tax.account_head == input_accounts.cess_non_advol_account + ) - # validating total tax - total_tax = 0 - for item, rate in item_wise_tax_rates.items(): - item_taxable_value = taxable_value_map.get(item, 0) - total_tax += item_taxable_value * rate / 100 + for item, rate in item_wise_tax_rates.items(): + multiplier = ( + item_qty_map.get(item, 0) + if is_non_cess_advol + else taxable_value_map.get(item, 0) / 100 + ) + total_tax += multiplier * rate - tax_difference = abs(total_tax - tax.tax_amount) + tax_difference = abs(total_tax - tax.tax_amount) - if tax_difference > 1: - frappe.throw( - _( - "Tax Row #{0}: Charge Type is set to Actual. However, Tax Amount {1}" - " is incorrect. Try setting the Charge Type to On Net Total." - ).format(row.idx, tax.tax_amount) - ) + if tax_difference > 1: + column = "On Item Quantity" if is_non_cess_advol else "On Net Total" + frappe.throw( + _( + "Tax Row #{0}: Charge Type is set to Actual. However, Tax Amount {1}" + " is incorrect. Try setting the Charge Type to {2}." + ).format(row.idx, tax.tax_amount, column) + ) def get_gl_entries(self): # company_currency is required by get_gl_dict 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 f0f5beb8d7..6dad39e4be 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 @@ -28,6 +28,7 @@ def test_create_bill_of_entry(self): pi = create_purchase_invoice(supplier="_Test Foreign Supplier", update_stock=1) # Create BOE + boe = make_bill_of_entry(pi.name) boe.items[0].customs_duty = 100 boe.bill_of_entry_no = "123" 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_advance_payment_entry.py b/india_compliance/gst_india/overrides/test_advance_payment_entry.py index 94429a6f77..f332d25932 100644 --- a/india_compliance/gst_india/overrides/test_advance_payment_entry.py +++ b/india_compliance/gst_india/overrides/test_advance_payment_entry.py @@ -7,9 +7,15 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import ( get_outstanding_reference_documents, ) +from erpnext.accounts.doctype.payment_reconciliation.payment_reconciliation import ( + adjust_allocations_for_taxes, +) from erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment import ( create_unreconcile_doc_for_selection, ) +from erpnext.controllers.accounts_controller import ( + get_advance_payment_entries_for_regional, +) from erpnext.controllers.stock_controller import show_accounting_ledger_preview from india_compliance.gst_india.utils.tests import create_transaction @@ -453,6 +459,54 @@ def assertPLEntries(self, payment_doc, expected_pl_entries): self.assertEqual(out_str, expected_out_str) +class TestRegionalOverrides(TestAdvancePaymentEntry): + def test_get_advance_payment_entries_for_regional(self): + payment_doc = self._create_payment_entry() + invoice_doc = self._create_sales_invoice(payment_doc) + + conditions = frappe._dict({"company": invoice_doc.get("company")}) + + payment_entry = get_advance_payment_entries_for_regional( + party_type="Customer", + party=invoice_doc.customer, + party_account=[invoice_doc.debit_to], + order_list=[], + order_doctype="Sales Order", + include_unallocated=True, + condition=conditions, + ) + + payment_entry_amount = payment_entry[0].get("amount") + self.assertNotEqual(400, payment_entry_amount) + + def test_adjust_allocations_for_taxes(self): + payment_doc = self._create_payment_entry() + invoice_doc = self._create_sales_invoice() + + pr = frappe.get_doc("Payment Reconciliation") + pr.company = "_Test Indian Registered Company" + pr.party_type = "Customer" + pr.party = invoice_doc.customer + pr.receivable_payable_account = invoice_doc.debit_to + + pr.get_unreconciled_entries() + invoices = [ + row.as_dict() + for row in pr.invoices + if row.invoice_number == invoice_doc.name + ] + payments = [ + row.as_dict() + for row in pr.payments + if row.reference_name == payment_doc.name + ] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.allocation[0].allocated_amount = 50 + + adjust_allocations_for_taxes(pr) + self.assertEqual(pr.allocation[0].allocated_amount, 42.37) # 50 / 1.18 + + def make_payment_reconciliation(payment_doc, invoice_doc, amount): pr = frappe.get_doc("Payment Reconciliation") pr.company = "_Test Indian Registered Company" diff --git a/india_compliance/gst_india/overrides/test_ineligible_itc.py b/india_compliance/gst_india/overrides/test_ineligible_itc.py index 9874336327..d9df895811 100644 --- a/india_compliance/gst_india/overrides/test_ineligible_itc.py +++ b/india_compliance/gst_india/overrides/test_ineligible_itc.py @@ -35,6 +35,7 @@ }, {"item_code": "Test Service Item", "qty": 3, "rate": 500}, {"item_code": "Test Ineligible Service Item", "qty": 2, "rate": 499}, + {"item_code": "_Test Trading Goods 1", "qty": 1, "rate": 100}, ] # Item Total # 20 * 5 + 19 * 3 + 1000 * 1 + 999 * 1 + 500 * 3 + 499 * 2 + 100 * 1 (Default) = 4754 diff --git a/india_compliance/gst_india/overrides/test_transaction.py b/india_compliance/gst_india/overrides/test_transaction.py index 8a84f91d60..3e16fcc995 100644 --- a/india_compliance/gst_india/overrides/test_transaction.py +++ b/india_compliance/gst_india/overrides/test_transaction.py @@ -6,15 +6,27 @@ import frappe from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import today +from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import ( + make_regional_gl_entries, +) from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return -from erpnext.accounts.party import _get_party_details +from erpnext.accounts.party import _get_party_details, get_regional_address_details +from erpnext.controllers.accounts_controller import ( + update_child_qty_rate, + update_gl_dict_with_regional_fields, +) +from erpnext.controllers.taxes_and_totals import get_regional_round_off_accounts from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice +from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + update_regional_gl_entries, +) from india_compliance.gst_india.constants import SALES_DOCTYPES from india_compliance.gst_india.overrides.transaction import DOCTYPES_WITH_GST_DETAIL from india_compliance.gst_india.utils.tests import ( _append_taxes, append_item, + create_purchase_invoice, create_transaction, ) @@ -905,3 +917,163 @@ def create_tax_accounts(account_name): **defaults, } ).insert(ignore_if_duplicate=True) + + +class TestRegionalOverrides(FrappeTestCase): + @change_settings( + "GST Settings", + {"round_off_gst_values": 1}, + ) + def test_get_regional_round_off_accounts(self): + + data = get_regional_round_off_accounts("_Test Indian Registered Company", []) + self.assertListEqual( + data, + [ + "Input Tax CGST - _TIRC", + "Input Tax SGST - _TIRC", + "Input Tax IGST - _TIRC", + "Output Tax CGST - _TIRC", + "Output Tax SGST - _TIRC", + "Output Tax IGST - _TIRC", + "Input Tax CGST RCM - _TIRC", + "Input Tax SGST RCM - _TIRC", + "Input Tax IGST RCM - _TIRC", + ], + ) + + @change_settings( + "GST Settings", + {"round_off_gst_values": 0}, + ) + def test_get_regional_round_off_accounts_with_round_off_unchecked(self): + + data = get_regional_round_off_accounts("_Test Indian Registered Company", []) + self.assertListEqual(data, []) + + def test_update_gl_dict_with_regional_fields(self): + + doc = frappe.get_doc( + {"doctype": "Sales Invoice", "company_gstin": "29AAHCM7727Q1ZI"} + ) + gl_entry = {} + update_gl_dict_with_regional_fields(doc, gl_entry) + + self.assertEqual(gl_entry.get("company_gstin", ""), "29AAHCM7727Q1ZI") + + def test_make_regional_gl_entries(self): + pi = create_purchase_invoice() + pi._has_ineligible_itc_items = True + + gl_entries = {"company_gstin": "29AAHCM7727Q1ZI"} + frappe.flags.through_repost_accounting_ledger = True + + make_regional_gl_entries(gl_entries, pi) + + frappe.flags.through_repost_accounting_ledger = False + self.assertEqual(pi._has_ineligible_itc_items, False) + + def test_update_regional_gl_entries(self): + gl_entry = {"company_gstin": "29AAHCM7727Q1ZI"} + doc = frappe.get_doc( + { + "doctype": "Sales Invoice", + "is_opening": "Yes", + "company_gstin": "29AAHCM7727Q1ZI", + } + ) + return_entry = update_regional_gl_entries(gl_entry, doc) + self.assertDictEqual(return_entry, gl_entry) + + def test_get_regional_address_details(self): + doctype = "Sales Order" + company = "_Test Indian Registered Company" + party_details = { + "customer": "_Test Registered Customer", + "customer_address": "_Test Registered Customer-Billing", + "billing_address_gstin": "24AANFA2641L1ZF", + "gst_category": "Registered Regular", + "company_gstin": "24AAQCA8719H1ZC", + } + + get_regional_address_details(party_details, doctype, company) + + self.assertEqual( + party_details.get("taxes_and_charges"), "Output GST In-state - _TIRC" + ) + self.assertEqual(party_details.get("place_of_supply"), "24-Gujarat") + self.assertTrue(party_details.get("taxes")) + + +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 cd8c2962b3..d272225ec1 100644 --- a/india_compliance/gst_india/overrides/transaction.py +++ b/india_compliance/gst_india/overrides/transaction.py @@ -128,10 +128,14 @@ def validate_item_wise_tax_detail(doc, gst_accounts): return item_taxable_values = defaultdict(float) + item_qty_map = defaultdict(float) + + cess_non_advol_account = get_gst_accounts_by_tax_type(doc.company, "cess_non_advol") for row in doc.items: item_key = row.item_code or row.item_name item_taxable_values[item_key] += row.taxable_value + item_qty_map[item_key] += row.qty for row in doc.taxes: if row.account_head not in gst_accounts: @@ -154,15 +158,25 @@ def validate_item_wise_tax_detail(doc, gst_accounts): # Sales Invoice is created with manual tax amount. So, when a sales return is created, # the tax amount is not recalculated, causing the issue. - item_taxable_value = item_taxable_values.get(item_name, 0) - tax_difference = abs(item_taxable_value * tax_rate / 100 - tax_amount) + + is_cess_non_advol = row.account_head in cess_non_advol_account + multiplier = ( + item_qty_map.get(item_name, 0) + if is_cess_non_advol + else item_taxable_values.get(item_name, 0) / 100 + ) + tax_difference = abs(multiplier * tax_rate - tax_amount) if tax_difference > 1: + correct_charge_type = ( + "On Item Quantity" if is_cess_non_advol else "On Net Total" + ) + frappe.throw( _( "Tax Row #{0}: Charge Type is set to Actual. However, Tax Amount {1} as computed for Item {2}" - " is incorrect. Try setting the Charge Type to On Net Total." - ).format(row.idx, tax_amount, bold(item_name)) + " is incorrect. Try setting the Charge Type to {3}" + ).format(row.idx, tax_amount, bold(item_name), correct_charge_type) ) @@ -470,12 +484,12 @@ def validate_charge_type_for_cess_non_advol_accounts(cess_non_advol_accounts, ta ) if ( - tax_row.charge_type != "On Item Quantity" + tax_row.charge_type not in ["On Item Quantity", "Actual"] and tax_row.account_head in cess_non_advol_accounts ): frappe.throw( _( - "Row #{0}: Charge Type must be On Item Quantity" + "Row #{0}: Charge Type must be On Item Quantity / Actual" " as it is a Cess Non Advol Account" ).format(tax_row.idx), title=_("Invalid Charge Type"), @@ -1432,9 +1446,6 @@ def before_print(doc, method=None, print_settings=None): ): return - if doc.get("group_same_items"): - ItemGSTDetails().update(doc) - set_gst_breakup(doc) @@ -1489,8 +1500,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..7ea7c6c1b1 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"], "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,87 @@ 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; + }, + + // Override datatable hook for column total calculation + get_datatable_options(datatable_options) { + datatable_options.hooks = { + columnTotal: custom_report_column_total, + }; + + return datatable_options; + }, }; 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] + ); } } +custom_report_column_total = function (...args) { + const summary_by = frappe.query_report.get_filter_value("summary_by"); + if (summary_by !== "Overview") + return frappe.utils.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/gst_sales_register_beta/test_sales_register_beta.py b/india_compliance/gst_india/report/gst_sales_register_beta/test_sales_register_beta.py new file mode 100644 index 0000000000..10c2567f94 --- /dev/null +++ b/india_compliance/gst_india/report/gst_sales_register_beta/test_sales_register_beta.py @@ -0,0 +1,697 @@ +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import getdate + +from india_compliance.gst_india.report.gst_sales_register_beta.gst_sales_register_beta import ( + execute, +) +from india_compliance.gst_india.utils.tests import create_sales_invoice + +today = getdate() + +FILTERS = { + "company": "_Test Indian Registered Company", + "date_range": [today, today], + "from_date": today, + "to_date": today, +} + + +EXPECTED_SUMMARY_BY_HSN = [ + { + "item_code": "_Test Service Item", + "gst_hsn_code": "999900", + "billing_address_gstin": None, + "company_gstin": "24AAQCA8719H1ZC", + "customer_name": "_Test Foreign Customer", + "place_of_supply": "96-Other Countries", + "invoice_total": -305000.0, + "returned_invoice_total": 0.0, + "gst_category": "Overseas", + "gst_treatment": "Zero-Rated", + "gst_rate": 0.0, + "taxable_value": -300000.0, + "total_amount": -300000.0, + "total_tax_amount": 0.0, + "invoice_category": "Credit/Debit Notes (Unregistered)", + "invoice_sub_category": "CDNUR", + "invoice_type": "EXPWOP", + }, + { + "item_code": "_Test Nil Rated Item", + "gst_hsn_code": "61149090", + "billing_address_gstin": None, + "company_gstin": "24AAQCA8719H1ZC", + "customer_name": "_Test Foreign Customer", + "place_of_supply": "96-Other Countries", + "invoice_total": -305000.0, + "returned_invoice_total": 0.0, + "gst_category": "Overseas", + "gst_treatment": "Zero-Rated", + "gst_rate": 0.0, + "taxable_value": -5000.0, + "total_amount": -5000.0, + "total_tax_amount": 0.0, + "invoice_category": "Credit/Debit Notes (Unregistered)", + "invoice_sub_category": "CDNUR", + "invoice_type": "EXPWOP", + }, + { + "item_code": "_Test Service Item", + "gst_hsn_code": "999900", + "billing_address_gstin": None, + "company_gstin": "24AAQCA8719H1ZC", + "customer_name": "_Test Foreign Customer", + "place_of_supply": "96-Other Countries", + "invoice_total": 545000.0, + "returned_invoice_total": 0.0, + "gst_category": "Overseas", + "gst_treatment": "Zero-Rated", + "gst_rate": 0.0, + "taxable_value": 500000.0, + "total_amount": 500000.0, + "total_tax_amount": 0.0, + "invoice_category": "Exports", + "invoice_sub_category": "EXPWP", + "invoice_type": "WPAY", + }, + { + "item_code": "_Test Nil Rated Item", + "gst_hsn_code": "61149090", + "billing_address_gstin": None, + "company_gstin": "24AAQCA8719H1ZC", + "customer_name": "_Test Foreign Customer", + "place_of_supply": "96-Other Countries", + "invoice_total": 545000.0, + "returned_invoice_total": 0.0, + "gst_category": "Overseas", + "gst_treatment": "Zero-Rated", + "gst_rate": 0.0, + "taxable_value": 45000.0, + "total_amount": 45000.0, + "total_tax_amount": 0.0, + "invoice_category": "Exports", + "invoice_sub_category": "EXPWP", + "invoice_type": "WPAY", + }, + { + "item_code": "_Test Service Item", + "gst_hsn_code": "999900", + "billing_address_gstin": None, + "company_gstin": "24AAQCA8719H1ZC", + "customer_name": "_Test Foreign Customer", + "place_of_supply": "96-Other Countries", + "invoice_total": 145000.0, + "returned_invoice_total": 0.0, + "gst_category": "Overseas", + "gst_treatment": "Zero-Rated", + "gst_rate": 0.0, + "taxable_value": 140000.0, + "total_amount": 140000.0, + "total_tax_amount": 0.0, + "invoice_category": "Exports", + "invoice_sub_category": "EXPWOP", + "invoice_type": "WOPAY", + }, + { + "item_code": "_Test Nil Rated Item", + "gst_hsn_code": "61149090", + "billing_address_gstin": None, + "company_gstin": "24AAQCA8719H1ZC", + "customer_name": "_Test Foreign Customer", + "place_of_supply": "96-Other Countries", + "invoice_total": 145000.0, + "returned_invoice_total": 0.0, + "gst_category": "Overseas", + "gst_treatment": "Zero-Rated", + "gst_rate": 0.0, + "taxable_value": 5000.0, + "total_amount": 5000.0, + "total_tax_amount": 0.0, + "invoice_category": "Exports", + "invoice_sub_category": "EXPWOP", + "invoice_type": "WOPAY", + }, + { + "item_code": "_Test Service Item", + "gst_hsn_code": "999900", + "billing_address_gstin": "29AABCR1718E1ZL", + "company_gstin": "24AAQCA8719H1ZC", + "customer_name": "_Test Registered Composition Customer", + "place_of_supply": "29-Karnataka", + "invoice_total": -295500.0, + "returned_invoice_total": 0.0, + "gst_category": "Registered Composition", + "gst_treatment": "Taxable", + "gst_rate": 18.0, + "taxable_value": -225000.0, + "total_amount": -265500.0, + "total_tax_amount": -40500.0, + "invoice_category": "Credit/Debit Notes (Registered)", + "invoice_type": "Regular B2B", + "invoice_sub_category": "CDNR", + }, + { + "item_code": "_Test Nil Rated Item", + "gst_hsn_code": "61149090", + "billing_address_gstin": "29AABCR1718E1ZL", + "company_gstin": "24AAQCA8719H1ZC", + "customer_name": "_Test Registered Composition Customer", + "place_of_supply": "29-Karnataka", + "invoice_total": -295500.0, + "returned_invoice_total": 0.0, + "gst_category": "Registered Composition", + "gst_treatment": "Nil-Rated", + "gst_rate": 0.0, + "taxable_value": -30000.0, + "total_amount": -30000.0, + "total_tax_amount": 0.0, + "invoice_category": "Nil-Rated, Exempted, Non-GST", + "invoice_type": "Inter-State to registered persons", + "invoice_sub_category": "Nil-Rated", + }, + { + "item_code": "_Test Service Item", + "gst_hsn_code": "999900", + "billing_address_gstin": "29AABCR1718E1ZL", + "company_gstin": "24AAQCA8719H1ZC", + "customer_name": "_Test Registered Composition Customer", + "place_of_supply": "29-Karnataka", + "invoice_total": 532000.0, + "returned_invoice_total": 0.0, + "gst_category": "Registered Composition", + "gst_treatment": "Taxable", + "gst_rate": 18.0, + "taxable_value": 400000.0, + "total_amount": 472000.0, + "total_tax_amount": 72000.0, + "invoice_category": "B2B, SEZ, DE", + "invoice_type": "Regular B2B", + "invoice_sub_category": "B2B Regular", + }, + { + "item_code": "_Test Nil Rated Item", + "gst_hsn_code": "61149090", + "billing_address_gstin": "29AABCR1718E1ZL", + "company_gstin": "24AAQCA8719H1ZC", + "customer_name": "_Test Registered Composition Customer", + "place_of_supply": "29-Karnataka", + "invoice_total": 532000.0, + "returned_invoice_total": 0.0, + "gst_category": "Registered Composition", + "gst_treatment": "Nil-Rated", + "gst_rate": 0.0, + "taxable_value": 60000.0, + "total_amount": 60000.0, + "total_tax_amount": 0.0, + "invoice_category": "Nil-Rated, Exempted, Non-GST", + "invoice_type": "Inter-State to registered persons", + "invoice_sub_category": "Nil-Rated", + }, + { + "item_code": "_Test Service Item", + "gst_hsn_code": "999900", + "billing_address_gstin": None, + "company_gstin": "24AAQCA8719H1ZC", + "customer_name": "_Test Unregistered Customer", + "place_of_supply": "24-Gujarat", + "invoice_total": 111200.0, + "returned_invoice_total": 0.0, + "gst_category": "Unregistered", + "gst_treatment": "Taxable", + "gst_rate": 18.0, + "taxable_value": 90000.0, + "total_amount": 106200.0, + "total_tax_amount": 16200.0, + "invoice_category": "B2C (Others)", + "invoice_sub_category": "B2C (Others)", + }, + { + "item_code": "_Test Nil Rated Item", + "gst_hsn_code": "61149090", + "billing_address_gstin": None, + "company_gstin": "24AAQCA8719H1ZC", + "customer_name": "_Test Unregistered Customer", + "place_of_supply": "24-Gujarat", + "invoice_total": 111200.0, + "returned_invoice_total": 0.0, + "gst_category": "Unregistered", + "gst_treatment": "Nil-Rated", + "gst_rate": 0.0, + "taxable_value": 5000.0, + "total_amount": 5000.0, + "total_tax_amount": 0.0, + "invoice_category": "Nil-Rated, Exempted, Non-GST", + "invoice_type": "Intra-State to unregistered persons", + "invoice_sub_category": "Nil-Rated", + }, + { + "item_code": "_Test Service Item", + "gst_hsn_code": "999900", + "billing_address_gstin": None, + "company_gstin": "24AAQCA8719H1ZC", + "customer_name": "_Test Unregistered Customer", + "place_of_supply": "24-Gujarat", + "invoice_total": 29780.0, + "returned_invoice_total": 0.0, + "gst_category": "Unregistered", + "gst_treatment": "Taxable", + "gst_rate": 18.0, + "taxable_value": 21000.0, + "total_amount": 24780.0, + "total_tax_amount": 3780.0, + "invoice_category": "B2C (Others)", + "invoice_sub_category": "B2C (Others)", + }, + { + "item_code": "_Test Nil Rated Item", + "gst_hsn_code": "61149090", + "billing_address_gstin": None, + "company_gstin": "24AAQCA8719H1ZC", + "customer_name": "_Test Unregistered Customer", + "place_of_supply": "24-Gujarat", + "invoice_total": 29780.0, + "returned_invoice_total": 0.0, + "gst_category": "Unregistered", + "gst_treatment": "Nil-Rated", + "gst_rate": 0.0, + "taxable_value": 5000.0, + "total_amount": 5000.0, + "total_tax_amount": 0.0, + "invoice_category": "Nil-Rated, Exempted, Non-GST", + "invoice_type": "Intra-State to unregistered persons", + "invoice_sub_category": "Nil-Rated", + }, + { + "item_code": "_Test Service Item", + "gst_hsn_code": "999900", + "billing_address_gstin": None, + "company_gstin": "24AAQCA8719H1ZC", + "customer_name": "_Test Unregistered Customer", + "place_of_supply": "29-Karnataka", + "invoice_total": 16800.0, + "returned_invoice_total": 0.0, + "gst_category": "Unregistered", + "gst_treatment": "Taxable", + "gst_rate": 18.0, + "taxable_value": 10000.0, + "total_amount": 11800.0, + "total_tax_amount": 1800.0, + "invoice_category": "B2C (Others)", + "invoice_sub_category": "B2C (Others)", + }, + { + "item_code": "_Test Nil Rated Item", + "gst_hsn_code": "61149090", + "billing_address_gstin": None, + "company_gstin": "24AAQCA8719H1ZC", + "customer_name": "_Test Unregistered Customer", + "place_of_supply": "29-Karnataka", + "invoice_total": 16800.0, + "returned_invoice_total": 0.0, + "gst_category": "Unregistered", + "gst_treatment": "Nil-Rated", + "gst_rate": 0.0, + "taxable_value": 5000.0, + "total_amount": 5000.0, + "total_tax_amount": 0.0, + "invoice_category": "Nil-Rated, Exempted, Non-GST", + "invoice_type": "Inter-State to unregistered persons", + "invoice_sub_category": "Nil-Rated", + }, +] + +EXPECTED_OVERVIEW = [ + { + "description": "B2B, SEZ, DE", + "indent": 0, + "taxable_value": 400000.0, + "igst_amount": 72000.0, + "cgst_amount": 0.0, + "sgst_amount": 0.0, + "total_cess_amount": 0.0, + }, + { + "description": "B2B Regular", + "indent": 1, + "taxable_value": 400000.0, + "igst_amount": 72000.0, + "cgst_amount": 0.0, + "sgst_amount": 0.0, + "total_cess_amount": 0.0, + }, + { + "description": "B2B Reverse Charge", + "indent": 1, + "taxable_value": 0, + "igst_amount": 0, + "cgst_amount": 0, + "sgst_amount": 0, + "total_cess_amount": 0, + }, + { + "description": "SEZ with payment", + "indent": 1, + "taxable_value": 0, + "igst_amount": 0, + "cgst_amount": 0, + "sgst_amount": 0, + "total_cess_amount": 0, + }, + { + "description": "SEZ without payment", + "indent": 1, + "taxable_value": 0, + "igst_amount": 0, + "cgst_amount": 0, + "sgst_amount": 0, + "total_cess_amount": 0, + }, + { + "description": "Deemed Exports", + "indent": 1, + "taxable_value": 0, + "igst_amount": 0, + "cgst_amount": 0, + "sgst_amount": 0, + "total_cess_amount": 0, + }, + { + "description": "B2C (Large)", + "indent": 0, + "taxable_value": 0, + "igst_amount": 0, + "cgst_amount": 0, + "sgst_amount": 0, + "total_cess_amount": 0, + }, + { + "description": "B2C (Large)", + "indent": 1, + "taxable_value": 0, + "igst_amount": 0, + "cgst_amount": 0, + "sgst_amount": 0, + "total_cess_amount": 0, + }, + { + "description": "Exports", + "indent": 0, + "taxable_value": 690000.0, + "igst_amount": 0.0, + "cgst_amount": 0.0, + "sgst_amount": 0.0, + "total_cess_amount": 0.0, + }, + { + "description": "Exports with payment", + "indent": 1, + "taxable_value": 545000.0, + "igst_amount": 0.0, + "cgst_amount": 0.0, + "sgst_amount": 0.0, + "total_cess_amount": 0.0, + }, + { + "description": "Exports without payment", + "indent": 1, + "taxable_value": 145000.0, + "igst_amount": 0.0, + "cgst_amount": 0.0, + "sgst_amount": 0.0, + "total_cess_amount": 0.0, + }, + { + "description": "B2C (Others)", + "indent": 0, + "taxable_value": 121000.0, + "igst_amount": 1800.0, + "cgst_amount": 9990.0, + "sgst_amount": 9990.0, + "total_cess_amount": 0.0, + }, + { + "description": "B2C (Others)", + "indent": 1, + "taxable_value": 121000.0, + "igst_amount": 1800.0, + "cgst_amount": 9990.0, + "sgst_amount": 9990.0, + "total_cess_amount": 0.0, + }, + { + "description": "Nil-Rated, Exempted, Non-GST", + "indent": 0, + "taxable_value": 45000.0, + "igst_amount": 0.0, + "cgst_amount": 0.0, + "sgst_amount": 0.0, + "total_cess_amount": 0.0, + }, + { + "description": "Nil-Rated", + "indent": 1, + "taxable_value": 45000.0, + "igst_amount": 0.0, + "cgst_amount": 0.0, + "sgst_amount": 0.0, + "total_cess_amount": 0.0, + }, + { + "description": "Exempted", + "indent": 1, + "taxable_value": 0, + "igst_amount": 0, + "cgst_amount": 0, + "sgst_amount": 0, + "total_cess_amount": 0, + }, + { + "description": "Non-GST", + "indent": 1, + "taxable_value": 0, + "igst_amount": 0, + "cgst_amount": 0, + "sgst_amount": 0, + "total_cess_amount": 0, + }, + { + "description": "Credit/Debit Notes (Registered)", + "indent": 0, + "taxable_value": -225000.0, + "igst_amount": -40500.0, + "cgst_amount": 0.0, + "sgst_amount": 0.0, + "total_cess_amount": 0.0, + }, + { + "description": "Credit/Debit Notes (Registered)", + "indent": 1, + "taxable_value": -225000.0, + "igst_amount": -40500.0, + "cgst_amount": 0.0, + "sgst_amount": 0.0, + "total_cess_amount": 0.0, + }, + { + "description": "Credit/Debit Notes (Unregistered)", + "indent": 0, + "taxable_value": -305000.0, + "igst_amount": 0.0, + "cgst_amount": 0.0, + "sgst_amount": 0.0, + "total_cess_amount": 0.0, + }, + { + "description": "Credit/Debit Notes (Unregistered)", + "indent": 1, + "taxable_value": -305000.0, + "igst_amount": 0.0, + "cgst_amount": 0.0, + "sgst_amount": 0.0, + "total_cess_amount": 0.0, + }, + { + "description": "Overlaping Invoices in Nil-Rated/Exempt/Non-GST", + "no_of_records": -5, + }, +] +INVOICES = [ + { + "customer": "_Test Unregistered Customer", + "place_of_supply": "29-Karnataka", + "is_out_state": 1, + "items": [ + { + "item_code": "_Test Nil Rated Item", + "rate": 100, + "qty": 50, + }, + { + "item_code": "_Test Service Item", + "rate": 500, + "qty": 20, + }, + ], + }, + { + "customer": "_Test Unregistered Customer", + "place_of_supply": "24-Gujarat", + "is_in_state": 1, + "items": [ + { + "item_code": "_Test Nil Rated Item", + "rate": 100, + "qty": 50, + }, + { + "item_code": "_Test Service Item", + "rate": 700, + "qty": 30, + }, + ], + }, + { + "company_gstin": "24AAQCA8719H1ZC", + "customer": "_Test Unregistered Customer", + "place_of_supply": "24-Gujarat", + "is_in_state": 1, + "items": [ + { + "item_code": "_Test Nil Rated Item", + "rate": 100, + "qty": 50, + }, + { + "item_code": "_Test Service Item", + "rate": 900, + "qty": 100, + }, + ], + }, + { + "company_gstin": "24AAQCA8719H1ZC", + "customer": "_Test Registered Composition Customer", + "place_of_supply": "29-Karnataka", + "is_out_state": 1, + "items": [ + { + "item_code": "_Test Nil Rated Item", + "rate": 1000, + "qty": 60, + }, + { + "item_code": "_Test Service Item", + "rate": 2000, + "qty": 200, + }, + ], + }, + { + "company_gstin": "24AAQCA8719H1ZC", + "customer": "_Test Registered Composition Customer", + "place_of_supply": "29-Karnataka", + "is_return": 1, + "is_out_state": 1, + "items": [ + { + "item_code": "_Test Nil Rated Item", + "rate": 500, + "qty": -60, + }, + { + "item_code": "_Test Service Item", + "rate": 1500, + "qty": -150, + }, + ], + }, + { + "company_gstin": "24AAQCA8719H1ZC", + "customer": "_Test Foreign Customer", + "place_of_supply": "96-Other Countries", + "items": [ + { + "item_code": "_Test Nil Rated Item", + "rate": 100, + "qty": 50, + }, + { + "item_code": "_Test Service Item", + "rate": 1400, + "qty": 100, + }, + ], + }, + { + "company_gstin": "24AAQCA8719H1ZC", + "customer": "_Test Foreign Customer", + "place_of_supply": "96-Other Countries", + "is_export_with_gst": 1, + "items": [ + { + "item_code": "_Test Nil Rated Item", + "rate": 500, + "qty": 90, + }, + { + "item_code": "_Test Service Item", + "rate": 2500, + "qty": 200, + }, + ], + }, + { + "company_gstin": "24AAQCA8719H1ZC", + "customer": "_Test Foreign Customer", + "place_of_supply": "96-Other Countries", + "is_return": 1, + "items": [ + { + "item_code": "_Test Nil Rated Item", + "rate": 100, + "qty": -50, + }, + { + "item_code": "_Test Service Item", + "rate": 2000, + "qty": -150, + }, + ], + }, +] + + +class TestSalesRegisterBeta(FrappeTestCase): + @classmethod + @change_settings("GST Settings", {"enable_overseas_transactions": 1}) + def setUpClass(cls): + super().setUpClass() + TestSalesRegisterBeta().create_sales_invoices() + + def create_sales_invoices(self): + for invoice in INVOICES: + create_sales_invoice(**invoice) + + def test_summary_by_hsn(self): + FILTERS["summary_by"] = "Summary by HSN" + report_data = execute(FILTERS) + + for index, invoice in enumerate(report_data[1]): + self.assertPartialDict(EXPECTED_SUMMARY_BY_HSN[index], invoice) + + def test_overview(self): + FILTERS["summary_by"] = "Overview" + report_data = execute(FILTERS) + + for index, invoice in enumerate(report_data[1]): + self.assertPartialDict(EXPECTED_OVERVIEW[index], invoice) + + def assertPartialDict(self, d1, d2): + self.assertIsInstance(d1, dict, "First argument is not a dictionary") + self.assertIsInstance(d2, dict, "Second argument is not a dictionary") + + if d1 != d2: + for key in d1: + if d1[key] != d2[key]: + standardMsg = f"{key}: {d1[key]} != {d2[key]}" + self.fail(standardMsg) 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/__init__.py b/india_compliance/gst_india/utils/__init__.py index 2c61bd17c0..87b9ba040f 100644 --- a/india_compliance/gst_india/utils/__init__.py +++ b/india_compliance/gst_india/utils/__init__.py @@ -677,6 +677,11 @@ def can_enable_api(settings): return settings.api_secret or frappe.conf.ic_api_secret +def get_full_gst_uom(uom, settings=None): + uom = get_gst_uom(uom, settings=settings) + return f"{uom}-{UOM_MAP.get(uom)}" + + def get_gst_uom(uom, settings=None): """Returns the GST UOM from ERPNext UOM""" settings = settings or frappe.get_cached_doc("GST Settings") diff --git a/india_compliance/gst_india/utils/gstr/gstr_1.py b/india_compliance/gst_india/utils/gstr/gstr_1.py index b4ea6e8eac..e4998fe457 100644 --- a/india_compliance/gst_india/utils/gstr/gstr_1.py +++ b/india_compliance/gst_india/utils/gstr/gstr_1.py @@ -9,8 +9,12 @@ from frappe.query_builder.functions import Date, IfNull, Sum from frappe.utils import getdate +from india_compliance.gst_india.utils import get_full_gst_uom + B2C_LIMIT = 2_50_000 +# TODO: Enum for Invoice Type + class GSTR1_Categories(Enum): """ @@ -18,7 +22,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 +63,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", }, @@ -164,6 +200,12 @@ def get_base_query(self): .where(self.si.docstatus == 1) .where(self.si.is_opening != "Yes") .where(IfNull(self.si.billing_address_gstin, "") != self.si.company_gstin) + .orderby( + self.si.posting_date, + self.si.name, + self.si_item.item_code, + order=Order.desc, + ) ) if self.additional_si_columns: @@ -259,7 +301,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 +333,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 +341,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 +367,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 +394,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,34 +422,45 @@ 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) def process_invoices(self, invoices): + settings = frappe.get_cached_doc("GST Settings") + for invoice in invoices: self.invoice_conditions = {} self.assign_categories(invoice) + invoice["uom"] = get_full_gst_uom(invoice.get("uom"), settings) def assign_categories(self, invoice): @@ -451,7 +505,9 @@ def get_invoices_for_hsn_wise_summary(self): query.gst_treatment, query.uom, ) - .orderby(query.posting_date, query.invoice_no, order=Order.desc) + .orderby( + query.posting_date, query.invoice_no, query.item_code, order=Order.desc + ) ) return query.run(as_dict=True) @@ -481,39 +537,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) - category_key["unique_records"].add(row.invoice_no) + for summary_row in summary.values(): + summary_row["no_of_records"] = len(summary_row["unique_records"]) - for category_key in summary.values(): - category_key["no_of_records"] = len(category_key["unique_records"]) + return summary - return list(summary.values()) + 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"]) + + # 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/gst_india/utils/tests.py b/india_compliance/gst_india/utils/tests.py index de450214b7..b302af186e 100644 --- a/india_compliance/gst_india/utils/tests.py +++ b/india_compliance/gst_india/utils/tests.py @@ -67,7 +67,9 @@ def create_transaction(**data): ) company_abbr = frappe.get_cached_value("Company", data.company, "abbr") or "_TIRC" - append_item(transaction, data, company_abbr) + + if not data.get("items"): + append_item(transaction, data, company_abbr) # Append taxes if data.is_in_state or data.is_in_state_rcm: diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py index 4cb7badbc7..8124ee5b66 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -154,7 +154,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": [ @@ -200,7 +200,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": [ @@ -394,6 +394,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";