From 41acfdd1a8022ab23805f6696364a3b991c07497 Mon Sep 17 00:00:00 2001 From: Sanket322 <113279972+Sanket322@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:36:44 +0530 Subject: [PATCH 01/24] fix: validate for cess non-advol (#2009) * fix: validate for cess non-advol * fix: remove duplicate code * fix: changes as per view * chore: fix type, less nesting --------- Co-authored-by: ljain112 (cherry picked from commit e0b7be6a0e5f72d7bbabe1096e68a0d10710db3e) --- .../doctype/bill_of_entry/bill_of_entry.py | 58 +++++++++++-------- .../gst_india/overrides/transaction.py | 26 +++++++-- 2 files changed, 55 insertions(+), 29 deletions(-) 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 e8e658d2c0..45a8d03a50 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 @@ -250,9 +250,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: @@ -274,33 +276,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/overrides/transaction.py b/india_compliance/gst_india/overrides/transaction.py index 1601a02ecc..01f030dc68 100644 --- a/india_compliance/gst_india/overrides/transaction.py +++ b/india_compliance/gst_india/overrides/transaction.py @@ -129,10 +129,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: @@ -155,15 +159,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) ) @@ -471,12 +485,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"), From 7acdd45835864aa6b24c65e7597606e4e875e64e Mon Sep 17 00:00:00 2001 From: Priyansh Shah <108476017+priyanshshah2442@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:39:12 +0530 Subject: [PATCH 02/24] test: test cases for gst sales register beta report (#1965) * test: test cases for gst sales register beta report * fix: linters and item_code * fix: remove gst_treatement and posting date while creating invoices * fix: remove gst_treatement and posting date while creating invoices * fix: modified test cases * test: super setup to cleanup data post tests * refactor: test cases for sales register and modified required orderby in sales register beta report * fix: test cases and utility function for appending items in sales invoice * test: update similar treatment for original test cases * test: modified test output for overview --------- (cherry picked from commit 8bc74113953c9d1f5548c4973230e4f2188c9c15) --- .../bill_of_entry/test_bill_of_entry.py | 1 + .../overrides/test_ineligible_itc.py | 1 + .../gst_sales_register_beta.js | 2 +- .../test_sales_register_beta.py | 697 ++++++++++++++++++ .../gst_india/utils/gstr/gstr_1.py | 10 +- india_compliance/gst_india/utils/tests.py | 4 +- 6 files changed, 712 insertions(+), 3 deletions(-) create mode 100644 india_compliance/gst_india/report/gst_sales_register_beta/test_sales_register_beta.py 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/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/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 d788b3c0ed..97fac25279 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 @@ -9,7 +9,7 @@ const INVOICE_TYPE = { "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"], "Credit/Debit Notes (Registered)": ["CDNR"], 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/utils/gstr/gstr_1.py b/india_compliance/gst_india/utils/gstr/gstr_1.py index 9ee9c6293a..447202a4e8 100644 --- a/india_compliance/gst_india/utils/gstr/gstr_1.py +++ b/india_compliance/gst_india/utils/gstr/gstr_1.py @@ -198,6 +198,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: @@ -494,7 +500,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) 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: From bbdd4ef5244c67c13ff1cec0582f9ea34e3938e1 Mon Sep 17 00:00:00 2001 From: Sanket322 <113279972+Sanket322@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:17:51 +0530 Subject: [PATCH 03/24] test: test-cases for regional_overrides (#2006) * fix: test-cases for regional_overrides * refactor: test case placement and only required test cases --------- Co-authored-by: Smit Vora (cherry picked from commit 35c48a293abc97f186f431ac31495e8529bfb189) # Conflicts: # india_compliance/gst_india/overrides/test_advance_payment_entry.py --- .../overrides/test_advance_payment_entry.py | 58 ++++++++++ .../gst_india/overrides/test_transaction.py | 101 +++++++++++++++++- 2 files changed, 157 insertions(+), 2 deletions(-) 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 6335e39fbd..1556fac102 100644 --- a/india_compliance/gst_india/overrides/test_advance_payment_entry.py +++ b/india_compliance/gst_india/overrides/test_advance_payment_entry.py @@ -6,9 +6,19 @@ 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, ) +<<<<<<< HEAD +======= +from erpnext.controllers.accounts_controller import ( + get_advance_payment_entries_for_regional, +) +from erpnext.controllers.stock_controller import show_accounting_ledger_preview +>>>>>>> 35c48a29 (test: test-cases for regional_overrides (#2006)) from india_compliance.gst_india.utils.tests import create_transaction @@ -359,6 +369,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_transaction.py b/india_compliance/gst_india/overrides/test_transaction.py index c280188f9f..3e16fcc995 100644 --- a/india_compliance/gst_india/overrides/test_transaction.py +++ b/india_compliance/gst_india/overrides/test_transaction.py @@ -6,16 +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.controllers.accounts_controller import update_child_qty_rate +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, ) @@ -908,6 +919,92 @@ def create_tax_accounts(account_name): ).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", From 550ca1fc8e457a4ce839765e1e14cb21044369b2 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Thu, 11 Apr 2024 15:53:14 +0530 Subject: [PATCH 04/24] chore: resolve conflicts --- .../gst_india/overrides/test_advance_payment_entry.py | 4 ---- 1 file changed, 4 deletions(-) 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 1556fac102..7194e7bda0 100644 --- a/india_compliance/gst_india/overrides/test_advance_payment_entry.py +++ b/india_compliance/gst_india/overrides/test_advance_payment_entry.py @@ -12,13 +12,9 @@ from erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment import ( create_unreconcile_doc_for_selection, ) -<<<<<<< HEAD -======= from erpnext.controllers.accounts_controller import ( get_advance_payment_entries_for_regional, ) -from erpnext.controllers.stock_controller import show_accounting_ledger_preview ->>>>>>> 35c48a29 (test: test-cases for regional_overrides (#2006)) from india_compliance.gst_india.utils.tests import create_transaction From 7751af2ec525a45a9ee62d4d10e5d4db133f81a2 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Thu, 11 Apr 2024 16:31:58 +0530 Subject: [PATCH 05/24] fix(minor): better way to override totals in gst sales beta report (#2022) (cherry picked from commit 19fbb834e675efcb771b2dfd649d36a194192ed7) --- .../gst_sales_register_beta.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) 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 97fac25279..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 @@ -97,6 +97,15 @@ frappe.query_reports["GST Sales Register Beta"] = { 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) { @@ -113,12 +122,10 @@ function set_sub_category_options(report) { } } -frappe_report_column_total = frappe.utils.report_column_total; - -// Override datatable hook for column total calculation -frappe.utils.report_column_total = function (...args) { +custom_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); + 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; From 2caf9e6cfe78ec8628ed528ab663f5b11a568448 Mon Sep 17 00:00:00 2001 From: Priyansh Shah <108476017+priyanshshah2442@users.noreply.github.com> Date: Thu, 11 Apr 2024 16:35:08 +0530 Subject: [PATCH 06/24] fix: uom as per gst (#2019) * fix: uom as per gst * chore: refactor and wrap original function --------- (cherry picked from commit 75352214d512a77f9c298e74f2907b4bfeb46e3e) --- india_compliance/gst_india/utils/__init__.py | 5 +++++ india_compliance/gst_india/utils/gstr/gstr_1.py | 5 +++++ 2 files changed, 10 insertions(+) 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 447202a4e8..e4998fe457 100644 --- a/india_compliance/gst_india/utils/gstr/gstr_1.py +++ b/india_compliance/gst_india/utils/gstr/gstr_1.py @@ -9,6 +9,8 @@ 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 @@ -453,9 +455,12 @@ 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): From 54f959b0354a6f8b62c7ebe5830ba008feaa9800 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Fri, 12 Apr 2024 15:58:15 +0530 Subject: [PATCH 07/24] fix(excel): remove default bg colors from excel export (cherry picked from commit d9f8f4652fba4ee1d3b6d461240f4dc26313b5a4) --- .../purchase_reconciliation_tool.py | 6 ++++++ india_compliance/gst_india/utils/exporter.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) 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 74abe18896..a849417079 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 @@ -772,6 +772,8 @@ def export_data(self): filters=self.filters, headers=self.match_summary_header, data=self.get_match_summary_data(), + default_data_format={"bg_color": self.COLOR_PALLATE.light_gray}, + default_header_format={"bg_color": self.COLOR_PALLATE.dark_gray}, ) if not self.is_supplier_specific: @@ -780,6 +782,8 @@ def export_data(self): filters=self.filters, headers=self.supplier_header, data=self.get_supplier_data(), + default_data_format={"bg_color": self.COLOR_PALLATE.light_gray}, + default_header_format={"bg_color": self.COLOR_PALLATE.dark_gray}, ) excel.create_sheet( @@ -788,6 +792,8 @@ def export_data(self): merged_headers=self.get_merge_headers(), headers=self.invoice_header, data=self.get_invoice_data(), + default_data_format={"bg_color": self.COLOR_PALLATE.light_gray}, + default_header_format={"bg_color": self.COLOR_PALLATE.dark_gray}, ) excel.remove_sheet("Sheet") diff --git a/india_compliance/gst_india/utils/exporter.py b/india_compliance/gst_india/utils/exporter.py index ea7f8402f6..7610c93125 100644 --- a/india_compliance/gst_india/utils/exporter.py +++ b/india_compliance/gst_india/utils/exporter.py @@ -66,7 +66,6 @@ class Worksheet: "height": 20, "vertical": "center", "wrap_text": False, - "bg_color": "f2f2f2", } ) filter_format = data_format.copy().update({"bg_color": None, "bold": True}) @@ -81,7 +80,6 @@ class Worksheet: "height": 30, "vertical": "center", "wrap_text": True, - "bg_color": "d9d9d9", } ) default_styles = frappe._dict( @@ -106,10 +104,18 @@ def create( filters=None, merged_headers=None, add_totals=True, + default_data_format=None, + default_header_format=None, ): """Create worksheet""" self.headers = headers + if default_data_format: + self.data_format.update(default_data_format) + + if default_header_format: + self.header_format.update(default_header_format) + self.ws = workbook.create_sheet(sheet_name) self.add_data(filters, is_filter=True) self.add_merged_header(merged_headers) From a3317784afd0fa09d76210f74a0d1a3a68218384 Mon Sep 17 00:00:00 2001 From: Daizy Modi Date: Sat, 13 Apr 2024 11:21:25 +0530 Subject: [PATCH 08/24] fix: Document Type as `CHL` in case of return Invoice with Nil-rated (#2028) **Issue** Document type was mapped incorrect in case of Sales and Purchase Return. **Before** https://github.com/resilient-tech/india-compliance/assets/54097382/7778eba7-0692-4b1d-867c-bbd9375162b1 **After** https://github.com/resilient-tech/india-compliance/assets/54097382/d2f65194-4447-4235-a85a-5918e7b8c000 (cherry picked from commit 2a34abf8bb2ad5d4d782f4193cef12876a3e97ae) --- india_compliance/gst_india/utils/e_waybill.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/india_compliance/gst_india/utils/e_waybill.py b/india_compliance/gst_india/utils/e_waybill.py index 7edcc48341..d613222814 100644 --- a/india_compliance/gst_india/utils/e_waybill.py +++ b/india_compliance/gst_india/utils/e_waybill.py @@ -1473,9 +1473,13 @@ def update_transaction_details(self): if not doc.is_export_with_gst: self.transaction_details.update(document_type="BIL") - if doc.doctype in ("Sales Invoice", "Purchase Invoice") and all( - item.gst_treatment in ("Nil-Rated", "Exempted", "Non-GST") - for item in doc.items + if ( + doc.doctype in ("Sales Invoice", "Purchase Invoice") + and not doc.is_return + and all( + item.gst_treatment in ("Nil-Rated", "Exempted", "Non-GST") + for item in doc.items + ) ): self.transaction_details.update(document_type="BIL") From 43be58a705bfebac4d72cd653a4732f9c6280ff1 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 16 Apr 2024 12:04:17 +0530 Subject: [PATCH 09/24] test: test cases in conjuction to v14 requirements --- .../gst_india/overrides/test_advance_payment_entry.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 7194e7bda0..96ff33c921 100644 --- a/india_compliance/gst_india/overrides/test_advance_payment_entry.py +++ b/india_compliance/gst_india/overrides/test_advance_payment_entry.py @@ -370,12 +370,13 @@ 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")}) + pe = frappe.qb.DocType("Payment Entry") + conditions = [pe.company == payment_doc.company] payment_entry = get_advance_payment_entries_for_regional( party_type="Customer", party=invoice_doc.customer, - party_account=[invoice_doc.debit_to], + party_account=invoice_doc.debit_to, order_list=[], order_doctype="Sales Order", include_unallocated=True, From 2ba68076c0951befbdba67c5a3bfcb00381f9d6e Mon Sep 17 00:00:00 2001 From: Sanket322 <113279972+Sanket322@users.noreply.github.com> Date: Thu, 18 Apr 2024 15:42:29 +0530 Subject: [PATCH 10/24] fix: cancel sales invoice if e-Invoice is cancelled (#2045) * fix: on cancel of e-invoice cancel it's sales invoice * fix: change message * chore: update button text (cherry picked from commit 5b25d3f1520caa68d7b61ec95116aa5117774054) --- .../gst_india/client_scripts/e_invoice_actions.js | 10 ++++++++-- india_compliance/gst_india/utils/e_invoice.py | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) 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 c761dc82b1..48e727ea74 100644 --- a/india_compliance/gst_india/client_scripts/e_invoice_actions.js +++ b/india_compliance/gst_india/client_scripts/e_invoice_actions.js @@ -154,8 +154,8 @@ function show_cancel_e_invoice_dialog(frm, callback) { : __("Cancel e-Invoice"), fields: get_cancel_e_invoice_dialog_fields(frm), primary_action_label: frm.doc.ewaybill - ? __("Cancel IRN & e-Waybill") - : __("Cancel IRN"), + ? __("Cancel IRN, e-Waybill & Invoice") + : __("Cancel IRN & Invoice"), primary_action(values) { frappe.call({ method: "india_compliance.gst_india.utils.e_invoice.cancel_e_invoice", @@ -174,6 +174,12 @@ function show_cancel_e_invoice_dialog(frm, callback) { india_compliance.primary_to_danger_btn(d); d.show(); + + $(` + + `).prependTo(d.wrapper); } function show_mark_e_invoice_as_cancelled_dialog(frm) { diff --git a/india_compliance/gst_india/utils/e_invoice.py b/india_compliance/gst_india/utils/e_invoice.py index 930a5e068d..c18b52019d 100644 --- a/india_compliance/gst_india/utils/e_invoice.py +++ b/india_compliance/gst_india/utils/e_invoice.py @@ -256,6 +256,7 @@ def cancel_e_invoice(docname, values): doc, values, result, "e-Invoice cancelled successfully" ) + doc.cancel() return send_updated_doc(doc) From b9b9257e372dfdf570506848c103e301a57004de Mon Sep 17 00:00:00 2001 From: Sanket322 <113279972+Sanket322@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:06:44 +0530 Subject: [PATCH 11/24] fix: handle on cancel for reco tool (#2046) * fix: handle_on_cancel_for_reco_tool * fix: update action and filter opening invoices * fix: only one gst inward supply will be there * fix: change in status * fix: set value in one query --------- (cherry picked from commit d6d129e9e63b0500d1254cacf00c6333c20bc2e0) --- .../doctype/bill_of_entry/bill_of_entry.py | 14 ++++++++++++++ .../purchase_reconciliation_tool/__init__.py | 1 + .../gst_india/overrides/purchase_invoice.py | 16 ++++++++++++++++ india_compliance/hooks.py | 1 + 4 files changed, 32 insertions(+) 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 45a8d03a50..1792a3208c 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 @@ -131,6 +131,20 @@ def on_cancel(self): self.ignore_linked_doctypes = ("GL Entry",) make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + if self.reconciliation_status != "Reconciled": + return + + frappe.db.set_value( + "GST Inward Supply", + {"link_doctype": "Bill of Entry", "link_name": self.name}, + { + "match_status": "", + "link_name": "", + "link_doctype": "", + "action": "No Action", + }, + ) + # Code adapted from AccountsController.on_trash def on_trash(self): if not frappe.db.get_single_value( diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py index 8e35860dd0..6d231d2dc1 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py @@ -434,6 +434,7 @@ def get_query(self, additional_fields=None, is_return=False): .on(pi_item.parent == self.PI.name) .where(self.PI.docstatus == 1) .where(IfNull(self.PI.reconciliation_status, "") != "Not Applicable") + .where(self.PI.is_opening == "NO") .groupby(self.PI.name) .select( *fields, diff --git a/india_compliance/gst_india/overrides/purchase_invoice.py b/india_compliance/gst_india/overrides/purchase_invoice.py index 4db9f6facb..0a175536b6 100644 --- a/india_compliance/gst_india/overrides/purchase_invoice.py +++ b/india_compliance/gst_india/overrides/purchase_invoice.py @@ -50,6 +50,22 @@ def validate(doc, method=None): set_reconciliation_status(doc) +def on_cancel(doc, method=None): + if doc.reconciliation_status != "Reconciled": + return + + frappe.db.set_value( + "GST Inward Supply", + {"link_doctype": "Purchase Invoice", "link_name": doc.name}, + { + "match_status": "", + "link_name": "", + "link_doctype": "", + "action": "No Action", + }, + ) + + def set_reconciliation_status(doc): reconciliation_status = "Not Applicable" diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py index 6c25714349..96aa416f3a 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -145,6 +145,7 @@ "before_gl_preview": "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate", "before_sl_preview": "india_compliance.gst_india.overrides.ineligible_itc.update_valuation_rate", "after_mapping": "india_compliance.gst_india.overrides.transaction.after_mapping", + "on_cancel": "india_compliance.gst_india.overrides.purchase_invoice.on_cancel", }, "Purchase Order": { "onload": "india_compliance.gst_india.overrides.transaction.onload", From 5b0cc6937dbb0fe975dea34bd911eaa45699be85 Mon Sep 17 00:00:00 2001 From: Sanket322 <113279972+Sanket322@users.noreply.github.com> Date: Tue, 23 Apr 2024 14:24:05 +0530 Subject: [PATCH 12/24] fix: avoid_duplicate in address (#2041) * fix: avoid_dupliace in address * refactor: unique address values --------- (cherry picked from commit 17cb1d05af2a8949e6f46c627001ad36260b4b18) --- india_compliance/gst_india/utils/gstin_info.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/india_compliance/gst_india/utils/gstin_info.py b/india_compliance/gst_india/utils/gstin_info.py index e7a67aa130..2f86211a63 100644 --- a/india_compliance/gst_india/utils/gstin_info.py +++ b/india_compliance/gst_india/utils/gstin_info.py @@ -121,8 +121,14 @@ def _get_address(address): def _extract_address_lines(address): """merge and divide address into exactly two lines""" - for key in address: - address[key] = address[key].strip(f"{whitespace},") + unique_values = set() + for key, value in address.copy().items(): + value = value.strip(f"{whitespace},") + if value in unique_values: + address.pop(key) + else: + address[key] = value + unique_values.add(value) address_line1 = ", ".join( titlecase(value) From 937ed11fd83afe5c5737f1466ad82bc2894183a2 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 23 Apr 2024 13:24:44 +0530 Subject: [PATCH 13/24] fix: correct condition for GST Invoice in case of supply to SEZ (cherry picked from commit 43c627aabd3337483b9e9ed5a02ae7015b1680b3) --- india_compliance/gst_india/client_scripts/sales_invoice.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/india_compliance/gst_india/client_scripts/sales_invoice.js b/india_compliance/gst_india/client_scripts/sales_invoice.js index d05296f790..434d4e2832 100644 --- a/india_compliance/gst_india/client_scripts/sales_invoice.js +++ b/india_compliance/gst_india/client_scripts/sales_invoice.js @@ -89,11 +89,9 @@ function is_gst_invoice(frm) { item.gst_treatment == "Nil-Rated" || item.gst_treatment == "Exempted" ); - if (frm.doc.place_of_supply === "96-Other Countries") { + if (frm.doc.items[0].gst_treatment === "Zero-Rated") return gst_invoice_conditions && frm.doc.is_export_with_gst; - } else { - return gst_invoice_conditions; - } + else return gst_invoice_conditions; } async function contains_gst_account(frm) { From 9b363f0a94b971d063f6811aab4ceed2e521e228 Mon Sep 17 00:00:00 2001 From: Sanket322 Date: Mon, 22 Apr 2024 15:02:52 +0530 Subject: [PATCH 14/24] fix: remove extra inward supplies and unlink invoice (cherry picked from commit 196921e0d4e812f05606c9fbb123211c2a8498d3) --- .../gst_inward_supply/gst_inward_supply.py | 5 ++++ india_compliance/gst_india/utils/gstr/gstr.py | 16 ++++++++++++ .../gst_india/utils/gstr/gstr_2a.py | 25 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.py b/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.py index 4663c1ef62..d2b822d68c 100644 --- a/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.py +++ b/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.py @@ -22,6 +22,11 @@ def before_save(self): ): update_docs_for_amendment(self) + def on_trash(self): + frappe.db.set_value( + "Purchase Invoice", self.link_name, "reconciliation_status", "Unreconciled" + ) + def create_inward_supply(transaction): filters = { diff --git a/india_compliance/gst_india/utils/gstr/gstr.py b/india_compliance/gst_india/utils/gstr/gstr.py index 6b7d16eb23..7bb907aec3 100644 --- a/india_compliance/gst_india/utils/gstr/gstr.py +++ b/india_compliance/gst_india/utils/gstr/gstr.py @@ -51,6 +51,7 @@ def __init__(self, company, gstin, return_period, data, gen_date_2b): self.setup() def setup(self): + self.existing_transaction = {} pass def create_transactions(self, category, suppliers): @@ -75,6 +76,20 @@ def create_transactions(self, category, suppliers): doctype="Purchase Reconciliation Tool", ) + if transaction.get("unique_key") in self.existing_transaction: + self.existing_transaction.pop(transaction.get("unique_key")) + + self.delete_missing_transactions() + + def delete_missing_transactions(self): + """ + For GSTR2a, transactions are reflected immediately after it's pushed to GSTR-1. + At times, it may later be removed from GSTR-1. + + In such cases, we need to delete such unfilled transactions not present in the latest data. + """ + return + def get_all_transactions(self, category, suppliers): transactions = [] for supplier in suppliers: @@ -101,6 +116,7 @@ def get_transaction(self, category, supplier, invoice): **self.get_supplier_details(supplier), **self.get_invoice_details(invoice), items=self.get_transaction_items(invoice), + unique_key=(supplier.ctin or "") + "-" + (invoice.inum or ""), ) def get_supplier_details(self, supplier): diff --git a/india_compliance/gst_india/utils/gstr/gstr_2a.py b/india_compliance/gst_india/utils/gstr/gstr_2a.py index ca9767d450..be13c8b4c4 100644 --- a/india_compliance/gst_india/utils/gstr/gstr_2a.py +++ b/india_compliance/gst_india/utils/gstr/gstr_2a.py @@ -16,6 +16,31 @@ class GSTR2a(GSTR): def setup(self): self.all_gstins = set() self.cancelled_gstins = {} + self.existing_transaction = self.get_existing_transaction() + + def get_existing_transaction(self): + category = type(self).__name__[6:] + + gst_is = frappe.qb.DocType("GST Inward Supply") + existing_transactions = ( + frappe.qb.from_(gst_is) + .select(gst_is.name, gst_is.supplier_gstin, gst_is.bill_no) + .where(gst_is.sup_return_period == self.return_period) + .where(gst_is.classification == category) + .where(gst_is.gstr_1_filled == 0) + ).run(as_dict=True) + + return { + transaction.get("supplier_gstin") + + "-" + + transaction.get("bill_no"): transaction.get("name") + for transaction in existing_transactions + } + + def delete_missing_transactions(self): + if self.existing_transaction: + for value in self.existing_transaction.values(): + frappe.delete_doc("GST Inward Supply", value) def get_supplier_details(self, supplier): supplier_details = { From b7eef8012ddbf7d67e17afd74895f2840ff0cd28 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 23 Apr 2024 14:21:30 +0530 Subject: [PATCH 15/24] chore: variable names and minor refactor (cherry picked from commit 5044f8804c43e2c79b3a4d7a3e3b81ee92c51fe1) --- india_compliance/gst_india/utils/gstr/gstr.py | 9 +++++++-- india_compliance/gst_india/utils/gstr/gstr_2a.py | 10 +++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/india_compliance/gst_india/utils/gstr/gstr.py b/india_compliance/gst_india/utils/gstr/gstr.py index 7bb907aec3..2754903db2 100644 --- a/india_compliance/gst_india/utils/gstr/gstr.py +++ b/india_compliance/gst_india/utils/gstr/gstr.py @@ -108,7 +108,7 @@ def get_supplier_transactions(self, category, supplier): ] def get_transaction(self, category, supplier, invoice): - return frappe._dict( + transaction = frappe._dict( company=self.company, company_gstin=self.gstin, # TODO: change classification to gstr_category @@ -116,9 +116,14 @@ def get_transaction(self, category, supplier, invoice): **self.get_supplier_details(supplier), **self.get_invoice_details(invoice), items=self.get_transaction_items(invoice), - unique_key=(supplier.ctin or "") + "-" + (invoice.inum or ""), ) + transaction["unique_key"] = ( + f"{transaction.get('supplier_gstin', '')}-{transaction.get('bill_no', '')}" + ) + + return transaction + def get_supplier_details(self, supplier): return {} diff --git a/india_compliance/gst_india/utils/gstr/gstr_2a.py b/india_compliance/gst_india/utils/gstr/gstr_2a.py index be13c8b4c4..770a0c7eee 100644 --- a/india_compliance/gst_india/utils/gstr/gstr_2a.py +++ b/india_compliance/gst_india/utils/gstr/gstr_2a.py @@ -31,16 +31,16 @@ def get_existing_transaction(self): ).run(as_dict=True) return { - transaction.get("supplier_gstin") - + "-" - + transaction.get("bill_no"): transaction.get("name") + f"{transaction.get('supplier_gstin', '')}-{transaction.get('bill_no', '')}": transaction.get( + "name" + ) for transaction in existing_transactions } def delete_missing_transactions(self): if self.existing_transaction: - for value in self.existing_transaction.values(): - frappe.delete_doc("GST Inward Supply", value) + for inward_supply_name in self.existing_transaction.values(): + frappe.delete_doc("GST Inward Supply", inward_supply_name) def get_supplier_details(self, supplier): supplier_details = { From 9bb6b5d731dd530db2bf3dddabc82b363fbfdba2 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 24 Apr 2024 07:36:46 +0530 Subject: [PATCH 16/24] fix: on trash update status dynamically (cherry picked from commit 05ff2c9e016da3a84cfcd0f95be6f321dc118d32) --- .../gst_india/doctype/gst_inward_supply/gst_inward_supply.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.py b/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.py index d2b822d68c..470b801d0a 100644 --- a/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.py +++ b/india_compliance/gst_india/doctype/gst_inward_supply/gst_inward_supply.py @@ -24,7 +24,7 @@ def before_save(self): def on_trash(self): frappe.db.set_value( - "Purchase Invoice", self.link_name, "reconciliation_status", "Unreconciled" + self.link_doctype, self.link_name, "reconciliation_status", "Unreconciled" ) From e74d04a9c7aac2256b7bceb5be50a31241f36491 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 24 Apr 2024 11:47:52 +0530 Subject: [PATCH 17/24] perf: dont create address copy (cherry picked from commit 50d0a0f4aceee353a1a2726590e375c5b96b02a2) --- india_compliance/gst_india/utils/gstin_info.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/india_compliance/gst_india/utils/gstin_info.py b/india_compliance/gst_india/utils/gstin_info.py index 2f86211a63..6bef4d00eb 100644 --- a/india_compliance/gst_india/utils/gstin_info.py +++ b/india_compliance/gst_india/utils/gstin_info.py @@ -122,13 +122,10 @@ def _extract_address_lines(address): """merge and divide address into exactly two lines""" unique_values = set() - for key, value in address.copy().items(): + for key, value in address.items(): value = value.strip(f"{whitespace},") - if value in unique_values: - address.pop(key) - else: - address[key] = value - unique_values.add(value) + address[key] = value if value not in unique_values else None + unique_values.add(value) address_line1 = ", ".join( titlecase(value) From 0c777502bda448a860a653e9bc03af22d834570c Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Fri, 26 Apr 2024 12:25:47 +0530 Subject: [PATCH 18/24] fix(auto-rcm): tds getting overwritten where rcm template was reset (#2056) * fix(auto-rcm): tds getting overwritten where rcm template was always reset * fix: check exact accounts and update taxes in reverse charge only if required * test: fix failing test cases (cherry picked from commit 632f609a999b75edf501b3b7e58e7a5e01668de0) --- .../test_purchase_reconciliation_tool.json | 14 ++- .../gst_india/overrides/transaction.py | 98 ++++++++++++++++--- india_compliance/hooks.py | 4 + 3 files changed, 103 insertions(+), 13 deletions(-) diff --git a/india_compliance/gst_india/data/test_purchase_reconciliation_tool.json b/india_compliance/gst_india/data/test_purchase_reconciliation_tool.json index 79bcf93555..18e328b3db 100644 --- a/india_compliance/gst_india/data/test_purchase_reconciliation_tool.json +++ b/india_compliance/gst_india/data/test_purchase_reconciliation_tool.json @@ -72,11 +72,21 @@ { "PURCHASE_INVOICE": { "bill_no": "BILL-23-00010", - "company_gstin": "29AABCR1718E1ZL" + "company_gstin": "29AABCR1718E1ZL", + "is_in_state": 0, + "is_out_state": 1 }, "INWARD_SUPPLY": { "bill_no": "BILL-23-00010", - "company_gstin": "29AABCR1718E1ZL" + "company_gstin": "29AABCR1718E1ZL", + "place_of_supply": "29-Karnataka", + "items": [ + { + "taxable_value": 10000, + "rate": 18, + "igst": 1800 + } + ] }, "RECONCILED_DATA": { "action": "No Action", diff --git a/india_compliance/gst_india/overrides/transaction.py b/india_compliance/gst_india/overrides/transaction.py index 01f030dc68..08681f3051 100644 --- a/india_compliance/gst_india/overrides/transaction.py +++ b/india_compliance/gst_india/overrides/transaction.py @@ -229,6 +229,44 @@ def validate_mandatory_fields(doc, fields, error_message=None): ) +def get_applicable_gst_accounts( + company, *, for_sales, is_inter_state, is_reverse_charge=False +): + all_gst_accounts = set() + applicable_gst_accounts = set() + + if for_sales: + account_types = ["Output"] + else: + account_types = ["Input"] + + if is_reverse_charge: + account_types.append("Reverse Charge") + + for account_type in account_types: + accounts = get_gst_accounts_by_type(company, account_type, throw=True) + + if not accounts: + continue + + for account_type, account_name in accounts.items(): + if not account_name: + continue + + if is_inter_state and account_type in ["cgst_account", "sgst_account"]: + all_gst_accounts.add(account_name) + continue + + if not is_inter_state and account_type == "igst_account": + all_gst_accounts.add(account_name) + continue + + applicable_gst_accounts.add(account_name) + all_gst_accounts.add(account_name) + + return all_gst_accounts, applicable_gst_accounts + + @frappe.whitelist() def get_valid_gst_accounts(company): frappe.has_permission("Item Tax Template", "read", throw=True) @@ -804,6 +842,7 @@ def get_gst_details(party_details, doctype, company, *, update_place_of_supply=F party_details = frappe.parse_json(party_details) gst_details = frappe._dict() + # Party/Address Defaults party_address_field = ( "customer_address" if is_sales_transaction else "supplier_address" ) @@ -815,6 +854,7 @@ def get_gst_details(party_details, doctype, company, *, update_place_of_supply=F party_details.update(party_gst_details) 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) @@ -844,6 +884,7 @@ def get_gst_details(party_details, doctype, company, *, update_place_of_supply=F if doctype == "Payment Entry": return gst_details + # Taxes Not Applicable if ( (destination_gstin and destination_gstin == source_gstin) # Internal transfer or ( @@ -875,6 +916,7 @@ def get_gst_details(party_details, doctype, company, *, update_place_of_supply=F else "Purchase Taxes and Charges Template" ) + # Tax Category in Transaction tax_template_by_category = get_tax_template_based_on_category( master_doctype, company, party_details ) @@ -889,6 +931,7 @@ def get_gst_details(party_details, doctype, company, *, update_place_of_supply=F if not gst_details.place_of_supply or not party_details.company_gstin: return gst_details + # Fetch template by perceived tax if default_tax := get_tax_template( master_doctype, company, @@ -1309,22 +1352,47 @@ def set_reverse_charge_as_per_gst_settings(doc): def set_reverse_charge(doc): doc.is_reverse_charge = 1 + is_inter_state = is_inter_state_supply(doc) + + # get defaults default_tax = get_tax_template( "Purchase Taxes and Charges Template", doc.company, - is_inter_state_supply(doc), + is_inter_state, doc.company_gstin[:2], doc.is_reverse_charge, ) - if default_tax: - doc.taxes_and_charges = default_tax - template = ( - get_taxes_and_charges("Purchase Taxes and Charges Template", default_tax) - or [] - ) - doc.set("taxes", template) - doc.run_method("calculate_taxes_and_totals") + if not default_tax: + return + + template = ( + get_taxes_and_charges("Purchase Taxes and Charges Template", default_tax) or [] + ) + + # compare accounts + all_gst_accounts, applicable_gst_accounts = get_applicable_gst_accounts( + doc.company, + for_sales=False, + is_inter_state=is_inter_state, + is_reverse_charge=True, + ) + existing_accounts = set( + row.account_head for row in doc.taxes if row.account_head in all_gst_accounts + ) + has_invalid_accounts = existing_accounts - applicable_gst_accounts + + if has_invalid_accounts: + return + + has_same_accounts = not (applicable_gst_accounts - existing_accounts) + + # update taxes + if doc.taxes_and_charges == default_tax and has_same_accounts: + return + + doc.taxes_and_charges = default_tax + doc.set("taxes", template) def validate_gstin_status(gstin, transaction_date): @@ -1381,6 +1449,16 @@ def validate_company_address_field(doc): return False +def before_validate_transaction(doc, method=None): + if ignore_gst_validations(doc): + return False + + if not doc.place_of_supply: + doc.place_of_supply = get_place_of_supply(doc, doc.doctype) + + set_reverse_charge_as_per_gst_settings(doc) + + def validate_transaction(doc, method=None): if ignore_gst_validations(doc): return False @@ -1432,8 +1510,6 @@ def validate_transaction(doc, method=None): validate_gst_category(doc.gst_category, gstin) - set_reverse_charge_as_per_gst_settings(doc) - valid_accounts = validate_gst_accounts(doc, is_sales_transaction) or () update_taxable_values(doc, valid_accounts) validate_item_wise_tax_detail(doc, valid_accounts) diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py index 96aa416f3a..cde17bc6ae 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -136,6 +136,7 @@ "india_compliance.gst_india.overrides.transaction.onload", ], "before_print": "india_compliance.gst_india.overrides.transaction.before_print", + "before_validate": "india_compliance.gst_india.overrides.transaction.before_validate_transaction", "validate": "india_compliance.gst_india.overrides.purchase_invoice.validate", "before_save": "india_compliance.gst_india.overrides.transaction.update_gst_details", "before_submit": [ @@ -150,6 +151,7 @@ "Purchase Order": { "onload": "india_compliance.gst_india.overrides.transaction.onload", "before_print": "india_compliance.gst_india.overrides.transaction.before_print", + "before_validate": "india_compliance.gst_india.overrides.transaction.before_validate_transaction", "validate": ( "india_compliance.gst_india.overrides.transaction.validate_transaction" ), @@ -166,6 +168,7 @@ "india_compliance.gst_india.overrides.purchase_receipt.onload", ], "before_print": "india_compliance.gst_india.overrides.transaction.before_print", + "before_validate": "india_compliance.gst_india.overrides.transaction.before_validate_transaction", "validate": "india_compliance.gst_india.overrides.purchase_receipt.validate", "before_save": "india_compliance.gst_india.overrides.transaction.update_gst_details", "before_submit": [ @@ -248,6 +251,7 @@ "Supplier Quotation": { "onload": "india_compliance.gst_india.overrides.transaction.onload", "before_print": "india_compliance.gst_india.overrides.transaction.before_print", + "before_validate": "india_compliance.gst_india.overrides.transaction.before_validate_transaction", "validate": ( "india_compliance.gst_india.overrides.transaction.validate_transaction" ), From e30ba31727744fdce154e491c41d3359328c594b Mon Sep 17 00:00:00 2001 From: Priyansh Shah <108476017+priyanshshah2442@users.noreply.github.com> Date: Fri, 26 Apr 2024 13:26:15 +0530 Subject: [PATCH 19/24] fix: avoiding addition of TDS rates for past financial year (#2077) closes #2068 (cherry picked from commit aea74b0398d7399d79df94924fa520da532d9b3e) --- .../income_tax_india/data/tds_details.json | 21 +++++++++++++++++++ .../income_tax_india/overrides/company.py | 5 ++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/india_compliance/income_tax_india/data/tds_details.json b/india_compliance/income_tax_india/data/tds_details.json index f23736c8c8..98dd32d8e7 100644 --- a/india_compliance/income_tax_india/data/tds_details.json +++ b/india_compliance/income_tax_india/data/tds_details.json @@ -1045,6 +1045,13 @@ "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0 + }, + { + "from_date": "2024-04-01", + "to_date": "2025-03-31", + "tax_withholding_rate": 5, + "single_threshold": 15000, + "cumulative_threshold": 0 } ] }, @@ -1063,6 +1070,13 @@ "tax_withholding_rate": 5, "single_threshold": 15000, "cumulative_threshold": 0 + }, + { + "from_date": "2024-04-01", + "to_date": "2025-03-31", + "tax_withholding_rate": 5, + "single_threshold": 15000, + "cumulative_threshold": 0 } ] }, @@ -1081,6 +1095,13 @@ "tax_withholding_rate": 20, "single_threshold": 15000, "cumulative_threshold": 0 + }, + { + "from_date": "2024-04-01", + "to_date": "2025-03-31", + "tax_withholding_rate": 20, + "single_threshold": 15000, + "cumulative_threshold": 0 } ] }, diff --git a/india_compliance/income_tax_india/overrides/company.py b/india_compliance/income_tax_india/overrides/company.py index 7d75e1810a..3032acc185 100644 --- a/india_compliance/income_tax_india/overrides/company.py +++ b/india_compliance/income_tax_india/overrides/company.py @@ -98,6 +98,9 @@ def get_tds_category_details(accounts): ) ) for rule in tds_rules: + rates = get_prospective_tds_rates(rule["rates"]) + if not rates: + continue tds_details.append( { "name": rule.get("name"), @@ -111,7 +114,7 @@ def get_tds_category_details(accounts): "consider_party_ledger_amount" ), "tax_on_excess_amount": rule.get("tax_on_excess_amount"), - "rates": get_prospective_tds_rates(rule["rates"]), + "rates": rates, } ) From bb59e0910c71ec3de1ccc27e018b699ecc796433 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Fri, 26 Apr 2024 19:21:35 +0530 Subject: [PATCH 20/24] fix: do not throw on "ignore_gst_validations" --- india_compliance/gst_india/overrides/transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/india_compliance/gst_india/overrides/transaction.py b/india_compliance/gst_india/overrides/transaction.py index 08681f3051..2c5feef6d7 100644 --- a/india_compliance/gst_india/overrides/transaction.py +++ b/india_compliance/gst_india/overrides/transaction.py @@ -1450,7 +1450,7 @@ def validate_company_address_field(doc): def before_validate_transaction(doc, method=None): - if ignore_gst_validations(doc): + if ignore_gst_validations(doc, throw=False): return False if not doc.place_of_supply: From c6c33abf24b0ad997671ae05dd9fbb12b909ada1 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Fri, 26 Apr 2024 19:49:30 +0530 Subject: [PATCH 21/24] fix: support tax collector as customer (#2066) (cherry picked from commit 76cf00d7493c6601d376e45948b9695290164dd0) # Conflicts: # india_compliance/gst_india/overrides/test_party.py --- .../gst_india/client_scripts/party.js | 9 --------- .../gst_india/constants/__init__.py | 5 ++++- .../gst_india/overrides/test_party.py | 18 +++++++++--------- india_compliance/gst_india/utils/__init__.py | 10 +++------- india_compliance/gst_india/utils/gstin_info.py | 3 ++- .../gst_india/utils/test_gstin_info.py | 2 +- india_compliance/patches.txt | 2 +- india_compliance/public/js/regex_constants.js | 4 +++- india_compliance/public/js/utils.js | 2 ++ 9 files changed, 25 insertions(+), 30 deletions(-) diff --git a/india_compliance/gst_india/client_scripts/party.js b/india_compliance/gst_india/client_scripts/party.js index 84d93f70a2..99d33036a5 100644 --- a/india_compliance/gst_india/client_scripts/party.js +++ b/india_compliance/gst_india/client_scripts/party.js @@ -1,4 +1,3 @@ -const TCS_REGEX = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[C]{1}[0-9A-Z]{1}$/; const PAN_REGEX = /^[A-Z]{5}[0-9]{4}[A-Z]{1}$/; function update_gstin_in_other_documents(doctype) { @@ -58,14 +57,6 @@ function validate_gstin(doctype) { frappe.throw(__("GSTIN/UIN should be 15 characters long")); } - if (TCS_REGEX.test(gstin)) { - frappe.throw( - __( - "e-Commerce Operator (TCS) GSTIN is not allowed to be set in Party/Address" - ) - ); - } - gstin = india_compliance.validate_gstin(gstin); frm.doc.gstin = gstin; diff --git a/india_compliance/gst_india/constants/__init__.py b/india_compliance/gst_india/constants/__init__.py index 52dab78965..eff4a5ae07 100644 --- a/india_compliance/gst_india/constants/__init__.py +++ b/india_compliance/gst_india/constants/__init__.py @@ -18,6 +18,7 @@ GST_PARTY_TYPES = ("Customer", "Supplier", "Company") +# Map for e-Invoice Supply Type GST_CATEGORIES = { "Registered Regular": "B2B", "Registered Composition": "B2B", @@ -27,6 +28,7 @@ "Deemed Export": "DEXP", "UIN Holders": "B2B", "Tax Deductor": "B2B", + "Tax Collector": "B2B", } EXPORT_TYPES = ( @@ -1392,6 +1394,7 @@ UNBODY = re.compile(r"^[0-9]{4}[A-Z]{3}[0-9]{5}[UO]{1}[N][A-Z0-9]{1}$") TDS = re.compile(r"^[0-9]{2}[A-Z]{4}[A-Z0-9]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[D][0-9A-Z]$") +TCS = re.compile(r"^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[C]{1}[0-9A-Z]{1}$") GSTIN_FORMATS = { "Registered Regular": REGISTERED, @@ -1401,9 +1404,9 @@ "Deemed Export": REGISTERED, "UIN Holders": UNBODY, "Tax Deductor": TDS, + "Tax Collector": TCS, } -TCS = re.compile(r"^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[C]{1}[0-9A-Z]{1}$") PAN_NUMBER = re.compile(r"^[A-Z]{5}[0-9]{4}[A-Z]{1}$") PINCODE_FORMAT = re.compile(r"^[1-9][0-9]{5}$") diff --git a/india_compliance/gst_india/overrides/test_party.py b/india_compliance/gst_india/overrides/test_party.py index 330c50cd8f..868e534934 100644 --- a/india_compliance/gst_india/overrides/test_party.py +++ b/india_compliance/gst_india/overrides/test_party.py @@ -1,5 +1,3 @@ -import re - import frappe from frappe.tests.utils import FrappeTestCase @@ -26,18 +24,20 @@ def test_validate_deemed_export_party(self): self.assertEqual(party.gst_category, "Deemed Export") def test_validate_new_party_with_tcs(self): +<<<<<<< HEAD party = frappe.new_doc("Customer") party.update( { "customer_name": "Flipkart India Private Limited", "gstin": "29AABCF8078M1C8", } +======= + # Allow TCS GSTIN + party = frappe.new_doc( + "Customer", + customer_name="Flipkart India Private Limited", + gstin="29AABCF8078M1C8", +>>>>>>> 76cf00d7 (fix: support tax collector as customer (#2066)) ) - self.assertRaisesRegex( - frappe.exceptions.ValidationError, - re.compile( - r"^(e-Commerce Operator \(TCS\) GSTIN is not allowed for transaction / party / address.*)$" - ), - party.insert, - ) + party.insert() diff --git a/india_compliance/gst_india/utils/__init__.py b/india_compliance/gst_india/utils/__init__.py index 87b9ba040f..4718f8be6b 100644 --- a/india_compliance/gst_india/utils/__init__.py +++ b/india_compliance/gst_india/utils/__init__.py @@ -212,13 +212,6 @@ def validate_gst_category(gst_category, gstin): ) ) - if TCS.match(gstin): - frappe.throw( - _( - "e-Commerce Operator (TCS) GSTIN is not allowed for transaction / party / address" - ), - ) - valid_gstin_format = GSTIN_FORMATS.get(gst_category) if not valid_gstin_format.match(gstin): frappe.throw( @@ -302,6 +295,9 @@ def guess_gst_category( if GSTIN_FORMATS["Tax Deductor"].match(gstin): return "Tax Deductor" + if GSTIN_FORMATS["Tax Collector"].match(gstin): + return "Tax Collector" + if GSTIN_FORMATS["Registered Regular"].match(gstin): if gst_category in ( "Registered Regular", diff --git a/india_compliance/gst_india/utils/gstin_info.py b/india_compliance/gst_india/utils/gstin_info.py index 6bef4d00eb..add8fe4516 100644 --- a/india_compliance/gst_india/utils/gstin_info.py +++ b/india_compliance/gst_india/utils/gstin_info.py @@ -14,6 +14,7 @@ "Input Service Distributor (ISD)": "Registered Regular", "Composition": "Registered Composition", "Tax Deductor": "Tax Deductor", + "Tax Collector (Electronic Commerce Operator)": "Tax Collector", "SEZ Unit": "SEZ", "SEZ Developer": "SEZ", "United Nation Body": "UIN Holders", @@ -156,9 +157,9 @@ def _extract_address_lines(address): # "SEZ Developer" 27AAJCS5738D1Z6 # "United Nation Body" 0717UNO00157UNO 0717UNO00211UN2 2117UNO00002UNF # "Consulate or Embassy of Foreign Country" 0717UNO00154UNU +# "Tax Collector (e-Commerce Operator)" 29AABCF8078M1C8 27AAECG3736E1C2 29AAFCB7707D1C1 # ###### CANNOT BE A PART OF GSTR1 ###### -# "Tax Collector (e-Commerce Operator)" 29AABCF8078M1C8 27AAECG3736E1C2 # "Non Resident Online Services Provider" 9917SGP29001OST Google # "Non Resident Taxable Person" diff --git a/india_compliance/gst_india/utils/test_gstin_info.py b/india_compliance/gst_india/utils/test_gstin_info.py index 4657bdf58a..3471f2173a 100644 --- a/india_compliance/gst_india/utils/test_gstin_info.py +++ b/india_compliance/gst_india/utils/test_gstin_info.py @@ -167,7 +167,7 @@ def test_tcs_gstin_info(self): { "gstin": "29AABCF8078M1C8", "business_name": "Flipkart India Private Limited", - "gst_category": "", + "gst_category": "Tax Collector", "status": "Active", "all_addresses": [ { diff --git a/india_compliance/patches.txt b/india_compliance/patches.txt index 97ba091a64..b4456aac94 100644 --- a/india_compliance/patches.txt +++ b/india_compliance/patches.txt @@ -3,7 +3,7 @@ execute:import frappe; frappe.delete_doc_if_exists("DocType", "GSTIN") [post_model_sync] india_compliance.patches.v14.set_default_for_overridden_accounts_setting -execute:from india_compliance.gst_india.setup import create_custom_fields; create_custom_fields() #46 +execute:from india_compliance.gst_india.setup import create_custom_fields; create_custom_fields() #47 execute:from india_compliance.gst_india.setup import create_property_setters; create_property_setters() #7 execute:from india_compliance.income_tax_india.setup import create_custom_fields; create_custom_fields() #1 india_compliance.patches.post_install.remove_old_fields #1 diff --git a/india_compliance/public/js/regex_constants.js b/india_compliance/public/js/regex_constants.js index 38a258e151..a442b826aa 100644 --- a/india_compliance/public/js/regex_constants.js +++ b/india_compliance/public/js/regex_constants.js @@ -6,14 +6,16 @@ const NRI_ID = "^[0-9]{4}[A-Z]{3}[0-9]{5}[N][R][0-9A-Z]{1}$"; const OIDAR = "^[9][9][0-9]{2}[A-Z]{3}[0-9]{5}[O][S][0-9A-Z]{1}$"; const UNBODY = "^[0-9]{4}[A-Z]{3}[0-9]{5}[UO]{1}[N][A-Z0-9]{1}$"; const TDS = "^[0-9]{2}[A-Z]{4}[A-Z0-9]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[D][0-9A-Z]$"; +const TCS = "^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[C]{1}[0-9A-Z]{1}$"; export const REGISTERED_REGEX = new RegExp([NORMAL, GOVT_DEPTID].join("|")); export const OVERSEAS_REGEX = new RegExp([NRI_ID, OIDAR].join("|")); export const UNBODY_REGEX = new RegExp(UNBODY); export const TDS_REGEX = new RegExp(TDS); +export const TCS_REGEX = new RegExp(TCS); export const GSTIN_REGEX = new RegExp( - [NORMAL, GOVT_DEPTID, NRI_ID, OIDAR, UNBODY, TDS].join("|") + [NORMAL, GOVT_DEPTID, NRI_ID, OIDAR, UNBODY, TDS, TCS].join("|") ); export const GST_INVOICE_NUMBER_FORMAT = new RegExp("^[^\\W_][A-Za-z\\d\\-/]{0,15}$"); diff --git a/india_compliance/public/js/utils.js b/india_compliance/public/js/utils.js index 5c6391a9c9..cf8974454c 100644 --- a/india_compliance/public/js/utils.js +++ b/india_compliance/public/js/utils.js @@ -4,6 +4,7 @@ import { OVERSEAS_REGEX, UNBODY_REGEX, TDS_REGEX, + TCS_REGEX, GST_INVOICE_NUMBER_FORMAT, } from "./regex_constants"; @@ -209,6 +210,7 @@ Object.assign(india_compliance, { } if (TDS_REGEX.test(gstin)) return "Tax Deductor"; + if (TCS_REGEX.test(gstin)) return "Tax Collector"; if (REGISTERED_REGEX.test(gstin)) return "Registered Regular"; if (UNBODY_REGEX.test(gstin)) return "UIN Holders"; if (OVERSEAS_REGEX.test(gstin)) return "Overseas"; From f4df0be10fd68b7fcfe20c58d448d5de2ee2a2e2 Mon Sep 17 00:00:00 2001 From: Lakshit Jain <108322669+ljain112@users.noreply.github.com> Date: Fri, 26 Apr 2024 19:50:21 +0530 Subject: [PATCH 22/24] fix: correct outward supply details using gst treatment (#2067) * fix: correct outward supply details using gst treatment * test: gstr-3b report refactor * test: change settings for test case * fix: changes as per review * fix: modify test cases * refactor: remove duplication and rename variable * fix: query --------- Co-authored-by: Smit Vora (cherry picked from commit f6e97b404a2f8dbbab3d2536eccfd0c45a907953) --- .../doctype/gstr_3b_report/gstr_3b_report.py | 426 +++++++++--------- .../gstr_3b_report/test_gstr_3b_report.py | 164 ++++++- 2 files changed, 359 insertions(+), 231 deletions(-) diff --git a/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py b/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py index 641ab857dd..1957199f44 100644 --- a/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py +++ b/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py @@ -13,13 +13,13 @@ from frappe.utils import cstr, flt, get_date_str, get_first_day, get_last_day from india_compliance.gst_india.constants import INVOICE_DOCTYPES +from india_compliance.gst_india.overrides.transaction import is_inter_state_supply from india_compliance.gst_india.report.gstr_3b_details.gstr_3b_details import ( IneligibleITC, ) -from india_compliance.gst_india.utils import ( - get_gst_accounts_by_type, - is_overseas_transaction, -) +from india_compliance.gst_india.utils import get_gst_accounts_by_type + +VALUES_TO_UPDATE = ["iamt", "camt", "samt", "csamt"] class GSTR3BReport(Document): @@ -60,6 +60,7 @@ def get_data(self): self.set_inward_nil_exempt(inward_nil_exempt) self.missing_field_invoices = self.get_missing_field_invoices() + self.report_dict = format_values(self.report_dict) self.json_output = frappe.as_json(self.report_dict) self.generation_status = "Generated" @@ -110,9 +111,9 @@ def set_itc_details(self, itc_details): for d in self.report_dict["itc_elg"]["itc_avl"]: itc_type = itc_eligible_type_map.get(d["ty"]) - for key in ["iamt", "camt", "samt", "csamt"]: + for key in VALUES_TO_UPDATE: d[key] = flt(itc_details.get(itc_type, {}).get(key)) - net_itc[key] += flt(d[key], 2) + net_itc[key] += d[key] def get_itc_reversal_entries(self): self.update_itc_reversal_from_journal_entry() @@ -137,7 +138,7 @@ def process_ineligible_credit(self, ineligible_credit): if not ineligible_credit: return - tax_amounts = ["camt", "samt", "iamt", "csamt"] + tax_amounts = VALUES_TO_UPDATE for row in ineligible_credit: if row.itc_classification == "Ineligible As Per Section 17(5)": @@ -145,15 +146,15 @@ def process_ineligible_credit(self, ineligible_credit): if key not in row: continue - self.report_dict["itc_elg"]["itc_rev"][0][key] += flt(row[key]) - self.report_dict["itc_elg"]["itc_net"][key] -= flt(row[key]) + self.report_dict["itc_elg"]["itc_rev"][0][key] += row[key] + self.report_dict["itc_elg"]["itc_net"][key] -= row[key] elif row.itc_classification == "ITC restricted due to PoS rules": for key in tax_amounts: if key not in row: continue - self.report_dict["itc_elg"]["itc_inelg"][1][key] += flt(row[key]) + self.report_dict["itc_elg"]["itc_inelg"][1][key] += row[key] def update_itc_reversal_from_journal_entry(self): reversal_entries = frappe.db.sql( @@ -179,12 +180,11 @@ def update_itc_reversal_from_journal_entry(self): else: index = 1 - for key in ["camt", "samt", "iamt", "csamt"]: + for key in VALUES_TO_UPDATE: if entry.account in self.account_heads.get(key): - self.report_dict["itc_elg"]["itc_rev"][index][key] += flt( - entry.amount - ) - net_itc[key] -= flt(entry.amount) + self.report_dict["itc_elg"]["itc_rev"][index][key] += entry.amount + + net_itc[key] -= entry.amount def get_itc_details(self): itc_amounts = frappe.db.sql( @@ -296,8 +296,97 @@ def get_inward_nil_exempt(self, state): def get_outward_supply_details(self, doctype, reverse_charge=None): self.get_outward_tax_invoices(doctype, reverse_charge=reverse_charge) - self.get_outward_items(doctype) - self.get_outward_tax_details(doctype) + self.get_invoice_item_wise_tax_details(doctype) + + def get_invoice_item_wise_tax_details(self, doctype): + item_details = self.get_outward_items(doctype) + tax_details = self.get_outward_tax_details(doctype) + docs = self.combine_item_and_taxes(doctype, item_details, tax_details) + + self.set_item_wise_tax_details(docs) + + def set_item_wise_tax_details(self, docs): + self.invoice_item_wise_tax_details = {} + item_wise_details = {} + account_head_gst_map = {} + + for key, values in self.account_heads.items(): + for value in values: + if value is not None: + account_head_gst_map[value] = key + + item_defaults = frappe._dict( + { + "camt": 0, + "samt": 0, + "iamt": 0, + "csamt": 0, + } + ) + + # Process tax and item details + for doc, details in docs.items(): + item_wise_details[doc] = {} + invoice_items = {} + item_code_gst_treatment_map = {} + + # Initialize invoice items with default values + for item in details["items"]: + item_code_gst_treatment_map[item.item_code or item.item_name] = ( + item.gst_treatment + ) + invoice_items.setdefault( + item.gst_treatment, + { + "taxable_value": 0, + **item_defaults, + }, + ) + + invoice_items[item.gst_treatment]["taxable_value"] += item.get( + "taxable_value", 0 + ) + + # Process tax details + for tax in details["taxes"]: + gst_tax_type = account_head_gst_map.get(tax.account_head) + + if not gst_tax_type: + continue + + if tax.item_wise_tax_detail: + try: + item_wise_detail = json.loads(tax.item_wise_tax_detail) + for item_code, tax_amounts in item_wise_detail.items(): + gst_treatment = item_code_gst_treatment_map.get(item_code) + invoice_items[gst_treatment][gst_tax_type] += tax_amounts[1] + + except ValueError: + continue + + item_wise_details[doc].update(invoice_items) + + self.invoice_item_wise_tax_details = item_wise_details + + def combine_item_and_taxes(self, doctype, item_details, tax_details): + response = frappe._dict() + + # Group tax details by parent document + for tax in tax_details: + if tax.parent not in response: + response[tax.parent] = frappe._dict(taxes=[], items=[], doctype=doctype) + + response[tax.parent]["taxes"].append(tax) + + # Group item details by parent document + for item in item_details: + if item.parent not in response: + response[item.parent] = frappe._dict( + taxes=[], items=[], doctype=doctype + ) + + response[item.parent]["items"].append(item) + return response def get_outward_tax_invoices(self, doctype, reverse_charge=None): self.invoice_map = {} @@ -326,18 +415,13 @@ def get_outward_tax_invoices(self, doctype, reverse_charge=None): self.invoice_map = {d.name: d for d in invoice_details} def get_outward_items(self, doctype): - self.invoice_items = frappe._dict() - self.is_nil_or_exempt = [] - self.is_non_gst = [] - if not self.invoice_map: - return + return {} item_details = frappe.db.sql( f""" SELECT - item_code, parent, taxable_value, item_tax_rate, - gst_treatment + item_code, item_name, parent, taxable_value, gst_treatment FROM `tab{doctype} Item` WHERE parent in ({", ".join(["%s"] * len(self.invoice_map))}) @@ -346,39 +430,17 @@ def get_outward_items(self, doctype): as_dict=1, ) - for d in item_details: - self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0) - self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) - - is_nil_rated = d.gst_treatment == "Nil-Rated" - is_exempted = d.gst_treatment == "Exempted" - is_non_gst = d.gst_treatment == "Non-GST" - - if ( - is_nil_rated or is_exempted - ) and d.item_code not in self.is_nil_or_exempt: - self.is_nil_or_exempt.append(d.item_code) - - if is_non_gst and d.item_code not in self.is_non_gst: - self.is_non_gst.append(d.item_code) + return item_details def get_outward_tax_details(self, doctype): - if doctype == "Sales Invoice": - tax_template = "Sales Taxes and Charges" - elif doctype == "Purchase Invoice": - tax_template = "Purchase Taxes and Charges" - - self.items_based_on_tax_rate = {} - self.invoice_cess = frappe._dict() - self.cgst_sgst_invoices = [] - if not self.invoice_map: - return + return {} + tax_template = f"{doctype.split()[0]} Taxes and Charges" tax_details = frappe.db.sql( f""" SELECT - parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount + parent, account_head, item_wise_tax_detail FROM `tab{tax_template}` WHERE parenttype = %s and docstatus = 1 @@ -386,196 +448,103 @@ def get_outward_tax_details(self, doctype): ORDER BY account_head """, (doctype, *self.invoice_map.keys()), + as_dict=1, ) - for parent, account, item_wise_tax_detail, tax_amount in tax_details: - if account in self.account_heads.get("csamt"): - self.invoice_cess.setdefault(parent, tax_amount) - else: - if item_wise_tax_detail: - try: - item_wise_tax_detail = json.loads(item_wise_tax_detail) - cgst_or_sgst = False - if account in self.account_heads.get( - "camt" - ) or account in self.account_heads.get("samt"): - cgst_or_sgst = True - - for item_code, tax_amounts in item_wise_tax_detail.items(): - if not ( - cgst_or_sgst - or account in self.account_heads.get("iamt") - or ( - item_code in self.is_non_gst + self.is_nil_or_exempt - ) - ): - continue - - tax_rate = tax_amounts[0] - if tax_rate: - if cgst_or_sgst: - tax_rate *= 2 - if parent not in self.cgst_sgst_invoices: - self.cgst_sgst_invoices.append(parent) - - rate_based_dict = ( - self.items_based_on_tax_rate.setdefault( - parent, {} - ).setdefault(tax_rate, []) - ) - if item_code not in rate_based_dict: - rate_based_dict.append(item_code) - except ValueError: - continue - - # Build itemised tax for export invoices, nil and exempted where tax table is blank - for invoice, items in self.invoice_items.items(): - invoice_details = self.invoice_map.get(invoice, {}) - if ( - invoice not in self.items_based_on_tax_rate - and not invoice_details.get("is_export_with_gst") - and is_overseas_transaction( - "Sales Invoice", - invoice_details.get("gst_category"), - invoice_details.get("place_of_supply"), - ) - ): - self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault( - 0, items.keys() - ) - else: - for item in items.keys(): - if ( - item in self.is_nil_or_exempt + self.is_non_gst - and item - not in self.items_based_on_tax_rate.get(invoice, {}).get(0, []) - ): - self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault( - 0, [] - ) - self.items_based_on_tax_rate[invoice][0].append(item) + return tax_details def set_outward_taxable_supplies(self): inter_state_supply_details = {} + gst_treatment_map = { + "Nil-Rated": "osup_nil_exmp", + "Exempted": "osup_nil_exmp", + "Zero-Rated": "osup_zero", + "Non-GST": "osup_nongst", + "Taxable": "osup_det", + } - for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): - invoice_details = self.invoice_map.get(inv, {}) + for inv, invoice_details in self.invoice_map.items(): + gst_treatment_details = self.invoice_item_wise_tax_details.get(inv, {}) gst_category = invoice_details.get("gst_category") place_of_supply = ( invoice_details.get("place_of_supply") or "00-Other Territory" ) - is_overseas_invoice = is_overseas_transaction( - "Sales Invoice", gst_category, place_of_supply + + doc = frappe._dict( + { + "gst_category": gst_category, + "place_of_supply": place_of_supply, + "company_gstin": self.gst_details.get("gstin"), + } ) - for rate, items in items_based_on_rate.items(): - for item_code, taxable_value in self.invoice_items.get(inv).items(): - if item_code in items: - if item_code in self.is_nil_or_exempt: - self.report_dict["sup_details"]["osup_nil_exmp"][ - "txval" - ] += taxable_value - elif item_code in self.is_non_gst: - self.report_dict["sup_details"]["osup_nongst"][ - "txval" - ] += taxable_value - elif rate == 0 or (is_overseas_invoice): - self.report_dict["sup_details"]["osup_zero"][ - "txval" - ] += taxable_value - - self.report_dict["sup_details"]["osup_zero"]["iamt"] += flt( - taxable_value * rate / 100, 2 - ) - else: - if inv in self.cgst_sgst_invoices: - tax_rate = rate / 2 - self.report_dict["sup_details"]["osup_det"][ - "camt" - ] += flt(taxable_value * tax_rate / 100, 2) - self.report_dict["sup_details"]["osup_det"][ - "samt" - ] += flt(taxable_value * tax_rate / 100, 2) - self.report_dict["sup_details"]["osup_det"][ - "txval" - ] += flt(taxable_value, 2) - else: - self.report_dict["sup_details"]["osup_det"][ - "iamt" - ] += flt(taxable_value * rate / 100, 2) - self.report_dict["sup_details"]["osup_det"][ - "txval" - ] += flt(taxable_value, 2) - - if ( - gst_category - in [ - "Unregistered", - "Registered Composition", - "UIN Holders", - ] - and self.gst_details.get("gst_state") - != place_of_supply.split("-")[1] - ): - inter_state_supply_details.setdefault( - (gst_category, place_of_supply), - { - "txval": 0.0, - "pos": place_of_supply.split("-")[0], - "iamt": 0.0, - }, - ) - inter_state_supply_details[ - (gst_category, place_of_supply) - ]["txval"] += flt(taxable_value, 2) - inter_state_supply_details[ - (gst_category, place_of_supply) - ]["iamt"] += flt(taxable_value * rate / 100, 2) - - if self.invoice_cess.get(inv): - - invoice_category = "osup_zero" if is_overseas_invoice else "osup_det" - - self.report_dict["sup_details"][invoice_category]["csamt"] += flt( - self.invoice_cess.get(inv), 2 - ) + is_inter_state = is_inter_state_supply(doc) + + for gst_treatment, details in gst_treatment_details.items(): + gst_treatment_section = gst_treatment_map.get(gst_treatment) + section = self.report_dict["sup_details"][gst_treatment_section] + + taxable_value = details.get("taxable_value") + + # updating taxable value and tax value + section["txval"] += taxable_value + for key in section: + if key in VALUES_TO_UPDATE: + section[key] += details.get(key, 0) + + # section 3.2 details + if not gst_treatment == "Taxable": + continue + + if ( + gst_category + in [ + "Unregistered", + "Registered Composition", + "UIN Holders", + ] + and is_inter_state + ): + inter_state_supply_details.setdefault( + (gst_category, place_of_supply), + { + "txval": 0.0, + "pos": place_of_supply.split("-")[0], + "iamt": 0.0, + }, + ) + + inter_state_supply_details[(gst_category, place_of_supply)][ + "txval" + ] += taxable_value + inter_state_supply_details[(gst_category, place_of_supply)][ + "iamt" + ] += details.get("iamt") self.set_inter_state_supply(inter_state_supply_details) def set_supplies_liable_to_reverse_charge(self): - for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): - for rate, items in items_based_on_rate.items(): - for item_code, taxable_value in self.invoice_items.get(inv).items(): - if item_code in items: - if inv in self.cgst_sgst_invoices: - tax_rate = rate / 2 - self.report_dict["sup_details"]["isup_rev"]["camt"] += flt( - taxable_value * tax_rate / 100, 2 - ) - self.report_dict["sup_details"]["isup_rev"]["samt"] += flt( - taxable_value * tax_rate / 100, 2 - ) - self.report_dict["sup_details"]["isup_rev"]["txval"] += flt( - taxable_value, 2 - ) - else: - self.report_dict["sup_details"]["isup_rev"]["iamt"] += flt( - taxable_value * rate / 100, 2 - ) - self.report_dict["sup_details"]["isup_rev"]["txval"] += flt( - taxable_value, 2 - ) + section = self.report_dict["sup_details"]["isup_rev"] + for inv, invoice_details in self.invoice_map.items(): + gst_treatment_section = self.invoice_item_wise_tax_details.get(inv, {}) + for item in gst_treatment_section.values(): + section["txval"] += item.get("taxable_value") + for key in section: + if key in VALUES_TO_UPDATE: + section[key] += item.get(key, 0) def set_inter_state_supply(self, inter_state_supply): - for key, value in inter_state_supply.items(): - if key[0] == "Unregistered": - self.report_dict["inter_sup"]["unreg_details"].append(value) + inter_state_supply_map = { + "Unregistered": "unreg_details", + "Registered Composition": "comp_details", + "UIN Holders": "uin_details", + } - if key[0] == "Registered Composition": - self.report_dict["inter_sup"]["comp_details"].append(value) + for key, value in inter_state_supply.items(): + section = inter_state_supply_map.get(key[0]) - if key[0] == "UIN Holders": - self.report_dict["inter_sup"]["uin_details"].append(value) + if section: + self.report_dict["inter_sup"][section].append(value) def get_company_gst_details(self): gst_details = frappe.get_all( @@ -678,6 +647,23 @@ def get_period(month, year=None): return month_no +def format_values(data, precision=2): + if isinstance(data, dict): + for key, value in data.items(): + if isinstance(value, (int, float)): + data[key] = flt(value, precision) + elif isinstance(value, dict) or isinstance(value, list): + format_values(value) + elif isinstance(data, list): + for i, item in enumerate(data): + if isinstance(item, (int, float)): + data[i] = flt(item, precision) + elif isinstance(item, dict) or isinstance(item, list): + format_values(item) + + return data + + @frappe.whitelist() def view_report(name): frappe.has_permission("GSTR 3B Report", throw=True) diff --git a/india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py b/india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py index 73b01f2ca3..2bb1784f1f 100644 --- a/india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py +++ b/india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py @@ -2,9 +2,9 @@ # See license.txt import json -import unittest import frappe +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import getdate from india_compliance.gst_india.utils.tests import ( @@ -13,11 +13,12 @@ ) -class TestGSTR3BReport(unittest.TestCase): +class TestGSTR3BReport(FrappeTestCase): def setUp(self): frappe.set_user("Administrator") filters = {"company": "_Test Indian Registered Company"} + self.maxDiff = None for doctype in ("Sales Invoice", "Purchase Invoice", "GSTR 3B Report"): frappe.db.delete(doctype, filters=filters) @@ -25,6 +26,7 @@ def setUp(self): def tearDownClass(cls): frappe.db.rollback() + @change_settings("GST Settings", {"enable_overseas_transactions": 1}) def test_gstr_3b_report(self): month_number_mapping = { 1: "January", @@ -41,28 +43,143 @@ def test_gstr_3b_report(self): 12: "December", } + gst_settings = frappe.get_cached_doc("GST Settings") + gst_settings.round_off_gst_values = 0 + gst_settings.save() + create_sales_invoices() create_purchase_invoices() + today = getdate() + ret_period = f"{today.month:02}{today.year}" + report = frappe.get_doc( { "doctype": "GSTR 3B Report", "company": "_Test Indian Registered Company", "company_address": "_Test Indian Registered Company-Billing", - "year": getdate().year, - "month": month_number_mapping.get(getdate().month), + "year": today.year, + "month": month_number_mapping.get(today.month), } ).insert() output = json.loads(report.json_output) - self.assertEqual(output["sup_details"]["osup_det"]["iamt"], 18) - self.assertEqual(output["sup_details"]["osup_det"]["txval"], 300) - self.assertEqual(output["sup_details"]["isup_rev"]["txval"], 100) - self.assertEqual(output["sup_details"]["isup_rev"]["camt"], 9) - self.assertEqual(output["itc_elg"]["itc_net"]["samt"], 40) + self.assertDictEqual( + output, + { + "gstin": "24AAQCA8719H1ZC", + "ret_period": ret_period, + # 3.1 + "sup_details": { + "isup_rev": { + "camt": 9.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 9.0, + "txval": 100.0, + }, + "osup_det": { + "camt": 18.0, + "csamt": 0.0, + "iamt": 37.98, + "samt": 18.0, + "txval": 411.0, + }, + "osup_nil_exmp": {"txval": 100.0}, + "osup_nongst": {"txval": 222.0}, + "osup_zero": {"csamt": 0.0, "iamt": 99.9, "txval": 999.0}, + }, + # 3.2 + "inter_sup": { + "comp_details": [{"iamt": 18.0, "pos": "29", "txval": 100.0}], + "uin_details": [], + "unreg_details": [{"iamt": 19.98, "pos": "06", "txval": 111.0}], + }, + # 4 + "itc_elg": { + "itc_avl": [ + { + "camt": 0.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 0.0, + "ty": "IMPG", + }, + { + "camt": 0.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 0.0, + "ty": "IMPS", + }, + { + "camt": 9.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 9.0, + "ty": "ISRC", + }, + { + "camt": 0.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 0.0, + "ty": "ISD", + }, + { + "camt": 31.5, + "csamt": 0.0, + "iamt": 0.0, + "samt": 31.5, + "ty": "OTH", + }, + ], + "itc_inelg": [ + { + "camt": 0.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 0.0, + "ty": "RUL", + }, + { + "camt": 0.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 0.0, + "ty": "OTH", + }, + ], + "itc_net": {"camt": 40.5, "csamt": 0.0, "iamt": 0.0, "samt": 40.5}, + "itc_rev": [ + { + "camt": 0.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 0.0, + "ty": "RUL", + }, + { + "camt": 0.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 0.0, + "ty": "OTH", + }, + ], + }, + # 5 + "inward_sup": { + "isup_details": [ + {"inter": 100.0, "intra": 0.0, "ty": "GST"}, + {"inter": 0.0, "intra": 0.0, "ty": "NONGST"}, + ] + }, + }, + ) def test_gst_rounding(self): - gst_settings = frappe.get_doc("GST Settings") + gst_settings = frappe.get_cached_doc("GST Settings") gst_settings.round_off_gst_values = 1 gst_settings.save() @@ -81,7 +198,6 @@ def test_gst_rounding(self): def create_sales_invoices(): create_sales_invoice(is_in_state=True) - create_sales_invoice(item_code="_Test Nil Rated Item") create_sales_invoice( customer="_Test Registered Composition Customer", is_out_state=True, @@ -90,6 +206,32 @@ def create_sales_invoices(): customer="_Test Unregistered Customer", is_in_state=True, ) + # Unregistered Out of state + create_sales_invoice( + customer="_Test Unregistered Customer", + is_out_state=True, + place_of_supply="06-Haryana", + rate=111, + ) + + # Same Item Nil-Rated + create_sales_invoice(item_tax_template="Nil-Rated - _TIRC") + + # Non Gst item + create_sales_invoice(item_code="_Test Non GST Item", rate=222) + + # Zero Rated + create_sales_invoice( + customer_address="_Test Registered Customer-Billing-1", + is_export_with_gst=False, + rate=444, + ) + create_sales_invoice( + customer_address="_Test Registered Customer-Billing-1", + is_export_with_gst=True, + is_out_state=True, + rate=555, + ) def create_purchase_invoices(): From 56b9f686ad4189a1e4b4c43c8ade99eabcb1c7ba Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 29 Apr 2024 10:51:58 +0530 Subject: [PATCH 23/24] chore: resolve conflicts for test case --- india_compliance/gst_india/overrides/test_party.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/india_compliance/gst_india/overrides/test_party.py b/india_compliance/gst_india/overrides/test_party.py index 868e534934..7e97eb6f51 100644 --- a/india_compliance/gst_india/overrides/test_party.py +++ b/india_compliance/gst_india/overrides/test_party.py @@ -24,20 +24,13 @@ def test_validate_deemed_export_party(self): self.assertEqual(party.gst_category, "Deemed Export") def test_validate_new_party_with_tcs(self): -<<<<<<< HEAD + # Allow TCS GSTIN party = frappe.new_doc("Customer") party.update( { "customer_name": "Flipkart India Private Limited", "gstin": "29AABCF8078M1C8", } -======= - # Allow TCS GSTIN - party = frappe.new_doc( - "Customer", - customer_name="Flipkart India Private Limited", - gstin="29AABCF8078M1C8", ->>>>>>> 76cf00d7 (fix: support tax collector as customer (#2066)) ) party.insert() From 193c1a8f15c0fe5a1eef5aa41d6ca45c54839a36 Mon Sep 17 00:00:00 2001 From: Vishakh Desai Date: Mon, 29 Apr 2024 11:31:16 +0530 Subject: [PATCH 24/24] fix: remove check digit validation for transporter id (cherry picked from commit 8cfa2090646bcca7deb6afa6549cfebecaedaff0) --- india_compliance/gst_india/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/india_compliance/gst_india/utils/__init__.py b/india_compliance/gst_india/utils/__init__.py index 4718f8be6b..4c3ad08f30 100644 --- a/india_compliance/gst_india/utils/__init__.py +++ b/india_compliance/gst_india/utils/__init__.py @@ -171,7 +171,7 @@ def validate_gstin( title=_("Invalid {0}").format(label), ) - if not (is_transporter_id and gstin.startswith("88")): + if not is_transporter_id: validate_gstin_check_digit(gstin, label) if is_tcs_gstin and not TCS.match(gstin):