diff --git a/india_compliance/gst_india/api_classes/base.py b/india_compliance/gst_india/api_classes/base.py index 9e2b00301a..cc333e5813 100644 --- a/india_compliance/gst_india/api_classes/base.py +++ b/india_compliance/gst_india/api_classes/base.py @@ -226,7 +226,6 @@ def is_ignored_error(self, response_json): pass def handle_http_code(self, status_code, response_json): - # GSP connectivity issues if status_code == 401 or ( status_code == 403 @@ -265,22 +264,23 @@ def mask_sensitive_info(self, log): output = log.output data = log.data request_body = data and data.get("body") + placeholder = "*****" for key in self.SENSITIVE_INFO: if key in request_headers: - request_headers[key] = "*****" + request_headers[key] = placeholder if output and key in output: - output[key] = "*****" + output[key] = placeholder if not data: continue if key in data: - data[key] = "*****" + data[key] = placeholder if request_body and key in request_body: - request_body[key] = "*****" + request_body[key] = placeholder def get_public_ip(): 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 7291c79f36..13600ac5f1 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/__init__.py @@ -349,6 +349,7 @@ def __init__(self, **kwargs): def get_all(self, additional_fields=None, names=None, only_names=False): query = self.get_query(additional_fields) + match_found = ("Reconciled", "Match Found") if only_names and not names: return @@ -360,7 +361,7 @@ def get_all(self, additional_fields=None, names=None, only_names=False): query = query.where( ( (self.PI.posting_date[self.from_date : self.to_date]) - & (IfNull(self.PI.reconciliation_status, "") != "Reconciled") + & (IfNull(self.PI.reconciliation_status, "").notin(match_found)) ) | (self.PI.name.isin(names)) ) @@ -368,7 +369,7 @@ def get_all(self, additional_fields=None, names=None, only_names=False): else: query = query.where( (self.PI.posting_date[self.from_date : self.to_date]) - & (IfNull(self.PI.reconciliation_status, "") != "Reconciled") + & (IfNull(self.PI.reconciliation_status, "").notin(match_found)) ) return query.run(as_dict=True) @@ -502,6 +503,7 @@ def __init__(self, **kwargs): def get_all(self, additional_fields=None, names=None, only_names=False): query = self.get_query(additional_fields) + match_found = ("Reconciled", "Match Found") if only_names and not names: return @@ -513,7 +515,7 @@ def get_all(self, additional_fields=None, names=None, only_names=False): query = query.where( ( (self.BOE.posting_date[self.from_date : self.to_date]) - & (IfNull(self.BOE.reconciliation_status, "") != "Reconciled") + & (IfNull(self.BOE.reconciliation_status, "").notin(match_found)) ) | (self.BOE.name.isin(names)) ) @@ -521,7 +523,7 @@ def get_all(self, additional_fields=None, names=None, only_names=False): else: query = query.where( (self.BOE.posting_date[self.from_date : self.to_date]) - & (IfNull(self.BOE.reconciliation_status, "") != "Reconciled") + & (IfNull(self.BOE.reconciliation_status, "").notin(match_found)) ) return query.run(as_dict=True) @@ -745,6 +747,8 @@ def reconcile(self, category, amended_category): """ Reconcile purchases and inward supplies for given category. """ + self.category = category + # GSTIN Level matching purchases = self.get_unmatched_purchase_or_bill_of_entry(category) inward_supplies = self.get_unmatched_inward_supply(category, amended_category) @@ -789,12 +793,13 @@ def reconcile_for_rule(self, purchases, inward_supplies, match_status, rules): for inward_supply_name, inward_supply in ( inward_supplies[supplier_gstin].copy().items() ): - if match_status == "Residual Match": - if ( - abs((purchase.bill_date - inward_supply.bill_date).days) - > 10 - ): - continue + if ( + match_status == "Residual Match" + and self.category != "CDNR" + and abs((purchase.bill_date - inward_supply.bill_date).days) + > 10 + ): + continue if not self.is_doc_matching(purchase, inward_supply, rules): continue diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_detail_comparision.html b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_detail_comparision.html index 6735501a0d..3bef8ae602 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_detail_comparision.html +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_detail_comparision.html @@ -4,7 +4,7 @@ 2A / 2B - Purchase + {{ purchase.doctype }} diff --git a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js index 06ba93e615..591ae0f694 100644 --- a/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js +++ b/india_compliance/gst_india/doctype/purchase_reconciliation_tool/purchase_reconciliation_tool.js @@ -16,12 +16,13 @@ const ALERT_HTML = `
You have missing GSTR-2B downloads
- ${api_enabled - ? ` + ${ + api_enabled + ? ` Download 2B ` - : "" - } + : "" + } `; @@ -96,30 +97,31 @@ frappe.ui.form.on("Purchase Reconciliation Tool", { frm.save(); }); + const action_group = __("Actions"); + // add custom buttons api_enabled ? frm.add_custom_button(__("Download 2A/2B"), () => new ImportDialog(frm)) : frm.add_custom_button( - __("Upload 2A/2B"), - () => new ImportDialog(frm, false) - ); + __("Upload 2A/2B"), + () => new ImportDialog(frm, false) + ); if (!frm.purchase_reconciliation_tool?.data?.length) return; if (frm.get_active_tab()?.df.fieldname == "invoice_tab") { frm.add_custom_button( __("Unlink"), () => unlink_documents(frm), - __("Actions") + action_group ); - frm.add_custom_button(__("dropdown-divider"), () => { }, __("Actions")); + frm.add_custom_button(__("dropdown-divider"), () => {}, action_group); } - ["Accept", "Pending", "Ignore"].forEach( - action => - frm.add_custom_button( - __(action), - () => apply_action(frm, action), - __("Actions") - ) + ["Accept", "Pending", "Ignore"].forEach(action => + frm.add_custom_button( + __(action), + () => apply_action(frm, action), + action_group + ) ); frm.$wrapper .find("[data-label='dropdown-divider']") @@ -131,10 +133,16 @@ frappe.ui.form.on("Purchase Reconciliation Tool", { ); // move actions button next to filters - for (let button of $(".custom-actions .inner-group-button")) { - if (button.innerText?.trim() != "Actions") continue; + for (const group_div of $(".custom-actions .inner-group-button")) { + const btn_label = group_div.querySelector("button").innerText?.trim(); + if (btn_label != action_group) continue; + $(".custom-button-group .inner-group-button").remove(); - $(button).appendTo($(".custom-button-group")); + + // to hide `Actions` button group on smaller screens + $(group_div).addClass("hidden-md"); + + $(group_div).appendTo($(".custom-button-group")); } }, @@ -191,8 +199,8 @@ frappe.ui.form.on("Purchase Reconciliation Tool", { method == "update_api_progress" ? __("Fetching data from GSTN") : __("Updating Inward Supply for Return Period {0}", [ - data.return_period, - ]); + data.return_period, + ]); frm.dashboard.show_progress( "Import GSTR Progress", @@ -377,6 +385,12 @@ class PurchaseReconciliationTool { fieldname: "is_reverse_charge", fieldtype: "Check", }, + { + label: "DocType", + fieldname: "purchase_doctype", + fieldtype: "Select", + options: ["Purchase Invoice", "Bill of Entry"], + }, ]; fields.forEach(field => (field.parent = "Purchase Reconciliation Tool")); @@ -453,47 +467,39 @@ class PurchaseReconciliationTool { me.dm = new EmailDialog(me.frm, row); }); - this.tabs.summary_tab.$datatable.on( - "click", - ".match-status", - async function (e) { - e.preventDefault(); - - const match_status = $(this).text(); - await me.filter_group.push_new_filter([ - "Purchase Reconciliation Tool", - "match_status", - "=", - match_status, - ]); - me.filter_group.apply(); - } - ); - - this.tabs.supplier_tab.$datatable.on( - "click", - ".supplier-gstin", - add_supplier_gstin_filter - ); - - this.tabs.invoice_tab.$datatable.on( - "click", - ".supplier-gstin", - add_supplier_gstin_filter - ); - - async function add_supplier_gstin_filter(e) { - e.preventDefault(); + const filter_map = { + // TAB: { SELECTOR: FIELDNAME } + summary: { ".match-status": "match_status" }, + supplier: { ".supplier-gstin": "supplier_gstin" }, + invoice: { + ".match-status": "match_status", + ".action-performed": "action", + ".supplier-gstin": "supplier_gstin", + }, + }; - const supplier_gstin = $(this).text().trim(); - await me.filter_group.push_new_filter([ - "Purchase Reconciliation Tool", - "supplier_gstin", - "=", - supplier_gstin, - ]); - me.filter_group.apply(); - } + Object.keys(filter_map).forEach(tab => { + Object.keys(filter_map[tab]).forEach(selector => { + this.tabs[`${tab}_tab`].$datatable.on( + "click", + selector, + async function (e) { + e.preventDefault(); + const value = $(this).text().trim(); + const field = filter_map[tab][selector]; + + await me.filter_group.push_new_filter([ + "Purchase Reconciliation Tool", + field, + "=", + value, + ]); + + me.filter_group.apply(); + } + ); + }); + }); } export_data(selected_row) { @@ -743,12 +749,15 @@ class PurchaseReconciliationTool { label: "Match Status", fieldname: "match_status", width: 120, + _value: (...args) => { + return `${args[0]}`; + }, }, { label: "GST Inward
Supply", fieldname: "inward_supply_name", fieldtype: "Link", - doctype: "GST Inward Supply", + options: "GST Inward Supply", align: "center", width: 120, }, @@ -787,6 +796,9 @@ class PurchaseReconciliationTool { { label: "Action", fieldname: "action", + _value: (...args) => { + return `${args[0]}`; + }, }, ]; } @@ -921,8 +933,9 @@ class DetailViewDialog { ? ["GST Inward Supply"] : ["Purchase Invoice", "Bill of Entry"], - read_only_depends_on: `eval: ${this.missing_doctype == "GST Inward Supply" - }`, + read_only_depends_on: `eval: ${ + this.missing_doctype == "GST Inward Supply" + }`, onchange: () => { const doctype = this.dialog.get_value("doctype"); @@ -1243,8 +1256,7 @@ class ImportDialog { download_gstr_by_period(only_missing) { if (only_missing && this.has_no_pending_download) { frappe.msgprint({ - message: - "There are no pending downloads for the selected period.", + message: "There are no pending downloads for the selected period.", title: "No Pending Downloads", indicator: "orange", }); @@ -1830,6 +1842,7 @@ async function create_new_purchase_invoice(row, company, company_gstin) { bill_no: doc.bill_no, bill_date: doc.bill_date, is_reverse_charge: ["Yes", 1].includes(doc.is_reverse_charge) ? 1 : 0, + is_return: ["CDNR", "CDNRA"].includes(doc.classification) ? 1 : 0, }; _set_value({ diff --git a/india_compliance/gst_india/overrides/test_transaction.py b/india_compliance/gst_india/overrides/test_transaction.py index 0722ae6aef..77702e6aa1 100644 --- a/india_compliance/gst_india/overrides/test_transaction.py +++ b/india_compliance/gst_india/overrides/test_transaction.py @@ -1155,9 +1155,11 @@ def test_so_and_po_after_item_update(self): class TestPlaceOfSupply(FrappeTestCase): def test_pos_sales_invoice(self): + # Sales Invoice with Shipping Address doc_args = { "doctype": "Sales Invoice", "customer": "_Test Registered Composition Customer", + "shipping_address_name": "_Test Indian Registered Company-Billing", } settings = ["Accounts Settings", None, "determine_address_tax_category_from"] @@ -1171,3 +1173,26 @@ def test_pos_sales_invoice(self): frappe.db.set_value(*settings, "Billing Address") doc = create_transaction(**doc_args) self.assertEqual(doc.place_of_supply, "29-Karnataka") + + frappe.db.set_value(*settings, "Shipping Address") + + # Sales Invoice with only Billing Address + doc_args = { + "doctype": "Sales Invoice", + "customer": "_Test Registered Composition Customer", + } + + settings = ["Accounts Settings", None, "determine_address_tax_category_from"] + + # (from Billing Address) + doc = create_transaction(**doc_args) + self.assertEqual(doc.place_of_supply, "29-Karnataka") # Billing Address + + # Sales Invoice for Unregistered Customer + doc_args = { + "doctype": "Sales Invoice", + "customer": "_Test Unregistered Customer", + } + + doc = create_transaction(**doc_args) + self.assertEqual(doc.place_of_supply, "24-Gujarat") # Company GSTIN diff --git a/india_compliance/gst_india/setup/property_setters.py b/india_compliance/gst_india/setup/property_setters.py index af9b7bef58..b0be7a4f2d 100644 --- a/india_compliance/gst_india/setup/property_setters.py +++ b/india_compliance/gst_india/setup/property_setters.py @@ -81,17 +81,23 @@ def get_property_setters(*, include_defaults=False): "property": "read_only", "value": "1", }, + { + "doctype": "Accounts Settings", + "fieldname": "add_taxes_from_item_tax_template", + "property": "description", + "value": "Overridden by India Compliance", + }, { "doctype": "Accounts Settings", "fieldname": "tax_settings_section", "property": "label", - "value": "Tax Settings (Overridden by India Compliance)", + "value": "Tax Settings", }, { "doctype": "Accounts Settings", "fieldname": "tax_settings_section", "property": "collapsible", - "value": "1", + "value": "0", }, { "doctype": "Purchase Reconciliation Tool", diff --git a/india_compliance/gst_india/utils/__init__.py b/india_compliance/gst_india/utils/__init__.py index 206eb10ba9..f7e2f366ed 100644 --- a/india_compliance/gst_india/utils/__init__.py +++ b/india_compliance/gst_india/utils/__init__.py @@ -386,35 +386,43 @@ def get_place_of_supply(party_details, doctype): :param party_details: A frappe._dict or document containing fields related to party """ - pos_basis = frappe.get_cached_value( - "Accounts Settings", "Accounts Settings", "determine_address_tax_category_from" - ) - - if pos_basis == "Shipping Address" and doctype in SALES_DOCTYPES: - # POS Basis Shipping Address is only applicable for Sales - pos_gstin = party_details.company_gstin - # fallback to company GSTIN for sales or supplier GSTIN for purchases # (in retail scenarios, customer / company GSTIN may not be set) - - elif doctype in SALES_DOCTYPES or doctype == "Payment Entry": + if doctype in SALES_DOCTYPES or doctype == "Payment Entry": # for exports, Place of Supply is set using GST category in absence of GSTIN if party_details.gst_category == "Overseas": return get_overseas_place_of_supply(party_details) + # customer address based on POS Basis + customer_address = party_details.customer_address + pos_basis = frappe.get_cached_value( + "Accounts Settings", + "Accounts Settings", + "determine_address_tax_category_from", + ) + + shipping_gstin = None if ( - party_details.gst_category == "Unregistered" - and party_details.customer_address + doctype != "Payment Entry" + and pos_basis == "Shipping Address" + and party_details.shipping_address_name ): + customer_address = party_details.shipping_address_name + shipping_gstin = frappe.db.get_value("Address", customer_address, "gstin") + + customer_gstin = shipping_gstin or party_details.billing_address_gstin + # for unregistered + if not customer_gstin and customer_address: gst_state_number, gst_state = frappe.db.get_value( "Address", - party_details.customer_address, + customer_address, ("gst_state_number", "gst_state"), ) if gst_state_number and gst_state: return f"{gst_state_number}-{gst_state}" - pos_gstin = party_details.billing_address_gstin or party_details.company_gstin + # for registered + pos_gstin = customer_gstin or party_details.company_gstin elif doctype == "Stock Entry": pos_gstin = party_details.bill_to_gstin or party_details.bill_from_gstin diff --git a/india_compliance/patches.txt b/india_compliance/patches.txt index dab87a295e..3dae9b4039 100644 --- a/india_compliance/patches.txt +++ b/india_compliance/patches.txt @@ -4,7 +4,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() #58 -execute:from india_compliance.gst_india.setup import create_property_setters; create_property_setters() #9 +execute:from india_compliance.gst_india.setup import create_property_setters; create_property_setters() #10 execute:from india_compliance.income_tax_india.setup import create_custom_fields; create_custom_fields() #2 india_compliance.patches.post_install.remove_old_fields #2 india_compliance.patches.post_install.set_gst_tax_type diff --git a/india_compliance/public/js/quick_entry.js b/india_compliance/public/js/quick_entry.js index ba9207193e..ab54f36294 100644 --- a/india_compliance/public/js/quick_entry.js +++ b/india_compliance/public/js/quick_entry.js @@ -4,6 +4,10 @@ class GSTQuickEntryForm extends frappe.ui.form.QuickEntryForm { this.skip_redirect_on_error = true; this.api_enabled = india_compliance.is_api_enabled() && gst_settings.autofill_party_info; + this.gstin_to_party_type_map = { + F: "Partnership", + C: "Company", + }; } async setup() { @@ -92,6 +96,14 @@ class GSTQuickEntryForm extends frappe.ui.form.QuickEntryForm { ignore_validation: true, onchange: () => { const d = this.dialog; + + if (["Customer", "Supplier"].includes(this.doctype)) { + d.set_value( + `${this.doctype.toLowerCase()}_type`, + this.gstin_to_party_type_map[d.doc._gstin[5]] || "Individual" + ); + } + if (this.api_enabled && !gst_settings.sandbox_mode) return autofill_fields(d); @@ -308,6 +320,7 @@ class AddressQuickEntryForm extends GSTQuickEntryForm { "Customer", "Supplier", "Company", + "Lead" ].includes(doc.doctype) ) return;