[IMP] web_widget_one2many_product_picker: Instant search mode

pull/1858/head
Alexandre D. Díaz 2021-04-28 17:47:50 +02:00
parent 417bb032a9
commit 699cc2e5bc
13 changed files with 1064 additions and 438 deletions

View File

@ -91,6 +91,8 @@ Widget options:
modify/create a record with the widget.
* ignore_warning > Enable/Disable display onchange warnings (False by default)
* instant_search > Enable/Disable instant search mode (False by default)
* trigger_refresh_fields > Fields in the main record that dispatch a widget refresh (["partner_id", "currency_id"] by default)
All widget options are optional.
Notice that you can call '_' method to use translations. This only can be used with this widget.

View File

@ -49,6 +49,8 @@ Widget options:
modify/create a record with the widget.
* ignore_warning > Enable/Disable display onchange warnings (False by default)
* instant_search > Enable/Disable instant search mode (False by default)
* trigger_refresh_fields > Fields in the main record that dispatch a widget refresh (["partner_id", "currency_id"] by default)
All widget options are optional.
Notice that you can call '_' method to use translations. This only can be used with this widget.

View File

@ -479,6 +479,10 @@ modify/create a record with the widget.</p>
</li>
<li><p class="first">ignore_warning &gt; Enable/Disable display onchange warnings (False by default)</p>
</li>
<li><p class="first">instant_search &gt; Enable/Disable instant search mode (False by default)</p>
</li>
<li><p class="first">trigger_refresh_fields &gt; Fields in the main record that dispatch a widget refresh ([“partner_id”, “currency_id”] by default)</p>
</li>
</ul>
<p>All widget options are optional.
Notice that you can call _ method to use translations. This only can be used with this widget.</p>

View File

@ -62,6 +62,15 @@ odoo.define(
this._super.apply(this, arguments);
},
/**
* @override
*/
_applyChanges: function() {
return this._super.apply(this, arguments).then(() => {
this._updateButtons();
});
},
/**
* Create or accept changes
*/
@ -227,7 +236,6 @@ odoo.define(
this.trigger_up("quick_record_updated", {
changes: ev.data.changes,
});
this._updateButtons();
}
}
},
@ -261,7 +269,6 @@ odoo.define(
saving: false,
});
this.model.unsetDirty(this.handle);
// Self._updateButtons();
this._enableQuickCreate();
},
});
@ -298,13 +305,25 @@ odoo.define(
this.trigger_up("restore_flip_card", {
success_callback: function() {
self.trigger_up("update_quick_record", {
id: record.id,
callback: function() {
self.model.unsetDirty(self.handle);
// Self._updateButtons();
self._enableQuickCreate();
},
// Qty are handled in a special way because can be modified without
// wait for server response
self.model.localData[record.id].data[
self.fieldMap.product_uom_qty
] = record.data[self.fieldMap.product_uom_qty];
// SaveRecord used to make a save point.
self.saveRecord(self.handle, {
stayInEdit: true,
reload: true,
savePoint: true,
viewType: "form",
}).then(() => {
self.trigger_up("update_quick_record", {
id: record.id,
callback: function() {
self.model.unsetDirty(self.handle);
self._enableQuickCreate();
},
});
});
},
block: true,
@ -314,29 +333,30 @@ odoo.define(
_discard: function() {
if (this._disabled) {
// Don't do anything if we are already creating a record
return Promise.resolve();
return;
}
this._disableQuickCreate();
const record = this.model.get(this.handle);
this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true,
});
// Rollback to restore the save point
this.model.discardChanges(this.handle, {
rollback: true,
});
const record = this.model.get(this.handle);
this.trigger_up("quick_record_updated", {
changes: record.data,
});
if (this.model.isNew(record.id)) {
this.update({}, {reload: false});
this.update({}, {reload: false}).then(() => {
if (!this.model.isNew(record.id)) {
this.model.unsetDirty(this.handle);
}
this.trigger_up("restore_flip_card");
this._updateButtons();
this._enableQuickCreate();
} else {
this.update({}, {reload: false}).then(() => {
this.model.unsetDirty(this.handle);
this.trigger_up("restore_flip_card");
this._updateButtons();
this._enableQuickCreate();
});
}
});
},
/**

View File

@ -42,7 +42,6 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
front: [],
back: [],
};
this._lazyUpdateRecord = _.debounce(this._updateRecord.bind(this), 450);
},
@ -51,8 +50,10 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
*
* @returns {Object}
*/
generateVirtualState: function() {
return this._generateVirtualState().then(this.recreate.bind(this));
generateVirtualState: function(simple_mode) {
return this._generateVirtualState(undefined, undefined, simple_mode).then(
this.recreate.bind(this)
);
},
/**
@ -80,11 +81,26 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
* @override
*/
destroy: function() {
this.abortTimeouts();
if (this.state) {
this.options.basicFieldParams.model.removeVirtualRecord(this.state.id);
}
this.$el.remove();
this.$card.off("");
this._super.apply(this, arguments);
},
abortTimeouts: function() {
if (this._timerOnChange) {
clearTimeout(this._timerOnChange);
this._timerOnChange = false;
}
if (this.state) {
const model = this.options.basicFieldParams.model;
model.updateRecordContext(this.state.id, {aborted: true});
}
},
/**
* @override
*/
@ -105,6 +121,12 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
* @returns {Promise}
*/
recreate: function(state) {
if (!this.getParent()) {
// It's a zombie record! ensure kill it!
this.destroy();
return;
}
if (state) {
this._setState(state);
}
@ -121,6 +143,15 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
return this._render();
},
markToDestroy: function() {
this.toDestroy = true;
this.$el.hide();
},
isMarkedToDestroy: function() {
return this.toDestroy === true;
},
/**
* Generates the URL for the given product using the selected field
*
@ -151,7 +182,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
price,
this.state.fields[field_name],
this.options.currencyField,
this.state.data
this.options.basicFieldParams.record.data
);
},
@ -184,12 +215,15 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
this.fields = this.getParent().state.fields;
this.fieldsInfo = this.getParent().state.fieldsInfo.form;
const model = this.options.basicFieldParams.model;
if (this.state && (!viewState || this.state.id !== viewState.id)) {
model.removeVirtualRecord(this.state.id);
}
this.state = viewState;
if (recordSearch) {
this.recordSearch = recordSearch;
}
const model = this.options.basicFieldParams.model;
this.is_virtual =
(this.state && model.isPureVirtual(this.state.id)) || false;
@ -203,6 +237,16 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
this._incProductQty(lazy_qty - 1);
}
}
this._setMasterUomMap();
},
_setMasterUomMap: function() {
this.master_uom_map = {
field_uom: "product_uom",
field_uom_qty: "product_uom_qty",
search_field_uom: "uom_id",
};
},
/**
@ -232,6 +276,9 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
auto_save: this.options.autoSave,
is_saving: record && record.context.saving,
lazy_qty: record && record.context.lazy_qty,
has_onchange: record && !record.context.not_onchange,
field_uom: this.master_uom_map.field_uom,
field_uom_qty: this.master_uom_map.field_uom_qty,
};
},
@ -243,25 +290,151 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
*/
_getInternalVirtualRecordContext: function() {
const context = {};
context["default_" + this.options.basicFieldParams.relation_field] =
this.options.basicFieldParams.state.id || null;
context[`default_${this.options.basicFieldParams.relation_field}`] =
this.options.basicFieldParams.record.id || null;
context[`default_${this.options.fieldMap.product}`] =
this.recordSearch.id || null;
context[`default_${this.master_uom_map.field_uom_qty}`] = 1.0;
return context;
},
/**
* Forced data used in virtual states.
* Be careful with the onchanges sequence. Think as user interaction, not as CRUD operation.
* Be careful with the onchanges sequence. Think as user interaction ("ADD", "DELETE", ... commands), not as CRUD operation.
*
* @private
* @returns {Object}
*/
_getInternalVirtualRecordData: function() {
const data = {};
data[this.options.fieldMap.product] = {
operation: "ADD",
id: this.recordSearch.id,
};
return data;
// To be overwritten
return {};
},
_generateVirtualStateSimple: function(context, def_values) {
const model = this.options.basicFieldParams.model;
return new Promise(resolve => {
const record_def = model.createVirtualDatapoint(
this.options.basicFieldParams.value.id,
{
context: context,
}
);
model.applyDefaultValues(record_def.record.id, def_values).then(() => {
const new_state = model.get(record_def.record.id);
const product_uom_id =
new_state.data[
this.options.fieldMap[this.master_uom_map.field_uom]
].id;
// Apply default values
model
.applyDefaultValues(
product_uom_id,
{
display_name: this.recordSearch[
this.master_uom_map.search_field_uom
][1],
},
{
fieldNames: ["display_name"],
}
)
.then(() => {
return model._fetchRelationalData(record_def.record);
})
.then(() => {
return model._postprocess(record_def.record);
})
.then(() => {
this._timerOnChange = setTimeout(
(current_batch_id, record_def) => {
this._timerOnChange = false;
if (
current_batch_id !=
this.options.basicFieldParams
.current_batch_id ||
record_def.record.context.aborted
) {
return;
}
model
._makeDefaultRecordNoDatapoint(
record_def.record,
record_def.params
)
.then(() => {
if (record_def.record.context.aborted) {
return;
}
model.updateRecordContext(
record_def.record.id,
{
not_onchange: false,
}
);
this.recreate(
model.get(record_def.record.id)
);
});
},
750,
this.options.basicFieldParams.current_batch_id,
record_def
);
resolve(model.get(record_def.record.id));
});
});
});
},
_generateVirtualStateFull: function(data, context, def_values) {
const model = this.options.basicFieldParams.model;
return new Promise(resolve => {
model
.createVirtualRecord(this.options.basicFieldParams.value.id, {
context: context,
})
.then(result => {
// Apply default values
model
.applyDefaultValues(result.record.id, def_values)
.then(() => {
const new_state = model.get(result.record.id);
const product_uom_id =
new_state.data[
this.options.fieldMap[
this.master_uom_map.field_uom
]
].id;
model
.applyDefaultValues(
product_uom_id,
{
display_name: this.recordSearch[
this.master_uom_map.search_field_uom
][1],
},
{
fieldNames: ["display_name"],
}
)
.then(() => {
const sdata = _.extend(
{},
this._getInternalVirtualRecordData(),
data
);
this._applyChanges(
result.record.id,
sdata,
result.params
).then(() =>
resolve(model.get(result.record.id))
);
});
});
});
});
},
/**
@ -270,19 +443,39 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
* @param {Object} context
* @returns {Object}
*/
_generateVirtualState: function(data, context) {
const model = this.options.basicFieldParams.model;
_generateVirtualState: function(data, context, simple_mode) {
const scontext = _.extend(
{},
this._getInternalVirtualRecordContext(),
context
);
// Force qty to 1.0 to launch correct onchanges
scontext[`default_${this.options.fieldMap.product_uom_qty}`] = 1.0;
const sdata = _.extend({}, this._getInternalVirtualRecordData(), data);
return model.createVirtualRecord(this.options.basicFieldParams.value.id, {
data: sdata,
context: scontext,
// Apply default values
const def_values = {
[this.options.fieldMap.product]: this.recordSearch.id,
[this.options.fieldMap[this.master_uom_map.field_uom_qty]]: 1.0,
[this.options.fieldMap[this.master_uom_map.field_uom]]: this
.recordSearch[this.master_uom_map.search_field_uom][0],
};
if (simple_mode) {
return this._generateVirtualStateSimple(scontext, def_values);
}
return this._generateVirtualStateFull(data, scontext, def_values);
},
/**
* Apply changes (with onchange)
*
* @param {Integer/String} record_id
* @param {Object} changes
* @param {Object} options
*/
_applyChanges: function(record_id, changes, options) {
const model = this.options.basicFieldParams.model;
return model._applyChange(record_id, changes, options).then(() => {
model.updateRecordContext(record_id, {
not_onchange: false,
});
this.recreate(model.get(record_id));
});
},
@ -482,15 +675,18 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
this.$el.find(to_find.join()).each((key, value) => {
const $elm = $(value);
const format_out = $elm.data("esc") || $elm.data("field");
$elm.html(
py.eval(format_out, _.extend({}, state_data, this.recordSearch))
const text_out = py.eval(
format_out,
_.extend({}, state_data, this.recordSearch)
);
$elm.html(text_out);
$elm.attr("title", text_out);
});
if (this.options.showDiscount) {
const field_map = this.options.fieldMap;
if (state_data) {
const has_discount = state_data[field_map.discount] > 0.0;
const has_discount = state_data[field_map.discount] !== 0.0;
this.$el
.find(".original_price,.discount_price")
.toggleClass("d-none", !has_discount);
@ -526,7 +722,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
price_reduce,
this.state.fields[field_map.price_unit],
this.options.currencyField,
this.state.data
this.options.basicFieldParams.record.data
)
);
},
@ -594,9 +790,14 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
if (record.context.saving) {
return Promise.resolve();
}
const changes = _.pick(record.data, this.options.fieldMap.product_uom_qty);
if (changes[this.options.fieldMap.product_uom_qty] === 0) {
changes[this.options.fieldMap.product_uom_qty] = 1;
const changes = _.pick(
record.data,
this.options.fieldMap[this.master_uom_map.field_uom_qty]
);
if (
changes[this.options.fieldMap[this.master_uom_map.field_uom_qty]] === 0
) {
changes[this.options.fieldMap[this.master_uom_map.field_uom_qty]] = 1;
}
this.$card.addClass("blocked");
return model.notifyChanges(record.id, changes).then(() => {
@ -627,15 +828,25 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
// wait for the Odoo response.
const model_record_data = model.localData[this.state.id].data;
if (
_.isNull(model_record_data[this.options.fieldMap.product_uom_qty])
_.isNull(
model_record_data[
this.options.fieldMap[this.master_uom_map.field_uom_qty]
]
)
) {
model_record_data[this.options.fieldMap.product_uom_qty] = 1;
model_record_data[
this.options.fieldMap[this.master_uom_map.field_uom_qty]
] = 1;
}
model_record_data[this.options.fieldMap.product_uom_qty] += amount;
model_record_data[
this.options.fieldMap[this.master_uom_map.field_uom_qty]
] += amount;
return model
.notifyChanges(record.id, {
[this.options.fieldMap.product_uom_qty]:
model_record_data[this.options.fieldMap.product_uom_qty],
[this.options.fieldMap[this.master_uom_map.field_uom_qty]]:
model_record_data[
this.options.fieldMap[this.master_uom_map.field_uom_qty]
],
})
.then(() => {
this._processDynamicFields();
@ -836,6 +1047,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
_onRestoreFlipCard: function(evt) {
this.$card.removeClass("active");
this.$front.removeClass("d-none");
const $img = this.$front.find("img");
const cur_img_src = $img.attr("src");
if (this.$card.hasClass("oe_flip_card_maximized")) {
this.$card.removeClass("oe_flip_card_maximized");
this.$card.on("transitionend", () => {
@ -859,6 +1072,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
if (evt.data.block) {
this.$card.addClass("blocked");
}
$img.attr("src", $img.data("srcAlt"));
$img.data("srcAlt", cur_img_src);
},
/**
@ -878,6 +1093,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
*/
_onQuickRecordUpdated: function(evt) {
this._processDynamicFields(Object.keys(evt.data.changes));
// This.recreate();
this.trigger_up("update_subtotal");
},
});

View File

@ -40,9 +40,7 @@ odoo.define(
// 'receive' more arguments.
this.options = parent.options;
this.mode = parent.mode;
this.search_data = parent._searchRecords;
this.search_group = parent._activeSearchGroup;
this.last_search_data_count = parent._lastSearchRecordsCount;
},
/**
@ -50,7 +48,7 @@ odoo.define(
*/
on_attach_callback: function() {
this._isInDom = true;
_.invoke(this.widgets, "on_attach_callback");
_.invoke(_.compact(this.widgets), "on_attach_callback");
},
/**
@ -58,7 +56,7 @@ odoo.define(
*/
on_detach_callback: function() {
this._isInDom = false;
_.invoke(this.widgets, "on_detach_callback");
_.invoke(_.compact(this.widgets), "on_detach_callback");
},
/**
@ -78,16 +76,10 @@ odoo.define(
},
/**
* @param {Object} search_data
* @param {Number} count
* @param {Object} search_group
*/
updateSearchData: function(search_data, count, search_group) {
this.search_data = search_data;
this.last_search_data_count = count;
updateSearchGroup: function(search_group) {
this.search_group = search_group;
this._loadMoreWorking = false;
this.$btnLoadMore.attr("disabled", false);
},
/**
@ -115,20 +107,11 @@ odoo.define(
});
},
/**
* Recreate the given widget by the state id
*
* @param {String} state_id
* @param {Object} new_state
*/
updateRecord: function(state_id, new_state) {
for (let eb = this.widgets.length - 1; eb >= 0; --eb) {
const widget = this.widgets[eb];
if (widget.state.id === state_id) {
widget.recreate(new_state);
break;
}
}
_isValidLineState: function(state) {
return (
state.data[this.options.field_map.product] &&
state.data[this.options.field_map.product].data.id
);
},
_isEqualState: function(state_a, state_b) {
@ -137,9 +120,31 @@ odoo.define(
}
const product_id_a =
state_a.data[this.options.field_map.product].data.id;
const product_uom_id_a =
state_a.data[this.options.field_map.product_uom].data.id;
const product_id_b =
state_b.data[this.options.field_map.product].data.id;
return product_id_a === product_id_b;
const product_uom_id_b =
state_b.data[this.options.field_map.product_uom].data.id;
return (
product_id_a === product_id_b &&
product_uom_id_a === product_uom_id_b
);
},
_existsWidgetWithState: function(state) {
for (let eb = this.widgets.length - 1; eb >= 0; --eb) {
const widget = this.widgets[eb];
if (
widget &&
widget.state &&
this._isEqualState(widget.state, state)
) {
return true;
}
}
return false;
},
/**
@ -152,63 +157,52 @@ odoo.define(
* @returns {Array}
*/
_processStatesToDestroy: function(states) {
// Get widgets to destroy
// Update states only affect to "non pure virtual" records
const to_destroy = [];
const to_add = [];
for (const state of states) {
for (let e = this.widgets.length - 1; e >= 0; --e) {
const widget = this.widgets[e];
if (widget && this._isEqualState(widget.state, state)) {
to_destroy.push(widget);
delete this.widgets[e];
}
}
}
// If doesn't exists other records with the same product, we need
// create a 'pure virtual' record again.
const to_add = [];
for (const index_destroy in to_destroy) {
const widget_destroyed = to_destroy[index_destroy];
let found = false;
// If already exists a widget for the product don't try create a new one
for (let eb = this.widgets.length - 1; eb >= 0; --eb) {
const widget = this.widgets[eb];
if (
widget &&
widget.state &&
this._isEqualState(widget.state, widget_destroyed.state)
) {
found = true;
break;
}
}
if (!found) {
// Get the new state ID if exists to link it with the new record
let state_id = null;
for (let eb = this.state.data.length - 1; eb >= 0; --eb) {
const state = this.state.data[eb];
if (this._isEqualState(state, widget_destroyed.state)) {
state_id = state.id;
break;
// If already exists a widget for the product don't try create a new one
let recreated = false;
if (!this._existsWidgetWithState(widget.state)) {
// Get the new state ID if exists to link it with the new record
// This happens when remove a record that have a new state info
for (
let eb = this.state.data.length - 1;
eb >= 0;
--eb
) {
const state = this.state.data[eb];
if (!this._isValidLineState(state)) {
continue;
}
if (this._isEqualState(state, widget.state)) {
widget.recreate(state);
recreated = true;
break;
}
}
}
if (!recreated) {
widget.markToDestroy();
to_destroy.push(widget);
const search_record = _.omit(
widget.recordSearch,
"__id"
);
to_add.push([
[search_record],
{
no_attach_widgets: false,
no_process_records: false,
position: widget.state.id,
},
]);
}
}
// "Lines" section doesn't show virtual records
if (
(state_id && this.search_group.name === "main_lines") ||
this.search_group.name !== "main_lines"
) {
const widget_product_id =
widget_destroyed.state.data[
this.options.field_map.product
].data.id;
const search_record = _.find(this.search_data, {
id: widget_product_id,
});
const new_search_record = _.extend({}, search_record, {
__id: state_id,
});
const card_id = widget_destroyed.$el.data("cardId");
to_add.push([[new_search_record], false, true, card_id]);
}
}
}
@ -225,12 +219,16 @@ odoo.define(
*/
_processCurrentStates: function() {
// Records to Update or Create
const model = this.getParent().getBasicFieldParams().model;
const to_destroy = [];
const to_add = [];
for (const index in this.state.data) {
const state = this.state.data[index];
if (!this._isValidLineState(state)) {
continue;
}
let exists = false;
let search_record_index = -1;
let search_record_index = false;
let search_record = false;
for (let e = this.widgets.length - 1; e >= 0; --e) {
const widget = this.widgets[e];
@ -238,24 +236,28 @@ odoo.define(
// Already processed widget (deleted)
continue;
}
if (this._isEqualState(widget.state, state)) {
var model = this.getParent().getBasicFieldParams().model;
var record = model.get(widget.state.id);
const is_equal_state = this._isEqualState(widget.state, state);
if (widget.isMarkedToDestroy()) {
exists = true;
} else if (is_equal_state) {
const record = model.get(widget.state.id);
model.updateRecordContext(state.id, {
lazy_qty: record.context.lazy_qty || 0,
});
widget.recreate(state);
exists = true;
break;
} else if (
}
if (
!is_equal_state &&
widget.recordSearch.id ===
state.data[this.options.field_map.product].data.id
state.data[this.options.field_map.product].data.id
) {
// Is a new record (can be other record for the same 'search record' or a replacement for a pure virtual)
search_record_index = widget.$el.index();
search_record_index = widget.state.id;
search_record = widget.recordSearch;
var model = this.getParent().getBasicFieldParams().model;
var record = model.get(widget.state.id);
const record = model.get(widget.state.id);
model.updateRecordContext(state.id, {
lazy_qty: record.context.lazy_qty || 0,
});
@ -273,16 +275,18 @@ odoo.define(
this.state.data = _.compact(this.state.data);
// Need add a new one?
if (!exists && search_record_index !== -1) {
// Add to create the new record
if (!exists && search_record_index) {
const new_search_record = _.extend({}, search_record, {
__id: state.id,
});
to_add.push([
[new_search_record],
false,
true,
search_record_index,
{
no_attach_widgets: true,
no_process_records: true,
position: search_record_index,
},
]);
}
}
@ -304,9 +308,15 @@ odoo.define(
const states_to_destroy = [];
for (const index in old_states) {
const old_state = old_states[index];
if (!this._isValidLineState(old_state)) {
continue;
}
let found = false;
for (const e in this.state.data) {
const current_state = this.state.data[e];
if (!this._isValidLineState(current_state)) {
continue;
}
if (this._isEqualState(current_state, old_state)) {
found = true;
break;
@ -323,47 +333,40 @@ odoo.define(
states_to_destroy
);
// Make widgets to destroy invisible to avoid render 'dance'
for (const widget of to_destroy_old) {
widget.$el.hide();
const [
destroyed_current,
to_add_current,
] = this._processCurrentStates();
const currentTasks = [];
const to_add = [].concat(to_add_current, to_add_virtual);
for (const params of to_add) {
currentTasks.push(this.appendSearchRecords.apply(this, params)[0]);
}
const oldTasks = [];
for (const params of to_add_virtual) {
oldTasks.push(this.appendSearchRecords.apply(this, params)[0]);
}
Promise.all(oldTasks).then(() => {
const [
to_destroy_current,
to_add_current,
] = this._processCurrentStates();
// Make widgets to destroy invisible to avoid render 'dance'
for (const widget of to_destroy_current) {
widget.$el.hide();
}
const currentTasks = [];
for (const params of to_add_current) {
currentTasks.push(
this.appendSearchRecords.apply(this, params)[0]
);
}
Promise.all(currentTasks).then(() => {
_.invoke(to_destroy_old, "destroy");
_.invoke(to_destroy_current, "destroy");
def.resolve();
});
Promise.all(currentTasks).then(() => {
_.invoke(to_destroy_old, "destroy");
_.invoke(destroyed_current, "destroy");
this.widgets = _.difference(this.widgets, to_destroy_old);
def.resolve();
});
return def;
},
clearRecords: function() {
_.invoke(_.compact(this.widgets), "destroy");
this.widgets = [];
if (this.$recordsContainer) {
this.$recordsContainer.empty();
}
},
/**
* @override
*/
_renderView: function() {
const oldWidgets = _.compact(this.widgets);
_.invoke(_.compact(this.widgets), "destroy");
this.widgets = [];
this.$recordsContainer = $("<DIV/>", {
class: "w-100 row",
@ -374,23 +377,14 @@ odoo.define(
this.$btnLoadMore = this.$extraButtonsContainer.find(
"#productPickerLoadMore"
);
this.search_data = this._sort_search_data(this.search_data);
return new Promise(resolve => {
const defs = this.appendSearchRecords(this.search_data, true);
Promise.all(defs).then(() => {
_.invoke(oldWidgets, "destroy");
this.$el.empty();
this.$el.append(this.$recordsContainer);
this.$el.append(this.$extraButtonsContainer);
this.showLoadMore(
this.last_search_data_count >= this.options.records_per_page
);
if (this._isInDom) {
_.invoke(this.widgets, "on_attach_callback");
}
return resolve();
});
});
// This.search_data = this._sort_search_data(this.search_data);
this.$el.empty();
this.$el.append(this.$recordsContainer);
this.$el.append(this.$extraButtonsContainer);
// This.showLoadMore(
// this.last_search_data_count >= this.options.records_per_page
// );
return this._super.apply(this, arguments);
},
/**
@ -405,7 +399,10 @@ odoo.define(
for (const index_state in this.state.data) {
const state_data = this.state.data[index_state];
if (state_data.data[field_name].res_id === data.id) {
if (
this._isValidLineState(state_data) &&
state_data.data[field_name].res_id === data.id
) {
data._order_value = state_data.res_id;
}
}
@ -431,32 +428,76 @@ odoo.define(
_processSearchRecords: function(results) {
const field_name = this.options.field_map.product;
const records = [];
const states = [];
var test_values = function(field_value, record_search) {
return (
(typeof field_value === "object" &&
field_value.data.id === record_search.id) ||
field_value === record_search.id
);
};
for (const index in results) {
const record_search = results[index];
let state_data_found = false;
// Analyze 'pure virtual' records
// Pure virtual records aren't linked with field list
// so we need search them linked in the widgets.
for (const index_widget in this.widgets) {
const widget = this.widgets[index_widget];
if (widget.isMarkedToDestroy()) {
continue;
}
if (
record_search.__id === widget.state.id ||
(!record_search.__id &&
widget.recordSearch.id === record_search.id)
) {
state_data_found = true;
if (widget.state) {
states.push(widget.state);
}
break;
}
}
// If already exists a widget with the search result
// avoid create a new one
if (state_data_found) {
continue;
}
// Analyze field records
// If not found any widget we need create a new one
// linked with the state record
for (const index_data in this.state.data) {
const state_record = this.state.data[index_data];
const field = state_record.data[field_name];
if (
(typeof field === "object" &&
field.data.id === record_search.id) ||
field === record_search.id
) {
if (!this._isValidLineState(state_record)) {
continue;
}
const field_value = state_record.data[field_name];
if (test_values(field_value, record_search)) {
records.push(
_.extend({}, record_search, {
__id: state_record.id,
})
);
states.push(state_record);
state_data_found = true;
}
}
if (!state_data_found) {
records.push(record_search);
}
}
return records;
return {
records: records,
states: states,
};
},
/**
@ -502,15 +543,12 @@ odoo.define(
* @param {Boolean} no_process_records
* @param {Number} position
*/
_appendSearchRecords: function(
search_records,
no_process_records,
position
) {
const processed_records = no_process_records
_appendSearchRecords: function(search_records, options) {
const processed_info = options.no_process_records
? search_records
: this._processSearchRecords(search_records);
_.each(processed_records, search_record => {
const records_to_add = processed_info.records || search_records;
_.each(records_to_add, search_record => {
const state_data = this._getRecordDataById(search_record.__id);
const widget_options = this._getRecordOptions(search_record);
widget_options.renderer_widget_index = this.widgets.length;
@ -524,7 +562,9 @@ odoo.define(
// Simulate new lines to dispatch get_default & onchange's to get the
// relevant data to print. This case increase the TTI time.
if (!state_data) {
const defVirtualState = ProductPickerRecord.generateVirtualState();
const defVirtualState = ProductPickerRecord.generateVirtualState(
this.options.instant_search
);
this.defsVirtualState.push(defVirtualState);
}
@ -536,15 +576,39 @@ odoo.define(
function(widget, widget_position) {
if (typeof widget_position !== "undefined") {
const $elm = this.$el.find(
`[data-card-id="${position}"]`
`[data-card-id="${widget_position}"]:first`
);
widget.$el.insertBefore($elm);
}
def.resolve();
}.bind(this, ProductPickerRecord, position)
}.bind(this, ProductPickerRecord, options.position)
);
this.defs.push(def);
});
// Destroy unused
if (options.cleanup) {
const num_widgets = this.widgets.length;
for (
let index_widget = num_widgets - 1;
index_widget >= 0;
--index_widget
) {
const widget = this.widgets[index_widget];
let found_state = false;
for (const state of processed_info.states) {
if (widget.state && widget.state.id === state.id) {
found_state = true;
break;
}
}
if (!found_state && widget.state) {
widget.destroy();
delete this.widgets[index_widget];
}
}
// Clean widget array
this.widgets = _.compact(this.widgets);
}
},
/**
@ -563,24 +627,20 @@ odoo.define(
* @param {Number} position
* @returns {Array}
*/
appendSearchRecords: function(
search_records,
no_attach_widgets,
no_process_records,
position
) {
appendSearchRecords: function(search_records, options = {}) {
this.trigger_up("loading_records");
this.defs = [];
this.defsVirtualState = [];
const cur_widget_index = this.widgets.length;
this._appendSearchRecords(search_records, no_process_records, position);
this._appendSearchRecords(search_records, options);
const defs = this.defs;
delete this.defs;
const defsVirtualState = this.defsVirtualState;
delete this.defsVirtualState;
return [
Promise.all(defs).then(() => {
if (!no_attach_widgets && this._isInDom) {
if (!options.no_attach_widgets && this._isInDom) {
const new_widgets = this.widgets.slice(cur_widget_index);
_.invoke(new_widgets, "on_attach_callback");
}
@ -597,7 +657,6 @@ odoo.define(
_onClickLoadMore: function() {
this.$btnLoadMore.attr("disabled", true);
this.trigger_up("load_more");
this._loadMoreWorking = true;
},
/**

View File

@ -0,0 +1,35 @@
// Copyright 2021 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.BasicController", function(require) {
"use strict";
const BasicController = require("web.BasicController");
BasicController.include({
/**
* This is necessary to refresh 'one2many_product_picker' when some 'trigger_refresh_fields' fields changes.
*
* @override
*/
_confirmChange: function(id, fields, e) {
return this._super.apply(this, arguments).then(() => {
const product_picker_widgets = _.filter(
this.renderer.allFieldWidgets[this.handle],
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();
}
}
});
},
});
});

View File

@ -7,19 +7,37 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
BasicModel.include({
/**
* @param {Number/String} handle
* @override
*/
init: function() {
this._super.apply(this, arguments);
},
/**
* This is necessary because 'pure virtual' records
* can be destroyed at any time.
*
* @param {String} id
* @returns {Boolean}
*/
exists: function(id) {
return !_.isEmpty(this.localData[id]);
},
/**
* @param {String} id
* @param {Object} context
*/
updateRecordContext: function(handle, context) {
this.localData[handle].context = _.extend(
updateRecordContext: function(id, context) {
this.localData[id].context = _.extend(
{},
this.localData[handle].context,
this.localData[id].context,
context
);
},
/**
* @param {Number/String} id
* @param {String} id
* @returns {Boolean}
*/
isPureVirtual: function(id) {
@ -28,7 +46,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
},
/**
* @param {Number/String} id
* @param {String} id
* @param {Boolean} status
*/
setPureVirtual: function(id, status) {
@ -41,7 +59,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
},
/**
* @param {Number/String} id
* @param {String} id
*/
unsetDirty: function(id) {
const data = this.localData[id];
@ -52,7 +70,204 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
},
/**
* Generates a virtual records without link it
* 'Pure virtual' records are not used by other
* elements so can be removed safesly
*
* @param {String} id
*/
removeVirtualRecord: function(id) {
if (!this.isPureVirtual(id)) {
return false;
}
const data = this.localData[id];
const to_remove = [];
this._visitChildren(data, item => {
to_remove.push(item.id);
});
to_remove.reverse();
for (const remove_id of to_remove) {
this.removeLine(remove_id);
delete this.localData[remove_id];
}
},
/**
* This is a cloned method from Odoo framework.
* Virtual records are processed in two parts,
* this is the second part and here we trigger onchange
* process.
*
* @param {Object} record
* @param {Object} params
* @returns {Promise}
*/
_makeDefaultRecordNoDatapoint: function(record, params) {
var self = this;
var targetView = params.viewType;
var fieldsInfo = params.fieldsInfo;
var fieldNames = Object.keys(fieldsInfo[targetView]);
var fields_key = _.without(fieldNames, "__last_update");
// Fields that are present in the originating view, that need to be initialized
// Hence preventing their value to crash when getting back to the originating view
var parentRecord =
params.parentID && this.localData[params.parentID].type === "list"
? this.localData[params.parentID]
: null;
if (parentRecord && parentRecord.viewType in parentRecord.fieldsInfo) {
var originView = parentRecord.viewType;
fieldNames = _.union(
fieldNames,
Object.keys(parentRecord.fieldsInfo[originView])
);
}
return this._rpc({
model: record.model,
method: "default_get",
args: [fields_key],
context: params.context,
}).then(function(result) {
// Interrupt point (used in instant search)
if (!self.exists(record.id)) {
return Promise.reject();
}
// We want to overwrite the default value of the handle field (if any),
// in order for new lines to be added at the correct position.
// -> This is a rare case where the defaul_get from the server
// will be ignored by the view for a certain field (usually "sequence").
var overrideDefaultFields = self._computeOverrideDefaultFields(
params.parentID,
params.position
);
if (overrideDefaultFields) {
result[overrideDefaultFields.field] = overrideDefaultFields.value;
}
return self
.applyDefaultValues(record.id, result, {fieldNames: fieldNames})
.then(function() {
if (!self.exists(record.id)) {
return Promise.reject();
}
var def = new Promise(function(resolve, reject) {
var always = function() {
if (record._warning) {
if (params.allowWarning) {
delete record._warning;
} else {
reject();
}
}
resolve();
};
self._performOnChange(record, fields_key)
.then(always)
.guardedCatch(always);
});
return def;
})
.then(function() {
if (!self.exists(record.id)) {
return Promise.reject();
}
return self._fetchRelationalData(record);
})
.then(function() {
if (!self.exists(record.id)) {
return Promise.reject();
}
return self._postprocess(record);
})
.then(function() {
if (!self.exists(record.id)) {
return Promise.reject();
}
// Save initial changes, so they can be restored later,
// if we need to discard.
self.save(record.id, {savePoint: true});
return record.id;
});
});
},
/**
* Virtual records are processed in two parts,
* this is the first part and here we create
* the state (without aditional process)
*
* @param {String} listID
* @param {Object} options
* @returns {Object}
*/
createVirtualDatapoint: function(listID, options) {
const list = this.localData[listID];
const context = _.extend({}, this._getContext(list), options.context);
const position = options ? options.position : "top";
const params = {
context: context,
fields: list.fields,
fieldsInfo: list.fieldsInfo,
parentID: list.id,
position: position,
viewType: list.viewType,
allowWarning: true,
doNotSetDirty: true,
};
var targetView = params.viewType;
var fields = params.fields;
var fieldsInfo = params.fieldsInfo;
// Fields that are present in the originating view, that need to be initialized
// Hence preventing their value to crash when getting back to the originating view
var parentRecord =
params.parentID && this.localData[params.parentID].type === "list"
? this.localData[params.parentID]
: null;
if (parentRecord && parentRecord.viewType in parentRecord.fieldsInfo) {
var originView = parentRecord.viewType;
fieldsInfo[targetView] = _.defaults(
{},
fieldsInfo[targetView],
parentRecord.fieldsInfo[originView]
);
fields = _.defaults({}, fields, parentRecord.fields);
}
const record = this._makeDataPoint({
modelName: list.model,
fields: fields,
fieldsInfo: fieldsInfo,
context: params.context,
parentID: params.parentID,
res_ids: params.res_ids,
viewType: targetView,
});
this.setPureVirtual(record.id, true);
this.updateRecordContext(record.id, {
ignore_warning: true,
not_onchange: true,
});
return {
record: record,
params: params,
};
},
/**
* Generates a virtual records without hard-link to any model.
*
* @param {Integer/String} listID
* @param {Object} options
@ -77,14 +292,14 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
return new Promise(resolve => {
this._makeDefaultRecord(list.model, params).then(recordID => {
this.setPureVirtual(recordID, true);
this.updateRecordContext(recordID, {ignore_warning: true});
if (options.data) {
this._applyChange(recordID, options.data, params).then(() => {
resolve(this.get(recordID));
});
} else {
resolve(this.get(recordID));
}
this.updateRecordContext(recordID, {
ignore_warning: true,
not_onchange: true,
});
resolve({
record: this.get(recordID),
params: params,
});
});
});
},
@ -92,7 +307,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
/**
* Adds support to avoid show onchange warnings.
* The implementation is a pure hack that clone
* the context and do a monkey path the
* the context and do a monkey patch to the
* 'trigger_up' method.
*
* @override
@ -112,5 +327,28 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
}
return this._super.apply(this, arguments);
},
/**
* This happens when the user discard main document changes (isn't a rollback)
*
* @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;
}
const element = this.localData[id];
this._visitChildren(element, function(elem) {
if (_.isEmpty(elem._changes)) {
if (elem.context.product_picker_modified) {
elem.context.product_picker_modified = false;
}
}
});
},
});
});

View File

@ -27,6 +27,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
"click .oe_btn_lines": "_onClickLines",
"click .oe_btn_search_group": "_onClickSearchGroup",
"search .oe_search_input": "_onSearch",
"input .oe_search_input": "_onInputSearch",
"focusin .oe_search_input": "_onFocusInSearch",
"show.bs.dropdown .o_cp_buttons": "_onShowSearchDropdown",
"click #product_picker_maximize": "_onClickMaximize",
@ -41,19 +42,17 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
}),
_auto_search_delay: 450,
_input_instant_search_time: 150,
// Model product.product fields
search_read_fields: ["id", "display_name"],
search_read_fields: ["id", "display_name", "uom_id"],
/**
* @override
*/
init: function(parent, name, record) {
init: function(parent) {
this._super.apply(this, arguments);
// This is the parent state
this.state = record;
// Use jquery 'extend' to have a 'deep' merge.
this.options = $.extend(
true,
@ -79,32 +78,31 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
if (this.view) {
this._processGroups();
}
this._currentSearchBatchID = 0;
this._lazyRenderSearchRecords = _.debounce(() => {
this.doRenderSearchRecords();
++this._currentSearchBatchID;
}, this._input_instant_search_time);
},
/**
* @override
*/
willStart: function() {
if (!this.view) {
return Promise.resolve();
}
if (this.mode === "readonly") {
this._updateSearchContext(-1);
} else {
this._updateSearchContext(0);
}
return Promise.all([
this._super.apply(this, arguments),
this._getSearchRecords(),
]);
return this._super.apply(this, arguments).then(() => {
if (this.isReadonly) {
// Show Lines
this._updateSearchContext(-1);
} else {
this._updateSearchContext(0);
}
});
},
/**
* Updates the lines counter badge
*/
updateBadgeLines: function() {
const records = this.parent_controller.model.get(this.state.id).data[
const records = this.parent_controller.model.get(this.record.id).data[
this.name
].data;
this.$badgeLines.text(records.length);
@ -116,7 +114,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
}
let prices = [];
const field_map = this.options.field_map;
const records = this.parent_controller.model.get(this.state.id).data[
const records = this.parent_controller.model.get(this.record.id).data[
this.name
].data;
if (this.options.show_discount) {
@ -145,7 +143,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
total,
this.value.fields[this.options.field_map.price_unit],
this.options.currency_field,
this.state.data
this.record.data
);
this.$totalZone.find(".total_price").html(total || 0.0);
},
@ -161,12 +159,13 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
domain: this.record.getDomain(this.recordParams),
field: this.field,
parentID: this.value.id,
state: this.state,
record: this.record,
model: this.parent_controller.model,
fieldName: this.name,
recordData: this.recordData,
value: this.value,
relation_field: this.state.fields[this.name].relation_field,
relation_field: this.record.fields[this.name].relation_field,
current_batch_id: this._currentSearchBatchID,
};
},
@ -252,21 +251,30 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
*/
_render: function() {
const def = this._super.apply(this, arguments);
if (def) {
this.renderer.updateSearchGroup(this._activeSearchGroup);
// Parent implementation can return 'undefined' :(
return (
def &&
def.then(() => {
if (
!this.$el.hasClass("oe_field_one2many_product_picker_maximized")
) {
this.$el.addClass("position-relative d-flex flex-column");
}
// Check maximize state
if (!this.$el.hasClass("oe_field_one2many_product_picker_maximized")) {
this.$el.addClass("position-relative d-flex flex-column");
}
return new Promise(resolve => {
this._getSearchRecords()
.then(records => {
return this.renderer.appendSearchRecords(records, {
cleanup: false,
});
})
.then(() => resolve());
}).then(() => {
if (this.options.show_subtotal) {
this._addTotalsZone();
}
})
);
});
}
return def;
},
/**
@ -274,10 +282,14 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
*/
doRenderSearchRecords: function() {
return new Promise(resolve => {
this._getSearchRecords().then(() => {
this.renderer.$el.scrollTop(0);
this.renderer._renderView().then(() => resolve());
});
this._getSearchRecords()
.then(records => {
this.renderer.$el.scrollTop(0);
return this.renderer.appendSearchRecords(records, {
cleanup: true,
});
})
.then(() => resolve());
});
},
@ -311,7 +323,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
* @param {Boolean} merge
* @returns {Deferred}
*/
_getSearchRecords: function(options, merge) {
_getSearchRecords: function(options) {
const arch = this.view.arch;
const search_mode = this.options.search[this._searchMode];
const field_name = this.options.field_map.product;
@ -367,24 +379,8 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
}
task.then(results => {
if (merge) {
this._searchRecords = _.union(
this._searchRecords || [],
results
);
} else {
this._searchRecords = results;
}
this._lastSearchRecordsCount = results.length;
this._searchOffset = offset + limit;
if (this.renderer) {
this.renderer.updateSearchData(
this._searchRecords,
this._lastSearchRecordsCount,
this._activeSearchGroup
);
}
this.renderer.showLoadMore(limit && results.length === limit);
resolve(results);
});
});
@ -531,9 +527,11 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
price_unit: "price_unit",
discount: "discount",
},
trigger_refresh_fields: ["partner_id", "currency_id"],
auto_save: false,
ignore_warning: false,
all_domain: [],
instant_search: false,
};
},
@ -588,12 +586,17 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
return [];
}
const field_name = this.options.field_map.product;
const lines = this.parent_controller.model.get(this.state.id).data[
const lines = this.parent_controller.model.get(this.record.id).data[
this.name
].data;
const ids = _.map(lines, line => {
return line.data[field_name].data.id;
});
// Here only get lines with product_id assigned
// This happens beacuse sale.order has lines for sections/comments
const ids = _.chain(lines)
.filter(line => line.data[field_name] !== false)
.map(line => {
return line.data[field_name].data.id;
})
.value();
return [["id", "in", ids]];
},
@ -614,6 +617,9 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
this._searchContext.order = [{name: "sequence"}, {name: "id"}];
this._searchContext.activeTest = false;
}
if (this.renderer) {
this.renderer.updateSearchGroup(this._activeSearchGroup);
}
},
/**
@ -621,6 +627,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
* that the search results. Use directy in-memory values.
*/
showLines: function() {
this.renderer.clearRecords();
this._updateSearchContext(-1);
this._clearSearchInput();
this.$btnLines
@ -635,6 +642,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
* @param {Number} group_id
*/
showGroup: function(group_id) {
this.renderer.clearRecords();
this._updateSearchContext(group_id);
this.doRenderSearchRecords();
this.$btnLines.removeClass("active");
@ -668,6 +676,15 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
this.doRenderSearchRecords();
},
_onInputSearch: function(evt) {
if (!this.options.instant_search) {
return;
}
this._searchContext.text = evt.target.value;
this._lazyRenderSearchRecords();
// This.doRenderSearchRecords()
},
/**
* Auto select all content when user enters into fields with this
* widget.
@ -707,7 +724,8 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
*/
_onCreateQuickRecord: function(evt) {
evt.stopPropagation();
this.parent_controller.model.setPureVirtual(evt.data.id, false);
var model = this.parent_controller.model;
model.setPureVirtual(evt.data.id, false);
if (this.options.auto_save) {
// Dont trigger state update
@ -718,16 +736,15 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
this.parent_controller
.saveRecord(undefined, {stayInEdit: true})
.then(() => {
// Because 'create' generates a new state and we can't know these new id we
// need force update all the current states.
this._setValue(
{operation: "UPDATE", id: evt.data.id},
{doNotSetDirty: true}
).then(() => {
if (evt.data.callback) {
evt.data.callback();
}
});
self.renderer.updateState(
model.get(self.parent_controller.handle).data[
self.name
],
{force: true}
);
if (evt.data.callback) {
evt.data.callback();
}
});
if (evt.data.callback) {
evt.data.callback();
@ -758,21 +775,15 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
self.parent_controller
.saveRecord(undefined, {stayInEdit: true})
.then(function() {
// Workaround to get updated values
self.parent_controller.model
.reload(self.value.id)
.then(function(result) {
var new_data = self.parent_controller.model.get(
result
);
self.value.data = new_data.data;
self.renderer.updateState(self.value, {
force: true,
});
if (callback) {
callback();
}
});
self.renderer.updateState(
self.parent_controller.model.get(
self.parent_controller.handle
).data[self.name],
{force: true}
);
if (callback) {
callback();
}
});
if (callback) {
callback();
@ -844,12 +855,9 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
if (this._isLoading) {
return;
}
this._getSearchRecords(
{
offset: this._searchOffset,
},
true
).then(records => {
this._getSearchRecords({
offset: this._searchOffset,
}).then(records => {
this.renderer.appendSearchRecords(records);
});
},
@ -872,7 +880,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
*/
_blockControlPanel: function(block) {
if (this.$buttons) {
this.$buttons.find("input,button").attr("disabled", block);
this.$buttons.find("button").attr("disabled", block);
}
},

View File

@ -83,11 +83,6 @@
.oe_flip_card_inner {
height: 100% !important;
box-shadow: 0px 0px 15px;
.img-fluid {
transform: translateY(-50%) !important;
top: 50%;
position: relative;
}
.oe_one2many_product_picker_title {
font-size: 1.95rem !important;
}
@ -142,6 +137,14 @@
$one2many-product-picker-transition-3d-time/2;
transform-style: preserve-3d;
.img-fluid {
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
z-index: -1;
position: absolute;
}
.position-absolute {
z-index: 1;
}
@ -150,8 +153,19 @@
font-size: 1rem;
}
.indicator_zones {
display: inline-flex;
flex-direction: column;
max-width: 50%;
align-items: flex-start;
> span.badge {
font-size: 0.8rem;
}
}
.badge_price {
top: 50%;
top: 55%;
right: -2px;
transform: translateY(-50%);
display: grid;
@ -190,8 +204,17 @@
.o_form_view.o_form_nosheet {
padding: $one2many-product-picker-card-form-padding;
.o_field_widget .o_input_dropdown > input {
height: unset;
.o_field_widget {
&:not(.widget_numeric_step) {
max-width: 95%;
}
.o_input_dropdown > input {
height: unset;
}
}
.btn.w-100 {
max-width: 95%;
}
}
}
@ -211,6 +234,16 @@
top: 50%;
left: 0;
transform: translateY(-50%);
.oe_one2many_product_picker_form_buttons {
display: flex;
padding: 0 3px;
justify-content: center;
.oe_record_remove {
flex-grow: 1;
}
}
}
}

View File

@ -86,6 +86,101 @@
>Load More</button>
</div>
</t>
<t t-name="One2ManyProductPicker.ActionButton">
<div class="safezone d-inline-block float-left m-0 pb-2 pr-2 text-left">
<t t-if="is_saving &amp;&amp; lazy_qty > 0">
<span
class="badge record_saving badge-warning font-weight-bold rounded-0 mt-0 px-2 py-3 product_qty"
><span class="lazy_product_qty" t-esc="lazy_qty || '1'" /> x <t
t-esc="state.data[field_map[field_uom]].data.display_name"
/></span>
</t>
<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-class="badge {{modified &amp;&amp; 'badge-warning' || 'badge-success'}} font-weight-bold rounded-0 mt-0 px-2 py-3 product_qty"
/>
</t>
<t t-else="">
<span
class="badge badge-primary font-weight-bold rounded-0 mt-0 px-2 py-3 add_product"
><i class="fa fa-plus" /> 1 <t
t-esc="state.data[field_map[field_uom]].data.display_name"
/></span>
</t>
</div>
</t>
<t t-name="One2ManyProductPicker.PriceZone">
<div class="position-absolute m-0 text-left badge_price">
<t t-if="show_discount">
<span
t-att-data-field="field_map.discount"
t-attf-data-esc="str({{field_map.discount}} * -1.0) +'%'"
class="badge badge-dark discount_price font-weight-bold rounded-0 mt-1 p-2"
/>
<span
t-att-data-field="field_map.price_unit"
t-attf-data-esc="'{{monetary('price_unit',true)}}'"
class="badge font-weight-bold rounded-0 original_price"
/>
<span
t-if="has_onchange"
class="badge badge-info price_unit font-weight-bold rounded-0 mt-1 p-2"
/>
</t>
<t t-else="has_onchange">
<span
t-att-data-field="field_map.price_unit"
t-attf-data-esc="'{{monetary('price_unit',true)}}'"
class="badge badge-info price_unit font-weight-bold rounded-0 mt-1 p-2"
/>
</t>
</div>
</t>
<t t-name="One2ManyProductPicker.FlipCard.Front">
<div
t-attf-class="oe_flip_card_front p-0 {{((modified || is_saving) &amp;&amp; 'border-warning') || (state &amp;&amp; !is_virtual &amp;&amp; 'border-success') || ''}}"
>
<t t-if="state">
<div class="indicator_zones float-left">
<t t-call="One2ManyProductPicker.ActionButton" />
<t t-call="One2ManyProductPicker.PriceZone" />
</div>
<span
data-field="display_name"
class="oe_one2many_product_picker_title position-absolute fixed-bottom p-1"
data-esc="display_name"
/>
<img
alt=""
class="img img-fluid"
t-att-src="image(state.data[field_map.product].data.id,'image_128')"
t-att-data-src-alt="image(state.data[field_map.product].data.id,'image_1024')"
/>
</t>
<t t-else="">
<span
class="oe_one2many_product_picker_title position-absolute fixed-bottom p-1"
t-esc="record_search.display_name"
/>
<img
alt=""
class="img img-fluid"
t-att-src="image(record_search.id,'image_128')"
t-att-data-src-alt="image(record_search.id,'image_1024')"
/>
</t>
</div>
</t>
<t t-name="One2ManyProductPicker.FlipCard.Back">
<div class="oe_flip_card_back">
<widget
name="product_picker_quick_create_form"
t-att-compare-key="field_map.product_uom"
/>
</div>
</t>
<t t-name="One2ManyProductPicker.FlipCard">
<div
class="oe_flip_container p-1 col-12 col-sm-8 col-md-6 col-lg-4 col-xl-3 col-xxl-2"
@ -95,101 +190,8 @@
t-attf-class="oe_flip_card {{!state &amp;&amp; 'disabled' || (auto_save &amp;&amp; (!is_virtual || is_saving) &amp;&amp; !state.data.id &amp;&amp; 'blocked') || ''}}"
>
<div class="oe_flip_card_inner text-center">
<div
t-attf-class="oe_flip_card_front p-0 {{((modified || is_saving) &amp;&amp; 'border-warning') || (state &amp;&amp; !is_virtual &amp;&amp; 'border-success') || ''}}"
>
<t t-if="state">
<t t-if="is_saving &amp;&amp; lazy_qty > 0">
<div
class="safezone position-absolute m-0 pb-2 pr-2 text-left"
>
<span
class="badge record_saving badge-warning font-weight-bold rounded-0 mt-0 px-2 py-3 product_qty"
><span
class="lazy_product_qty"
t-esc="lazy_qty || '1'"
/> x <t
t-esc="state.data[field_map.product_uom].data.display_name"
/></span>
</div>
</t>
<t t-elif="!is_virtual">
<div
class="safezone position-absolute m-0 pb-2 pr-2 text-left"
>
<span
t-att-data-field="field_map.product_uom_qty"
t-attf-data-esc="str({{field_map.product_uom_qty}}) + ' x ' + {{field_map.product_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"
/>
</div>
</t>
<t t-else="">
<div
class="safezone position-absolute m-0 pb-2 pr-2 text-left"
>
<span
class="badge badge-primary font-weight-bold rounded-0 mt-0 px-2 py-3 add_product"
><i class="fa fa-plus" /> 1 <t
t-esc="state.data[field_map.product_uom].data.display_name"
/></span>
</div>
</t>
<div class="position-absolute m-0 text-left badge_price">
<t t-if="show_discount">
<span
t-att-data-field="field_map.discount"
t-attf-data-esc="'-' + str({{field_map.discount}}) +'%'"
class="badge badge-dark discount_price font-weight-bold rounded-0 mt-1 p-2"
/>
<span
t-att-data-field="field_map.price_unit"
t-attf-data-esc="'{{monetary('price_unit',true)}}'"
class="badge font-weight-bold rounded-0 original_price"
/>
<span
class="badge badge-info price_unit font-weight-bold rounded-0 mt-1 p-2"
/>
</t>
<t t-else="">
<span
t-att-data-field="field_map.price_unit"
t-attf-data-esc="'{{monetary('price_unit',true)}}'"
class="badge badge-info price_unit font-weight-bold rounded-0 mt-1 p-2"
/>
</t>
</div>
<span
data-field="display_name"
class="oe_one2many_product_picker_title position-absolute fixed-bottom p-1"
data-esc="display_name"
/>
<img
alt=""
class="img img-fluid"
t-att-src="image(state.data[field_map.product].data.id,'image_512')"
t-att-data-src-alt="image(state.data[field_map.product].data.id,'image_1024')"
/>
</t>
<t t-else="">
<span
class="oe_one2many_product_picker_title position-absolute fixed-bottom p-1"
t-esc="record_search.display_name"
/>
<img
alt=""
class="img img-fluid"
t-att-src="image(record_search.id,'image_512')"
t-att-data-src-alt="image(record_search.id,'image_1024')"
/>
</t>
</div>
<div class="oe_flip_card_back">
<widget
name="product_picker_quick_create_form"
t-att-compare-key="field_map.product_uom"
/>
</div>
<t t-call="One2ManyProductPicker.FlipCard.Front" />
<t t-call="One2ManyProductPicker.FlipCard.Back" />
</div>
</div>
</div>

View File

@ -5,10 +5,10 @@
<button t-attf-class="btn btn-primary oe_record_add">Add</button>
</t>
<t t-elif="state == 'dirty'">
<button class="btn btn-success oe_record_change mr-2">
<button class="btn btn-success oe_record_change w-100">
<i class="fa fa-check" />
</button>
<button class="btn btn-warning oe_record_discard ml-2">
<button class="btn btn-warning oe_record_discard ml-1">
<i class="fa fa-times" />
</button>
</t>
@ -16,6 +16,9 @@
<button class="btn btn-danger oe_record_remove w-100"><i
class="fa fa-trash"
/> Remove</button>
<button class="btn btn-warning oe_record_discard ml-1">
<i class="fa fa-times" />
</button>
</t>
</div>
</t>

View File

@ -61,6 +61,10 @@
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/views/basic_model.js"
/>
<script
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/views/basic_controller.js"
/>
<script
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js"