summaryrefslogtreecommitdiff
path: root/addons/partner_autocomplete/static/src/js
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/partner_autocomplete/static/src/js
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/partner_autocomplete/static/src/js')
-rw-r--r--addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js366
-rw-r--r--addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js337
-rw-r--r--addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js155
3 files changed, 858 insertions, 0 deletions
diff --git a/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js b/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js
new file mode 100644
index 00000000..06002dd5
--- /dev/null
+++ b/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js
@@ -0,0 +1,366 @@
+odoo.define('partner.autocomplete.Mixin', function (require) {
+'use strict';
+
+var concurrency = require('web.concurrency');
+
+var core = require('web.core');
+var Qweb = core.qweb;
+var utils = require('web.utils');
+var _t = core._t;
+
+/**
+ * This mixin only works with classes having EventDispatcherMixin in 'web.mixins'
+ */
+var PartnerAutocompleteMixin = {
+ _dropPreviousOdoo: new concurrency.DropPrevious(),
+ _dropPreviousClearbit: new concurrency.DropPrevious(),
+ _timeout : 1000, // Timeout for Clearbit autocomplete in ms
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Get list of companies via Autocomplete API
+ *
+ * @param {string} value
+ * @returns {Promise}
+ * @private
+ */
+ _autocomplete: function (value) {
+ var self = this;
+ value = value.trim();
+ var isVAT = this._isVAT(value);
+ var odooSuggestions = [];
+ var clearbitSuggestions = [];
+ return new Promise(function (resolve, reject) {
+ var odooPromise = self._getOdooSuggestions(value, isVAT).then(function (suggestions){
+ odooSuggestions = suggestions;
+ });
+
+ // Only get Clearbit suggestions if not a VAT number
+ var clearbitPromise = isVAT ? false : self._getClearbitSuggestions(value).then(function (suggestions){
+ clearbitSuggestions = suggestions;
+ });
+
+ var concatResults = function () {
+ // Add Clearbit result with Odoo result (with unique domain)
+ if (clearbitSuggestions && clearbitSuggestions.length) {
+ var websites = odooSuggestions.map(function (suggestion) {
+ return suggestion.website;
+ });
+ clearbitSuggestions.forEach(function (suggestion) {
+ if (websites.indexOf(suggestion.domain) < 0) {
+ websites.push(suggestion.domain);
+ odooSuggestions.push(suggestion);
+ }
+ });
+ }
+
+ odooSuggestions = _.filter(odooSuggestions, function (suggestion) {
+ return !suggestion.ignored;
+ });
+ _.each(odooSuggestions, function(suggestion){
+ delete suggestion.ignored;
+ });
+ return resolve(odooSuggestions);
+ };
+
+ self._whenAll([odooPromise, clearbitPromise]).then(concatResults, concatResults);
+ });
+
+ },
+
+ /**
+ * Get enrichment data
+ *
+ * @param {Object} company
+ * @param {string} company.website
+ * @param {string} company.partner_gid
+ * @param {string} company.vat
+ * @returns {Promise}
+ * @private
+ */
+ _enrichCompany: function (company) {
+ return this._rpc({
+ model: 'res.partner',
+ method: 'enrich_company',
+ args: [company.website, company.partner_gid, company.vat],
+ });
+ },
+
+ /**
+ * Get the company logo as Base 64 image from url
+ *
+ * @param {string} url
+ * @returns {Promise}
+ * @private
+ */
+ _getCompanyLogo: function (url) {
+ return this._getBase64Image(url).then(function (base64Image) {
+ // base64Image equals "data:" if image not available on given url
+ return base64Image ? base64Image.replace(/^data:image[^;]*;base64,?/, '') : false;
+ }).catch(function () {
+ return false;
+ });
+ },
+
+ /**
+ * Get enriched data + logo before populating partner form
+ *
+ * @param {Object} company
+ * @returns {Promise}
+ */
+ _getCreateData: function (company) {
+ var self = this;
+
+ var removeUselessFields = function (company) {
+ var fields = 'label,description,domain,logo,legal_name,ignored,email'.split(',');
+ fields.forEach(function (field) {
+ delete company[field];
+ });
+
+ var notEmptyFields = "country_id,state_id".split(',');
+ notEmptyFields.forEach(function (field) {
+ if (!company[field]) delete company[field];
+ });
+ };
+
+ return new Promise(function (resolve) {
+ // Fetch additional company info via Autocomplete Enrichment API
+ var enrichPromise = self._enrichCompany(company);
+
+ // Get logo
+ var logoPromise = company.logo ? self._getCompanyLogo(company.logo) : false;
+ self._whenAll([enrichPromise, logoPromise]).then(function (result) {
+ var company_data = result[0];
+ var logo_data = result[1];
+
+ // The vat should be returned for free. This is the reason why
+ // we add it into the data of 'company' even if an error such as
+ // an insufficient credit error is raised.
+ if (company_data.error && company_data.vat) {
+ company.vat = company_data.vat;
+ }
+
+ if (company_data.error) {
+ if (company_data.error_message === 'Insufficient Credit') {
+ self._notifyNoCredits();
+ } else if (company_data.error_message === 'No Account Token') {
+ self._notifyAccountToken();
+ } else {
+ self.do_notify(false, company_data.error_message);
+ }
+ company_data = company;
+ }
+
+ if (_.isEmpty(company_data)) {
+ company_data = company;
+ }
+
+ // Delete attribute to avoid "Field_changed" errors
+ removeUselessFields(company_data);
+
+ // Assign VAT coming from parent VIES VAT query
+ if (company.vat) {
+ company_data.vat = company.vat;
+ }
+ resolve({
+ company: company_data,
+ logo: logo_data
+ });
+ });
+ });
+ },
+
+ /**
+ * Check connectivity
+ *
+ * @returns {boolean}
+ */
+ _isOnline: function () {
+ return navigator && navigator.onLine;
+ },
+
+ /**
+ * Validate: Not empty and length > 1
+ *
+ * @param {string} search_val
+ * @param {string} onlyVAT : Only valid VAT Number search
+ * @returns {boolean}
+ * @private
+ */
+ _validateSearchTerm: function (search_val, onlyVAT) {
+ if (onlyVAT) return this._isVAT(search_val);
+ else return search_val && search_val.length > 2;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns a promise which will be resolved with the base64 data of the
+ * image fetched from the given url.
+ *
+ * @private
+ * @param {string} url : the url where to find the image to fetch
+ * @returns {Promise}
+ */
+ _getBase64Image: function (url) {
+ return new Promise(function (resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function () {
+ utils.getDataURLFromFile(xhr.response).then(resolve);
+ };
+ xhr.open('GET', url);
+ xhr.responseType = 'blob';
+ xhr.onerror = reject;
+ xhr.send();
+ });
+ },
+
+ /**
+ * Use Clearbit Autocomplete API to return suggestions
+ *
+ * @param {string} value
+ * @returns {Promise}
+ * @private
+ */
+ _getClearbitSuggestions: function (value) {
+ var url = 'https://autocomplete.clearbit.com/v1/companies/suggest?query=' + value;
+ var def = $.ajax({
+ url: url,
+ dataType: 'json',
+ timeout: this._timeout,
+ success: function (suggestions) {
+ suggestions.map(function (suggestion) {
+ suggestion.label = suggestion.name;
+ suggestion.website = suggestion.domain;
+ suggestion.description = suggestion.website;
+ return suggestion;
+ });
+ return suggestions;
+ },
+ });
+
+ return this._dropPreviousClearbit.add(def);
+ },
+
+ /**
+ * Use Odoo Autocomplete API to return suggestions
+ *
+ * @param {string} value
+ * @param {boolean} isVAT
+ * @returns {Promise}
+ * @private
+ */
+ _getOdooSuggestions: function (value, isVAT) {
+ var method = isVAT ? 'read_by_vat' : 'autocomplete';
+
+ var def = this._rpc({
+ model: 'res.partner',
+ method: method,
+ args: [value],
+ }, {
+ shadow: true,
+ }).then(function (suggestions) {
+ suggestions.map(function (suggestion) {
+ suggestion.logo = suggestion.logo || '';
+ suggestion.label = suggestion.legal_name || suggestion.name;
+ if (suggestion.vat) suggestion.description = suggestion.vat;
+ else if (suggestion.website) suggestion.description = suggestion.website;
+
+ if (suggestion.country_id && suggestion.country_id.display_name) {
+ if (suggestion.description) suggestion.description += _.str.sprintf(' (%s)', suggestion.country_id.display_name);
+ else suggestion.description += suggestion.country_id.display_name;
+ }
+
+ return suggestion;
+ });
+ return suggestions;
+ });
+
+ return this._dropPreviousOdoo.add(def);
+ },
+ /**
+ * Check if searched value is possibly a VAT : 2 first chars = alpha + min 5 numbers
+ *
+ * @param {string} search_val
+ * @returns {boolean}
+ * @private
+ */
+ _isVAT: function (search_val) {
+ var str = this._sanitizeVAT(search_val);
+ return checkVATNumber(str);
+ },
+
+ /**
+ * Sanitize search value by removing all not alphanumeric
+ *
+ * @param {string} search_value
+ * @returns {string}
+ * @private
+ */
+ _sanitizeVAT: function (search_value) {
+ return search_value ? search_value.replace(/[^A-Za-z0-9]/g, '') : '';
+ },
+
+ /**
+ * Utility to wait for multiple promises
+ * Promise.all will reject all promises whenever a promise is rejected
+ * This utility will continue
+ *
+ * @param {Promise[]} promises
+ * @returns {Promise}
+ * @private
+ */
+ _whenAll: function (promises) {
+ return Promise.all(promises.map(function (p) {
+ return Promise.resolve(p);
+ }));
+ },
+
+ /**
+ * @private
+ * @returns {Promise}
+ */
+ _notifyNoCredits: function () {
+ var self = this;
+ return this._rpc({
+ model: 'iap.account',
+ method: 'get_credits_url',
+ args: ['partner_autocomplete'],
+ }).then(function (url) {
+ var title = _t('Not enough credits for Partner Autocomplete');
+ var content = Qweb.render('partner_autocomplete.insufficient_credit_notification', {
+ credits_url: url
+ });
+ self.do_notify(title, content, false, 'o_partner_autocomplete_no_credits_notify');
+ });
+ },
+
+ _notifyAccountToken: function () {
+ var self = this;
+ return this._rpc({
+ model: 'iap.account',
+ method: 'get_config_account_url',
+ args: []
+ }).then(function (url) {
+ var title = _t('IAP Account Token missing');
+ if (url){
+ var content = Qweb.render('partner_autocomplete.account_token', {
+ account_url: url
+ });
+ self.do_notify(title, content, false, 'o_partner_autocomplete_no_credits_notify');
+ }
+ else {
+ self.do_notify(title);
+ }
+ });
+ },
+};
+
+return PartnerAutocompleteMixin;
+
+});
diff --git a/addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js b/addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js
new file mode 100644
index 00000000..37a2b19c
--- /dev/null
+++ b/addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js
@@ -0,0 +1,337 @@
+odoo.define('partner.autocomplete.fieldchar', function (require) {
+'use strict';
+
+var basic_fields = require('web.basic_fields');
+var core = require('web.core');
+var field_registry = require('web.field_registry');
+var AutocompleteMixin = require('partner.autocomplete.Mixin');
+
+var QWeb = core.qweb;
+
+var FieldChar = basic_fields.FieldChar;
+
+/**
+ * FieldChar extension to suggest existing companies when changing the company
+ * name on a res.partner view (indeed, it is designed to change the "name",
+ * "website" and "image" fields of records of this model).
+ */
+var FieldAutocomplete = FieldChar.extend(AutocompleteMixin, {
+ className: 'o_field_partner_autocomplete',
+ debounceSuggestions: 400,
+ resetOnAnyFieldChange: true,
+
+ jsLibs: [
+ '/partner_autocomplete/static/lib/jsvat.js'
+ ],
+
+ events: _.extend({}, FieldChar.prototype.events, {
+ 'keyup': '_onKeyup',
+ 'mousedown .o_partner_autocomplete_suggestion': '_onMousedown',
+ 'focusout': '_onFocusout',
+ 'mouseenter .o_partner_autocomplete_suggestion': '_onHoverDropdown',
+ 'click .o_partner_autocomplete_suggestion': '_onSuggestionClicked',
+ }),
+
+ /**
+ * @constructor
+ * Prepares the basic rendering of edit mode by setting the root to be a
+ * div.dropdown.open.
+ * @see FieldChar.init
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+
+ // If the autocomplete is applied to vat field, only search valid vat number
+ this.onlyVAT = this.name === 'vat';
+
+ if (this.mode === 'edit') {
+ this.tagName = 'div';
+ this.className += ' dropdown open';
+ }
+
+ if (this.debounceSuggestions > 0) {
+ this._suggestCompanies = _.debounce(this._suggestCompanies.bind(this), this.debounceSuggestions);
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Check if the autocomplete should be active
+ * Active :
+ * - only when creating new record
+ * - on model res.partner and is_company=true
+ * - on model res.company
+ *
+ * @returns {boolean}
+ * @private
+ */
+ _isActive: function () {
+ return this.model === 'res.company' ||
+ (
+ this.model === 'res.partner'
+ && this.record.data.is_company
+ && !(this.record.data && this.record.data.id)
+ );
+ },
+
+ /**
+ *
+ * @private
+ */
+ _removeDropdown: function () {
+ if (this.$dropdown) {
+ this.$dropdown.remove();
+ this.$dropdown = undefined;
+ }
+ },
+
+ /**
+ * Adds the <input/> element and prepares it. Note: the dropdown rendering
+ * is handled outside of the rendering routine (but instead by reacting to
+ * user input).
+ *
+ * @override
+ * @private
+ */
+ _renderEdit: function () {
+ this.$el.empty();
+ // Prepare and add the input
+ this._prepareInput().appendTo(this.$el);
+ },
+
+ /**
+ * Selects the given company suggestions by notifying changes to the view
+ * for the "name", "website" and "image" fields. This is of course intended
+ * to work only with the "res.partner" form view.
+ *
+ * @private
+ * @param {Object} company
+ */
+ _selectCompany: function (company) {
+ var self = this;
+ this._getCreateData(company).then(function (data) {
+ if (data.logo) {
+ var logoField = self.model === 'res.partner' ? 'image_1920' : 'logo';
+ data.company[logoField] = data.logo;
+ }
+
+ // Some fields are unnecessary in res.company
+ if (self.model === 'res.company') {
+ var fields = 'comment,child_ids,bank_ids,additional_info'.split(',');
+ fields.forEach(function (field) {
+ delete data.company[field];
+ });
+ }
+
+ self._setOne2ManyField('bank_ids', data.company.bank_ids);
+ delete data.company.bank_ids;
+
+ self.trigger_up('field_changed', {
+ dataPointID: self.dataPointID,
+ changes: data.company,
+ onSuccess: function () {
+ // update the input's value directly
+ if (self.onlyVAT)
+ self.$input.val(self._formatValue(company.vat));
+ else
+ self.$input.val(self._formatValue(company.name));
+ },
+ });
+ });
+ this._removeDropdown();
+ },
+
+ _setOne2ManyField: function (field, list) {
+ var self = this;
+ var viewType = this.record.viewType;
+ if (list && this.record.fieldsInfo[viewType] && this.record.fieldsInfo[viewType][field]) {
+ list.forEach(function (item) {
+ var changes = {};
+ changes[field] = {
+ operation: 'CREATE',
+ data: item,
+ };
+
+ self.trigger_up('field_changed', {
+ dataPointID: self.dataPointID,
+ changes: changes,
+ });
+ });
+ }
+ },
+
+ /**
+ * Shows the dropdown with the suggestions. If one is
+ * already opened, it removes the old one before rerendering the dropdown.
+ *
+ * @private
+ */
+ _showDropdown: function () {
+ this._removeDropdown();
+ if (this.suggestions.length > 0) {
+ this.$dropdown = $(QWeb.render('partner_autocomplete.dropdown', {
+ suggestions: this.suggestions,
+ }));
+ this.$dropdown.appendTo(this.$el);
+ }
+ },
+
+ /**
+ * Shows suggestions according to the given value.
+ * Note: this method is debounced (@see init).
+ *
+ * @private
+ * @param {string} value - searched term
+ */
+ _suggestCompanies: function (value) {
+ var self = this;
+ if (this._validateSearchTerm(value, this.onlyVAT) && this._isOnline()) {
+ return this._autocomplete(value).then(function (suggestions) {
+ if (suggestions && suggestions.length) {
+ self.suggestions = suggestions;
+ self._showDropdown();
+ } else {
+ self._removeDropdown();
+ }
+ });
+ } else {
+ this._removeDropdown();
+ }
+ },
+
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called on focusout -> removes the suggestions dropdown.
+ *
+ * @private
+ */
+ _onFocusout: function () {
+ this._removeDropdown();
+ },
+
+ /**
+ * Called when hovering a suggestion in the dropdown -> sets it as active.
+ *
+ * @private
+ * @param {Event} e
+ */
+ _onHoverDropdown: function (e) {
+ this.$dropdown.find('.active').removeClass('active');
+ $(e.currentTarget).parent().addClass('active');
+ },
+
+ /**
+ * @override of FieldChar (called when the user is typing text)
+ * Checks the <input/> value and shows suggestions according to
+ * this value.
+ *
+ * @private
+ */
+ _onInput: function () {
+ this._super.apply(this, arguments);
+ if (this._isActive()) {
+ this._suggestCompanies(this.$input.val());
+ }
+ },
+
+ /**
+ * @override of FieldChar
+ * Changes the "up" and "down" key behavior when the dropdown is opened (to
+ * navigate through dropdown suggestions).
+ * Triggered by keydown to execute the navigation multiple times when the
+ * user keeps the "down" or "up" pressed.
+ *
+ * @private
+ * @param {Event} e
+ */
+ _onKeydown: function (e) {
+ switch (e.which) {
+ case $.ui.keyCode.UP:
+ case $.ui.keyCode.DOWN:
+ if (!this.$dropdown) {
+ break;
+ }
+ e.preventDefault();
+ var $suggestions = this.$dropdown.children();
+ var $active = $suggestions.filter('.active');
+ var $to;
+ if ($active.length) {
+ $to = e.which === $.ui.keyCode.DOWN ?
+ $active.next() :
+ $active.prev();
+ } else {
+ $to = $suggestions.first();
+ }
+ if ($to.length) {
+ $active.removeClass('active');
+ $to.addClass('active');
+ }
+ return;
+ }
+ this._super.apply(this, arguments);
+ },
+
+ /**
+ * Called on keyup events to:
+ * -> remove the suggestions dropdown when hitting the "escape" key
+ * -> select the highlighted suggestion when hitting the "enter" key
+ *
+ * @private
+ * @param {Event} e
+ */
+ _onKeyup: function (e) {
+ switch (e.which) {
+ case $.ui.keyCode.ESCAPE:
+ e.preventDefault();
+ this._removeDropdown();
+ break;
+ case $.ui.keyCode.ENTER:
+ if (!this.$dropdown) {
+ break;
+ }
+ e.preventDefault();
+ var $active = this.$dropdown.find('.o_partner_autocomplete_suggestion.active');
+ if (!$active.length) {
+ return;
+ }
+ this._selectCompany(this.suggestions[$active.data('index')]);
+ break;
+ }
+ },
+
+ /**
+ * Called on mousedown event on a suggestion -> prevent default
+ * action so that the <input/> element does not lose the focus.
+ *
+ * @private
+ * @param {Event} e
+ */
+ _onMousedown: function (e) {
+ e.preventDefault(); // prevent losing focus on suggestion click
+ },
+
+ /**
+ * Called when a dropdown suggestion is clicked -> trigger_up changes for
+ * some fields in the view (not only this <input/> one) with the associated
+ * data (@see _selectCompany).
+ *
+ * @private
+ * @param {Event} e
+ */
+ _onSuggestionClicked: function (e) {
+ e.preventDefault();
+ this._selectCompany(this.suggestions[$(e.currentTarget).data('index')]);
+ },
+});
+
+field_registry.add('field_partner_autocomplete', FieldAutocomplete);
+
+return FieldAutocomplete;
+});
diff --git a/addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js b/addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js
new file mode 100644
index 00000000..856bcf65
--- /dev/null
+++ b/addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js
@@ -0,0 +1,155 @@
+odoo.define('partner.autocomplete.many2one', function (require) {
+'use strict';
+
+var FieldMany2One = require('web.relational_fields').FieldMany2One;
+var core = require('web.core');
+var AutocompleteMixin = require('partner.autocomplete.Mixin');
+var field_registry = require('web.field_registry');
+
+var _t = core._t;
+
+var PartnerField = FieldMany2One.extend(AutocompleteMixin, {
+ jsLibs: [
+ '/partner_autocomplete/static/lib/jsvat.js'
+ ],
+
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this._addAutocompleteSource(this._searchSuggestions, {
+ placeholder: _t('Searching Autocomplete...'),
+ order: 20,
+ validation: this._validateSearchTerm,
+ });
+
+ this.additionalContext['show_vat'] = true;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Action : create popup form with pre-filled values from Autocomplete
+ *
+ * @param {Object} company
+ * @returns {Promise}
+ * @private
+ */
+ _createPartner: function (company) {
+ var self = this;
+ self.$('input').val('');
+
+ return self._getCreateData(company).then(function (data){
+ var context = {
+ 'default_is_company': true
+ };
+ _.each(data.company, function (val, key) {
+ context['default_' + key] = val && val.id ? val.id : val;
+ });
+
+ // if(data.company.street_name && !data.company.street_number) context.default_street_number = '';
+ if (data.logo) context.default_image_1920 = data.logo;
+
+ return self._searchCreatePopup("form", false, context);
+ });
+ },
+
+ /**
+ * Returns the display_name from a string which contains it but was altered
+ * as a result of the show_vat option.
+ * Note that the split is done on a 'figuredash', not a standard dash.
+ *
+ * @private
+ * @param {string} value
+ * @returns {string} display_name without TaxID
+ */
+ _getDisplayNameWithoutVAT: function (value) {
+ return value.split(' ‒ ')[0];
+ },
+
+ /**
+ * Modify autocomplete results rendering
+ * Add logo in the autocomplete results if logo is provided
+ *
+ * @private
+ */
+ _modifyAutompleteRendering: function (){
+ var api = this.$input.data('ui-autocomplete');
+ // FIXME: bugfix to prevent traceback in mobile apps due to override
+ // of Many2one widget with native implementation.
+ if (!api) {
+ return;
+ }
+ api._renderItem = function(ul, item){
+ ul.addClass('o_partner_autocomplete_dropdown');
+ var $a = $('<a/>')["html"](item.label);
+ if (item.logo){
+ var $img = $('<img/>').attr('src', item.logo);
+ $a.append($img);
+ }
+
+ return $("<li></li>")
+ .data("item.autocomplete",item)
+ .append($a)
+ .appendTo(ul)
+ .addClass(item.classname);
+ };
+ },
+
+ /**
+ * @override
+ * @private
+ */
+ _renderEdit: function (){
+ this.m2o_value = this._getDisplayNameWithoutVAT(this.m2o_value);
+ this._super.apply(this, arguments);
+ this._modifyAutompleteRendering();
+ },
+
+ /**
+ * Query Autocomplete and add results to the popup
+ *
+ * @override
+ * @param search_val {string}
+ * @returns {Promise}
+ * @private
+ */
+ _searchSuggestions: function (search_val) {
+ var self = this;
+ return new Promise(function (resolve, reject) {
+ if (self._isOnline()) {
+
+ self._autocomplete(search_val).then(function (suggestions) {
+ var choices = [];
+ if (suggestions && suggestions.length) {
+ _.each(suggestions, function (suggestion) {
+ var label = '<i class="fa fa-magic text-muted"/> ';
+ label += _.str.sprintf('%s, <span class="text-muted">%s</span>', suggestion.label, suggestion.description);
+
+ choices.push({
+ label: label,
+ action: function () {
+ self._createPartner(suggestion);
+ },
+ logo: suggestion.logo,
+ classname: 'o_partner_autocomplete_dropdown_item',
+ });
+ });
+ }
+
+ resolve(choices);
+ });
+ } else {
+ resolve([]);
+ }
+ });
+ },
+});
+
+field_registry.add('res_partner_many2one', PartnerField);
+
+return PartnerField;
+});