[IMP] web_widget_one2many_product_picker: Refactor code

- fix: Search with wildcard
- fix: Search input events on firefox
- fix: Update lazy qty when use the tiled form
- fix: Handle race conditions
- fix: Return promise when recreate 'zombie' record
- fix: Remove record
- imp: Performance
pull/2074/head
Alexandre D. Díaz 2021-11-08 20:57:52 +01:00
parent 6aa1f8e7b9
commit 898aca2511
26 changed files with 2631 additions and 1539 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,143 @@ 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-04 18:46+0000\n"
"PO-Revision-Date: 2022-03-04 19:49+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\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
#: code:addons/web_widget_one2many_product_picker/models/base.py:0
#, python-format
msgid "Can't write the %s: Duplicated product! (Already in database)"
msgstr "No se puede escribir el %s: Producto duplicado! (Actualmente en la base de datos)"
#. 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"
#~ msgid "HTTP Routing"
#~ msgstr "Ruta HTTP"
#~ msgid "Pricelist Item"
#~ msgstr "Item de Lista de precios"
#~ msgid "Product"
#~ msgstr "Producto"

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-04 18:46+0000\n"
"PO-Revision-Date: 2022-03-04 18:46+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,32 @@ 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
#: code:addons/web_widget_one2many_product_picker/models/base.py:0
#, python-format
msgid "Can't write the %s: Duplicated product! (Already in database)"
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 +81,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,7 +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)
* all_domain > The domain used in 'All' section ([] by default)
* auto_save_delay > The time (in milliseconds) to wait after the last interaction before launching the autosave (1500 by default)
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
@ -53,6 +53,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

@ -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"