Merge pull request #26395 from barredterra/address-autocomplete
feat: address autocomplete dialog
This commit is contained in:
commit
4eac35081c
13 changed files with 372 additions and 6 deletions
|
|
@ -111,6 +111,9 @@ def get_bootinfo():
|
|||
bootinfo.subscription_conf = add_subscription_conf()
|
||||
bootinfo.marketplace_apps = get_marketplace_apps()
|
||||
bootinfo.changelog_feed = get_changelog_feed_items()
|
||||
bootinfo.enable_address_autocompletion = frappe.db.get_single_value(
|
||||
"Geolocation Settings", "enable_address_autocompletion"
|
||||
)
|
||||
|
||||
if sentry_dsn := get_sentry_dsn():
|
||||
bootinfo.sentry_dsn = sentry_dsn
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2024, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Geolocation Settings", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2024-04-09 23:41:49.747820",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enable_address_autocompletion",
|
||||
"provider",
|
||||
"base_url",
|
||||
"api_key"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "provider",
|
||||
"fieldtype": "Select",
|
||||
"label": "Provider",
|
||||
"mandatory_depends_on": "enable_address_autocompletion",
|
||||
"options": "Geoapify\nNomatim\nHERE"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: [\"Geoapify\", \"HERE\"].includes(doc.provider)",
|
||||
"fieldname": "api_key",
|
||||
"fieldtype": "Password",
|
||||
"label": "API Key",
|
||||
"mandatory_depends_on": "eval: doc.enable_address_autocompletion && [\"Geoapify\", \"HERE\"].includes(doc.provider)"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_address_autocompletion",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Address Autocompletion"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.provider === \"Nomatim\"",
|
||||
"fieldname": "base_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Base URL",
|
||||
"mandatory_depends_on": "eval: doc.provider === \"Nomatim\" && doc.enable_address_autocompletion",
|
||||
"options": "URL"
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-22 09:18:34.306542",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Geolocation Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
# Copyright (c) 2024, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_url
|
||||
|
||||
from .providers.geoapify import Geoapify
|
||||
from .providers.here import Here
|
||||
from .providers.nomatim import Nomatim
|
||||
|
||||
|
||||
class GeolocationSettings(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
api_key: DF.Password | None
|
||||
base_url: DF.Data | None
|
||||
enable_address_autocompletion: DF.Check
|
||||
provider: DF.Literal["Geoapify", "Nomatim", "HERE"]
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def autocomplete(txt: str) -> list[dict]:
|
||||
if not txt:
|
||||
return []
|
||||
|
||||
settings = frappe.get_single("Geolocation Settings")
|
||||
if not settings.enable_address_autocompletion:
|
||||
return []
|
||||
|
||||
if settings.provider == "Geoapify":
|
||||
provider = Geoapify(settings.get_password("api_key"), frappe.local.lang)
|
||||
elif settings.provider == "Nomatim":
|
||||
provider = Nomatim(
|
||||
base_url=settings.base_url,
|
||||
referer=get_url(),
|
||||
lang=frappe.local.lang,
|
||||
)
|
||||
elif settings.provider == "HERE":
|
||||
provider = Here(settings.get_password("api_key"), frappe.local.lang)
|
||||
else:
|
||||
frappe.throw(_("This geolocation provider is not supported yet."))
|
||||
|
||||
return list(provider.autocomplete(txt))
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import json
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class Geoapify:
|
||||
def __init__(self, api_key: str, lang: str | None = None):
|
||||
self.api_key = api_key
|
||||
self.lang = lang
|
||||
self.base_url = "https://api.geoapify.com"
|
||||
|
||||
def autocomplete(self, query: str):
|
||||
params = {
|
||||
"text": query,
|
||||
"apiKey": self.api_key,
|
||||
"limit": 5,
|
||||
"format": "json",
|
||||
"lang": self.lang,
|
||||
}
|
||||
response = requests.get(f"{self.base_url}/v1/geocode/autocomplete", params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
results = response.json()["results"]
|
||||
for result in results:
|
||||
yield {
|
||||
"label": result["formatted"],
|
||||
"value": json.dumps(
|
||||
{
|
||||
"address_line1": result.get("address_line1"),
|
||||
"city": result.get("city"),
|
||||
"state": result.get("state"),
|
||||
"pincode": result.get("postcode"),
|
||||
"country": result.get("country"),
|
||||
}
|
||||
),
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import json
|
||||
|
||||
import pycountry
|
||||
import requests
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
class Here:
|
||||
def __init__(self, api_key: str, lang: str | None = None):
|
||||
self.lang = lang
|
||||
self.api_key = api_key
|
||||
self.base_url = "https://autocomplete.search.hereapi.com/v1"
|
||||
|
||||
def autocomplete(self, query: str):
|
||||
params = {
|
||||
"q": query,
|
||||
"apiKey": self.api_key,
|
||||
"limit": 5,
|
||||
"lang": self.lang,
|
||||
}
|
||||
response = requests.get(
|
||||
f"{self.base_url}/autocomplete",
|
||||
params=params,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
results = response.json()["items"]
|
||||
for result in results:
|
||||
address = result["address"]
|
||||
py_country = pycountry.countries.get(alpha_3=address.get("countryCode"))
|
||||
frappe_country = frappe.db.get_value("Country", {"code": py_country.alpha_2.lower()})
|
||||
yield {
|
||||
"label": address["label"],
|
||||
"value": json.dumps(
|
||||
{
|
||||
"address_line1": f'{address.get("street", "")} {address.get("houseNumber", "")}'.strip(),
|
||||
"city": address.get("city", ""),
|
||||
"state": address.get("state", ""),
|
||||
"pincode": address.get("postalCode", ""),
|
||||
"country": frappe_country or "",
|
||||
}
|
||||
),
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import json
|
||||
|
||||
import requests
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
class Nomatim:
|
||||
def __init__(self, base_url: str, referer: str, lang: str | None = None):
|
||||
self.lang = lang
|
||||
self.referer = referer
|
||||
self.base_url = base_url
|
||||
|
||||
def autocomplete(self, query: str):
|
||||
params = {
|
||||
"q": query,
|
||||
"format": "json",
|
||||
"limit": 5,
|
||||
"addressdetails": 1,
|
||||
"accept-language": self.lang,
|
||||
"layer": "address",
|
||||
}
|
||||
response = requests.get(
|
||||
f"{self.base_url}/search",
|
||||
params=params,
|
||||
headers={"Referer": self.referer},
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
results = response.json()
|
||||
for result in results:
|
||||
if "address" not in result:
|
||||
continue
|
||||
|
||||
address = result["address"]
|
||||
yield {
|
||||
"label": result["display_name"],
|
||||
"value": json.dumps(
|
||||
{
|
||||
"address_line1": f'{address.get("road")} {address.get("house_number", "")}'.strip(),
|
||||
"city": address.get("city") or address.get("town") or address.get("village"),
|
||||
"state": address.get("state"),
|
||||
"pincode": address.get("postcode"),
|
||||
"country": frappe.db.get_value("Country", {"code": address.get("country_code")}),
|
||||
}
|
||||
),
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Copyright (c) 2024, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestGeolocationSettings(FrappeTestCase):
|
||||
pass
|
||||
|
|
@ -104,3 +104,5 @@ import "./frappe/ui/chart.js";
|
|||
import "./frappe/ui/datatable.js";
|
||||
import "./frappe/ui/driver.js";
|
||||
import "./frappe/scanner";
|
||||
|
||||
import "./frappe/ui/address_autocomplete/autocomplete_dialog.js";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
frappe.provide("frappe.ui");
|
||||
|
||||
frappe.ui.AddressAutocompleteDialog = class AddressAutocompleteDialog {
|
||||
constructor(opts) {
|
||||
this.title = opts?.title || __("New Address");
|
||||
this.link_doctype = opts?.link_doctype;
|
||||
this.link_name = opts?.link_name;
|
||||
this.after_insert = opts?.after_insert;
|
||||
this.dialog = this._get_dialog();
|
||||
}
|
||||
|
||||
_get_dialog() {
|
||||
// sourcery skip: inline-immediately-returned-variable
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: this.title,
|
||||
fields: [
|
||||
{
|
||||
fieldname: "search",
|
||||
fieldtype: "Autocomplete",
|
||||
label: __("Search"),
|
||||
reqd: 1,
|
||||
get_query:
|
||||
"frappe.integrations.doctype.geolocation_settings.geolocation_settings.autocomplete",
|
||||
onchange: () => {
|
||||
// Disable "Create Address" button if mandatory fields are missing
|
||||
frappe.model.with_doctype("Address", () => {
|
||||
const address = this.parse_selected_value();
|
||||
const mandatory_fields = frappe
|
||||
.get_meta("Address")
|
||||
.fields.filter(
|
||||
(field) =>
|
||||
field.reqd &&
|
||||
!field.default &&
|
||||
field.fieldname !== "address_type"
|
||||
);
|
||||
const missing_fields = mandatory_fields.filter(
|
||||
(field) => !address[field.fieldname]
|
||||
);
|
||||
const is_valid = missing_fields.length === 0;
|
||||
if (is_valid) {
|
||||
dialog.enable_primary_action();
|
||||
} else {
|
||||
dialog.disable_primary_action();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
primary_action_label: __("Create Address"),
|
||||
primary_action: () => {
|
||||
// Insert the address into the database
|
||||
dialog.hide();
|
||||
|
||||
const address = this.parse_selected_value();
|
||||
address["doctype"] = "Address";
|
||||
address["links"] = [
|
||||
{
|
||||
link_doctype: this.link_doctype,
|
||||
link_name: this.link_name,
|
||||
},
|
||||
];
|
||||
frappe.db.insert(address).then((doc) => {
|
||||
this.after_insert && this.after_insert(doc);
|
||||
});
|
||||
},
|
||||
secondary_action_label: __("Edit Address in Form"),
|
||||
secondary_action: () => {
|
||||
// Open the address in the form view
|
||||
dialog.hide();
|
||||
|
||||
const address = this.parse_selected_value();
|
||||
frappe.new_doc("Address", address);
|
||||
},
|
||||
});
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
parse_selected_value() {
|
||||
const data = this.dialog.get_values();
|
||||
return JSON.parse(data.search);
|
||||
}
|
||||
|
||||
show() {
|
||||
this.dialog.show();
|
||||
}
|
||||
};
|
||||
|
|
@ -12,7 +12,7 @@ $.extend(frappe.contacts, {
|
|||
$(frm.fields_dict["address_html"].wrapper)
|
||||
.html(frappe.render_template("address_list", frm.doc.__onload))
|
||||
.find(".btn-address")
|
||||
.on("click", () => new_record("Address", frm.doc));
|
||||
.on("click", () => new_record("Address", frm));
|
||||
}
|
||||
|
||||
// render contact
|
||||
|
|
@ -20,7 +20,7 @@ $.extend(frappe.contacts, {
|
|||
$(frm.fields_dict["contact_html"].wrapper)
|
||||
.html(frappe.render_template("contact_list", frm.doc.__onload))
|
||||
.find(".btn-contact")
|
||||
.on("click", () => new_record("Contact", frm.doc));
|
||||
.on("click", () => new_record("Contact", frm));
|
||||
}
|
||||
},
|
||||
get_last_doc: function (frm) {
|
||||
|
|
@ -59,12 +59,23 @@ $.extend(frappe.contacts, {
|
|||
},
|
||||
});
|
||||
|
||||
function new_record(doctype, source_doc) {
|
||||
function new_record(doctype, frm) {
|
||||
frappe.dynamic_link = {
|
||||
doctype: source_doc.doctype,
|
||||
doc: source_doc,
|
||||
doctype: frm.doc.doctype,
|
||||
doc: frm.doc,
|
||||
fieldname: "name",
|
||||
};
|
||||
|
||||
return frappe.new_doc(doctype);
|
||||
if (frappe.boot.enable_address_autocompletion === 1 && doctype === "Address") {
|
||||
new frappe.ui.AddressAutocompleteDialog({
|
||||
title: __("New Address"),
|
||||
link_doctype: frm.doc.doctype,
|
||||
link_name: frm.doc.name,
|
||||
after_insert: function (doc) {
|
||||
frm.reload_doc();
|
||||
},
|
||||
}).show();
|
||||
} else {
|
||||
frappe.new_doc(doctype);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ dependencies = [
|
|||
"google-auth~=1.29.0",
|
||||
"posthog~=3.0.1",
|
||||
"vobject~=0.9.7",
|
||||
"pycountry~=24.6.1",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue