[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. modify/create a record with the widget.
* ignore_warning > Enable/Disable display onchange warnings (False by default) * 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. All widget options are optional.
Notice that you can call '_' method to use translations. This only can be used with this widget. 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. modify/create a record with the widget.
* ignore_warning > Enable/Disable display onchange warnings (False by default) * 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. All widget options are optional.
Notice that you can call '_' method to use translations. This only can be used with this widget. 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>
<li><p class="first">ignore_warning &gt; Enable/Disable display onchange warnings (False by default)</p> <li><p class="first">ignore_warning &gt; Enable/Disable display onchange warnings (False by default)</p>
</li> </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> </ul>
<p>All widget options are optional. <p>All widget options are optional.
Notice that you can call _ method to use translations. This only can be used with this widget.</p> 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); this._super.apply(this, arguments);
}, },
/**
* @override
*/
_applyChanges: function() {
return this._super.apply(this, arguments).then(() => {
this._updateButtons();
});
},
/** /**
* Create or accept changes * Create or accept changes
*/ */
@ -227,7 +236,6 @@ odoo.define(
this.trigger_up("quick_record_updated", { this.trigger_up("quick_record_updated", {
changes: ev.data.changes, changes: ev.data.changes,
}); });
this._updateButtons();
} }
} }
}, },
@ -261,7 +269,6 @@ odoo.define(
saving: false, saving: false,
}); });
this.model.unsetDirty(this.handle); this.model.unsetDirty(this.handle);
// Self._updateButtons();
this._enableQuickCreate(); this._enableQuickCreate();
}, },
}); });
@ -298,14 +305,26 @@ odoo.define(
this.trigger_up("restore_flip_card", { this.trigger_up("restore_flip_card", {
success_callback: function() { success_callback: function() {
// 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", { self.trigger_up("update_quick_record", {
id: record.id, id: record.id,
callback: function() { callback: function() {
self.model.unsetDirty(self.handle); self.model.unsetDirty(self.handle);
// Self._updateButtons();
self._enableQuickCreate(); self._enableQuickCreate();
}, },
}); });
});
}, },
block: true, block: true,
}); });
@ -314,29 +333,30 @@ odoo.define(
_discard: function() { _discard: function() {
if (this._disabled) { if (this._disabled) {
// Don't do anything if we are already creating a record // Don't do anything if we are already creating a record
return Promise.resolve(); return;
} }
this._disableQuickCreate(); 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, { this.model.discardChanges(this.handle, {
rollback: true, rollback: true,
}); });
const record = this.model.get(this.handle);
this.trigger_up("quick_record_updated", { this.trigger_up("quick_record_updated", {
changes: record.data, changes: record.data,
}); });
if (this.model.isNew(record.id)) {
this.update({}, {reload: false});
this.trigger_up("restore_flip_card");
this._updateButtons();
this._enableQuickCreate();
} else {
this.update({}, {reload: false}).then(() => { this.update({}, {reload: false}).then(() => {
if (!this.model.isNew(record.id)) {
this.model.unsetDirty(this.handle); this.model.unsetDirty(this.handle);
}
this.trigger_up("restore_flip_card"); this.trigger_up("restore_flip_card");
this._updateButtons(); this._updateButtons();
this._enableQuickCreate(); this._enableQuickCreate();
}); });
}
}, },
/** /**

View File

@ -42,7 +42,6 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
front: [], front: [],
back: [], back: [],
}; };
this._lazyUpdateRecord = _.debounce(this._updateRecord.bind(this), 450); this._lazyUpdateRecord = _.debounce(this._updateRecord.bind(this), 450);
}, },
@ -51,8 +50,10 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
* *
* @returns {Object} * @returns {Object}
*/ */
generateVirtualState: function() { generateVirtualState: function(simple_mode) {
return this._generateVirtualState().then(this.recreate.bind(this)); 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 * @override
*/ */
destroy: function() { destroy: function() {
this.abortTimeouts();
if (this.state) {
this.options.basicFieldParams.model.removeVirtualRecord(this.state.id);
}
this.$el.remove(); this.$el.remove();
this.$card.off(""); this.$card.off("");
this._super.apply(this, arguments); 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 * @override
*/ */
@ -105,6 +121,12 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
* @returns {Promise} * @returns {Promise}
*/ */
recreate: function(state) { recreate: function(state) {
if (!this.getParent()) {
// It's a zombie record! ensure kill it!
this.destroy();
return;
}
if (state) { if (state) {
this._setState(state); this._setState(state);
} }
@ -121,6 +143,15 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
return this._render(); 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 * 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, price,
this.state.fields[field_name], this.state.fields[field_name],
this.options.currencyField, 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.fields = this.getParent().state.fields;
this.fieldsInfo = this.getParent().state.fieldsInfo.form; 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; this.state = viewState;
if (recordSearch) { if (recordSearch) {
this.recordSearch = recordSearch; this.recordSearch = recordSearch;
} }
const model = this.options.basicFieldParams.model;
this.is_virtual = this.is_virtual =
(this.state && model.isPureVirtual(this.state.id)) || false; (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._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, auto_save: this.options.autoSave,
is_saving: record && record.context.saving, is_saving: record && record.context.saving,
lazy_qty: record && record.context.lazy_qty, 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() { _getInternalVirtualRecordContext: function() {
const context = {}; const context = {};
context["default_" + this.options.basicFieldParams.relation_field] = context[`default_${this.options.basicFieldParams.relation_field}`] =
this.options.basicFieldParams.state.id || null; 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; return context;
}, },
/** /**
* Forced data used in virtual states. * 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 * @private
* @returns {Object} * @returns {Object}
*/ */
_getInternalVirtualRecordData: function() { _getInternalVirtualRecordData: function() {
const data = {}; // To be overwritten
data[this.options.fieldMap.product] = { return {};
operation: "ADD", },
id: this.recordSearch.id,
}; _generateVirtualStateSimple: function(context, def_values) {
return data; 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 * @param {Object} context
* @returns {Object} * @returns {Object}
*/ */
_generateVirtualState: function(data, context) { _generateVirtualState: function(data, context, simple_mode) {
const model = this.options.basicFieldParams.model;
const scontext = _.extend( const scontext = _.extend(
{}, {},
this._getInternalVirtualRecordContext(), this._getInternalVirtualRecordContext(),
context context
); );
// Force qty to 1.0 to launch correct onchanges // Apply default values
scontext[`default_${this.options.fieldMap.product_uom_qty}`] = 1.0; const def_values = {
const sdata = _.extend({}, this._getInternalVirtualRecordData(), data); [this.options.fieldMap.product]: this.recordSearch.id,
return model.createVirtualRecord(this.options.basicFieldParams.value.id, { [this.options.fieldMap[this.master_uom_map.field_uom_qty]]: 1.0,
data: sdata, [this.options.fieldMap[this.master_uom_map.field_uom]]: this
context: scontext, .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) => { this.$el.find(to_find.join()).each((key, value) => {
const $elm = $(value); const $elm = $(value);
const format_out = $elm.data("esc") || $elm.data("field"); const format_out = $elm.data("esc") || $elm.data("field");
$elm.html( const text_out = py.eval(
py.eval(format_out, _.extend({}, state_data, this.recordSearch)) format_out,
_.extend({}, state_data, this.recordSearch)
); );
$elm.html(text_out);
$elm.attr("title", text_out);
}); });
if (this.options.showDiscount) { if (this.options.showDiscount) {
const field_map = this.options.fieldMap; const field_map = this.options.fieldMap;
if (state_data) { 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 this.$el
.find(".original_price,.discount_price") .find(".original_price,.discount_price")
.toggleClass("d-none", !has_discount); .toggleClass("d-none", !has_discount);
@ -526,7 +722,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
price_reduce, price_reduce,
this.state.fields[field_map.price_unit], this.state.fields[field_map.price_unit],
this.options.currencyField, 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) { if (record.context.saving) {
return Promise.resolve(); return Promise.resolve();
} }
const changes = _.pick(record.data, this.options.fieldMap.product_uom_qty); const changes = _.pick(
if (changes[this.options.fieldMap.product_uom_qty] === 0) { record.data,
changes[this.options.fieldMap.product_uom_qty] = 1; 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"); this.$card.addClass("blocked");
return model.notifyChanges(record.id, changes).then(() => { 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. // wait for the Odoo response.
const model_record_data = model.localData[this.state.id].data; const model_record_data = model.localData[this.state.id].data;
if ( 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 return model
.notifyChanges(record.id, { .notifyChanges(record.id, {
[this.options.fieldMap.product_uom_qty]: [this.options.fieldMap[this.master_uom_map.field_uom_qty]]:
model_record_data[this.options.fieldMap.product_uom_qty], model_record_data[
this.options.fieldMap[this.master_uom_map.field_uom_qty]
],
}) })
.then(() => { .then(() => {
this._processDynamicFields(); this._processDynamicFields();
@ -836,6 +1047,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
_onRestoreFlipCard: function(evt) { _onRestoreFlipCard: function(evt) {
this.$card.removeClass("active"); this.$card.removeClass("active");
this.$front.removeClass("d-none"); 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")) { if (this.$card.hasClass("oe_flip_card_maximized")) {
this.$card.removeClass("oe_flip_card_maximized"); this.$card.removeClass("oe_flip_card_maximized");
this.$card.on("transitionend", () => { this.$card.on("transitionend", () => {
@ -859,6 +1072,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
if (evt.data.block) { if (evt.data.block) {
this.$card.addClass("blocked"); 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) { _onQuickRecordUpdated: function(evt) {
this._processDynamicFields(Object.keys(evt.data.changes)); this._processDynamicFields(Object.keys(evt.data.changes));
// This.recreate();
this.trigger_up("update_subtotal"); this.trigger_up("update_subtotal");
}, },
}); });

View File

@ -40,9 +40,7 @@ odoo.define(
// 'receive' more arguments. // 'receive' more arguments.
this.options = parent.options; this.options = parent.options;
this.mode = parent.mode; this.mode = parent.mode;
this.search_data = parent._searchRecords;
this.search_group = parent._activeSearchGroup; this.search_group = parent._activeSearchGroup;
this.last_search_data_count = parent._lastSearchRecordsCount;
}, },
/** /**
@ -50,7 +48,7 @@ odoo.define(
*/ */
on_attach_callback: function() { on_attach_callback: function() {
this._isInDom = true; 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() { on_detach_callback: function() {
this._isInDom = false; 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 * @param {Object} search_group
*/ */
updateSearchData: function(search_data, count, search_group) { updateSearchGroup: function(search_group) {
this.search_data = search_data;
this.last_search_data_count = count;
this.search_group = search_group; this.search_group = search_group;
this._loadMoreWorking = false;
this.$btnLoadMore.attr("disabled", false);
}, },
/** /**
@ -115,20 +107,11 @@ odoo.define(
}); });
}, },
/** _isValidLineState: function(state) {
* Recreate the given widget by the state id return (
* state.data[this.options.field_map.product] &&
* @param {String} state_id state.data[this.options.field_map.product].data.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;
}
}
}, },
_isEqualState: function(state_a, state_b) { _isEqualState: function(state_a, state_b) {
@ -137,9 +120,31 @@ odoo.define(
} }
const product_id_a = const product_id_a =
state_a.data[this.options.field_map.product].data.id; 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 = const product_id_b =
state_b.data[this.options.field_map.product].data.id; 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} * @returns {Array}
*/ */
_processStatesToDestroy: function(states) { _processStatesToDestroy: function(states) {
// Get widgets to destroy
// Update states only affect to "non pure virtual" records
const to_destroy = []; const to_destroy = [];
const to_add = [];
for (const state of states) { for (const state of states) {
for (let e = this.widgets.length - 1; e >= 0; --e) { for (let e = this.widgets.length - 1; e >= 0; --e) {
const widget = this.widgets[e]; const widget = this.widgets[e];
if (widget && this._isEqualState(widget.state, state)) { 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 // 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) { let recreated = false;
const widget = this.widgets[eb]; if (!this._existsWidgetWithState(widget.state)) {
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 // Get the new state ID if exists to link it with the new record
let state_id = null; // This happens when remove a record that have a new state info
for (let eb = this.state.data.length - 1; eb >= 0; --eb) { for (
let eb = this.state.data.length - 1;
eb >= 0;
--eb
) {
const state = this.state.data[eb]; const state = this.state.data[eb];
if (this._isEqualState(state, widget_destroyed.state)) { if (!this._isValidLineState(state)) {
state_id = state.id; continue;
}
if (this._isEqualState(state, widget.state)) {
widget.recreate(state);
recreated = true;
break; break;
} }
} }
// "Lines" section doesn't show virtual records }
if ( if (!recreated) {
(state_id && this.search_group.name === "main_lines") || widget.markToDestroy();
this.search_group.name !== "main_lines" to_destroy.push(widget);
) { const search_record = _.omit(
const widget_product_id = widget.recordSearch,
widget_destroyed.state.data[ "__id"
this.options.field_map.product );
].data.id;
const search_record = _.find(this.search_data, { to_add.push([
id: widget_product_id, [search_record],
}); {
const new_search_record = _.extend({}, search_record, { no_attach_widgets: false,
__id: state_id, no_process_records: false,
}); position: widget.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() { _processCurrentStates: function() {
// Records to Update or Create // Records to Update or Create
const model = this.getParent().getBasicFieldParams().model;
const to_destroy = []; const to_destroy = [];
const to_add = []; const to_add = [];
for (const index in this.state.data) { for (const index in this.state.data) {
const state = this.state.data[index]; const state = this.state.data[index];
if (!this._isValidLineState(state)) {
continue;
}
let exists = false; let exists = false;
let search_record_index = -1; let search_record_index = false;
let search_record = false; let search_record = false;
for (let e = this.widgets.length - 1; e >= 0; --e) { for (let e = this.widgets.length - 1; e >= 0; --e) {
const widget = this.widgets[e]; const widget = this.widgets[e];
@ -238,24 +236,28 @@ odoo.define(
// Already processed widget (deleted) // Already processed widget (deleted)
continue; continue;
} }
if (this._isEqualState(widget.state, state)) {
var model = this.getParent().getBasicFieldParams().model; const is_equal_state = this._isEqualState(widget.state, state);
var record = model.get(widget.state.id); if (widget.isMarkedToDestroy()) {
exists = true;
} else if (is_equal_state) {
const record = model.get(widget.state.id);
model.updateRecordContext(state.id, { model.updateRecordContext(state.id, {
lazy_qty: record.context.lazy_qty || 0, lazy_qty: record.context.lazy_qty || 0,
}); });
widget.recreate(state); widget.recreate(state);
exists = true; exists = true;
break; break;
} else if ( }
if (
!is_equal_state &&
widget.recordSearch.id === 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) // 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; search_record = widget.recordSearch;
var model = this.getParent().getBasicFieldParams().model; const record = model.get(widget.state.id);
var record = model.get(widget.state.id);
model.updateRecordContext(state.id, { model.updateRecordContext(state.id, {
lazy_qty: record.context.lazy_qty || 0, lazy_qty: record.context.lazy_qty || 0,
}); });
@ -273,16 +275,18 @@ odoo.define(
this.state.data = _.compact(this.state.data); this.state.data = _.compact(this.state.data);
// Need add a new one? // Add to create the new record
if (!exists && search_record_index !== -1) { if (!exists && search_record_index) {
const new_search_record = _.extend({}, search_record, { const new_search_record = _.extend({}, search_record, {
__id: state.id, __id: state.id,
}); });
to_add.push([ to_add.push([
[new_search_record], [new_search_record],
false, {
true, no_attach_widgets: true,
search_record_index, no_process_records: true,
position: search_record_index,
},
]); ]);
} }
} }
@ -304,9 +308,15 @@ odoo.define(
const states_to_destroy = []; const states_to_destroy = [];
for (const index in old_states) { for (const index in old_states) {
const old_state = old_states[index]; const old_state = old_states[index];
if (!this._isValidLineState(old_state)) {
continue;
}
let found = false; let found = false;
for (const e in this.state.data) { for (const e in this.state.data) {
const current_state = this.state.data[e]; const current_state = this.state.data[e];
if (!this._isValidLineState(current_state)) {
continue;
}
if (this._isEqualState(current_state, old_state)) { if (this._isEqualState(current_state, old_state)) {
found = true; found = true;
break; break;
@ -323,47 +333,40 @@ odoo.define(
states_to_destroy states_to_destroy
); );
// Make widgets to destroy invisible to avoid render 'dance'
for (const widget of to_destroy_old) {
widget.$el.hide();
}
const oldTasks = [];
for (const params of to_add_virtual) {
oldTasks.push(this.appendSearchRecords.apply(this, params)[0]);
}
Promise.all(oldTasks).then(() => {
const [ const [
to_destroy_current, destroyed_current,
to_add_current, to_add_current,
] = this._processCurrentStates(); ] = this._processCurrentStates();
// Make widgets to destroy invisible to avoid render 'dance' const currentTasks = [];
for (const widget of to_destroy_current) { const to_add = [].concat(to_add_current, to_add_virtual);
widget.$el.hide(); for (const params of to_add) {
currentTasks.push(this.appendSearchRecords.apply(this, params)[0]);
} }
const currentTasks = [];
for (const params of to_add_current) {
currentTasks.push(
this.appendSearchRecords.apply(this, params)[0]
);
}
Promise.all(currentTasks).then(() => { Promise.all(currentTasks).then(() => {
_.invoke(to_destroy_old, "destroy"); _.invoke(to_destroy_old, "destroy");
_.invoke(to_destroy_current, "destroy"); _.invoke(destroyed_current, "destroy");
this.widgets = _.difference(this.widgets, to_destroy_old);
def.resolve(); def.resolve();
}); });
});
return def; return def;
}, },
clearRecords: function() {
_.invoke(_.compact(this.widgets), "destroy");
this.widgets = [];
if (this.$recordsContainer) {
this.$recordsContainer.empty();
}
},
/** /**
* @override * @override
*/ */
_renderView: function() { _renderView: function() {
const oldWidgets = _.compact(this.widgets); _.invoke(_.compact(this.widgets), "destroy");
this.widgets = []; this.widgets = [];
this.$recordsContainer = $("<DIV/>", { this.$recordsContainer = $("<DIV/>", {
class: "w-100 row", class: "w-100 row",
@ -374,23 +377,14 @@ odoo.define(
this.$btnLoadMore = this.$extraButtonsContainer.find( this.$btnLoadMore = this.$extraButtonsContainer.find(
"#productPickerLoadMore" "#productPickerLoadMore"
); );
this.search_data = this._sort_search_data(this.search_data); // 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.empty();
this.$el.append(this.$recordsContainer); this.$el.append(this.$recordsContainer);
this.$el.append(this.$extraButtonsContainer); this.$el.append(this.$extraButtonsContainer);
this.showLoadMore( // This.showLoadMore(
this.last_search_data_count >= this.options.records_per_page // this.last_search_data_count >= this.options.records_per_page
); // );
if (this._isInDom) { return this._super.apply(this, arguments);
_.invoke(this.widgets, "on_attach_callback");
}
return resolve();
});
});
}, },
/** /**
@ -405,7 +399,10 @@ odoo.define(
for (const index_state in this.state.data) { for (const index_state in this.state.data) {
const state_data = this.state.data[index_state]; 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; data._order_value = state_data.res_id;
} }
} }
@ -431,32 +428,76 @@ odoo.define(
_processSearchRecords: function(results) { _processSearchRecords: function(results) {
const field_name = this.options.field_map.product; const field_name = this.options.field_map.product;
const records = []; 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) { for (const index in results) {
const record_search = results[index]; const record_search = results[index];
let state_data_found = false; 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) { for (const index_data in this.state.data) {
const state_record = this.state.data[index_data]; const state_record = this.state.data[index_data];
const field = state_record.data[field_name]; if (!this._isValidLineState(state_record)) {
if ( continue;
(typeof field === "object" && }
field.data.id === record_search.id) || const field_value = state_record.data[field_name];
field === record_search.id if (test_values(field_value, record_search)) {
) {
records.push( records.push(
_.extend({}, record_search, { _.extend({}, record_search, {
__id: state_record.id, __id: state_record.id,
}) })
); );
states.push(state_record);
state_data_found = true; state_data_found = true;
} }
} }
if (!state_data_found) { if (!state_data_found) {
records.push(record_search); records.push(record_search);
} }
} }
return records; return {
records: records,
states: states,
};
}, },
/** /**
@ -502,15 +543,12 @@ odoo.define(
* @param {Boolean} no_process_records * @param {Boolean} no_process_records
* @param {Number} position * @param {Number} position
*/ */
_appendSearchRecords: function( _appendSearchRecords: function(search_records, options) {
search_records, const processed_info = options.no_process_records
no_process_records,
position
) {
const processed_records = no_process_records
? search_records ? search_records
: this._processSearchRecords(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 state_data = this._getRecordDataById(search_record.__id);
const widget_options = this._getRecordOptions(search_record); const widget_options = this._getRecordOptions(search_record);
widget_options.renderer_widget_index = this.widgets.length; 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 // Simulate new lines to dispatch get_default & onchange's to get the
// relevant data to print. This case increase the TTI time. // relevant data to print. This case increase the TTI time.
if (!state_data) { if (!state_data) {
const defVirtualState = ProductPickerRecord.generateVirtualState(); const defVirtualState = ProductPickerRecord.generateVirtualState(
this.options.instant_search
);
this.defsVirtualState.push(defVirtualState); this.defsVirtualState.push(defVirtualState);
} }
@ -536,15 +576,39 @@ odoo.define(
function(widget, widget_position) { function(widget, widget_position) {
if (typeof widget_position !== "undefined") { if (typeof widget_position !== "undefined") {
const $elm = this.$el.find( const $elm = this.$el.find(
`[data-card-id="${position}"]` `[data-card-id="${widget_position}"]:first`
); );
widget.$el.insertBefore($elm); widget.$el.insertBefore($elm);
} }
def.resolve(); def.resolve();
}.bind(this, ProductPickerRecord, position) }.bind(this, ProductPickerRecord, options.position)
); );
this.defs.push(def); 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 * @param {Number} position
* @returns {Array} * @returns {Array}
*/ */
appendSearchRecords: function( appendSearchRecords: function(search_records, options = {}) {
search_records,
no_attach_widgets,
no_process_records,
position
) {
this.trigger_up("loading_records"); this.trigger_up("loading_records");
this.defs = []; this.defs = [];
this.defsVirtualState = []; this.defsVirtualState = [];
const cur_widget_index = this.widgets.length; const cur_widget_index = this.widgets.length;
this._appendSearchRecords(search_records, no_process_records, position); this._appendSearchRecords(search_records, options);
const defs = this.defs; const defs = this.defs;
delete this.defs; delete this.defs;
const defsVirtualState = this.defsVirtualState; const defsVirtualState = this.defsVirtualState;
delete this.defsVirtualState; delete this.defsVirtualState;
return [ return [
Promise.all(defs).then(() => { 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); const new_widgets = this.widgets.slice(cur_widget_index);
_.invoke(new_widgets, "on_attach_callback"); _.invoke(new_widgets, "on_attach_callback");
} }
@ -597,7 +657,6 @@ odoo.define(
_onClickLoadMore: function() { _onClickLoadMore: function() {
this.$btnLoadMore.attr("disabled", true); this.$btnLoadMore.attr("disabled", true);
this.trigger_up("load_more"); 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({ 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 * @param {Object} context
*/ */
updateRecordContext: function(handle, context) { updateRecordContext: function(id, context) {
this.localData[handle].context = _.extend( this.localData[id].context = _.extend(
{}, {},
this.localData[handle].context, this.localData[id].context,
context context
); );
}, },
/** /**
* @param {Number/String} id * @param {String} id
* @returns {Boolean} * @returns {Boolean}
*/ */
isPureVirtual: function(id) { 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 * @param {Boolean} status
*/ */
setPureVirtual: function(id, 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) { unsetDirty: function(id) {
const data = this.localData[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 {Integer/String} listID
* @param {Object} options * @param {Object} options
@ -77,14 +292,14 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
return new Promise(resolve => { return new Promise(resolve => {
this._makeDefaultRecord(list.model, params).then(recordID => { this._makeDefaultRecord(list.model, params).then(recordID => {
this.setPureVirtual(recordID, true); this.setPureVirtual(recordID, true);
this.updateRecordContext(recordID, {ignore_warning: true}); this.updateRecordContext(recordID, {
if (options.data) { ignore_warning: true,
this._applyChange(recordID, options.data, params).then(() => { not_onchange: true,
resolve(this.get(recordID)); });
resolve({
record: this.get(recordID),
params: params,
}); });
} else {
resolve(this.get(recordID));
}
}); });
}); });
}, },
@ -92,7 +307,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
/** /**
* Adds support to avoid show onchange warnings. * Adds support to avoid show onchange warnings.
* The implementation is a pure hack that clone * 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. * 'trigger_up' method.
* *
* @override * @override
@ -112,5 +327,28 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
} }
return this._super.apply(this, arguments); 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_lines": "_onClickLines",
"click .oe_btn_search_group": "_onClickSearchGroup", "click .oe_btn_search_group": "_onClickSearchGroup",
"search .oe_search_input": "_onSearch", "search .oe_search_input": "_onSearch",
"input .oe_search_input": "_onInputSearch",
"focusin .oe_search_input": "_onFocusInSearch", "focusin .oe_search_input": "_onFocusInSearch",
"show.bs.dropdown .o_cp_buttons": "_onShowSearchDropdown", "show.bs.dropdown .o_cp_buttons": "_onShowSearchDropdown",
"click #product_picker_maximize": "_onClickMaximize", "click #product_picker_maximize": "_onClickMaximize",
@ -41,19 +42,17 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
}), }),
_auto_search_delay: 450, _auto_search_delay: 450,
_input_instant_search_time: 150,
// Model product.product fields // Model product.product fields
search_read_fields: ["id", "display_name"], search_read_fields: ["id", "display_name", "uom_id"],
/** /**
* @override * @override
*/ */
init: function(parent, name, record) { init: function(parent) {
this._super.apply(this, arguments); this._super.apply(this, arguments);
// This is the parent state
this.state = record;
// Use jquery 'extend' to have a 'deep' merge. // Use jquery 'extend' to have a 'deep' merge.
this.options = $.extend( this.options = $.extend(
true, true,
@ -79,32 +78,31 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
if (this.view) { if (this.view) {
this._processGroups(); this._processGroups();
} }
this._currentSearchBatchID = 0;
this._lazyRenderSearchRecords = _.debounce(() => {
this.doRenderSearchRecords();
++this._currentSearchBatchID;
}, this._input_instant_search_time);
}, },
/**
* @override
*/
willStart: function() { willStart: function() {
if (!this.view) { return this._super.apply(this, arguments).then(() => {
return Promise.resolve(); if (this.isReadonly) {
} // Show Lines
if (this.mode === "readonly") {
this._updateSearchContext(-1); this._updateSearchContext(-1);
} else { } else {
this._updateSearchContext(0); this._updateSearchContext(0);
} }
return Promise.all([ });
this._super.apply(this, arguments),
this._getSearchRecords(),
]);
}, },
/** /**
* Updates the lines counter badge * Updates the lines counter badge
*/ */
updateBadgeLines: function() { 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 this.name
].data; ].data;
this.$badgeLines.text(records.length); this.$badgeLines.text(records.length);
@ -116,7 +114,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
} }
let prices = []; let prices = [];
const field_map = this.options.field_map; 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 this.name
].data; ].data;
if (this.options.show_discount) { if (this.options.show_discount) {
@ -145,7 +143,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
total, total,
this.value.fields[this.options.field_map.price_unit], this.value.fields[this.options.field_map.price_unit],
this.options.currency_field, this.options.currency_field,
this.state.data this.record.data
); );
this.$totalZone.find(".total_price").html(total || 0.0); 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), domain: this.record.getDomain(this.recordParams),
field: this.field, field: this.field,
parentID: this.value.id, parentID: this.value.id,
state: this.state, record: this.record,
model: this.parent_controller.model, model: this.parent_controller.model,
fieldName: this.name, fieldName: this.name,
recordData: this.recordData, recordData: this.recordData,
value: this.value, 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() { _render: function() {
const def = this._super.apply(this, arguments); const def = this._super.apply(this, arguments);
if (def) {
this.renderer.updateSearchGroup(this._activeSearchGroup);
// Parent implementation can return 'undefined' :( // Check maximize state
return ( if (!this.$el.hasClass("oe_field_one2many_product_picker_maximized")) {
def &&
def.then(() => {
if (
!this.$el.hasClass("oe_field_one2many_product_picker_maximized")
) {
this.$el.addClass("position-relative d-flex flex-column"); 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) { if (this.options.show_subtotal) {
this._addTotalsZone(); this._addTotalsZone();
} }
}) });
); }
return def;
}, },
/** /**
@ -274,10 +282,14 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
*/ */
doRenderSearchRecords: function() { doRenderSearchRecords: function() {
return new Promise(resolve => { return new Promise(resolve => {
this._getSearchRecords().then(() => { this._getSearchRecords()
.then(records => {
this.renderer.$el.scrollTop(0); this.renderer.$el.scrollTop(0);
this.renderer._renderView().then(() => resolve()); 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 * @param {Boolean} merge
* @returns {Deferred} * @returns {Deferred}
*/ */
_getSearchRecords: function(options, merge) { _getSearchRecords: function(options) {
const arch = this.view.arch; const arch = this.view.arch;
const search_mode = this.options.search[this._searchMode]; const search_mode = this.options.search[this._searchMode];
const field_name = this.options.field_map.product; const field_name = this.options.field_map.product;
@ -367,24 +379,8 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
} }
task.then(results => { task.then(results => {
if (merge) {
this._searchRecords = _.union(
this._searchRecords || [],
results
);
} else {
this._searchRecords = results;
}
this._lastSearchRecordsCount = results.length;
this._searchOffset = offset + limit; this._searchOffset = offset + limit;
if (this.renderer) { this.renderer.showLoadMore(limit && results.length === limit);
this.renderer.updateSearchData(
this._searchRecords,
this._lastSearchRecordsCount,
this._activeSearchGroup
);
}
resolve(results); resolve(results);
}); });
}); });
@ -531,9 +527,11 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
price_unit: "price_unit", price_unit: "price_unit",
discount: "discount", discount: "discount",
}, },
trigger_refresh_fields: ["partner_id", "currency_id"],
auto_save: false, auto_save: false,
ignore_warning: false, ignore_warning: false,
all_domain: [], all_domain: [],
instant_search: false,
}; };
}, },
@ -588,12 +586,17 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
return []; return [];
} }
const field_name = this.options.field_map.product; 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 this.name
].data; ].data;
const ids = _.map(lines, line => { // 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; return line.data[field_name].data.id;
}); })
.value();
return [["id", "in", ids]]; 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.order = [{name: "sequence"}, {name: "id"}];
this._searchContext.activeTest = false; 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. * that the search results. Use directy in-memory values.
*/ */
showLines: function() { showLines: function() {
this.renderer.clearRecords();
this._updateSearchContext(-1); this._updateSearchContext(-1);
this._clearSearchInput(); this._clearSearchInput();
this.$btnLines this.$btnLines
@ -635,6 +642,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
* @param {Number} group_id * @param {Number} group_id
*/ */
showGroup: function(group_id) { showGroup: function(group_id) {
this.renderer.clearRecords();
this._updateSearchContext(group_id); this._updateSearchContext(group_id);
this.doRenderSearchRecords(); this.doRenderSearchRecords();
this.$btnLines.removeClass("active"); this.$btnLines.removeClass("active");
@ -668,6 +676,15 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
this.doRenderSearchRecords(); 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 * Auto select all content when user enters into fields with this
* widget. * widget.
@ -707,7 +724,8 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
*/ */
_onCreateQuickRecord: function(evt) { _onCreateQuickRecord: function(evt) {
evt.stopPropagation(); 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) { if (this.options.auto_save) {
// Dont trigger state update // Dont trigger state update
@ -718,17 +736,16 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
this.parent_controller this.parent_controller
.saveRecord(undefined, {stayInEdit: true}) .saveRecord(undefined, {stayInEdit: true})
.then(() => { .then(() => {
// Because 'create' generates a new state and we can't know these new id we self.renderer.updateState(
// need force update all the current states. model.get(self.parent_controller.handle).data[
this._setValue( self.name
{operation: "UPDATE", id: evt.data.id}, ],
{doNotSetDirty: true} {force: true}
).then(() => { );
if (evt.data.callback) { if (evt.data.callback) {
evt.data.callback(); evt.data.callback();
} }
}); });
});
if (evt.data.callback) { if (evt.data.callback) {
evt.data.callback(); evt.data.callback();
} }
@ -758,22 +775,16 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
self.parent_controller self.parent_controller
.saveRecord(undefined, {stayInEdit: true}) .saveRecord(undefined, {stayInEdit: true})
.then(function() { .then(function() {
// Workaround to get updated values self.renderer.updateState(
self.parent_controller.model self.parent_controller.model.get(
.reload(self.value.id) self.parent_controller.handle
.then(function(result) { ).data[self.name],
var new_data = self.parent_controller.model.get( {force: true}
result
); );
self.value.data = new_data.data;
self.renderer.updateState(self.value, {
force: true,
});
if (callback) { if (callback) {
callback(); callback();
} }
}); });
});
if (callback) { if (callback) {
callback(); callback();
} }
@ -844,12 +855,9 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
if (this._isLoading) { if (this._isLoading) {
return; return;
} }
this._getSearchRecords( this._getSearchRecords({
{
offset: this._searchOffset, offset: this._searchOffset,
}, }).then(records => {
true
).then(records => {
this.renderer.appendSearchRecords(records); this.renderer.appendSearchRecords(records);
}); });
}, },
@ -872,7 +880,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
*/ */
_blockControlPanel: function(block) { _blockControlPanel: function(block) {
if (this.$buttons) { 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 { .oe_flip_card_inner {
height: 100% !important; height: 100% !important;
box-shadow: 0px 0px 15px; box-shadow: 0px 0px 15px;
.img-fluid {
transform: translateY(-50%) !important;
top: 50%;
position: relative;
}
.oe_one2many_product_picker_title { .oe_one2many_product_picker_title {
font-size: 1.95rem !important; font-size: 1.95rem !important;
} }
@ -142,6 +137,14 @@
$one2many-product-picker-transition-3d-time/2; $one2many-product-picker-transition-3d-time/2;
transform-style: preserve-3d; transform-style: preserve-3d;
.img-fluid {
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
z-index: -1;
position: absolute;
}
.position-absolute { .position-absolute {
z-index: 1; z-index: 1;
} }
@ -150,8 +153,19 @@
font-size: 1rem; 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 { .badge_price {
top: 50%; top: 55%;
right: -2px; right: -2px;
transform: translateY(-50%); transform: translateY(-50%);
display: grid; display: grid;
@ -190,10 +204,19 @@
.o_form_view.o_form_nosheet { .o_form_view.o_form_nosheet {
padding: $one2many-product-picker-card-form-padding; padding: $one2many-product-picker-card-form-padding;
.o_field_widget .o_input_dropdown > input { .o_field_widget {
&:not(.widget_numeric_step) {
max-width: 95%;
}
.o_input_dropdown > input {
height: unset; height: unset;
} }
} }
.btn.w-100 {
max-width: 95%;
}
}
} }
.oe_flip_card_front { .oe_flip_card_front {
@ -211,6 +234,16 @@
top: 50%; top: 50%;
left: 0; left: 0;
transform: translateY(-50%); 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,60 +86,37 @@
>Load More</button> >Load More</button>
</div> </div>
</t> </t>
<t t-name="One2ManyProductPicker.FlipCard"> <t t-name="One2ManyProductPicker.ActionButton">
<div <div class="safezone d-inline-block float-left m-0 pb-2 pr-2 text-left">
class="oe_flip_container p-1 col-12 col-sm-8 col-md-6 col-lg-4 col-xl-3 col-xxl-2"
t-att-data-card-id="state &amp;&amp; state.id || record_search.id"
>
<div
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"> <t t-if="is_saving &amp;&amp; lazy_qty > 0">
<div
class="safezone position-absolute m-0 pb-2 pr-2 text-left"
>
<span <span
class="badge record_saving badge-warning font-weight-bold rounded-0 mt-0 px-2 py-3 product_qty" class="badge record_saving badge-warning font-weight-bold rounded-0 mt-0 px-2 py-3 product_qty"
><span ><span class="lazy_product_qty" t-esc="lazy_qty || '1'" /> x <t
class="lazy_product_qty" t-esc="state.data[field_map[field_uom]].data.display_name"
t-esc="lazy_qty || '1'"
/> x <t
t-esc="state.data[field_map.product_uom].data.display_name"
/></span> /></span>
</div>
</t> </t>
<t t-elif="!is_virtual"> <t t-elif="!is_virtual">
<div
class="safezone position-absolute m-0 pb-2 pr-2 text-left"
>
<span <span
t-att-data-field="field_map.product_uom_qty" t-att-data-field="field_map[field_uom_qty]"
t-attf-data-esc="str({{field_map.product_uom_qty}}) + ' x ' + {{field_map.product_uom}}.data.display_name" 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-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 t-else=""> <t t-else="">
<div
class="safezone position-absolute m-0 pb-2 pr-2 text-left"
>
<span <span
class="badge badge-primary font-weight-bold rounded-0 mt-0 px-2 py-3 add_product" class="badge badge-primary font-weight-bold rounded-0 mt-0 px-2 py-3 add_product"
><i class="fa fa-plus" /> 1 <t ><i class="fa fa-plus" /> 1 <t
t-esc="state.data[field_map.product_uom].data.display_name" t-esc="state.data[field_map[field_uom]].data.display_name"
/></span> /></span>
</t>
</div> </div>
</t> </t>
<t t-name="One2ManyProductPicker.PriceZone">
<div class="position-absolute m-0 text-left badge_price"> <div class="position-absolute m-0 text-left badge_price">
<t t-if="show_discount"> <t t-if="show_discount">
<span <span
t-att-data-field="field_map.discount" t-att-data-field="field_map.discount"
t-attf-data-esc="'-' + str({{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" class="badge badge-dark discount_price font-weight-bold rounded-0 mt-1 p-2"
/> />
<span <span
@ -148,10 +125,11 @@
class="badge font-weight-bold rounded-0 original_price" class="badge font-weight-bold rounded-0 original_price"
/> />
<span <span
t-if="has_onchange"
class="badge badge-info price_unit font-weight-bold rounded-0 mt-1 p-2" class="badge badge-info price_unit font-weight-bold rounded-0 mt-1 p-2"
/> />
</t> </t>
<t t-else=""> <t t-else="has_onchange">
<span <span
t-att-data-field="field_map.price_unit" t-att-data-field="field_map.price_unit"
t-attf-data-esc="'{{monetary('price_unit',true)}}'" t-attf-data-esc="'{{monetary('price_unit',true)}}'"
@ -159,6 +137,16 @@
/> />
</t> </t>
</div> </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 <span
data-field="display_name" data-field="display_name"
class="oe_one2many_product_picker_title position-absolute fixed-bottom p-1" class="oe_one2many_product_picker_title position-absolute fixed-bottom p-1"
@ -167,7 +155,7 @@
<img <img
alt="" alt=""
class="img img-fluid" class="img img-fluid"
t-att-src="image(state.data[field_map.product].data.id,'image_512')" 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-att-data-src-alt="image(state.data[field_map.product].data.id,'image_1024')"
/> />
</t> </t>
@ -179,17 +167,31 @@
<img <img
alt="" alt=""
class="img img-fluid" class="img img-fluid"
t-att-src="image(record_search.id,'image_512')" t-att-src="image(record_search.id,'image_128')"
t-att-data-src-alt="image(record_search.id,'image_1024')" t-att-data-src-alt="image(record_search.id,'image_1024')"
/> />
</t> </t>
</div> </div>
</t>
<t t-name="One2ManyProductPicker.FlipCard.Back">
<div class="oe_flip_card_back"> <div class="oe_flip_card_back">
<widget <widget
name="product_picker_quick_create_form" name="product_picker_quick_create_form"
t-att-compare-key="field_map.product_uom" t-att-compare-key="field_map.product_uom"
/> />
</div> </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"
t-att-data-card-id="state &amp;&amp; state.id || record_search.id"
>
<div
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">
<t t-call="One2ManyProductPicker.FlipCard.Front" />
<t t-call="One2ManyProductPicker.FlipCard.Back" />
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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