summaryrefslogtreecommitdiff
path: root/addons/partner_autocomplete/static/src
diff options
context:
space:
mode:
Diffstat (limited to 'addons/partner_autocomplete/static/src')
-rw-r--r--addons/partner_autocomplete/static/src/js/iap_credit_checker_widget.js94
-rw-r--r--addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js331
-rw-r--r--addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js339
-rw-r--r--addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js138
-rw-r--r--addons/partner_autocomplete/static/src/scss/partner_autocomplete.scss28
-rw-r--r--addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml33
6 files changed, 963 insertions, 0 deletions
diff --git a/addons/partner_autocomplete/static/src/js/iap_credit_checker_widget.js b/addons/partner_autocomplete/static/src/js/iap_credit_checker_widget.js
new file mode 100644
index 00000000000..ab25dab0029
--- /dev/null
+++ b/addons/partner_autocomplete/static/src/js/iap_credit_checker_widget.js
@@ -0,0 +1,94 @@
+odoo.define('iap.credit.checker', function (require) {
+'use strict';
+
+var widgetRegistry = require('web.widget_registry');
+var Widget = require('web.Widget');
+
+var core = require('web.core');
+var rpc = require('web.rpc');
+
+var QWeb = core.qweb;
+
+var IAPCreditChecker = Widget.extend({
+ className: 'o_field_iap_credit_checker',
+
+ /**
+ * @constructor
+ * Prepares the basic rendering of edit mode by setting the root to be a
+ * div.dropdown.open.
+ * @see FieldChar.init
+ */
+ init: function (parent, data, options) {
+ this._super.apply(this, arguments);
+ this.service_name = options.attrs.service_name;
+ },
+
+ /**
+ * @override
+ */
+ start: function () {
+ this.$widget = $(QWeb.render('partner_autocomplete.iap_credit_checker'));
+ this.$loading = this.$widget.find('.loading');
+ this.$sufficient = this.$widget.find('.sufficient');
+ this.$insufficient = this.$widget.find('.insufficient');
+ this.$buyLink = this.$widget.find('.oe_link');
+
+ this.$widget.appendTo(this.$el);
+
+ this._getLink();
+ this._getCredits();
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+ _getCredits: function () {
+ var self = this;
+ this._showLoading();
+
+ return rpc.query({
+ model: 'iap.account',
+ method: 'get_credits',
+ args: [this.service_name],
+ }, {
+ shadow: true,
+ }).then(function (credit) {
+ if (credit) self._showSufficient(credit);
+ else self._showInsufficient();
+ });
+ },
+
+ _getLink: function () {
+ var self = this;
+ return rpc.query({
+ model: 'iap.account',
+ method: 'get_credits_url',
+ args: [this.service_name],
+ }, {
+ shadow: true,
+ }).then(function (url) {
+ self.$buyLink.attr('href', url);
+ });
+ },
+
+ _showLoading: function () {
+ this.$loading.show();
+ this.$sufficient.hide();
+ this.$insufficient.hide();
+ },
+ _showSufficient: function (credits) {
+ this.$loading.hide();
+ this.$sufficient.show().find('.remaining_credits').text(credits);
+ this.$insufficient.hide();
+ },
+ _showInsufficient: function () {
+ this.$loading.hide();
+ this.$sufficient.hide();
+ this.$insufficient.show();
+ },
+});
+
+widgetRegistry.add('iap_credit_checker', IAPCreditChecker);
+
+return IAPCreditChecker;
+});
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 00000000000..05bd10c9114
--- /dev/null
+++ b/addons/partner_autocomplete/static/src/js/partner_autocomplete_core.js
@@ -0,0 +1,331 @@
+odoo.define('partner.autocomplete.core', function (require) {
+'use strict';
+
+var rpc = require('web.rpc');
+var concurrency = require('web.concurrency');
+
+return {
+ _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 {Deferred}
+ * @private
+ */
+ autocomplete: function (value) {
+ value = value.trim();
+ var def = $.Deferred(),
+ isVAT = this._isVAT(value),
+ odooSuggestions = [],
+ clearbitSuggestions = [];
+
+ var odooPromise = this._getOdooSuggestions(value, isVAT).then(function (suggestions){
+ odooSuggestions = suggestions;
+ });
+
+ // Only get Clearbit suggestions if not a VAT number
+ var clearbitPromise = isVAT ? false : this._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.website) < 0) {
+ odooSuggestions.push(suggestion);
+ }
+ });
+ }
+
+ return def.resolve(odooSuggestions);
+ };
+
+ this._whenAll([odooPromise, clearbitPromise]).then(concatResults, concatResults);
+
+ return def;
+ },
+
+ /**
+ * Get enrichment data
+ *
+ * @param {Object} company
+ * @returns {Deferred}
+ * @private
+ */
+ enrichCompany: function (company) {
+ return rpc.query({
+ 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 {Deferred}
+ * @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;
+ });
+ },
+
+ /**
+ * Get enriched data + logo before populating partner form
+ *
+ * @param {Object} company
+ * @returns {Deferred}
+ */
+ getCreateData: function (company) {
+ var removeUselessFields = function (company) {
+ var fields = 'label,description,domain,logo,legal_name'.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];
+ });
+ };
+
+ var def = $.Deferred();
+
+ // Fetch additional company info via Autocomplete Enrichment API
+ var enrichPromise = this.enrichCompany(company);
+
+ // Get logo
+ var logoPromise = company.logo ? this.getCompanyLogo(company.logo) : false;
+
+ $.when(enrichPromise, logoPromise).done(function (company_data, logo_data) {
+ 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;
+ def.resolve({
+ company: company_data,
+ logo: logo_data
+ });
+ });
+
+ return def;
+ },
+
+ /**
+ * 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 deferred 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 {Deferred}
+ */
+ _getBase64Image: function (url) {
+ var def = $.Deferred();
+ var xhr = new XMLHttpRequest();
+ xhr.onload = function () {
+ var reader = new FileReader();
+ reader.onloadend = function () {
+ def.resolve(reader.result);
+ };
+ reader.readAsDataURL(xhr.response);
+ };
+ xhr.open('GET', url);
+ xhr.responseType = 'blob';
+ xhr.onerror = def.reject.bind(def);
+ xhr.send();
+ return def;
+ },
+
+ /**
+ * Use Clearbit Autocomplete API to return suggestions
+ *
+ * @param {string} value
+ * @returns {Deferred}
+ * @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 {Deferred}
+ * @private
+ */
+ _getOdooSuggestions: function (value, isVAT) {
+ var method = isVAT ? 'read_by_vat' : 'autocomplete';
+
+ var def = rpc.query({
+ 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
+ * $.when will reject all promises whenever a promise is rejected
+ * This utility will continue
+ * Source : https://gist.github.com/fearphage/4341799
+ *
+ * @param array
+ * @returns {*}
+ * @private
+ */
+ _whenAll: function (array) {
+ var slice = [].slice;
+
+ var
+ resolveValues = arguments.length === 1 && $.isArray(array)
+ ? array
+ : slice.call(arguments)
+ , length = resolveValues.length
+ , remaining = length
+ , deferred = $.Deferred()
+ , i = 0
+ , failed = 0
+ , rejectContexts = Array(length)
+ , rejectValues = Array(length)
+ , resolveContexts = Array(length)
+ , value
+ ;
+
+ function updateFunc(index, contexts, values) {
+ return function () {
+ !(values === resolveValues) && failed++;
+ deferred.notifyWith(
+ contexts[index] = this
+ , values[index] = slice.call(arguments)
+ );
+ if (!(--remaining)) {
+ deferred[(!failed ? 'resolve' : 'reject') + 'With'](contexts, values);
+ }
+ };
+ }
+
+ for (; i < length; i++) {
+ if ((value = resolveValues[i]) && $.isFunction(value.promise)) {
+ value.promise()
+ .done(updateFunc(i, resolveContexts, resolveValues))
+ .fail(updateFunc(i, rejectContexts, rejectValues))
+ ;
+ }
+ else {
+ deferred.notifyWith(this, value);
+ --remaining;
+ }
+ }
+
+ if (!remaining) {
+ deferred.resolveWith(resolveContexts, resolveValues);
+ }
+
+ return deferred.promise();
+ },
+};
+});
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 00000000000..b1843ec8de1
--- /dev/null
+++ b/addons/partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js
@@ -0,0 +1,339 @@
+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 Autocomplete = require('partner.autocomplete.core');
+
+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({
+ 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;
+ Autocomplete.getCreateData(company).then(function (data) {
+ if (data.logo) {
+ var logoField = self.model === 'res.partner' ? 'image' : '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('child_ids', data.company.child_ids);
+ delete data.company.child_ids;
+
+ self._setOne2ManyField('bank_ids', data.company.bank_ids);
+ delete data.company.bank_ids;
+
+ self.trigger_up('field_changed', {
+ dataPointID: self.dataPointID,
+ changes: data.company,
+ });
+ });
+
+ // update the input's value directly
+ if (this.onlyVAT)
+ this.$input.val(this._formatValue(company.vat));
+ else
+ this.$input.val(this._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 (Autocomplete.validateSearchTerm(value, this.onlyVAT) && Autocomplete.isOnline()) {
+ return Autocomplete.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 00000000000..40780df75ce
--- /dev/null
+++ b/addons/partner_autocomplete/static/src/js/partner_autocomplete_many2one.js
@@ -0,0 +1,138 @@
+odoo.define('partner.autocomplete.many2one', function (require) {
+'use strict';
+
+var FieldMany2One = require('web.relational_fields').FieldMany2One;
+var core = require('web.core');
+var Autocomplete = require('partner.autocomplete.core');
+var field_registry = require('web.field_registry');
+
+var _t = core._t;
+
+var PartnerField = FieldMany2One.extend({
+ 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: Autocomplete.validateSearchTerm,
+ });
+
+ this.additionalContext['show_vat'] = true;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Action : create popup form with pre-filled values from Autocomplete
+ *
+ * @param {Object} company
+ * @returns {Deferred}
+ * @private
+ */
+ _createPartner: function (company) {
+ var self = this;
+ self.$('input').val('');
+
+ return Autocomplete.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 = data.logo;
+
+ return self._searchCreatePopup("form", false, context);
+ });
+ },
+
+ /**
+ * Modify autocomplete results rendering
+ * Add logo in the autocomplete results if logo is provided
+ *
+ * @private
+ */
+ _modifyAutompleteRendering: function (){
+ var api = this.$input.data('ui-autocomplete');
+ var renderWithoutLogo = api._renderItem;
+ api._renderItem = function ( ul, item ) {
+ var $li = renderWithoutLogo.call(this, ul, item);
+ if (item.logo){
+ var $a = $li.find('>a').addClass('o_partner_autocomplete_dropdown_item');
+ var $img = $('<img/>').attr('src', item.logo);
+ $a.append($img);
+ }
+
+ return $li;
+ };
+ },
+
+ /**
+ * @override
+ * @private
+ */
+ _renderEdit: function (){
+ this._super.apply(this, arguments);
+ this._modifyAutompleteRendering();
+ },
+
+ /**
+ * Query Autocomplete and add results to the popup
+ *
+ * @override
+ * @param search_val {string}
+ * @returns {Deferred}
+ * @private
+ */
+ _searchSuggestions: function (search_val) {
+ var def = $.Deferred();
+
+ if (Autocomplete.isOnline()) {
+ var self = this;
+
+ Autocomplete.autocomplete(search_val).then(function (suggestions) {
+ var choices = [];
+ if (suggestions && suggestions.length) {
+ choices.push({
+ label: _t('Create and Edit from Autocomplete :'),
+ });
+ _.each(suggestions, function (suggestion) {
+ var label =_.str.sprintf('%s - %s', suggestion.label, suggestion.description);
+ label = label.replace(new RegExp(search_val, "gi"), "<b>$&</b>");
+
+ choices.push({
+ label: label,
+ action: function () {
+ self._createPartner(suggestion);
+ },
+ classname: 'o_m2o_dropdown_option',
+ logo: suggestion.logo,
+ });
+ });
+ }
+
+ def.resolve(choices);
+ });
+ } else {
+ def.resolve([]);
+ }
+
+ return def;
+ },
+});
+
+field_registry.add('res_partner_many2one', PartnerField);
+
+return PartnerField;
+});
diff --git a/addons/partner_autocomplete/static/src/scss/partner_autocomplete.scss b/addons/partner_autocomplete/static/src/scss/partner_autocomplete.scss
new file mode 100644
index 00000000000..a63e2434b6f
--- /dev/null
+++ b/addons/partner_autocomplete/static/src/scss/partner_autocomplete.scss
@@ -0,0 +1,28 @@
+.o_field_partner_autocomplete.dropdown {
+ display: block;
+
+ > .o_partner_autocomplete_dropdown .dropdown-item {
+ min-width: 300px;
+ padding: 4px 8px;
+ > img {
+ float: left;
+ width: 36px;
+ height: 36px;
+ }
+ > .o_partner_autocomplete_info {
+ margin-left: 50px;
+ > * {
+ @include o-text-overflow(block);
+ }
+ }
+ }
+}
+
+.ui-autocomplete .ui-menu-item > a.o_partner_autocomplete_dropdown_item {
+ > img {
+ float: right;
+ width: 20px;
+ height: 20px;
+ }
+
+} \ No newline at end of file
diff --git a/addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml b/addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml
new file mode 100644
index 00000000000..bd6e85c8779
--- /dev/null
+++ b/addons/partner_autocomplete/static/src/xml/partner_autocomplete.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<templates>
+ <div t-name="partner_autocomplete.dropdown" class="o_partner_autocomplete_dropdown dropdown-menu show" role="menu">
+ <t t-foreach="suggestions" t-as="info">
+ <a role="menuitem" href="#"
+ t-attf-class="dropdown-item o_partner_autocomplete_suggestion clearfix#{info_index == 0 and ' active' or ''}"
+ t-att-data-index="info_index">
+ <img t-att-src="info['logo']" onerror="this.src='/base/static/img/company_image.png'" alt="Placeholder"/>
+ <div class="o_partner_autocomplete_info">
+ <strong><t t-esc="info['label'] or '&#160;'"/></strong>
+ <div><t t-esc="info['description']"/></div>
+ </div>
+ </a>
+ </t>
+ </div>
+
+ <div t-name="partner_autocomplete.iap_credit_checker" class="mt-2 row">
+ <div class="col-sm">
+ <div class="loading text-muted">
+ <i class="fa fa-spinner fa-spin"/> Checking remaining credit ...
+ </div>
+ <div class="insufficient">
+ <i class="fa fa-exclamation-triangle text-warning"/> Insufficient credit<br/>
+ </div>
+ <div class="sufficient">
+ <i class="fa fa-check-circle text-success"/> Remaining : <b class="remaining_credits"/> credits
+ </div>
+ </div>
+ <div class="col-sm text-right">
+ <a class="oe_link" target="_blank"><i class="fa fa-arrow-right"/> Buy more credits</a>
+ </div>
+ </div>
+</templates>