diff options
Diffstat (limited to 'addons/partner_autocomplete/static/src')
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 ' '"/></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> |
