Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions india_compliance/gst_india/client_scripts/party.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ function validate_gstin(doctype) {
frm.doc.gstin = gstin;
frm.refresh_field("gstin");

india_compliance.check_duplicate_gstin(gstin, frm.doctype, frm.docname);

if (!frm.fields_dict.pan) return;

// extract PAN from GSTIN
Expand Down Expand Up @@ -89,6 +91,8 @@ function validate_pan(doctype) {

frm.doc.pan = pan;
frm.refresh_field("pan");

india_compliance.check_duplicate_pan(pan, frm.doctype, frm.docname);
set_party_type(frm);
},
});
Expand Down
106 changes: 106 additions & 0 deletions india_compliance/gst_india/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
E_INVOICE_MASTER_CODES_URL,
GST_ACCOUNT_FIELDS,
GST_INVOICE_NUMBER_FORMAT,
GST_PARTY_TYPES,
GSTIN_FORMATS,
PAN_NUMBER,
PINCODE_FORMAT,
Expand Down Expand Up @@ -1131,3 +1132,108 @@ def has_permission_of_page(page_name, throw=False):
)

return True


@frappe.whitelist()
def check_duplicate_party(field, value, party_type, party=None):
"""
Check duplicates based on PAN/GSTIN for the given party type.
"""
if not value:
return

if party_type not in GST_PARTY_TYPES:
return

frappe.has_permission(party_type, doc=party, throw=True)

value = value.upper().strip()

# Check for duplicates
if field == "pan":
existing_parties = _get_duplicate_pan_party(value, party_type, party)
elif field == "gstin":
existing_parties = _get_duplicate_gstin_party(value, party_type, party)
else:
return

if not existing_parties:
return

# Show message
duplicate_links = []
for row in existing_parties:
party_link = get_link_to_form(party_type, row["name"])
if row["via_address"]:
address_link = get_link_to_form("Address", row["address"])
link_msg = _("{0} (via Address {1})").format(party_link, address_link)

else:
link_msg = party_link

Comment on lines +1165 to +1173

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix dict attribute access to avoid duplicate-GSTIN crash.
_get_duplicate_gstin_party() returns plain dicts, so row.name / row.via_address / row.address will raise AttributeError when duplicates exist. Use dict access (or normalize to frappe._dict) before formatting the message.

🛠️ Proposed fix
-    for row in existing_parties:
-        party_link = get_link_to_form(party_type, row.name)
-        if row.via_address:
-            address_link = get_link_to_form("Address", row.address)
+    for row in existing_parties:
+        party_name = row.get("name")
+        if not party_name:
+            continue
+        party_link = get_link_to_form(party_type, party_name)
+        if row.get("via_address"):
+            address_link = get_link_to_form("Address", row.get("address"))
             link_msg = _("{0} (via Address {1})").format(party_link, address_link)
         else:
             link_msg = party_link
🤖 Prompt for AI Agents
In `@india_compliance/gst_india/utils/__init__.py` around lines 1165 - 1173,
existing_parties from _get_duplicate_gstin_party() can be plain dicts, so using
attribute access like row.name / row.via_address / row.address will raise
AttributeError; update the loop that builds link_msg (referencing
existing_parties, get_link_to_form, party_type) to either normalize each row to
a frappe._dict (e.g., row = frappe._dict(row)) before use or switch to
dict-style access (row["name"], row.get("via_address"), row.get("address")) when
building party_link and address_link so duplicate-GSTIN cases do not crash.

duplicate_links.append(f"<li>{link_msg}</li>")
Comment thread
vorasmit marked this conversation as resolved.

msg = _("{0} {1} is already registered with the following {2}(s):").format(
field.capitalize(), frappe.bold(value), party_type
)
msg += f"<br><br><ul>{''.join(duplicate_links)}</ul>"

frappe.msgprint(msg=msg, indicator="orange")


def _get_duplicate_pan_party(pan, party_type, party=None):
filters = {"pan": ("=", pan)}
if party:
filters["name"] = ("!=", party)

return frappe.get_all(party_type, filters=filters)


def _get_duplicate_gstin_party(gstin, party_type, party=None):
party_table = frappe.qb.DocType(party_type)
address = frappe.qb.DocType("Address")
dynamic_link = frappe.qb.DocType("Dynamic Link")

party_query = (
frappe.qb.from_(party_table)
.select(
party_table.name,
frappe.qb.terms.ValueWrapper(None).as_("address_name"),
frappe.qb.terms.ValueWrapper(0).as_("via_address"),
)
.where(party_table.gstin == gstin)
)

if party:
party_query = party_query.where(party_table.name != party)

address_query = (
frappe.qb.from_(address)
.join(dynamic_link)
.on(dynamic_link.parent == address.name)
.select(
dynamic_link.link_name.as_("name"),
address.name.as_("address_name"),
frappe.qb.terms.ValueWrapper(1).as_("via_address"),
)
.where(dynamic_link.link_doctype == party_type)
.where(address.gstin == gstin)
)

if party:
address_query = address_query.where(dynamic_link.link_name != party)

results = (party_query + address_query).orderby("via_address").run(as_dict=True)

duplicates_dict = {}
for row in results:
if row.name in duplicates_dict:
continue

duplicates_dict[row.name] = {
"name": row.name,
"via_address": bool(row.via_address),
"address": row.address_name,
}

return list(duplicates_dict.values())
46 changes: 27 additions & 19 deletions india_compliance/public/js/quick_entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ class GSTQuickEntryForm extends frappe.ui.form.QuickEntryForm {
fieldtype: "Section Break",
description: this.api_enabled
? __(
`When you enter a GSTIN, the permanent address linked to it is
`When you enter a GSTIN, the permanent address linked to it is
autofilled.<br>
Change the {0} to autofill other addresses.`,
[frappe.meta.get_label("Address", "pincode")]
)
[frappe.meta.get_label("Address", "pincode")]
)
: "",
collapsible: 0,
},
Expand Down Expand Up @@ -97,10 +97,13 @@ class GSTQuickEntryForm extends frappe.ui.form.QuickEntryForm {
onchange: () => {
const d = this.dialog;

india_compliance.check_duplicate_gstin(d.doc._gstin, this.doctype);

if (["Customer", "Supplier"].includes(this.doctype)) {
d.set_value(
`${this.doctype.toLowerCase()}_type`,
this.gstin_to_party_type_map[d.doc._gstin[5]] || "Individual"
this.gstin_to_party_type_map[d.doc._gstin[5]] ||
"Individual"
);
}

Expand All @@ -109,7 +112,10 @@ class GSTQuickEntryForm extends frappe.ui.form.QuickEntryForm {

d.set_value(
"gst_category",
india_compliance.guess_gst_category(d.doc._gstin, d.doc.country)
india_compliance.guess_gst_category(
d.doc._gstin,
d.doc.country,
),
);
},
},
Expand Down Expand Up @@ -169,21 +175,23 @@ class PartyQuickEntryForm extends GSTQuickEntryForm {
collapsible: 0,
},
{
label: __("First Name"),
fieldname: "map_to_first_name",
fieldtype: "Data",
depends_on: "eval:doc.customer_type=='Company' || doc.supplier_type=='Company'",
},
{
label: __("First Name"),
fieldname: "map_to_first_name",
fieldtype: "Data",
depends_on:
"eval:doc.customer_type=='Company' || doc.supplier_type=='Company'",
},
{
fieldtype: "Column Break",
},
{
label: __("Last Name"),
fieldname: "map_to_last_name",
fieldtype: "Data",
depends_on: "eval:doc.customer_type=='Company' || doc.supplier_type=='Company'",
},
{
{
label: __("Last Name"),
fieldname: "map_to_last_name",
fieldtype: "Data",
depends_on:
"eval:doc.customer_type=='Company' || doc.supplier_type=='Company'",
},
{
fieldname: "primary_contact_section_2",
fieldtype: "Section Break",
collapsible: 0,
Expand Down Expand Up @@ -343,7 +351,7 @@ class AddressQuickEntryForm extends GSTQuickEntryForm {
"Customer",
"Supplier",
"Company",
"Lead"
"Lead",
].includes(doc.doctype)
)
return;
Expand Down
20 changes: 20 additions & 0 deletions india_compliance/public/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ Object.assign(india_compliance, {
return `${month}${year}`;
},

check_duplicate_gstin(gstin, party_type, party = null) {
if (!gstin || gstin.length !== 15) return;
this.check_duplicate_party("gstin", gstin, party_type, party);
},

check_duplicate_pan(pan, party_type, party = null) {
if (!pan || pan.length !== 10) return;
this.check_duplicate_party("pan", pan, party_type, party);
},

check_duplicate_party(field, value, party_type, party = null) {
if (!party_type) return;
if (!frappe.boot.gst_party_types.includes(party_type)) return;

frappe.call({
method: "india_compliance.gst_india.utils.check_duplicate_party",
args: { field, value, party_type, party },
});
},

get_gstin_query(party, party_type = "Company", exclude_isd = false) {
if (!party) {
frappe.show_alert({
Expand Down