[IMP] web,partner_autocomplete: Owl conversion of partner autocomplete widgets
The `field_partner_autocomplete` & `res_partner_many2one` widgets are now converted to Owl. It required some changes in the `Autocomplete` component as we needed to access the ref of the `<input/>` element to pass it to the `useInputField` of `CharField`. The hook `useInputField` was also modified to allow passing a ref directly as parameter instead of a ref name. X-original-commit: 0b8c86bc5b19baccf7324fd4b18d438278816a5a Part-of: odoo/odoo#102220
This commit is contained in:
@@ -23,12 +23,10 @@
|
||||
'auto_install': True,
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'partner_autocomplete/static/src/scss/partner_autocomplete.scss',
|
||||
'partner_autocomplete/static/src/js/partner_autocomplete_core.js',
|
||||
'partner_autocomplete/static/src/js/partner_autocomplete_fieldchar.js',
|
||||
'partner_autocomplete/static/src/js/partner_autocomplete_many2one.js',
|
||||
'partner_autocomplete/static/src/js/web_company_autocomplete.js',
|
||||
'partner_autocomplete/static/src/xml/**/*',
|
||||
'partner_autocomplete/static/lib/jsvat.js',
|
||||
'partner_autocomplete/static/src/scss/*',
|
||||
'partner_autocomplete/static/src/js/*',
|
||||
'partner_autocomplete/static/src/xml/*',
|
||||
],
|
||||
'web.tests_assets': [
|
||||
'partner_autocomplete/static/lib/**/*',
|
||||
|
||||
@@ -1,56 +1,52 @@
|
||||
/* global checkVATNumber */
|
||||
odoo.define('partner.autocomplete.Mixin', function (require) {
|
||||
'use strict';
|
||||
/** @odoo-module **/
|
||||
|
||||
var concurrency = require('web.concurrency');
|
||||
|
||||
var core = require('web.core');
|
||||
var Qweb = core.qweb;
|
||||
var utils = require('web.utils');
|
||||
var _t = core._t;
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { KeepLast } from "@web/core/utils/concurrency";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { renderToMarkup } from "@web/core/utils/render";
|
||||
import { getDataURLFromFile } from "@web/core/utils/urls";
|
||||
|
||||
/**
|
||||
* This mixin only works with classes having EventDispatcherMixin in 'web.mixins'
|
||||
* Get list of companies via Autocomplete API
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns {Promise}
|
||||
* @private
|
||||
*/
|
||||
var PartnerAutocompleteMixin = {
|
||||
_dropPreviousOdoo: new concurrency.DropPrevious(),
|
||||
_dropPreviousClearbit: new concurrency.DropPrevious(),
|
||||
_timeout : 1000, // Timeout for Clearbit autocomplete in ms
|
||||
export function usePartnerAutocomplete() {
|
||||
const keepLastOdoo = new KeepLast();
|
||||
const keepLastClearbit = new KeepLast();
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
const http = useService("http");
|
||||
const notification = useService("notification");
|
||||
const orm = useService("orm");
|
||||
|
||||
/**
|
||||
* Get list of companies via Autocomplete API
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns {Promise}
|
||||
* @private
|
||||
*/
|
||||
_autocomplete: function (value) {
|
||||
var self = this;
|
||||
function autocomplete(value, isVAT = false) {
|
||||
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){
|
||||
let odooSuggestions = [];
|
||||
let clearbitSuggestions = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
const odooPromise = getOdooSuggestions(value, isVAT).then((suggestions) => {
|
||||
odooSuggestions = suggestions;
|
||||
});
|
||||
|
||||
// Only get Clearbit suggestions if not a VAT number
|
||||
var clearbitPromise = isVAT ? false : self._getClearbitSuggestions(value).then(function (suggestions){
|
||||
const clearbitPromise = isVAT ? false : getClearbitSuggestions(value).then((suggestions) => {
|
||||
suggestions.forEach((suggestion) => {
|
||||
suggestion.label = suggestion.name;
|
||||
suggestion.website = suggestion.domain;
|
||||
suggestion.description = suggestion.website;
|
||||
});
|
||||
clearbitSuggestions = suggestions;
|
||||
});
|
||||
|
||||
var concatResults = function () {
|
||||
const concatResults = () => {
|
||||
// Add Clearbit result with Odoo result (with unique domain)
|
||||
if (clearbitSuggestions && clearbitSuggestions.length) {
|
||||
var websites = odooSuggestions.map(function (suggestion) {
|
||||
const websites = odooSuggestions.map((suggestion) => {
|
||||
return suggestion.website;
|
||||
});
|
||||
clearbitSuggestions.forEach(function (suggestion) {
|
||||
clearbitSuggestions.forEach((suggestion) => {
|
||||
if (websites.indexOf(suggestion.domain) < 0) {
|
||||
websites.push(suggestion.domain);
|
||||
odooSuggestions.push(suggestion);
|
||||
@@ -58,19 +54,18 @@ var PartnerAutocompleteMixin = {
|
||||
});
|
||||
}
|
||||
|
||||
odooSuggestions = _.filter(odooSuggestions, function (suggestion) {
|
||||
odooSuggestions = odooSuggestions.filter((suggestion) => {
|
||||
return !suggestion.ignored;
|
||||
});
|
||||
_.each(odooSuggestions, function(suggestion){
|
||||
delete suggestion.ignored;
|
||||
odooSuggestions.forEach((suggestion) => {
|
||||
delete suggestion.ignored;
|
||||
});
|
||||
return resolve(odooSuggestions);
|
||||
};
|
||||
|
||||
self._whenAll([odooPromise, clearbitPromise]).then(concatResults, concatResults);
|
||||
whenAll([odooPromise, clearbitPromise]).then(concatResults, concatResults);
|
||||
});
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enrichment data
|
||||
@@ -82,13 +77,13 @@ var PartnerAutocompleteMixin = {
|
||||
* @returns {Promise}
|
||||
* @private
|
||||
*/
|
||||
_enrichCompany: function (company) {
|
||||
return this._rpc({
|
||||
model: 'res.partner',
|
||||
method: 'enrich_company',
|
||||
args: [company.website, company.partner_gid, company.vat],
|
||||
});
|
||||
},
|
||||
function enrichCompany(company) {
|
||||
return orm.call(
|
||||
'res.partner',
|
||||
'enrich_company',
|
||||
[company.website, company.partner_gid, company.vat]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the company logo as Base 64 image from url
|
||||
@@ -97,14 +92,16 @@ var PartnerAutocompleteMixin = {
|
||||
* @returns {Promise}
|
||||
* @private
|
||||
*/
|
||||
_getCompanyLogo: function (url) {
|
||||
return this._getBase64Image(url).then(function (base64Image) {
|
||||
async function getCompanyLogo(url) {
|
||||
try {
|
||||
const base64Image = await getBase64Image(url)
|
||||
// base64Image equals "data:" if image not available on given url
|
||||
return base64Image ? base64Image.replace(/^data:image[^;]*;base64,?/, '') : false;
|
||||
}).catch(function () {
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enriched data + logo before populating partner form
|
||||
@@ -112,54 +109,54 @@ var PartnerAutocompleteMixin = {
|
||||
* @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) {
|
||||
function getCreateData(company) {
|
||||
const removeUselessFields = (company) => {
|
||||
// Delete attribute to avoid "Field_changed" errors
|
||||
const fields = ['label', 'description', 'domain', 'logo', 'legal_name', 'ignored', 'email', 'bank_ids', 'classList'];
|
||||
fields.forEach((field) => {
|
||||
delete company[field];
|
||||
});
|
||||
|
||||
var notEmptyFields = "country_id,state_id".split(',');
|
||||
notEmptyFields.forEach(function (field) {
|
||||
if (!company[field]) delete company[field];
|
||||
// Remove if empty and format it otherwise
|
||||
const many2oneFields = ['country_id', 'state_id'];
|
||||
many2oneFields.forEach((field) => {
|
||||
if (!company[field]) {
|
||||
delete company[field];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
return new Promise((resolve) => {
|
||||
// Fetch additional company info via Autocomplete Enrichment API
|
||||
var enrichPromise = self._enrichCompany(company);
|
||||
const enrichPromise = 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];
|
||||
|
||||
const logoPromise = company.logo ? getCompanyLogo(company.logo) : false;
|
||||
whenAll([enrichPromise, logoPromise]).then(([company_data, logo_data]) => {
|
||||
// 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.
|
||||
// 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.displayNotification({ message: company_data.error_message });
|
||||
notifyNoCredits();
|
||||
}
|
||||
else if (company_data.error_message === 'No Account Token') {
|
||||
notifyAccountToken();
|
||||
}
|
||||
else {
|
||||
notification.add(company_data.error_message);
|
||||
}
|
||||
company_data = company;
|
||||
}
|
||||
|
||||
if (_.isEmpty(company_data)) {
|
||||
if (!Object.keys(company_data).length) {
|
||||
company_data = company;
|
||||
}
|
||||
|
||||
// Delete attribute to avoid "Field_changed" errors
|
||||
removeUselessFields(company_data);
|
||||
|
||||
// Assign VAT coming from parent VIES VAT query
|
||||
@@ -172,33 +169,7 @@ var PartnerAutocompleteMixin = {
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -208,18 +179,18 @@ var PartnerAutocompleteMixin = {
|
||||
* @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);
|
||||
function getBase64Image(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onload = () => {
|
||||
getDataURLFromFile(xhr.response).then(resolve);
|
||||
};
|
||||
xhr.open('GET', url);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.onerror = reject;
|
||||
xhr.send();
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Use Clearbit Autocomplete API to return suggestions
|
||||
@@ -228,25 +199,11 @@ var PartnerAutocompleteMixin = {
|
||||
* @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);
|
||||
},
|
||||
async function getClearbitSuggestions(value) {
|
||||
const url = `https://autocomplete.clearbit.com/v1/companies/suggest?query=${value}`;
|
||||
const prom = http.get(url);
|
||||
return keepLastClearbit.add(prom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use Odoo Autocomplete API to return suggestions
|
||||
@@ -256,56 +213,31 @@ var PartnerAutocompleteMixin = {
|
||||
* @returns {Promise}
|
||||
* @private
|
||||
*/
|
||||
_getOdooSuggestions: function (value, isVAT) {
|
||||
var method = isVAT ? 'read_by_vat' : 'autocomplete';
|
||||
async function getOdooSuggestions(value, isVAT) {
|
||||
const 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;
|
||||
const prom = orm.silent.call(
|
||||
'res.partner',
|
||||
method,
|
||||
[value],
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
const suggestions = await keepLastOdoo.add(prom);
|
||||
suggestions.map((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;
|
||||
|
||||
return suggestion;
|
||||
});
|
||||
return suggestions;
|
||||
if (suggestion.country_id && suggestion.country_id.display_name) {
|
||||
if (suggestion.description) suggestion.description += ` (${suggestion.country_id.display_name})`;
|
||||
else suggestion.description += suggestion.country_id.display_name;
|
||||
}
|
||||
|
||||
return suggestion;
|
||||
});
|
||||
|
||||
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, '') : '';
|
||||
},
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to wait for multiple promises
|
||||
@@ -316,60 +248,49 @@ var PartnerAutocompleteMixin = {
|
||||
* @returns {Promise}
|
||||
* @private
|
||||
*/
|
||||
_whenAll: function (promises) {
|
||||
return Promise.all(promises.map(function (p) {
|
||||
function whenAll(promises) {
|
||||
return Promise.all(promises.map((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
|
||||
async function notifyNoCredits() {
|
||||
const url = await orm.call(
|
||||
'iap.account',
|
||||
'get_credits_url',
|
||||
['partner_autocomplete'],
|
||||
);
|
||||
const title = _t('Not enough credits for Partner Autocomplete');
|
||||
const content = renderToMarkup('partner_autocomplete.InsufficientCreditNotification', {
|
||||
credits_url: url
|
||||
});
|
||||
notification.add(content, {
|
||||
title,
|
||||
});
|
||||
}
|
||||
|
||||
async function notifyAccountToken() {
|
||||
const url = await orm.call(
|
||||
'iap.account',
|
||||
'get_config_account_url',
|
||||
[]
|
||||
);
|
||||
const title = _t('IAP Account Token missing');
|
||||
if (url) {
|
||||
const content = renderToMarkup('partner_autocomplete.AccountTokenMissingNotification', {
|
||||
account_url: url
|
||||
});
|
||||
self.displayNotification({
|
||||
notification.add(content, {
|
||||
title,
|
||||
message: utils.Markup(content),
|
||||
className: '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.displayNotification({
|
||||
title,
|
||||
message: utils.Markup(content),
|
||||
className: 'o_partner_autocomplete_no_credits_notify',
|
||||
});
|
||||
}
|
||||
else {
|
||||
self.displayNotification({ title });
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return PartnerAutocompleteMixin;
|
||||
|
||||
});
|
||||
}
|
||||
else {
|
||||
notification.add(title);
|
||||
}
|
||||
}
|
||||
return { autocomplete, getCreateData };
|
||||
}
|
||||
|
||||
@@ -1,341 +1,96 @@
|
||||
odoo.define('partner.autocomplete.fieldchar', function (require) {
|
||||
'use strict';
|
||||
/** @odoo-module **/
|
||||
/* global checkVATNumber */
|
||||
|
||||
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');
|
||||
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
|
||||
import { useChildRef } from "@web/core/utils/hooks";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { CharField } from "@web/views/fields/char/char_field";
|
||||
import { useInputField } from "@web/views/fields/input_field_hook";
|
||||
|
||||
var QWeb = core.qweb;
|
||||
import { usePartnerAutocomplete } from "@partner_autocomplete/js/partner_autocomplete_core"
|
||||
|
||||
var FieldChar = basic_fields.FieldChar;
|
||||
export class PartnerAutoCompleteCharField extends CharField {
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
/**
|
||||
* 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,
|
||||
this.partner_autocomplete = usePartnerAutocomplete();
|
||||
|
||||
jsLibs: [
|
||||
'/partner_autocomplete/static/lib/jsvat.js'
|
||||
],
|
||||
this.inputRef = useChildRef();
|
||||
useInputField({ getValue: () => this.props.value || "", parse: (v) => this.parse(v), ref: this.inputRef});
|
||||
}
|
||||
|
||||
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',
|
||||
}),
|
||||
sanitizeVAT(request) {
|
||||
return request ? request.replace(/[^A-Za-z0-9]/g, '') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
isVAT(request) {
|
||||
// checkVATNumber is defined in library jsvat.
|
||||
// It validates that the input has a valid VAT number format
|
||||
return checkVATNumber(this.sanitizeVAT(request));
|
||||
}
|
||||
|
||||
// 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 w-100';
|
||||
validateSearchTerm(request) {
|
||||
if (this.props.name == 'vat') {
|
||||
return this.isVAT(request);
|
||||
}
|
||||
|
||||
if (this.debounceSuggestions > 0) {
|
||||
this._suggestCompanies = _.debounce(this._suggestCompanies.bind(this), this.debounceSuggestions);
|
||||
else {
|
||||
return request && request.length > 2;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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));
|
||||
get sources() {
|
||||
return [
|
||||
{
|
||||
options: async (request) => {
|
||||
if (this.validateSearchTerm(request)) {
|
||||
const suggestions = await this.partner_autocomplete.autocomplete(request, this.isVAT(request));
|
||||
suggestions.forEach((suggestion) => {
|
||||
suggestion.classList = "partner_autocomplete_dropdown_char";
|
||||
});
|
||||
return suggestions;
|
||||
}
|
||||
else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
optionTemplate: "partner_autocomplete.CharFieldDropdownOption",
|
||||
placeholder: _t('Searching Autocomplete...'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async onSelect(option) {
|
||||
const data = await this.partner_autocomplete.getCreateData(Object.getPrototypeOf(option));
|
||||
|
||||
if (data.logo) {
|
||||
const logoField = this.props.record.resModel === 'res.partner' ? 'image_1920' : 'logo';
|
||||
data.company[logoField] = data.logo;
|
||||
}
|
||||
|
||||
// Some fields are unnecessary in res.company
|
||||
if (this.props.record.resModel === 'res.company') {
|
||||
const fields = ['comment', 'child_ids', 'additional_info'];
|
||||
fields.forEach((field) => {
|
||||
delete data.company[field];
|
||||
});
|
||||
}
|
||||
|
||||
// Format the many2one fields
|
||||
const many2oneFields = ['country_id', 'state_id'];
|
||||
many2oneFields.forEach((field) => {
|
||||
if (data.company[field]) {
|
||||
data.company[field] = [data.company[field].id, data.company[field].display_name];
|
||||
}
|
||||
});
|
||||
this._removeDropdown();
|
||||
},
|
||||
this.props.record.update(data.company);
|
||||
}
|
||||
}
|
||||
|
||||
_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,
|
||||
};
|
||||
PartnerAutoCompleteCharField.template = "partner_autocomplete.PartnerAutoCompleteCharField";
|
||||
PartnerAutoCompleteCharField.components = {
|
||||
...CharField.components,
|
||||
AutoComplete,
|
||||
};
|
||||
|
||||
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);
|
||||
// We need to make sure that the element containing the dropdown menu doesn't have the
|
||||
// style attribute overflow: hidden, otherwise the dropdown menu will be hidden by it
|
||||
this.$el.removeClass('o_text_overflow');
|
||||
this.$input.addClass('o_text_overflow');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
});
|
||||
registry.category("fields").add("field_partner_autocomplete", PartnerAutoCompleteCharField);
|
||||
|
||||
@@ -1,141 +1,72 @@
|
||||
odoo.define('partner.autocomplete.many2one', function (require) {
|
||||
'use strict';
|
||||
/** @odoo-module **/
|
||||
|
||||
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');
|
||||
import { Many2XAutocomplete } from '@web/views/fields/relational_utils';
|
||||
import { Many2OneField } from '@web/views/fields/many2one/many2one_field';
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
var _t = core._t;
|
||||
import { usePartnerAutocomplete } from "@partner_autocomplete/js/partner_autocomplete_core"
|
||||
|
||||
var PartnerField = FieldMany2One.extend(AutocompleteMixin, {
|
||||
jsLibs: [
|
||||
'/partner_autocomplete/static/lib/jsvat.js'
|
||||
],
|
||||
export class PartnerMany2XAutocomplete extends Many2XAutocomplete {
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this._addAutocompleteSource(this._searchSuggestions, {
|
||||
placeholder: _t('Searching Autocomplete...'),
|
||||
order: 20,
|
||||
validation: this._validateSearchTerm,
|
||||
});
|
||||
this.partner_autocomplete = usePartnerAutocomplete();
|
||||
}
|
||||
|
||||
this.additionalContext['show_vat'] = true;
|
||||
},
|
||||
validateSearchTerm(request) {
|
||||
return request && request.length > 2;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
get sources() {
|
||||
return super.sources.concat(
|
||||
{
|
||||
options: async (request) => {
|
||||
if (this.validateSearchTerm(request)) {
|
||||
const suggestions = await this.partner_autocomplete.autocomplete(request);
|
||||
suggestions.forEach((suggestion) => {
|
||||
suggestion.classList = "partner_autocomplete_dropdown_many2one";
|
||||
});
|
||||
return suggestions;
|
||||
}
|
||||
else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
optionTemplate: "partner_autocomplete.Many2oneDropdownOption",
|
||||
placeholder: _t('Searching Autocomplete...'),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
async onSelect(option) {
|
||||
if (option.partner_gid) { // Checks that it is a partner autocomplete option
|
||||
const data = await this.partner_autocomplete.getCreateData(Object.getPrototypeOf(option));
|
||||
let context = {
|
||||
'default_is_company': true
|
||||
};
|
||||
_.each(data.company, function (val, key) {
|
||||
|
||||
for (const [key, val] of Object.entries(data.company)) {
|
||||
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);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
if (data.logo) {
|
||||
context.default_image_1920 = data.logo;
|
||||
}
|
||||
return this.openMany2X({ context });
|
||||
}
|
||||
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);
|
||||
}
|
||||
else {
|
||||
return super.onSelect(option);
|
||||
}
|
||||
}
|
||||
|
||||
return $("<li></li>")
|
||||
.data("item.autocomplete",item)
|
||||
.append($a)
|
||||
.appendTo(ul)
|
||||
.addClass(item.classname);
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_renderEdit: function (){
|
||||
this._super.apply(this, arguments);
|
||||
this._modifyAutompleteRendering();
|
||||
},
|
||||
export class PartnerAutoCompleteMany2one extends Many2OneField {}
|
||||
|
||||
/**
|
||||
* 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()) {
|
||||
PartnerAutoCompleteMany2one.components = {
|
||||
...Many2OneField.components,
|
||||
Many2XAutocomplete: PartnerMany2XAutocomplete,
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
registry.category("fields").add("res_partner_many2one", PartnerAutoCompleteMany2one);
|
||||
|
||||
@@ -1,66 +1,32 @@
|
||||
.o_field_partner_autocomplete.dropdown {
|
||||
> .o_partner_autocomplete_dropdown .dropdown-item {
|
||||
min-width: 300px;
|
||||
padding: 4px 8px;
|
||||
> img {
|
||||
float: left;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
.ui-widget.ui-autocomplete {
|
||||
.ui-menu-item {
|
||||
&.partner_autocomplete_dropdown_char {
|
||||
> a {
|
||||
> img {
|
||||
float: left;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
> .o_partner_autocomplete_info {
|
||||
margin-left: 50px;
|
||||
> * {
|
||||
@include o-text-overflow(block);
|
||||
}
|
||||
> strong {
|
||||
font-weight: $font-weight-bolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
> .o_partner_autocomplete_info {
|
||||
margin-left: 50px;
|
||||
> * {
|
||||
@include o-text-overflow(block);
|
||||
&.partner_autocomplete_dropdown_many2one {
|
||||
> a {
|
||||
> img {
|
||||
float: right;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-autocomplete.o_partner_autocomplete_dropdown {
|
||||
> .ui-menu-item:nth-of-type(1n+16) {
|
||||
display: none;
|
||||
}
|
||||
> .o_partner_autocomplete_dropdown_item {
|
||||
> a {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
padding-right: 50px;
|
||||
padding-left: 40px;
|
||||
max-width: 400px;
|
||||
> img {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
right: 20px;
|
||||
top: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .ui-menu-item > a.ui-state-active .text-muted {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 700px) {
|
||||
.ui-autocomplete.o_partner_autocomplete_dropdown {
|
||||
> .ui-menu-item:nth-of-type(1n+13) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-height: 620px) {
|
||||
.ui-autocomplete.o_partner_autocomplete_dropdown {
|
||||
> .ui-menu-item:nth-of-type(1n+12) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-height: 570px) {
|
||||
.ui-autocomplete.o_partner_autocomplete_dropdown {
|
||||
> .ui-menu-item:nth-of-type(1n+11) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,42 @@
|
||||
<?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>
|
||||
<t t-name="partner_autocomplete.PartnerAutoCompleteCharField" t-inherit="web.CharField" owl="1">
|
||||
<xpath expr="//t[@t-else='']" position="before">
|
||||
<t t-elif="props.record.data.company_type === 'company'">
|
||||
<AutoComplete
|
||||
value="props.value || ''"
|
||||
sources="sources"
|
||||
onSelect.bind="onSelect"
|
||||
input="inputRef"
|
||||
/>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<!--
|
||||
@param {string} credits_url
|
||||
-->
|
||||
<div t-name="partner_autocomplete.insufficient_credit_notification" class="o-hidden-ios">
|
||||
<a class="btn btn-link" t-att-href="credits_url"><i class="fa fa-arrow-right"/> Buy more credits</a>
|
||||
</div>
|
||||
<t t-name="partner_autocomplete.CharFieldDropdownOption" owl="1">
|
||||
<img t-att-src="option.logo" onerror="this.src='/base/static/img/company_image.png'" alt="Placeholder"/>
|
||||
<div class="o_partner_autocomplete_info">
|
||||
<strong t-esc="option.label or ' '"/>
|
||||
<div t-esc="option.description"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div t-name="partner_autocomplete.account_token" class="">
|
||||
<a class="btn btn-link" t-att-href="account_url" ><i class="fa fa-arrow-right"/> Set Your Account Token</a>
|
||||
</div>
|
||||
<t t-name="partner_autocomplete.Many2oneDropdownOption" owl="1">
|
||||
<i class="fa fa-magic text-muted pe-1"/>
|
||||
<t t-esc="option.label or ' '"/>,
|
||||
<span class="text-muted" t-esc="option.description"/>
|
||||
<img class="ms-1" t-att-src="option.logo" onerror="this.src='/base/static/img/company_image.png'" alt="Placeholder"/>
|
||||
</t>
|
||||
|
||||
<t t-name="partner_autocomplete.InsufficientCreditNotification" owl="1">
|
||||
<div class="o-hidden-ios">
|
||||
<a class="btn btn-link" t-att-href="credits_url"><i class="fa fa-arrow-right"/> Buy more credits</a>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="partner_autocomplete.AccountTokenMissingNotification" owl="1">
|
||||
<div class="">
|
||||
<a class="btn btn-link" t-att-href="account_url" ><i class="fa fa-arrow-right"/> Set Your Account Token</a>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
@@ -1,115 +1,65 @@
|
||||
odoo.define('partner_autocomplete.tests', function (require) {
|
||||
"use strict";
|
||||
/** @odoo-module **/
|
||||
|
||||
var FormView = require('web.FormView');
|
||||
var concurrency = require('web.concurrency');
|
||||
var testUtils = require("web.test_utils");
|
||||
var AutocompleteField = require('partner.autocomplete.fieldchar');
|
||||
var PartnerField = require('partner.autocomplete.many2one');
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
click,
|
||||
editSelect,
|
||||
getFixture,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
|
||||
var createView = testUtils.createView;
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
function _compareResultFields(assert, form, fields, createData) {
|
||||
var type, formatted, $fieldInput;
|
||||
let target;
|
||||
|
||||
_.each(createData, function (val, key) {
|
||||
if (fields[key]) {
|
||||
if (key === 'image_1920') {
|
||||
if (val) val = 'data:image/png;base64,' + val;
|
||||
assert.hasAttrValue(form.$(".o_field_image img"), "data-src", val, 'image value should have been updated to "' + val + '"');
|
||||
} else {
|
||||
type = fields[key].type;
|
||||
$fieldInput = form.$('input[name="' + key + '"]');
|
||||
if ($fieldInput.length) {
|
||||
formatted = $fieldInput.val();
|
||||
formatted = type === 'integer' ? parseInt(formatted, 10) : formatted;
|
||||
assert.strictEqual(
|
||||
formatted,
|
||||
val === false ? 0 : val,
|
||||
key + ' value should have been updated to "' + val + '"'
|
||||
);
|
||||
}
|
||||
async function editInputNoChangeEvent(input, value) {
|
||||
// Note: we can't use editInput as it triggers the 'change' event which will close the autocomplete dropdown
|
||||
input.value = value;
|
||||
await triggerEvent(input, null, "input");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
QUnit.module('partner_autocomplete', {
|
||||
beforeEach() {
|
||||
target = getFixture();
|
||||
|
||||
// Make autocomplete input instantaneous
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
});
|
||||
}
|
||||
|
||||
var suggestions = [{
|
||||
name: "Odoo",
|
||||
website: "odoo.com",
|
||||
domain: "odoo.com",
|
||||
logo: "odoo.com/logo.png",
|
||||
vat: "BE0477472701"
|
||||
}];
|
||||
setupViewRegistries();
|
||||
const fakeHTTPService = {
|
||||
start() {
|
||||
return {
|
||||
get: (route) => {
|
||||
return Promise.resolve([
|
||||
{
|
||||
"name": "Odoo",
|
||||
"domain": "odoo.com",
|
||||
},
|
||||
{
|
||||
"name": "MyCompany",
|
||||
"domain": "mycompany.com",
|
||||
},
|
||||
{
|
||||
"name": "YourCompany",
|
||||
"domain": "yourcompany.com",
|
||||
},
|
||||
]);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("http", fakeHTTPService);
|
||||
},
|
||||
}, function () {
|
||||
|
||||
var enrichData = {};
|
||||
|
||||
var createData = {};
|
||||
|
||||
QUnit.module('partner_autocomplete', {
|
||||
before: function () {
|
||||
var fieldsToPatch = [PartnerField, AutocompleteField];
|
||||
_.each(fieldsToPatch, function (fieldToPatch) {
|
||||
testUtils.mock.patch(fieldToPatch, {
|
||||
_getBase64Image: function (url) {
|
||||
return Promise.resolve(url === "odoo.com/logo.png" ? "odoobase64" : "");
|
||||
},
|
||||
_isOnline: function () {
|
||||
return true;
|
||||
},
|
||||
_getCreateData: function (company) {
|
||||
var def = this._super.apply(this, arguments);
|
||||
def.then(function (data) {
|
||||
createData = data.company;
|
||||
});
|
||||
return def;
|
||||
},
|
||||
_enrichCompany: function (company) {
|
||||
return Promise.resolve(enrichData);
|
||||
},
|
||||
_getOdooSuggestions: function (value, isVAT) {
|
||||
var results = _.filter(suggestions, function (suggestion) {
|
||||
value = value ? value.toLowerCase() : '';
|
||||
if (isVAT) return (suggestion.vat.toLowerCase().indexOf(value) >= 0);
|
||||
else return (suggestion.name.toLowerCase().indexOf(value) >= 0);
|
||||
});
|
||||
return Promise.resolve(results);
|
||||
},
|
||||
_getClearbitSuggestions: function (value) {
|
||||
return this._getOdooSuggestions(value);
|
||||
},
|
||||
displayNotification: function ({ title, message, sticky }) {
|
||||
return this._super({
|
||||
type: 'warning',
|
||||
title: title,
|
||||
message: message,
|
||||
sticky: sticky,
|
||||
className: 'o_partner_autocomplete_test_notify'
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
testUtils.mock.patch(AutocompleteField, {
|
||||
debounceSuggestions: 0,
|
||||
});
|
||||
},
|
||||
beforeEach: function () {
|
||||
enrichData = {
|
||||
country_id: 20,
|
||||
state_id: false,
|
||||
partner_gid: 1,
|
||||
website: "odoo.com",
|
||||
comment: "Comment on Odoo",
|
||||
street: "40 Chaussée de Namur",
|
||||
city: "Ramillies",
|
||||
zip: "1367",
|
||||
phone: "+1 650-691-3277",
|
||||
vat: "BE0477472701",
|
||||
};
|
||||
|
||||
this.data = {
|
||||
const makeViewParams = {
|
||||
serverData: {
|
||||
models: {
|
||||
'res.partner': {
|
||||
fields: {
|
||||
company_type: {
|
||||
@@ -119,15 +69,15 @@ odoo.define('partner_autocomplete.tests', function (require) {
|
||||
searchable: true
|
||||
},
|
||||
name: {string: "Name", type: "char", searchable: true},
|
||||
parent_id: {string: "Company", type: "many2one", relation: "res.partner"},
|
||||
parent_id: {string: "Company", type: "many2one", relation: "res.partner", searchable: true},
|
||||
website: {string: "Website", type: "char", searchable: true},
|
||||
image_1920: {string: "Image", type: "binary", searchable: true},
|
||||
phone: {string: "Phone", type: "char", searchable: true},
|
||||
street: {string: "Street", type: "char", searchable: true},
|
||||
city: {string: "City", type: "char", searchable: true},
|
||||
zip: {string: "Zip", type: "char", searchable: true},
|
||||
state_id: {string: "State", type: "integer", searchable: true},
|
||||
country_id: {string: "Country", type: "integer", searchable: true},
|
||||
state_id: {string: "State", type: "many2one", relation: "res.country.state", searchable: true},
|
||||
country_id: {string: "Country", type: "many2one", relation: "res.country", searchable: true},
|
||||
comment: {string: "Comment", type: "char", searchable: true},
|
||||
vat: {string: "Vat", type: "char", searchable: true},
|
||||
is_company: {string: "Is company", type: "bool", searchable: true},
|
||||
@@ -135,288 +85,259 @@ odoo.define('partner_autocomplete.tests', function (require) {
|
||||
},
|
||||
records: [],
|
||||
onchanges: {
|
||||
company_type: function (obj) {
|
||||
company_type: (obj) => {
|
||||
obj.is_company = obj.company_type === 'company';
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
after: function () {
|
||||
testUtils.mock.unpatch(AutocompleteField);
|
||||
testUtils.mock.unpatch(PartnerField);
|
||||
'res.country': {
|
||||
fields: {
|
||||
display_name: {string: "Name", type: "char", searchable: true},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
name: 'United States',
|
||||
}],
|
||||
},
|
||||
'res.country.state': {
|
||||
fields: {
|
||||
display_name: {string: "Name", type: "char", searchable: true},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
name: 'California (US)',
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
resModel: "res.partner",
|
||||
type: "form",
|
||||
arch:
|
||||
`<form>
|
||||
<field name="company_type"/>
|
||||
<field name="name" widget="field_partner_autocomplete"/>
|
||||
<field name="parent_id" widget="res_partner_many2one"/>
|
||||
<field name="website"/>
|
||||
<field name="image_1920" widget="image"/>
|
||||
<field name="phone"/>
|
||||
<field name="street"/>
|
||||
<field name="city"/>
|
||||
<field name="state_id"/>
|
||||
<field name="zip"/>
|
||||
<field name="country_id"/>
|
||||
<field name="comment"/>
|
||||
<field name="vat" widget="field_partner_autocomplete"/>
|
||||
</form>`,
|
||||
async mockRPC(route, args) {
|
||||
if (route === "/web/dataset/call_kw/res.partner/autocomplete" || route === "/web/dataset/call_kw/res.partner/read_by_vat") {
|
||||
return Promise.resolve([
|
||||
{
|
||||
"partner_gid": 1,
|
||||
"website": "firstcompany.com",
|
||||
"name": "First company",
|
||||
"ignored": false,
|
||||
"vat": ""
|
||||
},
|
||||
{
|
||||
"partner_gid": 2,
|
||||
"website": "secondcompany.com",
|
||||
"name": "Second company",
|
||||
"ignored": false,
|
||||
"vat": ""
|
||||
},
|
||||
{
|
||||
"partner_gid": 3,
|
||||
"website": "thirdcompany.com",
|
||||
"name": "Third company",
|
||||
"ignored": false,
|
||||
"vat": ""
|
||||
},
|
||||
]);
|
||||
}
|
||||
else if (route === "/web/dataset/call_kw/res.partner/enrich_company") {
|
||||
return Promise.resolve({
|
||||
"partner_gid": 1,
|
||||
"website": "firstcompany.com",
|
||||
"name": "First company",
|
||||
'logo': false,
|
||||
"ignored": false,
|
||||
"vat": "Some VAT number",
|
||||
"street": "Some street",
|
||||
"city": "Some city",
|
||||
"zip": "1234",
|
||||
"phone": "+0123456789",
|
||||
"email": "info@firstcompany.com",
|
||||
"country_id": {
|
||||
'id': 1,
|
||||
'display_name': "United States",
|
||||
},
|
||||
"state_id": {
|
||||
'id': 1,
|
||||
'display_name': "California (US)",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QUnit.test("Partner autocomplete : Company type = Individual", async function (assert) {
|
||||
assert.expect(13);
|
||||
await makeView(makeViewParams);
|
||||
|
||||
// Set company type to Individual
|
||||
await editSelect(target, "[name='company_type'] > select", '"individual"');
|
||||
|
||||
const nameInput = target.querySelector("[name='name'] input");
|
||||
assert.doesNotHaveClass(nameInput, 'o-autocomplete--input', "The input for field 'name' should be a regular input");
|
||||
|
||||
const companyInput = target.querySelector("[name='parent_id'] input");
|
||||
|
||||
await click(companyInput, null);
|
||||
assert.containsNone(
|
||||
companyInput,
|
||||
".o-autocomplete--dropdown-menu",
|
||||
"There should be no option when input is empty"
|
||||
);
|
||||
|
||||
await editInputNoChangeEvent(companyInput, "od");
|
||||
assert.containsNone(
|
||||
companyInput,
|
||||
".o-autocomplete--dropdown-menu",
|
||||
"There should be no option when the length of the query is < 3"
|
||||
);
|
||||
|
||||
await editInputNoChangeEvent(companyInput, "company");
|
||||
const autocompleteContainer = companyInput.parentElement;
|
||||
assert.containsN(
|
||||
autocompleteContainer,
|
||||
".o-autocomplete--dropdown-item.partner_autocomplete_dropdown_many2one",
|
||||
6,
|
||||
"Clearbit and Odoo autocomplete options should be shown"
|
||||
);
|
||||
|
||||
// Click on the first option - "First company"
|
||||
await click(autocompleteContainer.querySelectorAll('ul li.partner_autocomplete_dropdown_many2one')[0], null);
|
||||
|
||||
const modalContent = target.querySelector('.o_dialog_container .modal-content');
|
||||
// Check that the fields of the modal have been pre-filled
|
||||
const expectedValues = {
|
||||
"website": "firstcompany.com",
|
||||
"name": "First company",
|
||||
"vat": "Some VAT number",
|
||||
"street": "Some street",
|
||||
"city": "Some city",
|
||||
"zip": "1234",
|
||||
"phone": "+0123456789",
|
||||
"country_id": "United States",
|
||||
"state_id": "California (US)",
|
||||
};
|
||||
for (const [fieldName, expectedValue] of Object.entries(expectedValues)) {
|
||||
assert.strictEqual(modalContent.querySelector(`[name=${fieldName}] input`).value, expectedValue, `${fieldName} should be pre-filled`);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("Partner autocomplete : Company type = Individual", function (assert) {
|
||||
assert.expect(2);
|
||||
var done = assert.async();
|
||||
createView({
|
||||
View: FormView,
|
||||
model: 'res.partner',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<form>' +
|
||||
'<field name="company_type"/>' +
|
||||
'<field name="name" widget="field_partner_autocomplete"/>' +
|
||||
'<field name="website"/>' +
|
||||
'<field name="image_1920" widget="image"/>' +
|
||||
'</form>',
|
||||
}).then(function (form){
|
||||
// Set company type to Individual
|
||||
var $company_type = form.$("select[name='company_type']");
|
||||
testUtils.fields.editSelect($company_type, '"individual"');
|
||||
|
||||
// Check input exists
|
||||
var $input = form.$(".o_field_partner_autocomplete > input:visible");
|
||||
assert.strictEqual($input.length, 1, "there should be an <input/> for the Partner field");
|
||||
|
||||
// Change input val and assert nothing happens
|
||||
testUtils.fields.editInput($input, "odoo")
|
||||
var $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
|
||||
assert.strictEqual($dropdown.length, 0, "there should not be an opened dropdown");
|
||||
|
||||
form.destroy();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
QUnit.test("Partner autocomplete : Company type = Company / Name search", async function (assert) {
|
||||
assert.expect(17);
|
||||
var fields = this.data['res.partner'].fields;
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'res.partner',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<form>' +
|
||||
'<field name="company_type"/>' +
|
||||
'<field name="name" widget="field_partner_autocomplete"/>' +
|
||||
'<field name="website"/>' +
|
||||
'<field name="image_1920" widget="image"/>' +
|
||||
'<field name="phone"/>' +
|
||||
'<field name="street"/>' +
|
||||
'<field name="city"/>' +
|
||||
'<field name="state_id"/>' +
|
||||
'<field name="zip"/>' +
|
||||
'<field name="country_id"/>' +
|
||||
'<field name="comment"/>' +
|
||||
'<field name="vat"/>' +
|
||||
'</form>',
|
||||
mockRPC: function (route) {
|
||||
if (route === "/web/static/img/placeholder.png"
|
||||
|| route === "odoo.com/logo.png"
|
||||
|| route === "data:image/png;base64,odoobase64") { // land here as it is not valid base64 content
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
// Set company type to Company
|
||||
var $company_type = form.$("select[name='company_type']");
|
||||
await testUtils.fields.editSelect($company_type, '"company"');
|
||||
assert.expect(12);
|
||||
await makeView(makeViewParams);
|
||||
|
||||
// Check input exists
|
||||
var $input = form.$(".o_field_partner_autocomplete > input:visible");
|
||||
assert.strictEqual($input.length, 1, "there should be an <input/> for the field");
|
||||
// Set company type to Company
|
||||
await editSelect(target, "[name='company_type'] > select", '"company"');
|
||||
|
||||
// Change input val and assert changes
|
||||
await testUtils.fields.editInput($input, "odoo");
|
||||
await testUtils.nextTick();
|
||||
var $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
|
||||
assert.strictEqual($dropdown.length, 1, "there should be an opened dropdown");
|
||||
assert.strictEqual($dropdown.children().length, 1, "there should be only ne proposition");
|
||||
const input = target.querySelector("[name='name'] .dropdown input");
|
||||
|
||||
await testUtils.dom.click($dropdown.find("a").first());
|
||||
$input = form.$(".o_field_partner_autocomplete > input");
|
||||
assert.strictEqual($input.val(), "Odoo", "Input value should have been updated to \"Odoo\"");
|
||||
assert.strictEqual(form.$("input.o_field_widget").val(), "odoo.com", "website value should have been updated to \"odoo.com\"");
|
||||
await click(input, null);
|
||||
assert.containsNone(
|
||||
input,
|
||||
".o-autocomplete--dropdown-menu",
|
||||
"There should be no option when input is empty"
|
||||
);
|
||||
|
||||
_compareResultFields(assert, form, fields, createData);
|
||||
await editInputNoChangeEvent(input, "od");
|
||||
assert.containsNone(
|
||||
input,
|
||||
".o-autocomplete--dropdown-menu",
|
||||
"There should be no option when the length of the query is < 3"
|
||||
);
|
||||
|
||||
// Try suggestion with bullshit query
|
||||
await testUtils.fields.editInput($input, "ZZZZZZZZZZZZZZZZZZZZZZ");
|
||||
$dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
|
||||
assert.strictEqual($dropdown.length, 0, "there should be no opened dropdown when no result");
|
||||
await editInputNoChangeEvent(input, "company");
|
||||
const autocompleteContainer = input.parentElement;
|
||||
assert.containsN(
|
||||
autocompleteContainer,
|
||||
".o-autocomplete--dropdown-item",
|
||||
6,
|
||||
"Clearbit and Odoo autocomplete options should be shown"
|
||||
);
|
||||
|
||||
// Try autocomplete again
|
||||
await testUtils.fields.editInput($input, "odoo");
|
||||
await testUtils.nextTick();
|
||||
$dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
|
||||
assert.strictEqual($dropdown.length, 1, "there should be an opened dropdown when typing odoo letters again");
|
||||
// Click on the first option - "First company"
|
||||
await click(autocompleteContainer.querySelectorAll('ul li')[0], null);
|
||||
|
||||
// Test if dropdown closes on focusout
|
||||
$input.trigger("focusout");
|
||||
await testUtils.nextTick();
|
||||
$dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
|
||||
assert.strictEqual($dropdown.length, 0, "unfocusing the input should close the dropdown");
|
||||
|
||||
form.destroy();
|
||||
// Check that the fields have been filled
|
||||
const expectedValues = {
|
||||
"website": "firstcompany.com",
|
||||
"name": "First company",
|
||||
"vat": "Some VAT number",
|
||||
"street": "Some street",
|
||||
"city": "Some city",
|
||||
"zip": "1234",
|
||||
"phone": "+0123456789",
|
||||
"country_id": "United States",
|
||||
"state_id": "California (US)",
|
||||
};
|
||||
for (const [fieldName, expectedValue] of Object.entries(expectedValues)) {
|
||||
assert.strictEqual(target.querySelector(`[name=${fieldName}] input`).value, expectedValue, `${fieldName} should be filled`);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("Partner autocomplete : Company type = Company / VAT search", async function (assert) {
|
||||
assert.expect(27);
|
||||
var fields = this.data['res.partner'].fields;
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'res.partner',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<form>' +
|
||||
'<field name="company_type"/>' +
|
||||
'<field name="name" widget="field_partner_autocomplete"/>' +
|
||||
'<field name="website"/>' +
|
||||
'<field name="image_1920" widget="image"/>' +
|
||||
'<field name="phone"/>' +
|
||||
'<field name="street"/>' +
|
||||
'<field name="city"/>' +
|
||||
'<field name="state_id"/>' +
|
||||
'<field name="zip"/>' +
|
||||
'<field name="country_id"/>' +
|
||||
'<field name="comment"/>' +
|
||||
'<field name="vat"/>' +
|
||||
'</form>',
|
||||
mockRPC: function (route) {
|
||||
if (route === "/web/static/img/placeholder.png"
|
||||
|| route === "odoo.com/logo.png"
|
||||
|| route === "data:image/png;base64,odoobase64") { // land here as it is not valid base64 content
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
// Set company type to Company
|
||||
var $company_type = form.$("select[name='company_type']");
|
||||
await testUtils.fields.editSelect($company_type, '"company"');
|
||||
assert.expect(12);
|
||||
|
||||
await makeView(makeViewParams);
|
||||
|
||||
// Check input exists
|
||||
var $input = form.$(".o_field_partner_autocomplete > input:visible");
|
||||
assert.strictEqual($input.length, 1, "there should be an <input/> for the field");
|
||||
// Set company type to Company
|
||||
await editSelect(target, "[name='company_type'] > select", '"company"');
|
||||
|
||||
// Set incomplete VAT and assert changes
|
||||
await testUtils.fields.editInput($input, "BE047747270")
|
||||
const input = target.querySelector("[name='vat'] .dropdown input");
|
||||
|
||||
var $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
|
||||
assert.strictEqual($dropdown.length, 0, "there should be no opened dropdown no results with incomplete VAT number");
|
||||
await click(input, null);
|
||||
assert.containsNone(
|
||||
input,
|
||||
".o-autocomplete--dropdown-menu",
|
||||
"There should be no option when input is empty"
|
||||
);
|
||||
|
||||
// Set complete VAT and assert changes
|
||||
// First suggestion (only vat result)
|
||||
await testUtils.fields.editInput($input, "BE0477472701")
|
||||
$dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
|
||||
assert.strictEqual($dropdown.length, 1, "there should be an opened dropdown");
|
||||
assert.strictEqual($dropdown.children().length, 1, "there should be one proposition for complete VAT number");
|
||||
await editInputNoChangeEvent(input, "blabla");
|
||||
assert.containsNone(
|
||||
input,
|
||||
".o-autocomplete--dropdown-menu",
|
||||
"There should be no option when the value doesn't have a valid VAT number format"
|
||||
);
|
||||
|
||||
await testUtils.dom.click($dropdown.find("a").first());
|
||||
await editInputNoChangeEvent(input, "BE0477472701");
|
||||
const autocompleteContainer = input.parentElement;
|
||||
assert.containsN(
|
||||
autocompleteContainer,
|
||||
".o-autocomplete--dropdown-item",
|
||||
3,
|
||||
"Odoo read_by_vat options should be shown"
|
||||
);
|
||||
|
||||
$input = form.$(".o_field_partner_autocomplete > input");
|
||||
assert.strictEqual($input.val(), "Odoo", "Input value should have been updated to \"Odoo\"");
|
||||
// Click on the first option - "First company"
|
||||
await click(autocompleteContainer.querySelectorAll('ul li')[0], null);
|
||||
|
||||
_compareResultFields(assert, form, fields, createData);
|
||||
await testUtils.nextTick();
|
||||
// Set complete VAT and assert changes
|
||||
// Second suggestion (only vat + clearbit result)
|
||||
await testUtils.fields.editInput($input, "BE0477472701")
|
||||
$dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
|
||||
assert.strictEqual($dropdown.length, 1, "there should be an opened dropdown");
|
||||
assert.strictEqual($dropdown.children().length, 1, "there should be one proposition for complete VAT number");
|
||||
|
||||
await testUtils.dom.click($dropdown.find("a").first());
|
||||
|
||||
$input = form.$(".o_field_partner_autocomplete > input");
|
||||
assert.strictEqual($input.val(), "Odoo", "Input value should have been updated to \"Odoo\"");
|
||||
|
||||
_compareResultFields(assert, form, fields, createData);
|
||||
|
||||
// Test if dropdown closes on focusout
|
||||
$input.trigger("focusout");
|
||||
$dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
|
||||
assert.strictEqual($dropdown.length, 0, "unfocusing the input should close the dropdown");
|
||||
|
||||
form.destroy();
|
||||
|
||||
});
|
||||
|
||||
QUnit.test("Partner autocomplete : render Many2one", function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(3);
|
||||
|
||||
var M2O_DELAY = PartnerField.prototype.AUTOCOMPLETE_DELAY;
|
||||
PartnerField.prototype.AUTOCOMPLETE_DELAY = 0;
|
||||
|
||||
createView({
|
||||
View: FormView,
|
||||
model: 'res.partner',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<form>' +
|
||||
'<field name="name"/>' +
|
||||
'<field name="parent_id" widget="res_partner_many2one"/>' +
|
||||
'</form>',
|
||||
}).then(async function (form) {
|
||||
var $input = form.$('.o_field_many2one[name="parent_id"] input:visible');
|
||||
assert.strictEqual($input.length, 1, "there should be an <input/> for the Many2one");
|
||||
|
||||
await testUtils.fields.editInput($input, 'odoo');
|
||||
|
||||
concurrency.delay(0).then(function () {
|
||||
var $dropdown = $input.autocomplete('widget');
|
||||
assert.strictEqual($dropdown.length, 1, "there should be an opened dropdown");
|
||||
assert.ok($dropdown.is('.o_partner_autocomplete_dropdown'),
|
||||
"there should be a partner_autocomplete");
|
||||
|
||||
PartnerField.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
|
||||
form.destroy();
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("Partner autocomplete : Notify not enough credits", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
enrichData = {
|
||||
error: true,
|
||||
error_message: 'Insufficient Credit',
|
||||
// Check that the fields have been filled
|
||||
const expectedValues = {
|
||||
"website": "firstcompany.com",
|
||||
"name": "First company",
|
||||
"vat": "Some VAT number",
|
||||
"street": "Some street",
|
||||
"city": "Some city",
|
||||
"zip": "1234",
|
||||
"phone": "+0123456789",
|
||||
"country_id": "United States",
|
||||
"state_id": "California (US)",
|
||||
};
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'res.partner',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<form>' +
|
||||
'<field name="company_type"/>' +
|
||||
'<field name="name" widget="field_partner_autocomplete"/>' +
|
||||
'</form>',
|
||||
services: {
|
||||
notification: {
|
||||
notify(notification) {
|
||||
assert.equal(notification.type, "warning");
|
||||
assert.equal(notification.className, "o_partner_autocomplete_test_notify");
|
||||
},
|
||||
},
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "get_credits_url"){
|
||||
return Promise.resolve('credits_url');
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
// Set company type to Company
|
||||
var $company_type = form.$("select[name='company_type']");
|
||||
await testUtils.fields.editSelect($company_type, '"company"');
|
||||
|
||||
var $input = form.$(".o_field_partner_autocomplete > input:visible");
|
||||
await testUtils.fields.editInput($input, "BE0477472701");
|
||||
|
||||
var $dropdown = form.$(".o_field_partner_autocomplete .dropdown-menu:visible");
|
||||
await testUtils.dom.click($dropdown.find("a").first());
|
||||
|
||||
form.destroy();
|
||||
for (const [fieldName, expectedValue] of Object.entries(expectedValues)) {
|
||||
assert.strictEqual(target.querySelector(`[name=${fieldName}] input`).value, expectedValue, `${fieldName} should be filled`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { useForwardRefToParent, useService } from "@web/core/utils/hooks";
|
||||
import { useDebounced } from "@web/core/utils/timing";
|
||||
import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service";
|
||||
import { usePosition } from "@web/core/position_hook";
|
||||
@@ -21,7 +21,7 @@ export class AutoComplete extends Component {
|
||||
value: this.props.value,
|
||||
});
|
||||
|
||||
this.inputRef = useRef("input");
|
||||
this.inputRef = useForwardRefToParent("input");
|
||||
this.root = useRef("root");
|
||||
this.debouncedOnInput = useDebounced(this.onInput, this.constructor.timeout);
|
||||
useExternalListener(window, "scroll", this.onWindowScroll, true);
|
||||
@@ -312,6 +312,7 @@ Object.assign(AutoComplete, {
|
||||
onInput: { type: Function, optional: true },
|
||||
onChange: { type: Function, optional: true },
|
||||
onBlur: { type: Function, optional: true },
|
||||
input: { type: Function, optional: true },
|
||||
},
|
||||
defaultProps: {
|
||||
placeholder: "",
|
||||
|
||||
@@ -17,7 +17,7 @@ const { useComponent, useEffect, useRef, useEnv } = owl;
|
||||
*/
|
||||
export function useInputField(params) {
|
||||
const env = useEnv();
|
||||
const inputRef = useRef(params.refName || "input");
|
||||
const inputRef = params.ref || useRef(params.refName || "input");
|
||||
const component = useComponent();
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user