[IMP] web_widget_one2many_product_picker: Control record per page by group and fix to two the number of decimals

pull/1858/head
Alexandre D. Díaz 2021-06-02 14:48:07 +02:00
parent dd9a97bc65
commit 0543c48c56
10 changed files with 325 additions and 154 deletions

View File

@ -47,7 +47,6 @@ You need to define the view fields. The view must be of ``form`` type.
Widget options:
~~~~~~~~~~~~~~~
* product_per_page > Integer -> Used to control the load more behaviour (16 by default)
* groups > Array of dictionaries -> Declare the groups
* name -> The group name
@ -58,6 +57,9 @@ Widget options:
* name -> The field name to order
* asc -> Flag to use 'asc' order
* records_per_page > Integer -> Used to control the load more behaviour (16 by default)
* active -> Boolean -> Select the default group to use ('false' by default = 'All' group)
* currency_field > Model field used to format monetary values ('currency_id' by default)
* field_map > Dictionary:

View File

@ -5,7 +5,6 @@ You need to define the view fields. The view must be of ``form`` type.
Widget options:
~~~~~~~~~~~~~~~
* product_per_page > Integer -> Used to control the load more behaviour (16 by default)
* groups > Array of dictionaries -> Declare the groups
* name -> The group name
@ -16,6 +15,9 @@ Widget options:
* name -> The field name to order
* asc -> Flag to use 'asc' order
* records_per_page > Integer -> Used to control the load more behaviour (16 by default)
* active -> Boolean -> Select the default group to use ('false' by default = 'All' group)
* currency_field > Model field used to format monetary values ('currency_id' by default)
* field_map > Dictionary:

View File

@ -406,8 +406,6 @@ You need to define the view fields. The view must be of <tt class="docutils lite
<div class="section" id="widget-options">
<h2><a class="toc-backref" href="#id3">Widget options:</a></h2>
<ul>
<li><p class="first">product_per_page &gt; Integer -&gt; Used to control the load more behaviour (16 by default)</p>
</li>
<li><p class="first">groups &gt; Array of dictionaries -&gt; Declare the groups</p>
<blockquote>
<ul>
@ -425,6 +423,10 @@ You need to define the view fields. The view must be of <tt class="docutils lite
</ul>
</blockquote>
</li>
<li><p class="first">records_per_page &gt; Integer -&gt; Used to control the load more behaviour (16 by default)</p>
</li>
<li><p class="first">active -&gt; Boolean -&gt; Select the default group to use (false by default = All group)</p>
</li>
</ul>
</blockquote>
</li>

View File

@ -34,8 +34,15 @@ odoo.define("web_widget_one2many_product_picker.tools", function(require) {
});
}
function float(value, field_info, digits) {
return field_utils.format.float(value, field_info, {
digits: digits,
});
}
return {
monetary: monetary,
float: float,
priceReduce: priceReduce,
};
});

View File

@ -28,6 +28,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
},
_click_card_delayed_time: 250,
_onchange_delay: 250,
/**
* @override
@ -48,6 +49,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
/**
* Generates a new virtual state and recreates the product card
*
* @param {Boolean} simple_mode
* @returns {Object}
*/
generateVirtualState: function(simple_mode) {
@ -186,6 +188,19 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
);
},
/**
* Prints the given field value using the selected format
*
* @private
* @param {String} price_field
* @returns {String}
*/
_getFloatFieldValue: function(field) {
const field_name = this.options.fieldMap[field];
const value = this.state.data[field_name];
return tools.float(value, this.state.fields[field_name]);
},
/**
* @private
* @param {String} d a stringified domain
@ -241,6 +256,10 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
this._setMasterUomMap();
},
/**
* Used to know what is the "main" uom
* @private
*/
_setMasterUomMap: function() {
this.master_uom_map = {
field_uom: "product_uom",
@ -269,9 +288,13 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
field_map: this.options.fieldMap,
widget: this,
monetary: this._getMonetaryFieldValue.bind(this),
floatFixed: this._getFloatFieldValue.bind(this),
show_discount: this.options.showDiscount,
is_virtual: this.is_virtual,
modified: record && record.context.product_picker_modified,
modified:
record &&
model.hasChanges(record.id) &&
!model.isPureVirtual(record.id),
active_model: "",
auto_save: this.options.autoSave,
is_saving: record && record.context.saving,
@ -310,6 +333,15 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
return {};
},
/**
* This generates a virtual record with delayed call to "get_default" & "onchange"
* used in "instant search" mode
*
* @private
* @param {Object} context
* @param {Object} def_values
* @returns {Promise}
*/
_generateVirtualStateSimple: function(context, def_values) {
const model = this.options.basicFieldParams.model;
return new Promise(resolve => {
@ -349,7 +381,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
(current_batch_id, record_def) => {
this._timerOnChange = false;
if (
current_batch_id !=
current_batch_id !==
this.options.basicFieldParams
.current_batch_id ||
record_def.record.context.aborted
@ -376,7 +408,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
);
});
},
750,
this._onchange_delay,
this.options.basicFieldParams.current_batch_id,
record_def
);
@ -387,6 +419,15 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
});
},
/**
* Generates a complete virtual record
*
* @private
* @param {Object} data
* @param {Object} context
* @param {Object} def_values
* @returns {Promise}
*/
_generateVirtualStateFull: function(data, context, def_values) {
const model = this.options.basicFieldParams.model;
return new Promise(resolve => {
@ -441,6 +482,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
* @private
* @param {Object} data
* @param {Object} context
* @param {Boolean} simple_mode
* @returns {Object}
*/
_generateVirtualState: function(data, context, simple_mode) {
@ -468,6 +510,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
* @param {Integer/String} record_id
* @param {Object} changes
* @param {Object} options
* @returns {Promise}
*/
_applyChanges: function(record_id, changes, options) {
const model = this.options.basicFieldParams.model;
@ -479,6 +522,9 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
});
},
/**
* @private
*/
_detachAllWidgets: function() {
_.invoke(this.widgets.front, "on_detach_callback");
_.invoke(this.widgets.back, "on_detach_callback");
@ -633,6 +679,9 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
});
},
/**
* @private
*/
_updateLazyQty: function() {
var model = this.options.basicFieldParams.model;
var record = model.get(this.state.id);
@ -664,12 +713,12 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
const state_data = record.data;
let to_find = [];
if (!_.isEmpty(fields)) {
if (_.isEmpty(fields)) {
to_find = ["[data-field]"];
} else {
to_find = _.map(fields, field => {
return _.str.sprintf("[data-field=%s]", [field]);
});
} else {
to_find = ["[data-field]"];
}
this.$el.find(to_find.join()).each((key, value) => {

View File

@ -107,6 +107,14 @@ odoo.define(
});
},
/**
* Because this widget doesn't support comments/sections line types
* we need check if the line is valid to be shown.
*
* @private
* @param {Object} state
* @returns {Boolean}
*/
_isValidLineState: function(state) {
return (
state.data[this.options.field_map.product] &&
@ -114,6 +122,12 @@ odoo.define(
);
},
/**
* @private
* @param {Object} state_a
* @param {Object} state_b
* @returns {Boolean}
*/
_isEqualState: function(state_a, state_b) {
if (state_a.id === state_b.id) {
return true;
@ -133,6 +147,11 @@ odoo.define(
);
},
/**
* @private
* @param {Object} state
* @returns {Boolean}
*/
_existsWidgetWithState: function(state) {
for (let eb = this.widgets.length - 1; eb >= 0; --eb) {
const widget = this.widgets[eb];
@ -388,6 +407,7 @@ odoo.define(
},
/**
* @private
* @param {Array} datas
* @returns {Array}
*/
@ -540,8 +560,7 @@ odoo.define(
*
* @private
* @param {Array} search_records
* @param {Boolean} no_process_records
* @param {Number} position
* @param {Object} options
*/
_appendSearchRecords: function(search_records, options) {
const processed_info = options.no_process_records
@ -569,8 +588,8 @@ odoo.define(
}
// At this point the widget will use the existing state (line) or
// the search data. Using search data instead of waiting for
// simulated state gives a low FCP time.
// a simple state data. Using simple state data instead of waiting for
// complete state (default + onchange) gives a low FCP time.
const def = $.Deferred();
ProductPickerRecord.appendTo(this.$recordsContainer).then(
function(widget, widget_position) {
@ -622,9 +641,7 @@ odoo.define(
* Append search records to the view
*
* @param {Array} search_records
* @param {Boolean} no_attach_widgets
* @param {Boolean} no_process_records
* @param {Number} position
* @param {Object} options
* @returns {Array}
*/
appendSearchRecords: function(search_records, options = {}) {
@ -690,6 +707,7 @@ odoo.define(
* Handle card flip.
* Used to create/update the record
*
* @private
* @param {CustomEvent} evt
*/
_onRecordFlip: function(evt) {

View File

@ -12,22 +12,19 @@ odoo.define("web_widget_one2many_product_picker.BasicController", function(requi
* @override
*/
_confirmChange: function(id, fields, e) {
id = id || this.handle;
return this._super.apply(this, arguments).then(() => {
if (this.renderer && !_.isEmpty(this.renderer.allFieldWidgets)) {
const product_picker_widgets = _.filter(
this.renderer.allFieldWidgets[this.handle],
this.renderer.allFieldWidgets[id],
item => item.attrs.widget === "one2many_product_picker"
);
for (const widget of product_picker_widgets) {
const trigger_fields = widget.options.trigger_refresh_fields || [];
if (
_.difference(trigger_fields, fields).length !==
trigger_fields.length
) {
widget._reset(this.model.get(this.handle), e);
// Force re-launch onchanges on 'pure virtual' records
widget.renderer.clearRecords();
widget._render();
}
_.invoke(
product_picker_widgets,
"onDocumentConfirmChanges",
fields,
e
);
}
});
},

View File

@ -74,6 +74,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
* elements so can be removed safesly
*
* @param {String} id
* @returns {Boolean}
*/
removeVirtualRecord: function(id) {
if (!this.isPureVirtual(id)) {
@ -91,6 +92,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
this.removeLine(remove_id);
delete this.localData[remove_id];
}
return true;
},
/**
@ -158,6 +160,10 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
return Promise.reject();
}
var def = new Promise(function(resolve, reject) {
// Interrupt point (used in instant search)
if (!self.exists(record.id)) {
return Promise.reject();
}
var always = function() {
if (record._warning) {
if (params.allowWarning) {
@ -329,28 +335,142 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
},
/**
* This happens when the user discard main document changes (isn't a rollback)
* Because records can be removed at any time we
* need check if the record still existing.
* Necessary for 'instant search' feature.
*
* @override
*/
discardChanges: function(id, options) {
this._super.apply(this, arguments);
options = options || {};
var isNew = this.isNew(id);
var rollback = "rollback" in options ? options.rollback : isNew;
if (rollback) {
return;
_applyOnChange: function(values, record) {
if (!this.exists(record.id)) {
return Promise.reject();
}
const element = this.localData[id];
this._visitChildren(element, function(elem) {
if (
elem &&
elem.context &&
elem.context.product_picker_modified &&
_.isEmpty(elem._changes)
return this._super.apply(this, arguments);
},
/**
* @param {String} recordID
* @returns {Boolean}
*/
hasChanges: function(recordID) {
const record = this.localData[recordID];
return record && !_.isEmpty(record._changes);
},
/**
* @param {Object} model_fields
* @param {String} model
* @param {String} search_val
* @param {Array} domain
* @param {Array} fields
* @param {Object} orderby
* @param {String} operator
* @param {Number} limit
* @param {Number} offset
* @param {Object} context
* @returns {Promise}
*/
fetchNameSearchFull: function(
model_fields,
model,
search_val,
domain,
fields,
orderby,
operator,
limit,
offset,
context
) {
elem.context.product_picker_modified = false;
return this._rpc({
model: model,
method: "name_search",
kwargs: {
name: search_val,
args: domain || [],
operator: operator || "ilike",
limit: this.limit,
context: context || {},
},
}).then(results => {
const record_ids = results.map(item => item[0]);
return this.fetchGenericRecords(
model_fields,
model,
[["id", "in", record_ids]],
fields,
orderby,
limit,
offset,
context
);
});
},
/**
* @param {Object} model_fields
* @param {String} model
* @param {Array} domain
* @param {Array} fields
* @param {Array} orderby
* @param {Number} limit
* @param {Number} offset
* @param {Object} context
* @returns {Promise}
*/
fetchGenericRecords: function(
model_fields,
model,
domain,
fields,
orderby,
limit,
offset,
context
) {
return this._rpc({
model: model,
method: "search_read",
fields: fields,
domain: domain,
limit: limit,
offset: offset,
orderBy: orderby,
kwargs: {context: context},
}).then(result => {
for (const index in result) {
const record = result[index];
for (const fieldName in record) {
const field = model_fields[fieldName];
if (field.type !== "many2one") {
record[fieldName] = this._parseServerValue(
model_fields[fieldName],
record[fieldName]
);
}
}
}
return result;
});
},
fetchModelFieldsInfo: function(model) {
return this._rpc({
model: model,
method: "fields_get",
args: [
false,
[
"store",
"searchable",
"type",
"string",
"relation",
"selection",
"related",
],
],
context: this.getSession().user_context,
});
},
});

View File

@ -42,7 +42,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
}),
_auto_search_delay: 450,
_input_instant_search_time: 150,
_input_instant_search_time: 100,
// Model product.product fields
search_read_fields: ["id", "display_name", "uom_id"],
@ -88,7 +88,18 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
},
willStart: function() {
return this._super.apply(this, arguments).then(() => {
return this._super
.apply(this, arguments)
.then(() => {
const arch = this.view.arch;
const field_name = this.options.field_map.product;
const field_info = this.view.fieldsInfo[arch.tag][field_name];
const model = this.view.viewFields[field_info.name].relation;
this._modelName = model;
return this.parent_controller.model.fetchModelFieldsInfo(model);
})
.then(fields_info => {
this._fieldsInfo = fields_info;
if (this.isReadonly) {
// Show Lines
this._updateSearchContext(-1);
@ -169,6 +180,26 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
};
},
/**
* Because the widget shows "pure virtual" information, we don't have any 'onchange' linked.
* This method forces 'refresh' the widget if the selected fields was changed.
*
* @param {Array} fields
* @param {Event} e
*/
onDocumentConfirmChanges: function(fields, e) {
const trigger_fields = this.options.trigger_refresh_fields || [];
if (_.difference(trigger_fields, fields).length !== trigger_fields.length) {
this._reset(
this.parent_controller.model.get(this.parent_controller.handle),
e
);
// Force re-launch onchanges on 'pure virtual' records
this.renderer.clearRecords();
this._render();
}
},
/**
* @override
*/
@ -191,6 +222,9 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
group_def.active = !hasUserActive;
hasUserActive = true;
}
if (!group_def.records_per_page) {
group_def.records_per_page = 16;
}
this.searchGroups.push(group_def);
}
@ -200,6 +234,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
domain: this.options.all_domain,
order: false,
active: !hasUserActive,
records_per_page: 16,
});
this._activeSearchGroup = this.searchGroups[0];
},
@ -305,6 +340,16 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
this.updateSubtotalPrice();
},
/**
* Replace placeholders for search
* - $number_search -> Is a number
* - $search -> Is a string
*
* @private
* @param {Number/String} value
* @param {String} format
* @returns {Number/String}
*/
_getSearchValue: function(value, format) {
if (format === "$number_search") {
return Number(value);
@ -320,15 +365,10 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
*
* @private
* @param {Dictionary} options
* @param {Boolean} merge
* @returns {Deferred}
*/
_getSearchRecords: function(options) {
const arch = this.view.arch;
const search_mode = this.options.search[this._searchMode];
const field_name = this.options.field_map.product;
const field_info = this.view.fieldsInfo[arch.tag][field_name];
const model = this.view.viewFields[field_info.name].relation;
const orderby = this._searchContext.order;
const fields = this.search_read_fields;
@ -344,7 +384,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
},
this.value.getContext()
);
const limit = soptions.limit || this.options.records_per_page;
const limit = soptions.limit || this._activeSearchGroup.records_per_page;
const offset = soptions.offset || 0;
return new Promise(resolve => {
@ -355,8 +395,9 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
search_mode.name_search_value
);
const operator = search_mode.operator;
task = this._doSearchRecordsNameSearch(
model,
task = this.parent_controller.model.fetchNameSearchFull(
this._fieldsInfo,
this._modelName,
search_val,
domain,
fields,
@ -367,8 +408,9 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
context
);
} else {
task = this._doSearchRecords(
model,
task = this.parent_controller.model.fetchGenericRecords(
this._fieldsInfo,
this._modelName,
domain,
fields,
orderby,
@ -386,76 +428,6 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
});
},
_doSearchRecordsNameSearch: function(
model,
search_val,
domain,
fields,
orderby,
operator,
limit,
offset,
context
) {
return new Promise(resolve => {
this._rpc({
model: model,
method: "name_search",
kwargs: {
name: search_val,
args: domain || [],
operator: operator || "ilike",
limit: this.limit,
context: context || {},
},
}).then(results => {
const record_ids = results.map(item => item[0]);
this._doSearchRecords(
model,
[["id", "in", record_ids]],
fields,
orderby,
limit,
offset,
context
).then(records => {
resolve(records);
});
});
});
},
/**
* @param {String} model
* @param {Array} domain
* @param {Array} fields
* @param {Array} orderby
* @param {Number} limit
* @param {Number} offset
* @param {Object} context
* @returns {Promise}
*/
_doSearchRecords: function(
model,
domain,
fields,
orderby,
limit,
offset,
context
) {
return this._rpc({
model: model,
method: "search_read",
fields: fields,
domain: domain,
limit: limit,
offset: offset,
orderBy: orderby,
kwargs: {context: context},
});
},
/**
* @private
* @param {MouseEvent} evt
@ -514,7 +486,6 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
_getDefaultOptions: function() {
return {
currency_field: "currency_id",
records_per_page: 16,
show_subtotal: true,
show_discount: false,
edit_discount: false,
@ -540,7 +511,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
* This domain is used to get the records to display.
*
* @private
* @param {Object} active_search
* @param {Object} search_mode
* @returns {Array}
*/
_getFullSearchDomain: function(search_mode) {
@ -671,18 +642,24 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
return this._super.apply(this, arguments);
},
/**
* @private
* @param {SearchEvent} evt
*/
_onSearch: function(evt) {
this._searchContext.text = evt.target.value;
this.doRenderSearchRecords();
},
/**
* @private
* @param {InputEvent} evt
*/
_onInputSearch: function(evt) {
if (!this.options.instant_search) {
return;
}
if (this.options.instant_search) {
this._searchContext.text = evt.target.value;
this._lazyRenderSearchRecords();
// This.doRenderSearchRecords()
}
},
/**
@ -751,10 +728,6 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
}
});
} else {
// This is used to know when need use 'yellow' color
this.parent_controller.model.updateRecordContext(evt.data.id, {
product_picker_modified: true,
});
// This will trigger an "state" update
this._setValue({operation: "ADD", id: evt.data.id}).then(() => {
if (evt.data.callback) {
@ -764,6 +737,11 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
}
},
/**
* @param {Number} id
* @param {Object} data
* @param {Function} callback
*/
_doUpdateQuickRecord: function(id, data, callback) {
if (this.options.auto_save) {
var self = this;
@ -790,10 +768,6 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
}
});
} else {
// This is used to know when need use 'yellow' color
this.parent_controller.model.updateRecordContext(id, {
product_picker_modified: true,
});
// This will trigger an "state" update
this._setValue({operation: "UPDATE", id: id, data: data}).then(
function() {

View File

@ -98,7 +98,7 @@
<t t-elif="!is_virtual">
<span
t-att-data-field="field_map[field_uom_qty]"
t-attf-data-esc="str({{field_map[field_uom_qty]}}) + ' x ' + {{field_map[field_uom]}}.data.display_name"
t-attf-data-esc="str({{floatFixed(field_map[field_uom_qty])}}) + ' x ' + {{field_map[field_uom]}}.data.display_name"
t-attf-class="badge {{modified &amp;&amp; 'badge-warning' || 'badge-success'}} font-weight-bold rounded-0 mt-0 px-2 py-3 product_qty"
/>
</t>