3
0
Fork 0

Merge PR #2074 into 13.0

Signed-off-by pedrobaeza
13.0
OCA-git-bot 2022-03-24 15:03:50 +00:00
commit 750976c199
27 changed files with 2615 additions and 1538 deletions

View File

@ -86,6 +86,7 @@ Widget options:
* show_discount > Enable/Disable display discount (False by default)
* show_subtotal > Enable/Disable show subtotal (True by default)
* auto_save > Enable/Disable auto save (False by default)
* auto_save_delay > The time (in milliseconds) to wait after the last interaction before launching the autosave (1500 by default)
* all_domain > The domain used in 'All' section ([] by default)
If using auto save feature, you should keep in mind that the "Save" and "Discard" buttons
@ -95,6 +96,7 @@ Widget options:
* 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)
* auto_focus > Keep the focus on the search box after performing a search (True by default)
All widget options are optional.
Notice that you can call '_' method to use translations. This only can be used with this widget.
@ -215,7 +217,8 @@ Known issues / Roadmap
======================
* Translations in the xml 'options' attribute of the field that use the widget can't be exported automatically to be translated
* The product card animations can be improved. Currently the card is recreated, so we lost some elements to apply correct effects.
* The product card animations can be improved. Currently the card is recreated, so we lost some elements to apply correct effects
* sale.order onchanges that affects to "order_line" subfields will be ommitted to increase the performance
Bug Tracker
===========
@ -244,6 +247,8 @@ Contributors
* Pedro M. Baeza
* David Vidal
* Giovanni Serra <giovanni@gslab.it>
Maintainers
~~~~~~~~~~~

View File

@ -1,2 +1,3 @@
# Copyright 2020 Tecnativa - Alexandre Díaz
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import models

View File

@ -6,118 +6,130 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 12.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2021-02-17 13:45+0000\n"
"Last-Translator: claudiagn <claudia.gargallo@qubiq.es>\n"
"Language-Team: none\n"
"POT-Creation-Date: 2022-03-24 13:16+0000\n"
"PO-Revision-Date: 2022-03-24 14:17+0100\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.3.2\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: \n"
"X-Generator: Poedit 3.0.1\n"
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:6
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:0
#, python-format
msgid "Accept"
msgstr "Aceptar"
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:0
#, python-format
msgid "Add"
msgstr "Añadir"
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js:193
#: code:addons/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js:0
#, python-format
msgid "All"
msgstr "Todo"
#. module: web_widget_one2many_product_picker
#: model:ir.model,name:web_widget_one2many_product_picker.model_base
msgid "Base"
msgstr ""
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:35
#: code:addons/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js:0
#, python-format
msgid "By Name"
msgstr "Por Nombre"
#. module: web_widget_one2many_product_picker
#: code:addons/web_widget_one2many_product_picker/models/base.py:0
#, python-format
msgid "Can't create the %s: Duplicated product! (Already in database)"
msgstr ""
"No se puede crear el %s: Producto duplicado! (Actualmente en la base de "
"datos)"
#. module: web_widget_one2many_product_picker
#: code:addons/web_widget_one2many_product_picker/models/base.py:0
#, python-format
msgid "Can't create the %s: Duplicated product! (Inside query)"
msgstr "No se puede crear el %s: Producto duplicado! (En la petición)"
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:0
#, python-format
msgid "Discard"
msgstr "Descartar"
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:0
#, python-format
msgid "Groups"
msgstr "Grupos"
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:43
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:0
#, python-format
msgid "LOADING..."
msgstr "CARGANDO..."
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:0
#, python-format
msgid "Lines"
msgstr "Línias"
msgstr "Líneas"
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:67
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:0
#, python-format
msgid "Load More"
msgstr "Carga más"
msgstr "Cargar más"
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_modif_price.xml:11
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_modif_price.xml:0
#, python-format
msgid "Price:"
msgstr "Precio:"
#. module: web_widget_one2many_product_picker
#: model:ir.model,name:web_widget_one2many_product_picker.model_product_product
msgid "Product"
msgstr "Producto"
msgid "Price"
msgstr "Precio"
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:13
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:0
#, python-format
msgid "Remove"
msgstr "Eliminar"
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:23
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:0
#, python-format
msgid "Search..."
msgstr "Buscar..."
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:58
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:0
#, python-format
msgid "Subtotal:"
msgstr "Subtotal:"
#. module: web_widget_one2many_product_picker
#: model:ir.model.fields,help:web_widget_one2many_product_picker.field_product_product__image_variant_medium
msgid ""
"This field holds the image used as image for the product variantor product "
"image medium, limited to 512x512px."
msgstr ""
"Aquest camp conté la imatge que s'utilitza com a imatge per al mitjà "
"d'imatge del producte que varia el producte, limitada a 512x512px."
#. module: web_widget_one2many_product_picker
#: model:ir.model.fields,help:web_widget_one2many_product_picker.field_product_product__image_variant_big
msgid ""
"This field holds the image used as image for the product variantor product "
"image, limited to 1024x1024px."
msgstr ""
"Aquest camp conté la imatge que s'utilitza com a imatge per al mitjà "
"d'imatge del producte que varia el producte, limitada a 1024x1024px."
#. module: web_widget_one2many_product_picker
#: model:ir.model.fields,field_description:web_widget_one2many_product_picker.field_product_product__image_variant_big
msgid "Variant Image Big (Computed)"
msgstr "Imagen variante grande (calculada)"
#. module: web_widget_one2many_product_picker
#: model:ir.model.fields,field_description:web_widget_one2many_product_picker.field_product_product__image_variant_medium
msgid "Variant Image Medium (Computed)"
msgstr "Imagen variante media (calculada)"
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js:341
#: code:addons/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/record.js:0
#, python-format
msgid "[No widget %s]"
msgstr "[Sin widget %s]"
#~ msgid "Add 1"
#~ msgstr "Añadir 1"

View File

@ -6,6 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 13.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-03-24 13:16+0000\n"
"PO-Revision-Date: 2022-03-24 13:16+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@ -13,6 +15,13 @@ msgstr ""
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:0
#, python-format
msgid "Accept"
msgstr ""
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:0
@ -27,6 +36,11 @@ msgstr ""
msgid "All"
msgstr ""
#. module: web_widget_one2many_product_picker
#: model:ir.model,name:web_widget_one2many_product_picker.model_base
msgid "Base"
msgstr ""
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/js/widgets/field_one2many_product_picker.js:0
@ -34,6 +48,26 @@ msgstr ""
msgid "By Name"
msgstr ""
#. module: web_widget_one2many_product_picker
#: code:addons/web_widget_one2many_product_picker/models/base.py:0
#, python-format
msgid "Can't create the %s: Duplicated product! (Already in database)"
msgstr ""
#. module: web_widget_one2many_product_picker
#: code:addons/web_widget_one2many_product_picker/models/base.py:0
#, python-format
msgid "Can't create the %s: Duplicated product! (Inside query)"
msgstr ""
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:0
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml:0
#, python-format
msgid "Discard"
msgstr ""
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:0
@ -41,6 +75,13 @@ msgstr ""
msgid "Groups"
msgstr ""
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:0
#, python-format
msgid "LOADING..."
msgstr ""
#. module: web_widget_one2many_product_picker
#. openerp-web
#: code:addons/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker.xml:0

View File

@ -0,0 +1 @@
from . import base

View File

@ -0,0 +1,60 @@
# Copyright 2022 Tecnativa - Alexandre D. Díaz
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import _, api, models
from odoo.exceptions import ValidationError
class BaseModel(models.BaseModel):
_inherit = "base"
@api.model
def _check_product_picker_duplicated_products(self, vals_list):
relation = self.env.context.get("product_picker_relation")
if relation != self._name or not len(vals_list):
return
product_field = self.env.context.get("product_picker_product_field")
product_ids = [
values[product_field] for values in vals_list if product_field in values
]
num_products = len(product_ids)
if not num_products:
return
elif num_products != len(set(product_ids)):
raise ValidationError(
_("Can't create the %s: Duplicated product! (Inside query)") % relation
)
relation_field = self.env.context.get("product_picker_relation_field")
# All records have the same 'relation id' when created with the product picker
relation_id = vals_list[0][relation_field]
# When write maybe need get the value from the record
if not relation_id:
field_obj = self[relation_field]
if field_obj:
relation_id = relation_id.id
has_product = (
self.search(
[
(relation_field, "=", relation_id),
(product_field, "in", product_ids),
],
count=True,
limit=1,
)
!= 0
)
if has_product:
raise ValidationError(
_("Can't create the %s: Duplicated product! (Already in database)")
% relation
)
@api.model_create_multi
def create(self, vals_list):
"""Avoid create lines that have a product currently used when use the product picker"""
self._check_product_picker_duplicated_products(vals_list)
return super().create(vals_list)
def write(self, values):
"""Avoid write lines that have a product currently used when use the product picker"""
self._check_product_picker_duplicated_products([values])
return super().write(values)

View File

@ -44,6 +44,7 @@ Widget options:
* show_discount > Enable/Disable display discount (False by default)
* show_subtotal > Enable/Disable show subtotal (True by default)
* auto_save > Enable/Disable auto save (False by default)
* auto_save_delay > The time (in milliseconds) to wait after the last interaction before launching the autosave (1500 by default)
* all_domain > The domain used in 'All' section ([] by default)
If using auto save feature, you should keep in mind that the "Save" and "Discard" buttons
@ -53,6 +54,7 @@ Widget options:
* 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)
* auto_focus > Keep the focus on the search box after performing a search (True 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

@ -3,3 +3,5 @@
* Alexandre D. Díaz
* Pedro M. Baeza
* David Vidal
* Giovanni Serra <giovanni@gslab.it>

View File

@ -1,2 +1,3 @@
* Translations in the xml 'options' attribute of the field that use the widget can't be exported automatically to be translated
* The product card animations can be improved. Currently the card is recreated, so we lost some elements to apply correct effects.
* The product card animations can be improved. Currently the card is recreated, so we lost some elements to apply correct effects
* sale.order onchanges that affects to "order_line" subfields will be ommitted to increase the performance

View File

@ -3,7 +3,7 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
<meta name="generator" content="Docutils: http://docutils.sourceforge.net/" />
<title>Web Widget One2Many Product Picker</title>
<style type="text/css">
@ -474,6 +474,8 @@ You need to define the view fields. The view must be of <tt class="docutils lite
</li>
<li><p class="first">auto_save &gt; Enable/Disable auto save (False by default)</p>
</li>
<li><p class="first">auto_save_delay &gt; The time (in milliseconds) to wait after the last interaction before launching the autosave (1500 by default)</p>
</li>
<li><p class="first">all_domain &gt; The domain used in All section ([] by default)</p>
<p>If using auto save feature, you should keep in mind that the “Save” and “Discard” buttons
will lose part of its functionality as the document will be saved every time you
@ -485,6 +487,8 @@ modify/create a record with the widget.</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>
<li><p class="first">auto_focus &gt; Keep the focus on the search box after performing a search (True 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>
@ -605,7 +609,8 @@ accept changes.</p>
<h1><a class="toc-backref" href="#id10">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>Translations in the xml options attribute of the field that use the widget cant be exported automatically to be translated</li>
<li>The product card animations can be improved. Currently the card is recreated, so we lost some elements to apply correct effects.</li>
<li>The product card animations can be improved. Currently the card is recreated, so we lost some elements to apply correct effects</li>
<li>sale.order onchanges that affects to “order_line” subfields will be ommitted to increase the performance</li>
</ul>
</div>
<div class="section" id="bug-tracker">
@ -636,6 +641,8 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
</ul>
</blockquote>
</li>
<li><p class="first">Giovanni Serra &lt;<a class="reference external" href="mailto:giovanni&#64;gslab.it">giovanni&#64;gslab.it</a>&gt;</p>
</li>
</ul>
</div>
<div class="section" id="maintainers">

View File

@ -3,17 +3,35 @@
odoo.define("web_widget_one2many_product_picker.tools", function(require) {
"use strict";
const field_utils = require("web.field_utils");
var field_utils = require("web.field_utils");
/**
* Truncate floats
*
* @param {Number} value
* @param {Object} field_info
* @param {Array} digist
* @returns {Number}
*/
function float(value, field_info, digist) {
var options = digist && {digist: digist};
return field_utils.format.float(value, field_info, options);
}
/**
* Calculate the price with discount
*
* @param {Number} price
* @param {Number} discount
* @param {Array} digist
* @returns {Number}
*/
function priceReduce(price, discount) {
return price * (1.0 - discount / 100.0);
function priceReduce(price, discount, digist) {
var price_reduce = price * (1.0 - discount / 100.0);
if (digist) {
return float(price_reduce, undefined, digist);
}
return price_reduce;
}
/**
@ -34,12 +52,6 @@ odoo.define("web_widget_one2many_product_picker.tools", function(require) {
});
}
function float(value, field_info, digits) {
return field_utils.format.float(value, field_info, {
digits: digits,
});
}
return {
monetary: monetary,
float: float,

View File

@ -6,27 +6,23 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
) {
"use strict";
const core = require("web.core");
const Widget = require("web.Widget");
const widgetRegistry = require("web.widget_registry");
const ProductPickerQuickCreateFormView = require("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView")
var core = require("web.core");
var Widget = require("web.Widget");
var widgetRegistry = require("web.widget_registry");
var ProductPickerQuickCreateFormView = require("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView")
.ProductPickerQuickCreateFormView;
const qweb = core.qweb;
var qweb = core.qweb;
/**
* This widget render a Form. Used by FieldOne2ManyProductPicker
*/
const ProductPickerQuickCreateForm = Widget.extend({
var ProductPickerQuickCreateForm = Widget.extend({
className: "oe_one2many_product_picker_quick_create",
xmlDependencies: [
"/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_create.xml",
],
custom_events: {
reload_view: "_onReloadView",
},
/**
* @override
*/
@ -51,54 +47,53 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
* @override
*/
start: function() {
const def1 = this._super.apply(this, arguments);
const form_arch = this._generateFormArch();
const fieldsView = {
arch: form_arch,
fields: this.fields,
viewFields: this.fields,
base_model: this.basicFieldParams.field.relation,
type: "form",
model: this.basicFieldParams.field.relation,
};
var self = this;
return this._super.apply(this, arguments).then(function() {
var form_arch = self._generateFormArch();
var fieldsView = {
arch: form_arch,
fields: self.fields,
viewFields: self.fields,
base_model: self.basicFieldParams.field.relation,
type: "form",
model: self.basicFieldParams.field.relation,
};
const node_context = this.node.attr("context") || "{}";
this.nodeContext = py.eval(node_context, {
active_id: this.res_id || false,
var node_context = self.node.attr("context") || "{}";
self.nodeContext = py.eval(node_context, {
active_id: self.res_id || false,
});
var refinedContext = _.extend(
{},
self.main_state.getContext(),
self.nodeContext
);
_.extend(refinedContext, self.editContext);
self.formView = new ProductPickerQuickCreateFormView(fieldsView, {
context: refinedContext,
compareKey: self.compareKey,
fieldMap: self.fieldMap,
modelName: self.basicFieldParams.field.relation,
userContext: self.getSession().user_context,
ids: self.res_id ? [self.res_id] : [],
currentId: self.res_id || undefined,
mode: self.res_id && self.readonly ? "readonly" : "edit",
recordID: self.id,
index: 0,
parentID: self.basicFieldParams.parentID,
default_buttons: false,
withControlPanel: false,
model: self.basicFieldParams.model,
mainRecordData: self.getParent().getParent().state,
});
return self.formView.getController(self).then(function(controller) {
self.controller = controller;
self.$el.empty();
self.controller.appendTo(self.$el);
self.trigger_up("back_form_loaded");
return controller;
});
});
const refinedContext = _.extend(
{},
this.main_state.getContext(),
this.nodeContext
);
_.extend(refinedContext, this.editContext);
this.formView = new ProductPickerQuickCreateFormView(fieldsView, {
context: refinedContext,
compareKey: this.compareKey,
fieldMap: this.fieldMap,
modelName: this.basicFieldParams.field.relation,
userContext: this.getSession().user_context,
ids: this.res_id ? [this.res_id] : [],
currentId: this.res_id || undefined,
mode: this.res_id && this.readonly ? "readonly" : "edit",
recordID: this.id,
index: 0,
parentID: this.basicFieldParams.parentID,
default_buttons: false,
withControlPanel: false,
model: this.basicFieldParams.model,
mainRecordData: this.getParent().getParent().state,
});
// If (this.id) {
// this.basicFieldParams.model.save(this.id, {savePoint: true});
// }
const def2 = this.formView.getController(this).then(controller => {
this.controller = controller;
this.$el.empty();
this.controller.appendTo(this.$el);
});
return Promise.all([def1, def2]);
},
on_attach_callback: function() {
@ -112,7 +107,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
* @returns {String}
*/
_generateFormArch: function() {
let template =
var template =
"<templates><t t-name='One2ManyProductPicker.QuickCreateForm'>";
template += this.basicFieldParams.field.views.form.arch;
template += "</t></templates>";
@ -122,42 +117,6 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
record_search: this.searchRecord,
});
},
/**
* @private
* @param {CustomEvent} evt
*/
_onReloadView: function(evt) {
this.editContext = {
ignore_onchanges: [this.compareKey],
base_record_id: evt.data.baseRecordID || null,
base_record_res_id: evt.data.baseRecordResID || null,
base_record_compare_value: evt.data.baseRecordCompareValue || null,
};
if (evt.data.baseRecordCompareValue === evt.data.compareValue) {
this.res_id = evt.data.baseRecordResID;
this.id = evt.data.baseRecordID;
this.start();
} else {
this.getParent()
._generateVirtualState({}, this.editContext)
.then(state => {
const data = {};
data[this.compareKey] = {
operation: "ADD",
id: evt.data.compareValue,
};
this.basicFieldParams.model
._applyChange(state.id, data)
.then(() => {
this.res_id = state.res_id;
this.id = state.id;
this.start();
});
});
}
},
});
widgetRegistry.add(

View File

@ -10,19 +10,19 @@ odoo.define(
* is used by the RecordQuickCreate in One2ManyProductPicker views.
*/
const QuickCreateFormView = require("web.QuickCreateFormView");
const BasicModel = require("web.BasicModel");
const core = require("web.core");
var QuickCreateFormView = require("web.QuickCreateFormView");
var BasicModel = require("web.BasicModel");
var core = require("web.core");
const qweb = core.qweb;
var qweb = core.qweb;
BasicModel.include({
_applyOnChange: function(values, record, viewType) {
// Ignore changes by record context 'ignore_onchanges' fields
if ("ignore_onchanges" in record.context) {
const ignore_changes = record.context.ignore_onchanges;
for (const index in ignore_changes) {
const field_name = ignore_changes[index];
var ignore_changes = record.context.ignore_onchanges;
for (var index in ignore_changes) {
var field_name = ignore_changes[index];
delete values[field_name];
}
delete record.context.ignore_onchanges;
@ -31,7 +31,7 @@ odoo.define(
},
});
const ProductPickerQuickCreateFormRenderer = QuickCreateFormView.prototype.config.Renderer.extend(
var ProductPickerQuickCreateFormRenderer = QuickCreateFormView.prototype.config.Renderer.extend(
{
/**
* @override
@ -45,7 +45,7 @@ odoo.define(
}
);
const ProductPickerQuickCreateFormController = QuickCreateFormView.prototype.config.Controller.extend(
var ProductPickerQuickCreateFormController = QuickCreateFormView.prototype.config.Controller.extend(
{
events: _.extend({}, QuickCreateFormView.prototype.events, {
"click .oe_record_add": "_onClickAdd",
@ -66,8 +66,9 @@ odoo.define(
* @override
*/
_applyChanges: function() {
return this._super.apply(this, arguments).then(() => {
this._updateButtons();
var self = this;
return this._super.apply(this, arguments).then(function() {
self._updateButtons();
});
},
@ -75,17 +76,13 @@ odoo.define(
* Create or accept changes
*/
auto: function() {
const record = this.model.get(this.handle);
if (
record.context.has_changes_confirmed ||
typeof record.context.has_changes_confirmed === "undefined"
) {
var record = this.model.get(this.handle);
if (!record.context.has_changes_unconfirmed) {
return;
}
const state = this._getRecordState();
if (state === "new") {
if (this.model.isNew(record.id)) {
this._add();
} else if (state === "dirty") {
} else if (this.model.isDirty(record.id)) {
this._change();
}
},
@ -99,16 +96,19 @@ odoo.define(
* @returns {Object}
*/
_getRecordState: function() {
const record = this.model.get(this.handle);
let state = "record";
if (this.model.isNew(record.id)) {
var record = this.model.get(this.handle);
var state = "record";
if (
this.model.isNew(record.id) &&
this.model.isPureVirtual(record.id)
) {
state = "new";
} else if (record.isDirty()) {
} else if (record.context.has_changes_unconfirmed) {
state = "dirty";
}
if (state === "new") {
for (const index in this.mainRecordData.data) {
const recordData = this.mainRecordData.data[index];
for (var index in this.mainRecordData.data) {
var recordData = this.mainRecordData.data[index];
if (recordData.ref === record.ref) {
if (record.isDirty()) {
state = "dirty";
@ -174,8 +174,8 @@ odoo.define(
* @returns {Boolean}
*/
_needReloadCard: function(fields_changed) {
for (const index in fields_changed) {
const field = fields_changed[index];
for (var index in fields_changed) {
var field = fields_changed[index];
if (field === this.fieldMap[this.compareKey]) {
return true;
}
@ -192,51 +192,18 @@ odoo.define(
* @param {ChangeEvent} ev
*/
_onFieldChanged: function(ev) {
const fields_changed = Object.keys(ev.data.changes);
if (this._needReloadCard(fields_changed)) {
const field = ev.data.changes[fields_changed[0]];
let new_value = false;
if (typeof field === "object") {
new_value = field.id;
} else {
new_value = field;
}
const reload_values = {
compareValue: new_value,
};
const record = this.model.get(this.handle);
if ("base_record_id" in record.context) {
reload_values.baseRecordID = record.context.base_record_id;
reload_values.baseRecordResID =
record.context.base_record_res_id;
reload_values.baseRecordCompareValue =
record.context.base_record_compare_value;
} else {
let old_value = record.data[this.compareKey];
if (typeof old_value === "object") {
old_value = old_value.data.id;
}
reload_values.baseRecordID = record.id;
reload_values.baseRecordResID = record.ref;
reload_values.baseRecordCompareValue = old_value;
}
this.trigger_up("reload_view", reload_values);
// Discard current change
ev.data.changes = {};
} else {
this._super.apply(this, arguments);
if (!_.isEmpty(ev.data.changes)) {
if (this.model.isPureVirtual(this.handle)) {
this.model.unsetDirty(this.handle);
}
this.model.updateRecordContext(this.handle, {
has_changes_confirmed: false,
});
this.trigger_up("quick_record_updated", {
changes: ev.data.changes,
});
this._super.apply(this, arguments);
if (!_.isEmpty(ev.data.changes)) {
if (this.model.isPureVirtual(this.handle)) {
this.model.unsetDirty(this.handle);
}
this.model.updateRecordContext(this.handle, {
has_changes_unconfirmed: true,
});
this.trigger_up("quick_record_updated", {
changes: ev.data.changes,
highlight: {qty: true},
});
}
},
@ -244,12 +211,14 @@ odoo.define(
* @returns {Deferred}
*/
_add: function() {
var self = this;
if (this._disabled) {
// Don't do anything if we are already creating a record
return Promise.resolve();
return $.Deferred().resolve();
}
this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true,
need_notify: true,
modified: true,
});
this._disableQuickCreate();
return this.saveRecord(this.handle, {
@ -257,105 +226,145 @@ odoo.define(
reload: true,
savePoint: true,
viewType: "form",
}).then(() => {
const record = this.model.get(this.handle);
this.model.updateRecordContext(this.handle, {saving: true});
this.trigger_up("restore_flip_card", {
success_callback: () => {
this.trigger_up("create_quick_record", {
}).then(function() {
var record = self.model.get(self.handle);
self.model.updateRecordContext(record.id, {
has_changes_unconfirmed: false,
lazy_qty: record.data[self.fieldMap.product_uom_qty],
});
self.trigger_up("block_card", {status: true});
self.trigger_up("modify_quick_record", {
id: record.id,
});
self.trigger_up("restore_flip_card", {
success_callback: function() {
self.trigger_up("create_quick_record", {
id: record.id,
callback: () => {
this.model.updateRecordContext(this.handle, {
saving: false,
});
this.model.unsetDirty(this.handle);
this._enableQuickCreate();
on_onchange: function() {
self.trigger_up("block_card", {status: false});
self._enableQuickCreate();
},
});
},
block: true,
});
});
},
_remove: function() {
var self = this;
if (this._disabled) {
return Promise.resolve();
return $.Deferred().resolve();
}
this.model.updateRecordContext(this.handle, {
need_notify: true,
modified: true,
});
this._disableQuickCreate();
this.trigger_up("restore_flip_card", {block: true});
const record = this.model.get(this.handle);
this.trigger_up("list_record_remove", {
var record = this.model.get(this.handle);
this.trigger_up("block_card", {status: true});
this.trigger_up("modify_quick_record", {
id: record.id,
});
this.trigger_up("restore_flip_card", {
success_callback: function() {
self.trigger_up("list_record_remove", {
id: record.id,
on_onchange: function() {
self.trigger_up("block_card", {status: false});
self._enableQuickCreate();
},
});
},
});
},
_change: function() {
const self = this;
var self = this;
if (this._disabled) {
// Don't do anything if we are already creating a record
return Promise.resolve();
return $.Deferred().resolve();
}
var record = self.model.get(self.handle);
if (
!this.model.isDirty(this.handle) ||
!record.context.has_changes_unconfirmed
) {
this.trigger_up("restore_flip_card");
return $.Deferred().resolve();
}
this._disableQuickCreate();
this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true,
});
const record = this.model.get(this.handle);
this.trigger_up("restore_flip_card", {
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(() => {
this.model.updateRecordContext(this.handle, {
need_notify: true,
modified: true,
});
this._disableQuickCreate();
// SaveRecord used to make a save point.
return this.saveRecord(this.handle, {
stayInEdit: true,
reload: true,
savePoint: true,
viewType: "form",
}).then(function() {
record = self.model.get(self.handle);
self.model.updateRecordContext(record.id, {
has_changes_unconfirmed: false,
lazy_qty: record.data[self.fieldMap.product_uom_qty],
});
self.trigger_up("block_card", {status: true});
self.trigger_up("modify_quick_record", {
id: record.id,
});
self.trigger_up("restore_flip_card", {
success_callback: function() {
self.trigger_up("update_quick_record", {
id: record.id,
callback: function() {
self.model.unsetDirty(self.handle);
on_onchange: function() {
self.trigger_up("block_card", {status: false});
self._enableQuickCreate();
},
});
});
},
block: true,
},
});
});
},
_discard: function() {
var self = this;
if (this._disabled) {
// Don't do anything if we are already creating a record
return;
return $.Deferred().resolve();
}
var record = self.model.get(self.handle);
if (
!this.model.isDirty(this.handle) ||
!record.context.has_changes_unconfirmed
) {
this.trigger_up("restore_flip_card");
return $.Deferred().resolve();
}
this._disableQuickCreate();
this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true,
has_changes_unconfirmed: false,
});
this._disableQuickCreate();
// 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,
});
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();
return this.update({}, {reload: false}).then(function() {
record = self.model.get(self.handle);
self.trigger_up("quick_record_updated", {
changes: record.data,
});
self.trigger_up("restore_flip_card", {
success_callback: function() {
self._updateButtons();
self._enableQuickCreate();
},
});
});
},
@ -397,7 +406,7 @@ odoo.define(
}
);
const ProductPickerQuickCreateFormView = QuickCreateFormView.extend({
var ProductPickerQuickCreateFormView = QuickCreateFormView.extend({
config: _.extend({}, QuickCreateFormView.prototype.config, {
Renderer: ProductPickerQuickCreateFormRenderer,
Controller: ProductPickerQuickCreateFormController,

View File

@ -1,3 +1,4 @@
/* global py */
// Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define(
@ -5,21 +6,23 @@ odoo.define(
function(require) {
"use strict";
const core = require("web.core");
const Widget = require("web.Widget");
const ProductPickerQuickModifPriceFormView = require("web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView")
var core = require("web.core");
var Widget = require("web.Widget");
var widgetRegistry = require("web.widget_registry");
var ProductPickerQuickModifPriceFormView = require("web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView")
.ProductPickerQuickModifPriceFormView;
const qweb = core.qweb;
var qweb = core.qweb;
/**
* This widget render a Form. Used by FieldOne2ManyProductPicker
*/
const ProductPickerQuickModifPriceForm = Widget.extend({
var ProductPickerQuickModifPriceForm = Widget.extend({
className: "oe_one2many_product_picker_quick_modif_price",
xmlDependencies: [
"/web_widget_one2many_product_picker/static/src/xml/one2many_product_picker_quick_modif_price.xml",
],
events: {
"click .oe_record_change": "_onClickChange",
"click .oe_record_discard": "_onClickDiscard",
@ -30,13 +33,14 @@ odoo.define(
*/
init: function(parent, options) {
this._super.apply(this, arguments);
this.trigger_up("pause_auto_save");
this.state = options.state;
this.main_state = options.main_state;
this.node = options.node;
this.fields = options.fields;
this.fieldMap = options.fieldMap;
this.searchRecord = options.searchRecord;
this.fieldsInfo = options.fieldsInfo;
this.fieldsInfo = _.extend({}, options.fieldsInfo);
this.readonly = options.readonly;
this.basicFieldParams = options.basicFieldParams;
this.canEditPrice = options.canEditPrice;
@ -45,56 +49,76 @@ odoo.define(
this.res_id = this.state && this.state.res_id;
this.id = this.state && this.state.id;
this.editContext = {};
this._fieldsInvisible = [];
},
/**
* @override
*/
start: function() {
const def1 = this._super.apply(this, arguments);
const fieldsView = {
arch: this._generateFormArch(),
fields: this.fields,
viewFields: this.fields,
base_model: this.basicFieldParams.field.relation,
type: "form",
model: this.basicFieldParams.field.relation,
};
this.formView = new ProductPickerQuickModifPriceFormView(fieldsView, {
context: this.main_state.getContext(),
fieldMap: this.fieldMap,
modelName: this.basicFieldParams.field.relation,
userContext: this.getSession().user_context,
ids: this.res_id ? [this.res_id] : [],
currentId: this.res_id || undefined,
mode: this.res_id && this.readonly ? "readonly" : "edit",
recordID: this.id,
index: 0,
parentID: this.basicFieldParams.parentID,
default_buttons: true,
withControlPanel: false,
model: this.basicFieldParams.model,
parentRecordData: this.basicFieldParams.recordData,
currencyField: this.currencyField,
disable_autofocus: true,
});
if (this.id) {
this.basicFieldParams.model.save(this.id, {savePoint: true});
}
const def2 = this.formView.getController(this).then(controller => {
this.controller = controller;
this.$(".modal-body").empty();
this.controller.appendTo(this.$(".modal-body"));
this.$el.on("hidden.bs.modal", this._onModalHidden.bind(this));
});
var self = this;
this._super.apply(this, arguments).then(function() {
var fieldsView = {
arch: self._generateFormArch(),
fields: self.fields,
viewFields: self.fields,
base_model: self.basicFieldParams.field.relation,
type: "form",
model: self.basicFieldParams.field.relation,
};
return Promise.all([def1, def2]);
var node_context = self.node.attr("context") || "{}";
self.nodeContext = py.eval(node_context, {
active_id: self.res_id || false,
});
var refinedContext = _.extend(
{},
self.main_state.getContext(),
self.nodeContext
);
_.extend(refinedContext, self.editContext);
self.formView = new ProductPickerQuickModifPriceFormView(
fieldsView,
{
context: refinedContext,
fieldMap: self.fieldMap,
modelName: self.basicFieldParams.field.relation,
userContext: self.getSession().user_context,
ids: self.res_id ? [self.res_id] : [],
currentId: self.res_id || undefined,
mode: self.res_id && self.readonly ? "readonly" : "edit",
recordID: self.id,
index: 0,
parentID: self.basicFieldParams.parentID,
default_buttons: true,
withControlPanel: false,
model: self.basicFieldParams.model,
parentRecordData: self.getParent().getParent().state,
currencyField: self.currencyField,
disable_autofocus: true,
}
);
if (self.id) {
self.basicFieldParams.model.save(self.id, {savePoint: true});
}
return self.formView.getController(self).then(function(controller) {
self.controller = controller;
self.$(".modal-body").empty();
self.controller.appendTo(self.$(".modal-body"));
self.$el.on("hidden.bs.modal", self._onModalHidden.bind(self));
self.$el.find(".oe_record_change").removeClass("d-none");
return controller;
});
});
},
/**
* @override
*/
destroy: function() {
this._restoreNoFetch();
this.trigger_up("resume_auto_save");
this.$el.off("hidden.bs.modal");
this._super.apply(this, arguments);
},
@ -108,29 +132,32 @@ odoo.define(
* @returns {String}
*/
_generateFormArch: function() {
const wanted_field_states = this._getWantedFieldState();
let template =
var wanted_field_states = this._getWantedFieldState();
var template =
"<templates><t t-name='One2ManyProductPicker.QuickModifPrice.Form'>";
template += this.basicFieldParams.field.views.form.arch;
template += "</t></templates>";
qweb.add_template(template);
const $arch = $(
var $arch = $(
qweb.render("One2ManyProductPicker.QuickModifPrice.Form", {
field_map: this.fieldMap,
record_search: this.searchRecord,
})
);
const field_names = Object.keys(
var field_names = Object.keys(
this.basicFieldParams.field.views.form.fields
);
let gen_arch = "<form><group>";
for (const index in field_names) {
const field_name = field_names[index];
const $field = $arch.find("field[name='" + field_name + "']");
const modifiers = $field.attr("modifiers")
var gen_arch = "<form><group>";
for (var index in field_names) {
var field_name = field_names[index];
var $field = $arch.find("field[name='" + field_name + "']");
var modifiers = $field.attr("modifiers")
? JSON.parse($field.attr("modifiers"))
: {};
if (!modifiers.invisible && !(field_name in wanted_field_states)) {
this._fieldsInvisible.push(field_name);
}
modifiers.invisible = !(field_name in wanted_field_states);
modifiers.readonly = wanted_field_states[field_name];
$field.attr("modifiers", JSON.stringify(modifiers));
@ -160,12 +187,40 @@ odoo.define(
* @returns {Object}
*/
_getWantedFieldState: function() {
const wantedFieldState = {};
var wantedFieldState = {};
wantedFieldState[this.fieldMap.discount] = !this.canEditDiscount;
wantedFieldState[this.fieldMap.price_unit] = !this.canEditPrice;
return wantedFieldState;
},
/**
* @private
*/
_onClickDiscard: function(ev) {
if (this.controller) {
this._hideControlButtons(true);
this.controller._onClickDiscard(ev);
}
},
/**
* @private
*/
_onClickChange: function(ev) {
var self = this;
if (!this.controller) {
return;
}
self._hideControlButtons(true);
this.controller._onClickChange(ev).then(function(res) {
if (res) {
self.$el.modal("hide");
} else {
self._hideControlButtons(false);
}
});
},
/**
* @private
*/
@ -173,84 +228,28 @@ odoo.define(
this.destroy();
},
_hideControlButtons: function(status) {
this.$el.find(".oe_record_change").toggleClass("d-none", status);
this.$el.find(".oe_record_discard").toggleClass("d-none", status);
},
/**
* @private
* @param {MouseEvent} ev
*/
_onClickChange: function(ev) {
ev.stopPropagation();
const model = this.basicFieldParams.model;
model.updateRecordContext(this.id, {
has_changes_confirmed: true,
});
const is_virtual = model.isPureVirtual(this.id);
// If is a 'pure virtual' record, save it in the selected list
if (is_virtual) {
if (model.isDirty(this.id)) {
this._disableQuickCreate();
this.controller
.saveRecord(this.id, {
stayInEdit: true,
reload: true,
savePoint: true,
viewType: "form",
})
.then(() => {
this._enableQuickCreate();
model.unsetDirty(this.id);
this.trigger_up("create_quick_record", {
id: this.id,
});
});
}
} else {
// If is a "normal" record, update it
this.trigger_up("update_quick_record", {
id: this.id,
});
model.unsetDirty(this.id);
_restoreNoFetch: function() {
var record = this.basicFieldParams.model.get(this.id);
for (var field_name of this._fieldsInvisible) {
record.fieldsInfo[record.viewType][field_name].__no_fetch = false;
}
},
/**
* @private
* @param {MouseEvent} ev
*/
_onClickDiscard: function(ev) {
ev.stopPropagation();
const model = this.basicFieldParams.model;
model.discardChanges(this.id, {
rollback: true,
});
this.trigger_up("update_quick_record", {
id: this.id,
});
},
/**
* @private
*/
_disableQuickCreate: function() {
// Ensures that the record won't be created twice
this.$el.addClass("o_disabled");
this.$("input:not(:disabled),button:not(:disabled)")
.addClass("o_temporarily_disabled")
.attr("disabled", "disabled");
},
/**
* @private
*/
_enableQuickCreate: function() {
// Allows to create again
this.$el.removeClass("o_disabled");
this.$("input.o_temporarily_disabled,button.o_temporarily_disabled")
.removeClass("o_temporarily_disabled")
.attr("disabled", false);
this._fieldsInvisible = [];
},
});
widgetRegistry.add(
"product_picker_quick_modif_price_form",
ProductPickerQuickModifPriceForm
);
return ProductPickerQuickModifPriceForm;
}
);

View File

@ -10,23 +10,24 @@ odoo.define(
* is used by the RecordQuickCreate in One2ManyProductPicker views.
*/
const QuickCreateFormView = require("web.QuickCreateFormView");
const core = require("web.core");
const tools = require("web_widget_one2many_product_picker.tools");
var QuickCreateFormView = require("web.QuickCreateFormView");
var core = require("web.core");
var tools = require("web_widget_one2many_product_picker.tools");
const qweb = core.qweb;
var qweb = core.qweb;
const ProductPickerQuickModifPriceFormRenderer = QuickCreateFormView.prototype.config.Renderer.extend(
var ProductPickerQuickModifPriceFormRenderer = QuickCreateFormView.prototype.config.Renderer.extend(
{
/**
* @override
*/
start: function() {
var self = this;
this.$el.addClass(
"oe_one2many_product_picker_form_view o_xxs_form_view"
);
return this._super.apply(this, arguments).then(() => {
this._appendPrice();
return this._super.apply(this, arguments).then(function() {
self._appendPrice();
});
},
@ -42,7 +43,7 @@ odoo.define(
}
);
const ProductPickerQuickModifPriceFormController = QuickCreateFormView.prototype.config.Controller.extend(
var ProductPickerQuickModifPriceFormController = QuickCreateFormView.prototype.config.Controller.extend(
{
/**
* @override
@ -50,18 +51,20 @@ odoo.define(
init: function(parent, model, renderer, params) {
this.fieldMap = params.fieldMap;
this.context = params.context;
this._super.apply(this, arguments);
this.currencyField = params.currencyField;
this.parentRecordData = params.parentRecordData;
this.fields = parent.state.fields;
this._super.apply(this, arguments);
},
/**
* @override
*/
start: function() {
return this._super.apply(this, arguments).then(() => {
const record = this.model.get(this.handle);
this._updatePrice(record.data);
var self = this;
return this._super.apply(this, arguments).then(function() {
var record = self.model.get(self.handle);
self._updatePrice(record.data);
});
},
@ -70,8 +73,21 @@ odoo.define(
*/
_onFieldChanged: function(ev) {
this._super.apply(this, arguments);
const record = this.model.get(this.handle);
this._updatePrice(_.extend({}, record.data, ev.data.changes));
if (!_.isEmpty(ev.data.changes)) {
this.model.updateRecordContext(this.handle, {
has_changes_unconfirmed: true,
});
}
},
/**
* @override
*/
_applyChanges: function() {
return this._super.apply(this, arguments).then(() => {
var record = this.model.get(this.handle);
this._updatePrice(record.data);
});
},
/**
@ -79,7 +95,7 @@ odoo.define(
* @param {Object} values
*/
_updatePrice: function(values) {
const price_reduce = tools.priceReduce(
var price_reduce = tools.priceReduce(
values[this.fieldMap.price_unit],
values[this.fieldMap.discount]
);
@ -88,16 +104,136 @@ odoo.define(
.html(
tools.monetary(
price_reduce,
this.getParent().state.fields[this.fieldMap.price_unit],
this.fields[this.fieldMap.price_unit],
this.currencyField,
values
)
);
},
/**
* @private
* @param {MouseEvent} ev
*/
_onClickChange: function(ev) {
var self = this;
var def = $.Deferred();
ev.stopPropagation();
this.model.updateRecordContext(this.handle, {
has_changes_unconfirmed: false,
});
if (!this.model.isDirty(this.handle)) {
return def.resolve();
}
this._disableQuickCreate();
this.trigger_up("quick_record_updated", {
changes: _.extend(
{},
this.model.localData[this.handle].data,
this.model.localData[this.handle]._changes
),
highlight: {price: true},
});
this.model.updateRecordContext(this.handle, {
need_notify: true,
modified: true,
});
this.trigger_up("block_card", {status: true});
this.trigger_up("modify_quick_record", {
id: this.handle,
});
var is_virtual = this.model.isPureVirtual(this.handle);
// If is a 'pure virtual' record, save it in the selected list
if (is_virtual) {
this.saveRecord(this.handle, {
stayInEdit: true,
reload: true,
savePoint: true,
viewType: "form",
}).then(function() {
def.resolve(true);
self.trigger_up("create_quick_record", {
id: self.handle,
on_onchange: function() {
self.trigger_up("block_card", {status: false});
self._enableQuickCreate();
},
});
});
} else {
def.resolve(true);
// If is a "normal" record, update it
this.trigger_up("update_quick_record", {
id: this.handle,
on_onchange: function() {
self.trigger_up("block_card", {status: false});
self._enableQuickCreate();
},
});
}
return def;
},
/**
* @private
* @param {MouseEvent} ev
*/
_onClickDiscard: function(ev) {
var self = this;
ev.stopPropagation();
if (!this.model.isDirty(this.handle)) {
return true;
}
this.model.discardChanges(this.handle, {
rollback: true,
});
this.model.updateRecordContext(this.handle, {
need_notify: false,
modified: false,
});
this.trigger_up("block_card", {status: true});
this.trigger_up("modify_quick_record", {
id: this.handle,
});
this.trigger_up("update_quick_record", {
id: this.handle,
on_onchange: function() {
self.trigger_up("block_card", {status: false});
self._enableQuickCreate();
},
});
return true;
},
/**
* @private
*/
_disableQuickCreate: function() {
// Ensures that the record won't be created twice
this.$el.addClass("o_disabled");
this.$("input:not(:disabled),button:not(:disabled)")
.addClass("o_temporarily_disabled")
.attr("disabled", "disabled");
},
/**
* @private
*/
_enableQuickCreate: function() {
// Allows to create again
this.$el.removeClass("o_disabled");
this.$("input.o_temporarily_disabled,button.o_temporarily_disabled")
.removeClass("o_temporarily_disabled")
.attr("disabled", false);
},
}
);
const ProductPickerQuickModifPriceFormView = QuickCreateFormView.extend({
var ProductPickerQuickModifPriceFormView = QuickCreateFormView.extend({
config: _.extend({}, QuickCreateFormView.prototype.config, {
Renderer: ProductPickerQuickModifPriceFormRenderer,
Controller: ProductPickerQuickModifPriceFormController,

View File

@ -5,15 +5,14 @@ odoo.define(
function(require) {
"use strict";
const core = require("web.core");
const BasicRenderer = require("web.BasicRenderer");
const One2ManyProductPickerRecord = require("web_widget_one2many_product_picker.One2ManyProductPickerRecord");
const ProductPickerQuickCreateForm = require("web_widget_one2many_product_picker.ProductPickerQuickCreateForm");
var core = require("web.core");
var BasicRenderer = require("web.BasicRenderer");
var One2ManyProductPickerRecord = require("web_widget_one2many_product_picker.One2ManyProductPickerRecord");
const qweb = core.qweb;
var qweb = core.qweb;
/* This is the renderer of the main widget */
const One2ManyProductPickerRenderer = BasicRenderer.extend({
var One2ManyProductPickerRenderer = BasicRenderer.extend({
className: "oe_one2many_product_picker_view",
events: {
@ -23,8 +22,7 @@ odoo.define(
record_flip: "_onRecordFlip",
},
DELAY_GET_RECORDS: 150,
MIN_PERC_GET_RECORDS: 0.9,
_instant_search_onchange_delay: 250,
/**
* @override
@ -59,15 +57,6 @@ odoo.define(
_.invoke(_.compact(this.widgets), "on_detach_callback");
},
/**
* @param {Object} widget
*/
removeWidget: function(widget) {
const index = this.widgets.indexOf(widget);
widget.destroy();
delete this.widgets[index];
},
/**
* @override
*/
@ -95,18 +84,33 @@ odoo.define(
* @override
*/
updateState: function(state, params) {
const force_update = params.force;
var self = this;
var force_update = params.force;
delete params.force;
const sparams = _.extend({}, params, {noRender: true});
var sparams = _.extend({}, params, {noRender: true});
if (!force_update && _.isEqual(this.state.data, state.data)) {
return this._super(state, sparams);
}
const old_state = _.clone(this.state.data);
return this._super(state, sparams).then(() => {
this._updateStateRecords(old_state);
var old_state = _.clone(this.state.data);
return this._super(state, sparams).then(function() {
self._updateStateRecords(old_state);
});
},
canBeUpdated: function() {
var model = this.getParent().getBasicFieldParams().model;
for (var widget of this.widgets) {
if (!widget.state) {
return false;
}
var record = model.localData[widget.state.id];
if (record.context.in_timeout) {
return false;
}
}
return true;
},
/**
* Because this widget doesn't support comments/sections line types
* we need check if the line is valid to be shown.
@ -117,53 +121,41 @@ odoo.define(
*/
_isValidLineState: function(state) {
return (
state &&
state.data[this.options.field_map.product] &&
state.data[this.options.field_map.product].data &&
typeof state.data[this.options.field_map.product].data.id !==
"undefined"
);
},
getProductIdFromState: function(state) {
return (
state &&
state.data[this.options.field_map.product] &&
state.data[this.options.field_map.product].data &&
state.data[this.options.field_map.product].data.id
);
},
/**
* @private
* @param {Object} state_a
* @param {Object} state_b
* @returns {Boolean}
*/
_isEqualState: function(state_a, state_b) {
if (state_a.id === state_b.id) {
return true;
}
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;
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
);
getWidgetsByProduct: function(product_id) {
var self = this;
return _.filter(this.widgets, function(item) {
return (
self.getProductIdFromState(item.state) === product_id ||
item.recordSearch.id === product_id
);
});
},
/**
* @private
* @param {Object} state
* @returns {Boolean}
*/
_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;
getWidgetsWithoutOnchange: function() {
var model = this.getParent().getBasicFieldParams().model;
return _.filter(this.widgets, function(item) {
return (
model.localData[item.state.id] &&
model.localData[item.state.id].context.not_onchange
);
});
},
/**
@ -175,58 +167,62 @@ odoo.define(
* @param {Array} states
* @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)) {
// 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"
);
_processStatesToDestroy: function(old_states) {
var self = this;
// States to remove
// In 12.0, Odoo generates new ids for the states, so
// all states will be removed and restored because it's
// not possible identify a record without this id
var to_destroy_ids = [];
for (var index in old_states) {
var old_state = old_states[index];
if (!this._isValidLineState(old_state)) {
continue;
}
var in_current_state = _.some(this.state.data, function(state) {
return (
self._isValidLineState(state) && state.id === old_state.id
);
});
if (!in_current_state) {
to_destroy_ids.push(old_state.id);
}
}
to_add.push([
[search_record],
{
no_attach_widgets: false,
no_process_records: false,
position: widget.state.id,
},
]);
}
var model = this.getParent().getBasicFieldParams().model;
var to_destroy = [];
for (var widget of this.widgets) {
if (!widget) {
continue;
}
// Verify that doesn't exists any dead widget
// This is necessary beceause auto-save uses
// ADD + SAVE that generates two different
// state ids
var state_has_onchange =
widget.state && !widget.state.context.not_onchange;
var state_has_modified =
widget.state && !widget.state.context.modified;
if (
!state_has_modified &&
state_has_onchange &&
!model.isPureVirtual(widget.state.id)
) {
to_destroy.push(widget);
continue;
}
for (var index_destroy in to_destroy_ids) {
const state_id = to_destroy_ids[index_destroy];
if (widget.state.id === state_id) {
to_destroy.push(widget);
break;
}
}
}
return [to_destroy, to_add];
return to_destroy;
},
/**
@ -236,67 +232,71 @@ odoo.define(
* @private
* @returns {Array}
*/
_processCurrentStates: function() {
_processCurrentStates: function(old_states) {
var to_destroy = this._processStatesToDestroy(old_states);
// 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];
var model = this.getParent().getBasicFieldParams().model;
var to_add = [];
for (var index in this.state.data) {
var state = this.state.data[index];
if (!this._isValidLineState(state)) {
continue;
}
let exists = false;
let search_record_index = false;
let search_record = false;
for (let e = this.widgets.length - 1; e >= 0; --e) {
const widget = this.widgets[e];
if (!widget || !widget.state) {
var exists = false;
var search_record_index = false;
var search_record = false;
for (var widget of this.widgets) {
if (!widget) {
// Already processed widget (deleted)
continue;
}
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,
});
var record = model.get(widget.state.id);
// Re-use widgets is possible
var is_to_destroy = _.findIndex(to_destroy, widget) >= 0;
var is_widget_usable =
widget.state.id === state.id ||
widget.recordSearch.id ===
state.data[this.options.field_map.product].data.id;
if (is_widget_usable) {
if (is_to_destroy) {
to_destroy = _.without(to_destroy, widget);
}
if (record) {
model.updateRecordContext(state.id, {
lazy_qty: record.context.lazy_qty || 0,
saving: record.context.saving || false,
need_notify: record.context.need_notify || false,
need_save: record.context.need_save || false,
});
}
// Ensure use the updated state
widget.recreate(state);
exists = true;
break;
}
if (
!is_equal_state &&
} else if (
widget.state &&
!model.isPureVirtual(widget.state.id) &&
widget.recordSearch.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.state.id;
search_record = widget.recordSearch;
const record = model.get(widget.state.id);
model.updateRecordContext(state.id, {
lazy_qty: record.context.lazy_qty || 0,
});
}
// Remove "pure virtual" records that have the same product that the new record
if (
widget.is_virtual &&
this._isEqualState(widget.state, state)
) {
to_destroy.push(widget);
delete this.widgets[e];
const in_search_records = _.some(
this.search_records,
function(item) {
return item.id === search_record.id;
}
);
if (in_search_records) {
// Is a new record (can be other record for the same 'search record')
search_record_index = widget.state.id;
}
}
}
this.state.data = _.compact(this.state.data);
// Add to create the new record
if (!exists && search_record_index) {
const new_search_record = _.extend({}, search_record, {
var new_search_record = _.extend({}, search_record, {
__id: state.id,
});
to_add.push([
@ -309,68 +309,121 @@ odoo.define(
]);
}
}
return [to_add.reverse(), to_destroy];
},
return [to_destroy, to_add];
/**
* This method checks and appends the missing
* 'pure virtual' records
*
* @returns {Deferred}
*/
checkVirtualRecords: function() {
if (this.search_group.name === "main_lines") {
return $.when();
}
var tasks = [];
var to_add = this._processVirtualRecords();
for (var params of to_add) {
tasks.push(this.appendSearchRecords.apply(this, params)[0]);
}
return $.when(tasks);
},
/**
* This method checks the current widgets to generate the
* missing 'pure virtual' record objects.
*
* @returns {Array}
*/
_processVirtualRecords: function() {
var model = this.getParent().getBasicFieldParams().model;
var products_done = [];
var to_add = [];
for (var search_record of this.search_records) {
var widgets = this.getWidgetsByProduct(search_record.id);
if (_.isEmpty(widgets)) {
to_add.push([
[_.omit(search_record, "__id")],
{
no_attach_widgets: true,
no_process_records: true,
},
]);
continue;
}
// Only add 'pure virtual' records if don't have a line
var existing_widgets = _.filter(widgets, function(widget) {
return !widget.isMarkedToDestroy();
});
var need_virtual = !_.some(existing_widgets, function(widget) {
return widget.state && !model.isPureVirtual(widget.state.id);
});
if (
need_virtual &&
products_done.indexOf(search_record.id) === -1
) {
var has_virtual = _.some(existing_widgets, function(widget) {
return (
!widget.state ||
(widget.state && model.isPureVirtual(widget.state.id))
);
});
if (!has_virtual) {
var search_record_index = _.max(widgets, function(widget) {
return widget.$el.index();
}).state.id;
to_add.push([
[_.omit(search_record, "__id")],
{
no_attach_widgets: true,
no_process_records: true,
position: search_record_index,
},
]);
products_done.push(search_record.id);
}
}
}
return to_add;
},
/**
* When the state change this method tries to update current records, delete
* or update them.
* Thanks to this we don't need re-render 'pure virtual' records.
* NOTE: The first load of the records don't trigger this method.
*
* @private
* @param {Object} old_states
* @returns {Deferred}
*/
_updateStateRecords: function(old_states) {
// States to remove
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;
}
}
if (!found) {
states_to_destroy.push(old_state);
}
}
const def = $.Deferred();
this.state.data = _.compact(this.state.data);
const [to_destroy_old, to_add_virtual] = this._processStatesToDestroy(
states_to_destroy
);
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) {
var self = this;
var record_defs = this._processCurrentStates(old_states);
var to_add_current = record_defs[0];
var to_destroy = record_defs[1];
_.invoke(to_destroy, "markToDestroy");
var currentTasks = [];
for (var params of to_add_current) {
currentTasks.push(this.appendSearchRecords.apply(this, params)[0]);
}
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;
return $.when(currentTasks)
.then(function() {
return self.checkVirtualRecords();
})
.then(function() {
var widgets_to_destroy = _.filter(self.widgets, function(
widget
) {
return widget.isMarkedToDestroy();
});
self.widgets = _.difference(self.widgets, widgets_to_destroy);
_.invoke(widgets_to_destroy, "destroy");
return true;
});
},
clearRecords: function() {
@ -413,12 +466,12 @@ odoo.define(
*/
_sort_search_data: function(datas) {
if (this.search_group.name === "main_lines") {
const field_name = this.options.field_map.product;
for (const index_datas in datas) {
const data = datas[index_datas];
var field_name = this.options.field_map.product;
for (var index_datas in datas) {
var data = datas[index_datas];
for (const index_state in this.state.data) {
const state_data = this.state.data[index_state];
for (var index_state in this.state.data) {
var state_data = this.state.data[index_state];
if (
this._isValidLineState(state_data) &&
state_data.data[field_name].res_id === data.id
@ -427,9 +480,11 @@ odoo.define(
}
}
}
const sorted_datas = _.chain(datas)
var sorted_datas = _.chain(datas)
.sortBy("_order_value")
.map(item => _.omit(item, "_order_value"))
.map(function(item) {
return _.omit(item, "_order_value");
})
.value()
.reverse();
return sorted_datas;
@ -446,9 +501,10 @@ odoo.define(
* @returns {Array}
*/
_processSearchRecords: function(results) {
const field_name = this.options.field_map.product;
const records = [];
const states = [];
var model = this.getParent().getBasicFieldParams().model;
var field_name = this.options.field_map.product;
var records = [];
var states = [];
var test_values = function(field_value, record_search) {
return (
@ -458,59 +514,62 @@ odoo.define(
);
};
for (const index in results) {
const record_search = results[index];
let state_data_found = false;
for (var index in results) {
var record_search = results[index];
// 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;
}
var widget_created = false;
for (var index_data in this.state.data) {
var state_record = this.state.data[index_data];
var field_value = state_record.data[field_name];
if (
record_search.__id === widget.state.id ||
(!record_search.__id &&
widget.recordSearch.id === record_search.id)
!this._isValidLineState(state_record) ||
!test_values(field_value, record_search)
) {
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];
if (!this._isValidLineState(state_record)) {
continue;
}
const field_value = state_record.data[field_name];
if (test_values(field_value, record_search)) {
widget_created = true;
// At this point the result has a state (line)
// Search if already exists a widget using the state
var widget = _.find(this.widgets, function(widget) {
return (
!widget.isMarkedToDestroy() &&
widget.state &&
widget.state.id === state_record.id
);
});
if (widget) {
// Don't need create a new widget (record)
states.push(widget.state);
} else {
// Need create a new widget linked with the state
records.push(
_.extend({}, record_search, {
__id: state_record.id,
})
);
states.push(state_record);
state_data_found = true;
}
}
if (widget_created) {
continue;
}
if (!state_data_found) {
records.push(record_search);
var widgets = this.getWidgetsByProduct(record_search.id);
// Only can exists 'pure virtual' if no 'lines' assigned
if (_.isEmpty(widgets)) {
var has_virtual = _.some(widgets, function(widget) {
return (
!widget.isMarkedToDestroy() &&
(!widget.state ||
(widget.state &&
model.isPureVirtual(widget.state.id)))
);
});
if (!has_virtual) {
// The result need a 'pure virtual' record
records.push(_.omit(record_search, "__id"));
}
}
}
@ -526,8 +585,8 @@ odoo.define(
* @returns {Object}
*/
_getRecordDataById: function(id) {
for (const index in this.state.data) {
const record = this.state.data[index];
for (var index in this.state.data) {
var record = this.state.data[index];
if (record.id === id) {
return record;
}
@ -561,73 +620,55 @@ odoo.define(
* @private
* @param {Array} search_records
* @param {Object} options
* @returns {Array}
*/
_appendSearchRecords: function(search_records, options) {
const processed_info = options.no_process_records
? search_records
: this._processSearchRecords(search_records);
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;
const ProductPickerRecord = new One2ManyProductPickerRecord(
this,
var self = this;
var processed_info =
!options.no_process_records &&
this._processSearchRecords(search_records);
var records_to_add =
(processed_info && processed_info.records) || search_records;
_.each(records_to_add, function(search_record) {
// Get record state (if can)
var state_data = self._getRecordDataById(search_record.__id);
var widget_options = self._getRecordOptions(search_record);
widget_options.renderer_widget_index = self.widgets.length;
var ProductPickerRecord = new One2ManyProductPickerRecord(
self,
state_data,
widget_options
);
this.widgets.push(ProductPickerRecord);
self.widgets.push(ProductPickerRecord);
// 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(
this.options.instant_search
);
this.defsVirtualState.push(defVirtualState);
var defVirtualState = ProductPickerRecord.generateVirtualState({
onchange_delay: self.options.instant_search
? self._instant_search_onchange_delay
: 0,
});
self.defsVirtualState.push(defVirtualState);
}
// At this point the widget will use the existing state (line) or
// a simple state data. Using simple state data instead of waiting for
// complete state (default + onchange) gives a low FCP time.
const def = $.Deferred();
ProductPickerRecord.appendTo(this.$recordsContainer).then(
var def = ProductPickerRecord.appendTo(self.$recordsContainer).then(
function(widget, widget_position) {
if (typeof widget_position !== "undefined") {
const $elm = this.$el.find(
var $elm = self.$el.find(
`[data-card-id="${widget_position}"]:first`
);
widget.$el.insertBefore($elm);
widget.$el.insertAfter($elm);
}
def.resolve();
}.bind(this, ProductPickerRecord, options.position)
}.bind(self, ProductPickerRecord, options.position)
);
this.defs.push(def);
self.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);
}
return records_to_add;
},
/**
@ -645,25 +686,80 @@ odoo.define(
* @returns {Array}
*/
appendSearchRecords: function(search_records, options = {}) {
var self = this;
if (options.clear) {
this.clearRecords();
}
this.trigger_up("loading_records");
this.defs = [];
this.defsVirtualState = [];
const cur_widget_index = this.widgets.length;
var cur_widget_index = this.widgets.length;
this._appendSearchRecords(search_records, options);
const defs = this.defs;
var defs = this.defs;
delete this.defs;
const defsVirtualState = this.defsVirtualState;
var defsVirtualState = this.defsVirtualState;
delete this.defsVirtualState;
return [
Promise.all(defs).then(() => {
if (!options.no_attach_widgets && this._isInDom) {
const new_widgets = this.widgets.slice(cur_widget_index);
$.when(defs).then(function() {
if (!options.no_attach_widgets && self._isInDom) {
var new_widgets = self.widgets.slice(cur_widget_index);
_.invoke(new_widgets, "on_attach_callback");
}
// Destroy unused
if (options.cleanup) {
self.search_records = _.compact(search_records);
var widgets_to_destroy = _.filter(self.widgets, function(
widget
) {
return (
widget.isMarkedToDestroy() ||
!_.some(self.search_records, function(
search_record
) {
return (
search_record.id ===
widget.recordSearch.id &&
!_.some(self.widgets, function(
comp_widget
) {
return (
comp_widget !== widget &&
comp_widget.state &&
comp_widget.recordSearch.id ===
widget.recordSearch.id
);
})
);
})
);
});
_.invoke(widgets_to_destroy, "destroy");
self.widgets = _.difference(
self.widgets,
widgets_to_destroy
);
} else {
self.search_records = self.search_records || [];
for (var search_record of search_records) {
var has_search_record = _.some(
self.search_records,
function(item) {
return (
item.id === search_record.id &&
item.__id === search_record.__id
);
}
);
if (!has_search_record) {
self.search_records.push(search_record);
}
}
}
return true;
}),
Promise.all(defsVirtualState).then(() => {
this.trigger_up("loading_records", {finished: true});
$.when(defsVirtualState).then(function() {
self.trigger_up("loading_records", {finished: true});
}),
];
},
@ -682,8 +778,8 @@ odoo.define(
* @param {Integer} index
*/
doWidgetFlip: function(index) {
const widget = this.widgets[index];
const $actived_card = this.$el.find(".active");
var widget = this.widgets[index];
var $actived_card = this.$el.find(".active");
if (widget.$card.hasClass("active")) {
widget.$card.removeClass("active");
widget.$card.find(".oe_flip_card_front").removeClass("d-none");
@ -692,11 +788,11 @@ odoo.define(
widget._processWidgetFields(widget.$back);
widget._processWidgets(widget.$back);
widget._processDynamicFields();
$.when(widget.defs).then(() => {
$.when(widget.defs).then(function() {
$actived_card.removeClass("active");
$actived_card.find(".oe_flip_card_front").removeClass("d-none");
widget.$card.addClass("active");
setTimeout(() => {
setTimeout(function() {
widget.$(".oe_flip_card_front").addClass("d-none");
}, 200);
});
@ -711,17 +807,25 @@ odoo.define(
* @param {CustomEvent} evt
*/
_onRecordFlip: function(evt) {
const prev_widget_index = evt.data.prev_widget_index;
if (typeof prev_widget_index !== "undefined") {
var prev_widget_index = evt.data.prev_widget_index;
if (
typeof prev_widget_index !== "undefined" &&
this.widgets[prev_widget_index]
) {
// Only check 'back' widgets so there is where the form was created
for (const index in this.widgets[prev_widget_index].widgets.back) {
const widget = this.widgets[prev_widget_index].widgets.back[
for (var index in this.widgets[prev_widget_index].widgets.back) {
var widget = this.widgets[prev_widget_index].widgets.back[
index
];
if (widget instanceof ProductPickerQuickCreateForm) {
if (
widget.controller &&
widget.className ===
"oe_one2many_product_picker_quick_create"
) {
widget.controller.auto();
}
}
this.widgets[prev_widget_index].recreate();
}
},
});

View File

@ -1,32 +0,0 @@
// 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) {
id = id || this.handle;
return this._super.apply(this, arguments).then(() => {
if (this.renderer && !_.isEmpty(this.renderer.allFieldWidgets)) {
const product_picker_widgets = _.filter(
this.renderer.allFieldWidgets[id],
item => item.attrs.widget === "one2many_product_picker"
);
_.invoke(
product_picker_widgets,
"onDocumentConfirmChanges",
fields,
e
);
}
});
},
});
});

View File

@ -3,7 +3,8 @@
odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
"use strict";
const BasicModel = require("web.BasicModel");
var BasicModel = require("web.BasicModel");
var FieldOne2ManyProductPicker = require("web_widget_one2many_product_picker.FieldOne2ManyProductPicker");
BasicModel.include({
/**
@ -36,12 +37,21 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
);
},
/**
* @param {String} id
* @returns {Boolean}
*/
isSaving: function(id) {
var data = this.localData[id];
return data._virtual || false;
},
/**
* @param {String} id
* @returns {Boolean}
*/
isPureVirtual: function(id) {
const data = this.localData[id];
var data = this.localData[id];
return data._virtual || false;
},
@ -50,7 +60,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
* @param {Boolean} status
*/
setPureVirtual: function(id, status) {
const data = this.localData[id];
var data = this.localData[id];
if (status) {
data._virtual = true;
} else {
@ -62,9 +72,9 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
* @param {String} id
*/
unsetDirty: function(id) {
const data = this.localData[id];
var data = this.localData[id];
data._isDirty = false;
this._visitChildren(data, r => {
this._visitChildren(data, function(r) {
r._isDirty = false;
});
},
@ -81,14 +91,14 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
return false;
}
const data = this.localData[id];
const to_remove = [];
this._visitChildren(data, item => {
var data = this.localData[id];
var to_remove = [];
this._visitChildren(data, function(item) {
to_remove.push(item.id);
});
to_remove.reverse();
for (const remove_id of to_remove) {
for (var remove_id of to_remove) {
this.removeLine(remove_id);
delete this.localData[remove_id];
}
@ -103,7 +113,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
*
* @param {Object} record
* @param {Object} params
* @returns {Promise}
* @returns {Deferred}
*/
_makeDefaultRecordNoDatapoint: function(record, params) {
var self = this;
@ -128,15 +138,20 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
);
}
return this._rpc({
model: record.model,
method: "default_get",
args: [fields_key],
context: params.context,
}).then(function(result) {
return this._rpc(
{
model: record.model,
method: "default_get",
args: [fields_key],
context: params.context,
},
{
shadow: true,
}
).then(function(result) {
// Interrupt point (used in instant search)
if (!self.exists(record.id)) {
return Promise.reject();
return $.Deferred().reject();
}
// We want to overwrite the default value of the handle field (if any),
@ -157,44 +172,43 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
.applyDefaultValues(record.id, result, {fieldNames: fieldNames})
.then(function() {
if (!self.exists(record.id)) {
return Promise.reject();
return $.Deferred().reject();
}
var def = new Promise(function(resolve, reject) {
// Interrupt point (used in instant search)
if (!self.exists(record.id)) {
return Promise.reject();
}
var always = function() {
if (record._warning) {
if (params.allowWarning) {
delete record._warning;
} else {
reject();
}
var def = $.Deferred();
// Interrupt point (used in instant search)
if (!self.exists(record.id)) {
return $.Deferred().reject();
}
var always = function() {
if (record._warning) {
if (params.allowWarning) {
delete record._warning;
} else {
def.reject();
}
resolve();
};
self._performOnChange(record, fields_key)
.then(always)
.guardedCatch(always);
});
}
def.resolve();
};
self._performOnChange(record, fields_key)
.then(always)
.guardedCatch(always);
return def;
})
.then(function() {
if (!self.exists(record.id)) {
return Promise.reject();
return $.Deferred().reject();
}
return self._fetchRelationalData(record);
})
.then(function() {
if (!self.exists(record.id)) {
return Promise.reject();
return $.Deferred().reject();
}
return self._postprocess(record);
})
.then(function() {
if (!self.exists(record.id)) {
return Promise.reject();
return $.Deferred().reject();
}
// Save initial changes, so they can be restored later,
// if we need to discard.
@ -215,11 +229,11 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
* @returns {Object}
*/
createVirtualDatapoint: function(listID, options) {
const list = this.localData[listID];
const context = _.extend({}, this._getContext(list), options.context);
var list = this.localData[listID];
var context = _.extend({}, this._getContext(list), options.context);
const position = options ? options.position : "top";
const params = {
var position = options ? options.position : "top";
var params = {
context: context,
fields: list.fields,
fieldsInfo: list.fieldsInfo,
@ -228,6 +242,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
viewType: list.viewType,
allowWarning: true,
doNotSetDirty: true,
postonchange_values: options.onchange_values,
};
var targetView = params.viewType;
@ -251,7 +266,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
fields = _.defaults({}, fields, parentRecord.fields);
}
const record = this._makeDataPoint({
var record = this._makeDataPoint({
modelName: list.model,
fields: fields,
fieldsInfo: fieldsInfo,
@ -263,7 +278,8 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
this.setPureVirtual(record.id, true);
this.updateRecordContext(record.id, {
ignore_warning: true,
not_onchange: true,
not_onchange: true, // To know is the record has the initial onchange applied
shadow: true, // To avoid show the loading backdrop
});
return {
@ -280,11 +296,16 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
* @returns {Deferred}
*/
createVirtualRecord: function(listID, options) {
const list = this.localData[listID];
const context = _.extend({}, this._getContext(list), options.context);
var self = this;
var list = this.localData[listID];
var context = _.extend(
{shadow: true},
this._getContext(list),
options.context
);
const position = options ? options.position : "top";
const params = {
var position = options ? options.position : "top";
var params = {
context: context,
fields: list.fields,
fieldsInfo: list.fieldsInfo,
@ -295,18 +316,17 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
doNotSetDirty: true,
};
return new Promise(resolve => {
this._makeDefaultRecord(list.model, params).then(recordID => {
this.setPureVirtual(recordID, true);
this.updateRecordContext(recordID, {
ignore_warning: true,
not_onchange: true,
});
resolve({
record: this.get(recordID),
params: params,
});
return this._makeDefaultRecord(list.model, params).then(function(recordID) {
self.setPureVirtual(recordID, true);
self.updateRecordContext(recordID, {
ignore_warning: true,
not_onchange: true,
shadow: true,
});
return {
record: self.get(recordID),
params: params,
};
});
},
@ -319,16 +339,29 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
* @override
*/
_performOnChange: function(record) {
if (record && record.context && record.context.ignore_warning) {
const this_mp = _.clone(this);
const super_call = this.trigger_up;
this_mp.trigger_up = function(event_name, data) {
if (event_name === "warning" && data.type === "dialog") {
// Do nothing
return;
}
return super_call.apply(this, arguments);
}.bind(this);
if (record && record.context) {
record.context.not_onchange = false;
var this_mp = _.clone(this);
if (record.context.shadow) {
// Force use 'shadow'
var super__rpc_call = this._rpc;
this_mp._rpc = function(params, options) {
options = options || {};
options.shadow = true;
return super__rpc_call.call(this, params, options);
}.bind(this);
}
if (record.context.ignore_warning) {
var super_trigger_up_call = this.trigger_up;
// Avoid show warnings
this_mp.trigger_up = function(event_name, data) {
if (event_name === "warning" && data.type === "dialog") {
// Do nothing
return;
}
return super_trigger_up_call.apply(this, arguments);
}.bind(this);
}
return this._super.apply(this_mp, arguments);
}
return this._super.apply(this, arguments);
@ -343,17 +376,94 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
*/
_applyOnChange: function(values, record) {
if (!this.exists(record.id)) {
return Promise.reject();
return $.Deferred().reject();
}
return this._super.apply(this, arguments);
},
/**
* Allow add multiple records at the same time
*
* @override
*/
_applyX2ManyChange: function(record, fieldName, command) {
if (command.operation === "ADD_MULTIPLE") {
var self = this;
var localID =
(record._changes && record._changes[fieldName]) ||
record.data[fieldName];
var list = this.localData[localID];
list._changes = list._changes || [];
// For now, we are in the context of a one2many field
// the command should look like this:
// { operation: 'ADD', ids: [id0,id1,id2,...] }
// The corresponding record may contain value for fields that
// are unknown in the list (e.g. fields that are in the
// subrecord form view but not in the kanban or list view), so
// to ensure that onchanges are correctly handled, we extend the
// list's fields with those in the created record
_.each(command.ids, function(id) {
var newRecord = self.localData[id];
_.defaults(list.fields, newRecord.fields);
_.defaults(list.fieldsInfo, newRecord.fieldsInfo);
newRecord.fields = list.fields;
newRecord.fieldsInfo = list.fieldsInfo;
newRecord.viewType = list.viewType;
list._cache[newRecord.res_id] = newRecord.id;
list._changes.push(command);
});
}
return this._super.apply(this, arguments);
},
/**
* This is necessary to avoid calculate onchanges that
* affects to order_line.
*
* @override
*/
_buildOnchangeSpecs: function(record) {
var specs = this._super.apply(this, arguments);
// This is necessary to improve the performance
if (record.model === "sale.order" && specs) {
// Its a change from product picker?
// WORKAROUND: Done in this way to reutilice odoo methods
var need_clean = false;
var parent_controller = this.getParent();
if (
parent_controller &&
parent_controller.renderer &&
!_.isEmpty(parent_controller.renderer.allFieldWidgets)
) {
var order_line_widget = _.find(
parent_controller.renderer.allFieldWidgets[
parent_controller.handle
],
{name: "order_line"}
);
need_clean =
order_line_widget instanceof FieldOne2ManyProductPicker &&
!_.isEmpty(this.localData[record.data.order_line]);
}
if (need_clean) {
var new_specs = _.clone(specs);
for (var key in specs) {
if (key.startsWith("order_line.")) {
delete new_specs[key];
}
}
return new_specs;
}
}
return specs;
},
/**
* @param {String} recordID
* @returns {Boolean}
*/
hasChanges: function(recordID) {
const record = this.localData[recordID];
var record = this.localData[recordID];
return record && !_.isEmpty(record._changes);
},
@ -368,7 +478,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
* @param {Number} limit
* @param {Number} offset
* @param {Object} context
* @returns {Promise}
* @returns {Deferred}
*/
fetchNameSearchFull: function(
model_fields,
@ -382,6 +492,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
offset,
context
) {
var self = this;
return this._rpc({
model: model,
method: "name_search",
@ -392,9 +503,11 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
limit: this.limit,
context: context || {},
},
}).then(results => {
const record_ids = results.map(item => item[0]);
return this.fetchGenericRecords(
}).then(function(results) {
var record_ids = results.map(function(item) {
return item[0];
});
return self.fetchGenericRecords(
model_fields,
model,
[["id", "in", record_ids]],
@ -416,7 +529,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
* @param {Number} limit
* @param {Number} offset
* @param {Object} context
* @returns {Promise}
* @returns {Deferred}
*/
fetchGenericRecords: function(
model_fields,
@ -428,6 +541,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
offset,
context
) {
var self = this;
return this._rpc({
model: model,
method: "search_read",
@ -437,13 +551,13 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function(require) {
offset: offset,
orderBy: orderby,
kwargs: {context: context},
}).then(result => {
for (const index in result) {
const record = result[index];
for (const fieldName in record) {
const field = model_fields[fieldName];
}).then(function(result) {
for (var index in result) {
var record = result[index];
for (var fieldName in record) {
var field = model_fields[fieldName];
if (field.type !== "many2one") {
record[fieldName] = this._parseServerValue(
record[fieldName] = self._parseServerValue(
model_fields[fieldName],
record[fieldName]
);

View File

@ -0,0 +1,65 @@
// 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.FormController", function(require) {
"use strict";
var FormController = require("web.FormController");
FormController.include({
custom_events: _.extend({}, FormController.prototype.custom_events, {
using_product_picker: "_onUsingProductPicker",
}),
/**
* Disable product picker while saving
*
* @override
*/
saveRecord: function() {
var self = this;
var always = function(changedFields) {
self.renderer.invokeProductPicker(self.handle, "onDocumentSave", false);
return changedFields;
};
this.renderer.invokeProductPicker(this.handle, "onDocumentSave", true);
return this._super
.apply(this, arguments)
.then(always)
.guardedCatch(always);
},
/**
* This is necessary to refresh 'one2many_product_picker' when some 'trigger_refresh_fields' fields changes.
*
* @override
*/
_confirmChange: function(id, fields, e) {
var self = this;
id = id || this.handle;
return this._super.apply(this, arguments).then(function(resetWidgets) {
if (self.renderer) {
self.renderer.invokeProductPicker(
id,
"onDocumentConfirmChanges",
fields,
e
);
}
return resetWidgets;
});
},
/**
* @private
* @param {CustomEvent} ev
*/
_onUsingProductPicker: function(ev) {
this.model.updateRecordContext(this.handle, {
product_picker_field: ev.data.field,
product_picker_product_field: ev.data.product_field,
product_picker_relation: ev.data.relation,
product_picker_relation_field: ev.data.relation_field,
});
},
});
});

View File

@ -0,0 +1,25 @@
// Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.FormRenderer", function(require) {
"use strict";
var FormRenderer = require("web.FormRenderer");
FormRenderer.include({
/**
* Invoke the selected method on all product picer defined
*/
invokeProductPicker: function(recordID, method_name, ...params) {
if (_.isEmpty(this.allFieldWidgets)) {
return;
}
var product_picker_widgets = _.filter(
this.allFieldWidgets[recordID],
function(item) {
return item.attrs.widget === "one2many_product_picker";
}
);
_.invoke(product_picker_widgets, method_name, ...params);
},
});
});

View File

@ -1,22 +1,22 @@
/* global py */
// Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.BasicView", function(require) {
odoo.define("web_widget_one2many_product_picker.FormView", function(require) {
"use strict";
const core = require("web.core");
const pyUtils = require("web.py_utils");
const BasicView = require("web.BasicView");
var core = require("web.core");
var pyUtils = require("web.py_utils");
var FormView = require("web.FormView");
const _t = core._t;
var _t = core._t;
// Add ref to _() -> _t() call
const PY_t = new py.PY_def.fromJSON(function() {
const args = py.PY_parseArgs(arguments, ["str"]);
var PY_t = new py.PY_def.fromJSON(function() {
var args = py.PY_parseArgs(arguments, ["str"]);
return py.str.fromJSON(_t(args.str.toJSON()));
});
BasicView.include({
FormView.include({
/**
* @override
*/

View File

@ -1,4 +1,27 @@
.oe_field_one2many_product_picker {
/** FIX AUTO-SCROLL ON SAMSUNG DEVICES **/
* {
overflow-anchor: none !important;
scroll-snap-stop: normal !important;
overscroll-behavior: unset !important;
scroll-behavior: unset !important;
}
.container,
header,
footer,
.container-fluid,
body,
div,
span,
section {
overflow-anchor: none !important;
scroll-snap-stop: normal !important;
overscroll-behavior: unset !important;
scroll-behavior: unset !important;
}
/** END FIX **/
&.oe_field_one2many_product_picker_maximized {
position: fixed;
top: 0;
@ -15,6 +38,17 @@
}
}
&.disabled {
.badge {
filter: opacity(0.6);
}
pointer-events: none;
> div.loading {
display: inline-block;
}
}
> div {
width: unset !important;
}
@ -30,6 +64,19 @@
}
}
div.loading {
position: absolute;
background-color: white;
padding: 0.5em;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 2px solid gray;
z-index: 2;
box-shadow: 2px 2px 5px;
display: none;
}
.o_cp_buttons {
width: 100%;
@ -57,6 +104,11 @@
margin-right: 0;
}
.o_catch_attention {
animation: none;
outline: 1px solid fade-in(theme-color("warning"), 0.5);
}
.oe_flip_card {
user-select: none;
background-color: transparent;
@ -67,9 +119,17 @@
height $one2many-product-picker-transition-3d-time;
height: $one2many-product-picker-card-min-height;
&.blocked {
.badge {
filter: opacity(0.7);
}
div.loading {
display: inline-block;
}
}
&.disabled {
filter: grayscale(100%);
opacity: 0.5;
filter: grayscale(1);
}
&.oe_flip_card_maximized {
@ -138,7 +198,7 @@
transform-style: preserve-3d;
.img-fluid {
transform: translate(-50%, -50%);
transform: translate(-50%, -50%) !important;
top: 50%;
left: 50%;
z-index: -1;
@ -234,6 +294,7 @@
top: 50%;
left: 0;
transform: translateY(-50%);
width: 100%;
.oe_one2many_product_picker_form_buttons {
display: flex;
@ -263,4 +324,39 @@
}
}
}
/** Extra tools **/
.icon-waiting {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
/* clears the 'X' from Internet Explorer */
input[type="search"]::-ms-clear {
display: none;
width: 0;
height: 0;
}
input[type="search"]::-ms-reveal {
display: none;
width: 0;
height: 0;
}
/* clears the 'X' from Chrome */
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button,
input[type="search"]::-webkit-search-results-decoration {
display: none;
}
/** Modal Price **/
.oe_product_picker_quick_modif_price {
.modal-body {
min-height: 7em;
}
}
}

View File

@ -40,6 +40,12 @@
aria-describedby="btnGroupAddon2"
/>
<div class="input-group-append">
<button
id="product_picker_clear_input"
class='o_fullscreen btn btn-secondary'
>
<i class='fa fa-eraser' />
</button>
<button
id="product_picker_maximize"
class='o_fullscreen btn btn-primary'
@ -88,7 +94,7 @@
</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">
<t t-if="need_notify || need_save || is_saving">
<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
@ -97,15 +103,23 @@
</t>
<t t-elif="!is_virtual">
<span
t-att-data-field="field_map[field_uom_qty]"
t-attf-data-esc="'{{floatFixed(field_map[field_uom_qty])}}' + ' x ' + {{field_map[field_uom]}}.data.display_name"
t-attf-class="badge {{modified &amp;&amp; 'badge-warning' || 'badge-success'}} font-weight-bold rounded-0 mt-0 px-2 py-3 product_qty"
/>
t-attf-class="badge record_saving {{modified &amp;&amp; 'badge-warning' || 'badge-success'}} font-weight-bold rounded-0 mt-0 px-2 py-3 product_qty"
><span
t-att-data-field="field_map[field_uom_qty]"
t-attf-data-esc="str(floatFixed('{{field_map[field_uom_qty]}}'))"
class="lazy_product_qty"
/> x <span
t-att-data-field="field_map[field_uom]"
t-attf-data-esc="obj.{{field_map[field_uom]}}.data.display_name"
/></span>
</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
><i class="fa fa-plus" /><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>
@ -116,12 +130,12 @@
<t t-if="show_discount">
<span
t-att-data-field="field_map.discount"
t-attf-data-esc="str({{field_map.discount}} * -1.0) +'%'"
t-attf-data-esc="str(obj.{{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)}}'"
data-esc="str(monetary('price_unit'))"
class="badge font-weight-bold rounded-0 original_price"
/>
<span
@ -132,7 +146,7 @@
<t t-else="has_onchange">
<span
t-att-data-field="field_map.price_unit"
t-attf-data-esc="'{{monetary('price_unit',true)}}'"
data-esc="str(monetary('price_unit'))"
class="badge badge-info price_unit font-weight-bold rounded-0 mt-1 p-2"
/>
</t>
@ -140,7 +154,7 @@
</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-attf-class="oe_flip_card_front p-0 {{((need_notify || need_save || 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">
@ -150,7 +164,7 @@
<span
data-field="display_name"
class="oe_one2many_product_picker_title position-absolute fixed-bottom p-1"
data-esc="display_name"
data-esc="obj.display_name"
/>
<img
alt=""
@ -175,6 +189,9 @@
</t>
<t t-name="One2ManyProductPicker.FlipCard.Back">
<div class="oe_flip_card_back">
<span class="icon-waiting">
<i class="fa fa-cog fa-spin fa-3x fa-fw" />
</span>
<widget
name="product_picker_quick_create_form"
t-att-compare-key="field_map.product_uom"
@ -183,12 +200,11 @@
</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"
class="oe_flip_container p-1 col-12 col-sm-6 col-md-4 col-lg-4 col-xl-4 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 t-attf-class="oe_flip_card {{!state &amp;&amp; 'disabled' || ''}}">
<div class="loading">LOADING...</div>
<div class="oe_flip_card_inner text-center">
<t t-call="One2ManyProductPicker.FlipCard.Front" />
<t t-call="One2ManyProductPicker.FlipCard.Back" />
@ -207,12 +223,13 @@
>
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body p-0" />
<div class="modal-body p-0">
<span class="icon-waiting">
<i class="fa fa-cog fa-spin fa-3x fa-fw" />
</span>
</div>
<div class="modal-footer">
<button
class="btn btn-success oe_record_change mr-auto"
data-dismiss="modal"
>
<button class="btn btn-success oe_record_change mr-auto d-none">
<i class="fa fa-check" />
</button>
<button

View File

@ -2,22 +2,22 @@
<t t-name="One2ManyProductPicker.QuickCreate.FormButtons">
<div class="oe_one2many_product_picker_form_buttons">
<t t-if="state == 'new'">
<button t-attf-class="btn btn-primary oe_record_add">Add</button>
<button t-attf-class="btn btn-primary oe_record_add w-100">Add</button>
</t>
<t t-elif="state == 'dirty'">
<button class="btn btn-success oe_record_change w-100">
<i class="fa fa-check" />
<button class="btn btn-success oe_record_change w-50">
<i class="fa fa-check" /> Accept
</button>
<button class="btn btn-warning oe_record_discard ml-1">
<i class="fa fa-times" />
<button class="btn btn-warning oe_record_discard ml-1 w-50">
<i class="fa fa-times" /> Discard
</button>
</t>
<t t-else="">
<button class="btn btn-danger oe_record_remove w-100"><i
<button class="btn btn-danger oe_record_remove w-50"><i
class="fa fa-trash"
/> Remove</button>
<button class="btn btn-warning oe_record_discard ml-1">
<i class="fa fa-times" />
<button class="btn btn-warning oe_record_discard ml-1 w-50">
<i class="fa fa-times" /> Discard
</button>
</t>
</div>

View File

@ -53,17 +53,21 @@
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/views/One2ManyProductPicker/quick_modif_price_form.js"
/>
<script
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/views/basic_view.js"
/>
<script
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"
src="/web_widget_one2many_product_picker/static/src/js/views/form_view.js"
/>
<script
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/views/form_controller.js"
/>
<script
type="text/javascript"
src="/web_widget_one2many_product_picker/static/src/js/views/form_renderer.js"
/>
<script
type="text/javascript"