API Address selection

Hi everyone,

I am trying to figure out how feasable it is in a doctype form to have an autocomplete list of addresses in a custom app.

The API call is the below :

"https://api-adresse.data.gouv.fr/search/?q=8+bd+du+port"

There is an autocomplete field type, so I guess that is it, unfortunately, I could not find any doc on that field type.

Many thanks for your help!

@gmeunier you need to write some custom code to fetch results from third party APIs for autocomplete. In this case you need to override default query.

https://frappeframework.com/docs/v14/user/en/guides/app-development/overriding-link-query-by-custom-script#2-calling-a-different-method-to-generate-results

Autocomplete fields can override queries just like link fields. So what you need is:

  1. Python whitelisted function that follows link query function but queries external API instead of DB.
  2. Client script to override query on the field (same as link field)

Thanks a lot Ankush.

So I started to do so:
For a doctype ‘tiers’ I updates the corresponding tiers.js file:

frappe.ui.form.on("Tiers", "onload", function(frm) {
	frm.set_query("adresse", function() {
		return {
			query: "asso.queries.autocomplete_adresse"
		};
	});
});

I also created the corresponding python file with the following code:

@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def autocomplete_adresse(doctype, txt, searchfield, start, page_len, filters):
    return ( 
        {
        'a' : 'pomme',
        'b' : 'poire',
        'c' : 'orange'
        }
       )

At then end I get the following error message:

Traceback (most recent call last):
File “apps/frappe/frappe/app.py”, line 56, in application
response = frappe.api.handle()
File “apps/frappe/frappe/api.py”, line 53, in handle
return _RESTAPIHandler(call, doctype, name).get_response()
File “apps/frappe/frappe/api.py”, line 69, in get_response
return self.handle_method()
File “apps/frappe/frappe/api.py”, line 79, in handle_method
return frappe.handler.handle()
File “apps/frappe/frappe/handler.py”, line 48, in handle
data = execute_cmd(cmd)
File “apps/frappe/frappe/handler.py”, line 86, in execute_cmd
return frappe.call(method, **frappe.form_dict)
File “apps/frappe/frappe/init.py”, line 1596, in call
return fn(*args, **newargs)
File “apps/frappe/frappe/utils/typing_validations.py”, line 33, in wrapper
return func(*args, **kwargs)
File “apps/frappe/frappe/desk/search.py”, line 292, in wrapper
sanitize_searchfield(kwargs[“searchfield”])
KeyError: ‘searchfield’

Well, I am not an IT guy and I have a very poor knowledge of Python. Right now, that is out of my competencies.

Your return statement in the JS side has missing “filters” section.

filters: {"is_service_item": "Yes"}
1 Like

Thank for your reply, unfortunately, that is not it.
With my son we traced the code and on Python side, searchfield, start, page_len, doctype are not transmitted : only txt and query.
Now we are trying to figure out how txt and query are being provided from js to python but have not found out yet.

It is best to examine the working code. Try to find a code in the ERPNext side in Github.

JS Script

frappe.ui.form.on("Tiers", "onload", function(frm) {

	frm.set_query("adresse", function () {
		return {
			query: "asso.queries.autocomplete_adresse",
			filters: {"is_service_item": "Yes"}
		}
	});

});

Request Data

{
	"type": "POST",
	"args": {
		"txt": "",
		"query": "asso.queries.autocomplete_adresse"
	},
	"headers": {},
	"error_handlers": {},
	"url": "/api/method/asso.queries.autocomplete_adresse"
}

Response Data

{
	"exception": "KeyError: 'searchfield'"
}

ok people, here is my solution
(remember, I am no ITS guy)

First of all, I have in my document 6 fields:
address_input : to look for the address, that is an autocomplete field
And 5 other fields to explode the address in its different parts

  • address_name
  • address_postcode
  • address_city
  • address_citycode
  • address_id

In the Doctype .js file (my Docytype is called “Member”)

frappe.ui.form.on("Member", {

	onload_post_render: async function(frm) {
		// Ajax Wait !
		let timeout = null;
		
		cur_frm.fields_dict.address_input.$input.on("keypress", function(evt){
			clearTimeout(timeout);
			timeout = setTimeout(function () {
				doAddress(frm);
			}, 500);
		});
		cur_frm.fields_dict.address_input.$input.on("focus", function(evt){
			clearTimeout(timeout);
			timeout = setTimeout(function () {
				doAddress(frm);
			}, 500);
		})
	},
	address_input: function(frm) {
		console.log('address_input');
		if (typeof myobject != "undefined") {
			var result = myobject.find(el => el.label == cur_frm.fields_dict.address_input.$input.val());
			console.log(result);
			if (result) {
				frm.set_value("address_name", result.name);
				frm.set_value("address_postcode", result.postcode);
				frm.set_value("address_city", result.city);
				frm.set_value("address_citycode", result.citycode);
				frm.set_value("address_id", result.id);
			}
		}		
	}
});

async function doAddress(frm) {
	frappe.call({
	  method: 'autocomplete_adresse',
	  args: {arg0:cur_frm.fields_dict.address_input.$input.val()},
	  callback: function(r) {
		if (r.message!="No answer") {
			myobject = r.message.features.map(function(el) {
				return {
					label: el.properties.label,
					//lat: el.geometry.coordinates[1],
					//lon: el.geometry.coordinates[0],
					name: el.properties.name,
					postcode: el.properties.postcode,
					citycode: el.properties.citycode,
					city: el.properties.city,
					id: el.properties.id,
					boundingbox: null
				};
			});
			adresses = myobject.map(el => el.label);
			cur_frm.fields_dict.address_input.set_data(adresses);
			//console.log('adresses : ' + adresses);
		}else{
			//console.log('adresses : ' + r.message);
			cur_frm.fields_dict.address_input.set_data(r.message);
		};
	  }
	});
  };

Through the bench I looked for New Server Script

image

Then,
I called the script “autocomplete_adresse”
I selected “API”
API Method I wrote “autocomplete_adresse”

image

As for the Python script:

qry = "https://api-adresse.data.gouv.fr/search/?q=" + frappe.form_dict.arg0 + "&limit=3"
rslt0 = frappe.make_get_request(qry)

if rslt0.get('features'):
    frappe.response['message'] = rslt0
else:
    frappe.response['message'] = "No answer"

I hope that will help others!

3 Likes

How is working at the user side? Is it smooth?

1 Like

Hi Türker,

Thanks for your interest.

For me at least it is smooth.
But I am the only user!

The 500ms delay aims to:

  • limit the number of queries done by the server
    and therefore to have an appropriate user response delay.
    It is quite smooth, I also tested it from an Android phone.
    1 second is too much (impression of not working)
    250ms creates several queries while the user has not finished its first typing intention.

  • limit the number of queries to the government API server (limit of 50 requests per second - possibility to host the API through docker is then offered)
    I usually have 2 to 3 queries being sent out for one address being picked up.

Again, I’m not a developer, there must be a range for adding up efficiency.

Try it yourself, the code is straight forward.

1 Like