[FIX] web_widget_one2many_product_picker: Subtotal with discounts

pull/1858/head
Alexandre D. Díaz 2020-12-09 15:57:54 +01:00
parent f36b75005d
commit 9fa7deb549
14 changed files with 794 additions and 842 deletions

View File

@ -175,87 +175,6 @@ Other example for 'purchase.order.line' fields:
Usage Usage
===== =====
You need to define the view fields. The view must be of ``form`` type.
This is an example that uses the 'sale.order.line' fields:
.. code:: xml
<field
name="order_line"
attrs="{'readonly': [('state', 'in', ('done','cancel'))]}"
nolabel="1"
mode="form"
widget="one2many_product_picker"
options="{'search': [{'name': 'Test', 'domain': [['name', 'ilike', '$search']]}] ,'edit_discount': True, 'show_discount': True, 'groups': [{'name': 'desk', 'string': _('Desks'), 'domain': [('name', 'ilike', '%desk%')], 'order': [{'name': 'id', 'asc': true}]}, {'name': 'chair', 'string': _('Chairs'), 'domain': [('name', 'ilike', '%chair%')]}]}"
>
<form>
<field name="state" invisible="1" />
<field name="display_type" invisible="1" />
<field name="currency_id" invisible="1" />
<field name="discount" widget="numeric_step" options="{'max': 100}" invisible="1"/>
<field name="price_unit" widget="numeric_step" invisible="1"/>
<field name="name" invisible="1" />
<field name="product_id" invisible="1" />
<field name="order_id" invisible="1"/>
<field name="product_uom_qty" class="mb-1" widget="numeric_step" context="{
'partner_id': parent.partner_id,
'quantity': product_uom_qty,
'pricelist': parent.pricelist_id,
'uom': product_uom,
'company_id': parent.company_id
}" />
<field name="product_uom" force_save="1" attrs="{
'readonly': [('state', 'in', ('sale','done', 'cancel'))],
'required': [('display_type', '=', False)],
}" context="{'company_id': parent.company_id}" class="mb-2" options="{'no_open': True, 'no_create': True, 'no_edit': True}" />
</form>
</field>
Other example for 'purchase.order.line' fields:
.. code:: xml
<field
name="order_line"
attrs="{'readonly': [('state', 'in', ('done','cancel'))]}"
nolabel="1"
widget="one2many_product_picker"
mode="form"
options="{'search': [{'name': _('Name'), 'domain': [['name', 'ilike', '$search']]}, {'name': _('Price'), 'domain': [['list_price', '=', $number_search]]}], 'field_map': {'product_uom_qty': 'product_qty'}, 'groups': [{'name': _('Desk'), 'domain': [['name', 'ilike', 'desk']], 'order': {'name': 'id', 'asc': true}}, {'name': _('Chairs'), 'domain': [['name', 'ilike', 'chair']]}]}"
>
<form>
<field name="name" invisible="1" />
<field name="product_id" invisible="1" />
<field name="price_unit" invisible="1" />
<field name="currency_id" invisible="1" />
<field name="order_id" invisible="1" />
<field name="date_planned" class="mb-1" />
<field name="product_qty" class="mb-1" widget="numeric_step" required="1" />
<field name="product_uom" class="mb-2" options="{'no_open': True, 'no_create': True, 'no_edit': True}" />
</form>
</field>
** In this example we don't use 'field_map' option because the default match with the sale.order.line field names.
Default context:
~~~~~~~~~~~~~~~~
The widget sends a defaults context with the 'search_read' request:
* active_search_group_name > Contains the name of the active search group
* 'all' > Is the hard-coded name for the 'All' group
* 'main_lines' > Is the hard-coded name for the 'Lines' group
* active_search_involved_fields > Contains an array of dictionaries with the fields used with the searchbox content
* 'type' > Can be 'text' or 'number'
* 'field' > The field name
* 'oper' > The operator used
Preview: Preview:
~~~~~~~~ ~~~~~~~~

View File

@ -1,84 +1,3 @@
You need to define the view fields. The view must be of ``form`` type.
This is an example that uses the 'sale.order.line' fields:
.. code:: xml
<field
name="order_line"
attrs="{'readonly': [('state', 'in', ('done','cancel'))]}"
nolabel="1"
mode="form"
widget="one2many_product_picker"
options="{'search': [{'name': 'Test', 'domain': [['name', 'ilike', '$search']]}] ,'edit_discount': True, 'show_discount': True, 'groups': [{'name': 'desk', 'string': _('Desks'), 'domain': [('name', 'ilike', '%desk%')], 'order': [{'name': 'id', 'asc': true}]}, {'name': 'chair', 'string': _('Chairs'), 'domain': [('name', 'ilike', '%chair%')]}]}"
>
<form>
<field name="state" invisible="1" />
<field name="display_type" invisible="1" />
<field name="currency_id" invisible="1" />
<field name="discount" widget="numeric_step" options="{'max': 100}" invisible="1"/>
<field name="price_unit" widget="numeric_step" invisible="1"/>
<field name="name" invisible="1" />
<field name="product_id" invisible="1" />
<field name="order_id" invisible="1"/>
<field name="product_uom_qty" class="mb-1" widget="numeric_step" context="{
'partner_id': parent.partner_id,
'quantity': product_uom_qty,
'pricelist': parent.pricelist_id,
'uom': product_uom,
'company_id': parent.company_id
}" />
<field name="product_uom" force_save="1" attrs="{
'readonly': [('state', 'in', ('sale','done', 'cancel'))],
'required': [('display_type', '=', False)],
}" context="{'company_id': parent.company_id}" class="mb-2" options="{'no_open': True, 'no_create': True, 'no_edit': True}" />
</form>
</field>
Other example for 'purchase.order.line' fields:
.. code:: xml
<field
name="order_line"
attrs="{'readonly': [('state', 'in', ('done','cancel'))]}"
nolabel="1"
widget="one2many_product_picker"
mode="form"
options="{'search': [{'name': _('Name'), 'domain': [['name', 'ilike', '$search']]}, {'name': _('Price'), 'domain': [['list_price', '=', $number_search]]}], 'field_map': {'product_uom_qty': 'product_qty'}, 'groups': [{'name': _('Desk'), 'domain': [['name', 'ilike', 'desk']], 'order': {'name': 'id', 'asc': true}}, {'name': _('Chairs'), 'domain': [['name', 'ilike', 'chair']]}]}"
>
<form>
<field name="name" invisible="1" />
<field name="product_id" invisible="1" />
<field name="price_unit" invisible="1" />
<field name="currency_id" invisible="1" />
<field name="order_id" invisible="1" />
<field name="date_planned" class="mb-1" />
<field name="product_qty" class="mb-1" widget="numeric_step" required="1" />
<field name="product_uom" class="mb-2" options="{'no_open': True, 'no_create': True, 'no_edit': True}" />
</form>
</field>
** In this example we don't use 'field_map' option because the default match with the sale.order.line field names.
Default context:
~~~~~~~~~~~~~~~~
The widget sends a defaults context with the 'search_read' request:
* active_search_group_name > Contains the name of the active search group
* 'all' > Is the hard-coded name for the 'All' group
* 'main_lines' > Is the hard-coded name for the 'Lines' group
* active_search_involved_fields > Contains an array of dictionaries with the fields used with the searchbox content
* 'type' > Can be 'text' or 'number'
* 'field' > The field name
* 'oper' > The operator used
Preview: Preview:
~~~~~~~~ ~~~~~~~~

View File

@ -372,38 +372,37 @@ ul.auto-toc {
<p><strong>Table of contents</strong></p> <p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents"> <div class="contents local topic" id="contents">
<ul class="simple"> <ul class="simple">
<li><a class="reference internal" href="#installation" id="id2">Installation</a></li> <li><a class="reference internal" href="#installation" id="id1">Installation</a></li>
<li><a class="reference internal" href="#configuration" id="id3">Configuration</a><ul> <li><a class="reference internal" href="#configuration" id="id2">Configuration</a><ul>
<li><a class="reference internal" href="#widget-options" id="id4">Widget options:</a></li> <li><a class="reference internal" href="#widget-options" id="id3">Widget options:</a></li>
<li><a class="reference internal" href="#default-context" id="id5">Default context:</a></li> <li><a class="reference internal" href="#default-context" id="id4">Default context:</a></li>
<li><a class="reference internal" href="#examples" id="id6">Examples:</a></li> <li><a class="reference internal" href="#examples" id="id5">Examples:</a></li>
</ul> </ul>
</li> </li>
<li><a class="reference internal" href="#usage" id="id7">Usage</a><ul> <li><a class="reference internal" href="#usage" id="id6">Usage</a><ul>
<li><a class="reference internal" href="#id1" id="id8">Default context:</a></li> <li><a class="reference internal" href="#preview" id="id7">Preview:</a></li>
<li><a class="reference internal" href="#preview" id="id9">Preview:</a></li>
</ul> </ul>
</li> </li>
<li><a class="reference internal" href="#known-issues-roadmap" id="id10">Known issues / Roadmap</a></li> <li><a class="reference internal" href="#known-issues-roadmap" id="id8">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id11">Bug Tracker</a></li> <li><a class="reference internal" href="#bug-tracker" id="id9">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id12">Credits</a><ul> <li><a class="reference internal" href="#credits" id="id10">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id13">Authors</a></li> <li><a class="reference internal" href="#authors" id="id11">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id14">Contributors</a></li> <li><a class="reference internal" href="#contributors" id="id12">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id15">Maintainers</a></li> <li><a class="reference internal" href="#maintainers" id="id13">Maintainers</a></li>
</ul> </ul>
</li> </li>
</ul> </ul>
</div> </div>
<div class="section" id="installation"> <div class="section" id="installation">
<h1><a class="toc-backref" href="#id2">Installation</a></h1> <h1><a class="toc-backref" href="#id1">Installation</a></h1>
<p>Its advisable to install web_widget_numeric_step to have a better usability on touch screens.</p> <p>Its advisable to install web_widget_numeric_step to have a better usability on touch screens.</p>
</div> </div>
<div class="section" id="configuration"> <div class="section" id="configuration">
<h1><a class="toc-backref" href="#id3">Configuration</a></h1> <h1><a class="toc-backref" href="#id2">Configuration</a></h1>
<p>Create or edit a new view and use the new widget called one2many_product_picker. <p>Create or edit a new view and use the new widget called one2many_product_picker.
You need to define the view fields. The view must be of <tt class="docutils literal">form</tt> type.</p> You need to define the view fields. The view must be of <tt class="docutils literal">form</tt> type.</p>
<div class="section" id="widget-options"> <div class="section" id="widget-options">
<h2><a class="toc-backref" href="#id4">Widget options:</a></h2> <h2><a class="toc-backref" href="#id3">Widget options:</a></h2>
<ul> <ul>
<li><p class="first">product_per_page &gt; Integer -&gt; Used to control the load more behaviour (16 by default)</p> <li><p class="first">product_per_page &gt; Integer -&gt; Used to control the load more behaviour (16 by default)</p>
</li> </li>
@ -474,7 +473,7 @@ options=&quot;{'search': [{'name': _('Starts With'), 'domain': [('name', '=like'
</pre> </pre>
</div> </div>
<div class="section" id="default-context"> <div class="section" id="default-context">
<h2><a class="toc-backref" href="#id5">Default context:</a></h2> <h2><a class="toc-backref" href="#id4">Default context:</a></h2>
<p>The widget sends a defaults context with the search_read request:</p> <p>The widget sends a defaults context with the search_read request:</p>
<blockquote> <blockquote>
<ul> <ul>
@ -499,7 +498,7 @@ options=&quot;{'search': [{'name': _('Starts With'), 'domain': [('name', '=like'
</blockquote> </blockquote>
</div> </div>
<div class="section" id="examples"> <div class="section" id="examples">
<h2><a class="toc-backref" href="#id6">Examples:</a></h2> <h2><a class="toc-backref" href="#id5">Examples:</a></h2>
<p>This is an example that uses the sale.order.line fields:</p> <p>This is an example that uses the sale.order.line fields:</p>
<pre class="code xml literal-block"> <pre class="code xml literal-block">
<span class="nt">&lt;field</span> <span class="nt">&lt;field</span>
@ -559,105 +558,23 @@ options=&quot;{'search': [{'name': _('Starts With'), 'domain': [('name', '=like'
</div> </div>
</div> </div>
<div class="section" id="usage"> <div class="section" id="usage">
<h1><a class="toc-backref" href="#id7">Usage</a></h1> <h1><a class="toc-backref" href="#id6">Usage</a></h1>
<p>You need to define the view fields. The view must be of <tt class="docutils literal">form</tt> type.
This is an example that uses the sale.order.line fields:</p>
<pre class="code xml literal-block">
<span class="nt">&lt;field</span>
<span class="na">name=</span><span class="s">&quot;order_line&quot;</span>
<span class="na">attrs=</span><span class="s">&quot;{'readonly': [('state', 'in', ('done','cancel'))]}&quot;</span>
<span class="na">nolabel=</span><span class="s">&quot;1&quot;</span>
<span class="na">mode=</span><span class="s">&quot;form&quot;</span>
<span class="na">widget=</span><span class="s">&quot;one2many_product_picker&quot;</span>
<span class="na">options=</span><span class="s">&quot;{'search': [{'name': 'Test', 'domain': [['name', 'ilike', '$search']]}] ,'edit_discount': True, 'show_discount': True, 'groups': [{'name': 'desk', 'string': _('Desks'), 'domain': [('name', 'ilike', '%desk%')], 'order': [{'name': 'id', 'asc': true}]}, {'name': 'chair', 'string': _('Chairs'), 'domain': [('name', 'ilike', '%chair%')]}]}&quot;</span>
<span class="nt">&gt;</span>
<span class="nt">&lt;form&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;state&quot;</span> <span class="na">invisible=</span><span class="s">&quot;1&quot;</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;display_type&quot;</span> <span class="na">invisible=</span><span class="s">&quot;1&quot;</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;currency_id&quot;</span> <span class="na">invisible=</span><span class="s">&quot;1&quot;</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;discount&quot;</span> <span class="na">widget=</span><span class="s">&quot;numeric_step&quot;</span> <span class="na">options=</span><span class="s">&quot;{'max': 100}&quot;</span> <span class="na">invisible=</span><span class="s">&quot;1&quot;</span><span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;price_unit&quot;</span> <span class="na">widget=</span><span class="s">&quot;numeric_step&quot;</span> <span class="na">invisible=</span><span class="s">&quot;1&quot;</span><span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;name&quot;</span> <span class="na">invisible=</span><span class="s">&quot;1&quot;</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;product_id&quot;</span> <span class="na">invisible=</span><span class="s">&quot;1&quot;</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;order_id&quot;</span> <span class="na">invisible=</span><span class="s">&quot;1&quot;</span><span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;product_uom_qty&quot;</span> <span class="na">class=</span><span class="s">&quot;mb-1&quot;</span> <span class="na">widget=</span><span class="s">&quot;numeric_step&quot;</span> <span class="na">context=</span><span class="s">&quot;{
'partner_id': parent.partner_id,
'quantity': product_uom_qty,
'pricelist': parent.pricelist_id,
'uom': product_uom,
'company_id': parent.company_id
}&quot;</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;product_uom&quot;</span> <span class="na">force_save=</span><span class="s">&quot;1&quot;</span> <span class="na">attrs=</span><span class="s">&quot;{
'readonly': [('state', 'in', ('sale','done', 'cancel'))],
'required': [('display_type', '=', False)],
}&quot;</span> <span class="na">context=</span><span class="s">&quot;{'company_id': parent.company_id}&quot;</span> <span class="na">class=</span><span class="s">&quot;mb-2&quot;</span> <span class="na">options=</span><span class="s">&quot;{'no_open': True, 'no_create': True, 'no_edit': True}&quot;</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;/form&gt;</span>
<span class="nt">&lt;/field&gt;</span>
</pre>
<p>Other example for purchase.order.line fields:</p>
<pre class="code xml literal-block">
<span class="nt">&lt;field</span>
<span class="na">name=</span><span class="s">&quot;order_line&quot;</span>
<span class="na">attrs=</span><span class="s">&quot;{'readonly': [('state', 'in', ('done','cancel'))]}&quot;</span>
<span class="na">nolabel=</span><span class="s">&quot;1&quot;</span>
<span class="na">widget=</span><span class="s">&quot;one2many_product_picker&quot;</span>
<span class="na">mode=</span><span class="s">&quot;form&quot;</span>
<span class="na">options=</span><span class="s">&quot;{'search': [{'name': _('Name'), 'domain': [['name', 'ilike', '$search']]}, {'name': _('Price'), 'domain': [['list_price', '=', $number_search]]}], 'field_map': {'product_uom_qty': 'product_qty'}, 'groups': [{'name': _('Desk'), 'domain': [['name', 'ilike', 'desk']], 'order': {'name': 'id', 'asc': true}}, {'name': _('Chairs'), 'domain': [['name', 'ilike', 'chair']]}]}&quot;</span>
<span class="nt">&gt;</span>
<span class="nt">&lt;form&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;name&quot;</span> <span class="na">invisible=</span><span class="s">&quot;1&quot;</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;product_id&quot;</span> <span class="na">invisible=</span><span class="s">&quot;1&quot;</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;price_unit&quot;</span> <span class="na">invisible=</span><span class="s">&quot;1&quot;</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;currency_id&quot;</span> <span class="na">invisible=</span><span class="s">&quot;1&quot;</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;order_id&quot;</span> <span class="na">invisible=</span><span class="s">&quot;1&quot;</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;date_planned&quot;</span> <span class="na">class=</span><span class="s">&quot;mb-1&quot;</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;product_qty&quot;</span> <span class="na">class=</span><span class="s">&quot;mb-1&quot;</span> <span class="na">widget=</span><span class="s">&quot;numeric_step&quot;</span> <span class="na">required=</span><span class="s">&quot;1&quot;</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;field</span> <span class="na">name=</span><span class="s">&quot;product_uom&quot;</span> <span class="na">class=</span><span class="s">&quot;mb-2&quot;</span> <span class="na">options=</span><span class="s">&quot;{'no_open': True, 'no_create': True, 'no_edit': True}&quot;</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;/form&gt;</span>
<span class="nt">&lt;/field&gt;</span>
</pre>
<p>** In this example we dont use field_map option because the default match with the sale.order.line field names.</p>
<div class="section" id="id1">
<h2><a class="toc-backref" href="#id8">Default context:</a></h2>
<p>The widget sends a defaults context with the search_read request:</p>
<blockquote>
<ul>
<li><p class="first">active_search_group_name &gt; Contains the name of the active search group</p>
<blockquote>
<ul class="simple">
<li>all &gt; Is the hard-coded name for the All group</li>
<li>main_lines &gt; Is the hard-coded name for the Lines group</li>
</ul>
</blockquote>
</li>
<li><p class="first">active_search_involved_fields &gt; Contains an array of dictionaries with the fields used with the searchbox content</p>
<blockquote>
<ul class="simple">
<li>type &gt; Can be text or number</li>
<li>field &gt; The field name</li>
<li>oper &gt; The operator used</li>
</ul>
</blockquote>
</li>
</ul>
</blockquote>
</div>
<div class="section" id="preview"> <div class="section" id="preview">
<h2><a class="toc-backref" href="#id9">Preview:</a></h2> <h2><a class="toc-backref" href="#id7">Preview:</a></h2>
<blockquote> <blockquote>
<img alt="https://raw.githubusercontent.com/OCA/web/12.0/web_widget_one2many_product_picker/static/img/product_picker.gif" src="https://raw.githubusercontent.com/OCA/web/12.0/web_widget_one2many_product_picker/static/img/product_picker.gif" /> <img alt="https://raw.githubusercontent.com/OCA/web/12.0/web_widget_one2many_product_picker/static/img/product_picker.gif" src="https://raw.githubusercontent.com/OCA/web/12.0/web_widget_one2many_product_picker/static/img/product_picker.gif" />
</blockquote> </blockquote>
</div> </div>
</div> </div>
<div class="section" id="known-issues-roadmap"> <div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#id10">Known issues / Roadmap</a></h1> <h1><a class="toc-backref" href="#id8">Known issues / Roadmap</a></h1>
<ul class="simple"> <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>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>
</ul> </ul>
</div> </div>
<div class="section" id="bug-tracker"> <div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id11">Bug Tracker</a></h1> <h1><a class="toc-backref" href="#id9">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/web/issues">GitHub Issues</a>. <p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/web/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported. In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed If you spotted it first, help us smashing it by providing a detailed and welcomed
@ -665,15 +582,15 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
<p>Do not contact contributors directly about support or help with technical issues.</p> <p>Do not contact contributors directly about support or help with technical issues.</p>
</div> </div>
<div class="section" id="credits"> <div class="section" id="credits">
<h1><a class="toc-backref" href="#id12">Credits</a></h1> <h1><a class="toc-backref" href="#id10">Credits</a></h1>
<div class="section" id="authors"> <div class="section" id="authors">
<h2><a class="toc-backref" href="#id13">Authors</a></h2> <h2><a class="toc-backref" href="#id11">Authors</a></h2>
<ul class="simple"> <ul class="simple">
<li>Tecnativa</li> <li>Tecnativa</li>
</ul> </ul>
</div> </div>
<div class="section" id="contributors"> <div class="section" id="contributors">
<h2><a class="toc-backref" href="#id14">Contributors</a></h2> <h2><a class="toc-backref" href="#id12">Contributors</a></h2>
<ul> <ul>
<li><p class="first"><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:</p> <li><p class="first"><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:</p>
<blockquote> <blockquote>
@ -687,7 +604,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
</ul> </ul>
</div> </div>
<div class="section" id="maintainers"> <div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id15">Maintainers</a></h2> <h2><a class="toc-backref" href="#id13">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p> <p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a> <a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose <p>OCA, or the Odoo Community Association, is a nonprofit organization whose

View File

@ -3,6 +3,8 @@
odoo.define("web_widget_one2many_product_picker.tools", function ( odoo.define("web_widget_one2many_product_picker.tools", function (
require require
) { ) {
"use strict";
var field_utils = require("web.field_utils"); var field_utils = require("web.field_utils");
/** /**
@ -10,18 +12,21 @@ odoo.define("web_widget_one2many_product_picker.tools", function (
* *
* @param {Number} price * @param {Number} price
* @param {Number} discount * @param {Number} discount
* @returns {Number}
*/ */
function priceReduce (price, discount) { function priceReduce (price, discount) {
return price * (1.0 - discount / 100.0); return price * (1.0 - discount / 100.0);
}; }
/** /**
* Print formatted price using the 'currency_field' * Print formatted price using the 'currency_field'
* info in 'data'. * info in 'data'.
* *
* @param {Number} value * @param {Number} value
* @param {Object} field_info,
* @param {String} currency_field * @param {String} currency_field
* @param {Object} data * @param {Object} data
* @returns {String}
*/ */
function monetary (value, field_info, currency_field, data) { function monetary (value, field_info, currency_field, data) {
return field_utils.format.monetary( return field_utils.format.monetary(
@ -32,7 +37,7 @@ odoo.define("web_widget_one2many_product_picker.tools", function (
currency_field: currency_field, currency_field: currency_field,
field_digits: true, field_digits: true,
}); });
}; }
return { return {
monetary: monetary, monetary: monetary,

View File

@ -1,12 +1,16 @@
// Copyright 2020 Tecnativa - Alexandre Díaz // Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", function (require) { odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", function (
require
) {
"use strict"; "use strict";
var core = require("web.core"); var core = require("web.core");
var Widget = require("web.Widget"); var Widget = require("web.Widget");
var widgetRegistry = require("web.widget_registry"); var widgetRegistry = require("web.widget_registry");
var ProductPickerQuickCreateFormView = require("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView").ProductPickerQuickCreateFormView; var ProductPickerQuickCreateFormView = require(
"web_widget_one2many_product_picker.ProductPickerQuickCreateFormView"
).ProductPickerQuickCreateFormView;
var qweb = core.qweb; var qweb = core.qweb;
@ -42,6 +46,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
this.id = this.state && this.state.id; this.id = this.state && this.state.id;
this.editContext = {}; this.editContext = {};
}, },
/** /**
* @override * @override
*/ */
@ -65,8 +70,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateForm", f
var refinedContext = _.extend( var refinedContext = _.extend(
{}, {},
this.main_state.getContext(), this.main_state.getContext(),
this.nodeContext, this.nodeContext);
);
_.extend(refinedContext, this.editContext); _.extend(refinedContext, this.editContext);
this.formView = new ProductPickerQuickCreateFormView(fieldsView, { this.formView = new ProductPickerQuickCreateFormView(fieldsView, {
context: refinedContext, context: refinedContext,

View File

@ -1,6 +1,8 @@
// Copyright 2020 Tecnativa - Alexandre Díaz // Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView", function (require) { odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView", function (
require
) {
"use strict"; "use strict";
/** /**
@ -17,28 +19,34 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
BasicModel.include({ BasicModel.include({
_applyOnChange: function (values, record, viewType) { _applyOnChange: function (values, record, viewType) {
if ('ignore_onchanges' in record.context) { if ('ignore_onchanges' in record.context) {
for (var field_name of record.context['ignore_onchanges']) { var ignore_changes = record.context.ignore_onchanges;
for (var index in ignore_changes) {
var field_name = ignore_changes[index];
delete values[field_name]; delete values[field_name];
} }
delete record.context['ignore_onchanges']; delete record.context.ignore_onchanges;
} }
return this._super(values, record, viewType); return this._super(values, record, viewType);
}, },
}); });
var ProductPickerQuickCreateFormRenderer = QuickCreateFormView.prototype.config.Renderer.extend( var ProductPickerQuickCreateFormRenderer =
QuickCreateFormView.prototype.config.Renderer.extend(
{ {
/** /**
* @override * @override
*/ */
start: function () { start: function () {
this.$el.addClass("oe_one2many_product_picker_form_view o_xxs_form_view"); this.$el.addClass(
"oe_one2many_product_picker_form_view o_xxs_form_view");
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
}, },
} }
); );
var ProductPickerQuickCreateFormController = QuickCreateFormView.prototype.config.Controller.extend( var ProductPickerQuickCreateFormController =
QuickCreateFormView.prototype.config.Controller.extend(
{ {
events: _.extend({}, QuickCreateFormView.prototype.events, { events: _.extend({}, QuickCreateFormView.prototype.events, {
"click .oe_record_add": "_onClickAdd", "click .oe_record_add": "_onClickAdd",
@ -81,9 +89,11 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
} }
} }
} }
this.$el.find(".oe_one2many_product_picker_form_buttons").remove(); this.$el.find(
".oe_one2many_product_picker_form_buttons").remove();
this.$el.find(".o_form_view").append( this.$el.find(".o_form_view").append(
qweb.render("One2ManyProductPicker.QuickCreate.FormButtons", { qweb.render(
"One2ManyProductPicker.QuickCreate.FormButtons", {
state: state, state: state,
}) })
); );
@ -93,7 +103,9 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* @private * @private
*/ */
_disableQuickCreate: function () { _disableQuickCreate: function () {
this._disabled = true; // ensures that the record won't be created twice
// Ensures that the record won't be created twice
this._disabled = true;
this.$el.addClass("o_disabled"); this.$el.addClass("o_disabled");
this.$("input:not(:disabled)") this.$("input:not(:disabled)")
.addClass("o_temporarily_disabled") .addClass("o_temporarily_disabled")
@ -104,7 +116,9 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
* @private * @private
*/ */
_enableQuickCreate: function () { _enableQuickCreate: function () {
this._disabled = false; // allows to create again
// Allows to create again
this._disabled = false;
this.$el.removeClass("o_disabled"); this.$el.removeClass("o_disabled");
this.$("input.o_temporarily_disabled") this.$("input.o_temporarily_disabled")
.removeClass("o_temporarily_disabled") .removeClass("o_temporarily_disabled")
@ -114,6 +128,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
/** /**
* @private * @private
* @param {Array[String]} fields_changed * @param {Array[String]} fields_changed
* @returns {Boolean}
*/ */
_needReloadCard: function (fields_changed) { _needReloadCard: function (fields_changed) {
for (var index in fields_changed) { for (var index in fields_changed) {
@ -145,20 +160,23 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
} }
var reload_values = { var reload_values = {
compareValue: new_value, compareValue: new_value,
} };
var record = this.model.get(this.handle); var record = this.model.get(this.handle);
if (!('base_record_id' in record.context)) { if (!('base_record_id' in record.context)) {
var old_value = record.data[this.compareKey]; var old_value = record.data[this.compareKey];
if (typeof old_value === 'object') { if (typeof old_value === 'object') {
old_value = old_value.data.id; old_value = old_value.data.id;
} }
reload_values['baseRecordID'] = record.id; reload_values.baseRecordID = record.id;
reload_values['baseRecordResID'] = record.ref; reload_values.baseRecordResID = record.ref;
reload_values['baseRecordCompareValue'] = old_value; reload_values.baseRecordCompareValue = old_value;
} else { } else {
reload_values['baseRecordID'] = record.context.base_record_id; reload_values.baseRecordID =
reload_values['baseRecordResID'] = record.context.base_record_res_id; record.context.base_record_id;
reload_values['baseRecordCompareValue'] = record.context.base_record_compare_value; reload_values.baseRecordResID =
record.context.base_record_res_id;
reload_values.baseRecordCompareValue =
record.context.base_record_compare_value;
} }
this.trigger_up("reload_view", reload_values); this.trigger_up("reload_view", reload_values);
@ -170,7 +188,9 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
if (this.model.isPureVirtual(this.handle)) { if (this.model.isPureVirtual(this.handle)) {
this.model.unsetDirty(this.handle); this.model.unsetDirty(this.handle);
} }
this.model.updateRecordContext(this.handle, {has_changes_confirmed: false}); this.model.updateRecordContext(this.handle, {
has_changes_confirmed: false,
});
this.trigger_up("quick_record_updated", { this.trigger_up("quick_record_updated", {
changes: ev.data.changes, changes: ev.data.changes,
}); });
@ -184,7 +204,8 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
*/ */
_add: function () { _add: function () {
if (this._disabled) { if (this._disabled) {
// don't do anything if we are already creating a record
// Don't do anything if we are already creating a record
return $.Deferred(); return $.Deferred();
} }
var self = this; var self = this;
@ -211,7 +232,9 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
*/ */
_onClickAdd: function (ev) { _onClickAdd: function (ev) {
ev.stopPropagation(); ev.stopPropagation();
this.model.updateRecordContext(this.handle, {has_changes_confirmed: true}); this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true,
});
this._add(); this._add();
}, },
@ -221,7 +244,9 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
*/ */
_onClickRemove: function (ev) { _onClickRemove: function (ev) {
ev.stopPropagation(); ev.stopPropagation();
this.trigger_up("list_record_remove", {id: this.renderer.state.id}); this.trigger_up("list_record_remove", {
id: this.renderer.state.id,
});
}, },
/** /**
@ -230,7 +255,9 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
*/ */
_onClickChange: function (ev) { _onClickChange: function (ev) {
ev.stopPropagation(); ev.stopPropagation();
this.model.updateRecordContext(this.handle, {has_changes_confirmed: true}); this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true,
});
var record = this.model.get(this.handle); var record = this.model.get(this.handle);
this.trigger_up("update_quick_record", { this.trigger_up("update_quick_record", {
id: record.id, id: record.id,
@ -239,6 +266,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickCreateFormView
this.model.unsetDirty(this.handle); this.model.unsetDirty(this.handle);
this._updateButtons(); this._updateButtons();
}, },
/** /**
* @private * @private
* @param {MouseEvent} ev * @param {MouseEvent} ev

View File

@ -1,11 +1,15 @@
// Copyright 2020 Tecnativa - Alexandre Díaz // Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm", function (require) { odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm", function (
require
) {
"use strict"; "use strict";
var core = require("web.core"); var core = require("web.core");
var Widget = require("web.Widget"); var Widget = require("web.Widget");
var ProductPickerQuickModifPriceFormView = require("web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView").ProductPickerQuickModifPriceFormView; var ProductPickerQuickModifPriceFormView = require(
"web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView"
).ProductPickerQuickModifPriceFormView;
var qweb = core.qweb; var qweb = core.qweb;
@ -101,7 +105,8 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
*/ */
_generateFormArch: function () { _generateFormArch: function () {
var wanted_field_states = this._getWantedFieldState(); var wanted_field_states = this._getWantedFieldState();
var template = "<templates><t t-name='One2ManyProductPicker.QuickModifPrice.Form'>"; var template =
"<templates><t t-name='One2ManyProductPicker.QuickModifPrice.Form'>";
template += this.basicFieldParams.field.views.form.arch; template += this.basicFieldParams.field.views.form.arch;
template += "</t></templates>"; template += "</t></templates>";
qweb.add_template(template); qweb.add_template(template);
@ -115,7 +120,8 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
for (var index in field_names) { for (var index in field_names) {
var field_name = field_names[index]; var field_name = field_names[index];
var $field = $arch.find("field[name='"+field_name+"']"); var $field = $arch.find("field[name='"+field_name+"']");
var modifiers = $field.attr("modifiers") ? JSON.parse($field.attr("modifiers")) : {}; var modifiers =
$field.attr("modifiers") ? JSON.parse($field.attr("modifiers")) : {};
modifiers.invisible = false; modifiers.invisible = false;
modifiers.readonly = wanted_field_states[field_name]; modifiers.readonly = wanted_field_states[field_name];
$field.attr("modifiers", JSON.stringify(modifiers)); $field.attr("modifiers", JSON.stringify(modifiers));

View File

@ -1,6 +1,8 @@
// Copyright 2020 Tecnativa - Alexandre Díaz // Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView", function (require) { odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceFormView", function (
require
) {
"use strict"; "use strict";
/** /**
@ -14,14 +16,16 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
var qweb = core.qweb; var qweb = core.qweb;
var ProductPickerQuickModifPriceFormRenderer = QuickCreateFormView.prototype.config.Renderer.extend( var ProductPickerQuickModifPriceFormRenderer =
QuickCreateFormView.prototype.config.Renderer.extend(
{ {
/** /**
* @override * @override
*/ */
start: function () { start: function () {
var self = this; var self = this;
this.$el.addClass("oe_one2many_product_picker_form_view o_xxs_form_view"); this.$el.addClass(
"oe_one2many_product_picker_form_view o_xxs_form_view");
return this._super.apply(this, arguments).then(function () { return this._super.apply(this, arguments).then(function () {
self._appendPrice(); self._appendPrice();
self._appendButtons(); self._appendButtons();
@ -32,9 +36,11 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
* @private * @private
*/ */
_appendButtons: function () { _appendButtons: function () {
this.$el.find(".oe_one2many_product_picker_form_buttons").remove(); this.$el.find(
".oe_one2many_product_picker_form_buttons").remove();
this.$el.append( this.$el.append(
qweb.render("One2ManyProductPicker.QuickModifPrice.FormButtons", { qweb.render(
"One2ManyProductPicker.QuickModifPrice.FormButtons", {
mode: this.mode, mode: this.mode,
}) })
); );
@ -48,12 +54,12 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
this.$el.append( this.$el.append(
qweb.render("One2ManyProductPicker.QuickModifPrice.Price") qweb.render("One2ManyProductPicker.QuickModifPrice.Price")
); );
} },
} }
); );
var ProductPickerQuickModifPriceFormController = QuickCreateFormView.prototype.config.Controller.extend( var ProductPickerQuickModifPriceFormController =
QuickCreateFormView.prototype.config.Controller.extend(
{ {
events: _.extend({}, QuickCreateFormView.prototype.events, { events: _.extend({}, QuickCreateFormView.prototype.events, {
"click .oe_record_change": "_onClickChange", "click .oe_record_change": "_onClickChange",
@ -84,7 +90,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
/** /**
* @override * @override
*/ */
_onFieldChanged: function (ev) { _onFieldChanged: function () {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this._updatePrice(); this._updatePrice();
}, },
@ -94,7 +100,9 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
*/ */
_updatePrice: function () { _updatePrice: function () {
var record = this.model.get(this.handle); var record = this.model.get(this.handle);
var price_reduce = tools.priceReduce(record.data[this.fieldMap.price_unit], record.data[this.fieldMap.discount]); var price_reduce = tools.priceReduce(
record.data[this.fieldMap.price_unit],
record.data[this.fieldMap.discount]);
this.renderer.$el.find(".oe_price").html( this.renderer.$el.find(".oe_price").html(
tools.monetary( tools.monetary(
price_reduce, price_reduce,
@ -109,7 +117,9 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
* @private * @private
*/ */
_disableQuickCreate: function () { _disableQuickCreate: function () {
this._disabled = true; // ensures that the record won't be created twice
// Ensures that the record won't be created twice
this._disabled = true;
this.$el.addClass("o_disabled"); this.$el.addClass("o_disabled");
this.$("input:not(:disabled)") this.$("input:not(:disabled)")
.addClass("o_temporarily_disabled") .addClass("o_temporarily_disabled")
@ -120,7 +130,9 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
* @private * @private
*/ */
_enableQuickCreate: function () { _enableQuickCreate: function () {
this._disabled = false; // allows to create again
// Allows to create again
this._disabled = false;
this.$el.removeClass("o_disabled"); this.$el.removeClass("o_disabled");
this.$("input.o_temporarily_disabled") this.$("input.o_temporarily_disabled")
.removeClass("o_temporarily_disabled") .removeClass("o_temporarily_disabled")
@ -134,8 +146,11 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
_onClickChange: function (ev) { _onClickChange: function (ev) {
var self = this; var self = this;
ev.stopPropagation(); ev.stopPropagation();
this.model.updateRecordContext(this.handle, {has_changes_confirmed: true}); this.model.updateRecordContext(this.handle, {
has_changes_confirmed: true,
});
var is_virtual = this.model.isPureVirtual(this.handle); var is_virtual = this.model.isPureVirtual(this.handle);
// If is a 'pure virtual' record, save it in the selected list // If is a 'pure virtual' record, save it in the selected list
if (is_virtual) { if (is_virtual) {
if (this.model.isDirty(this.handle)) { if (this.model.isDirty(this.handle)) {
@ -148,6 +163,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
}).then(function () { }).then(function () {
self._enableQuickCreate(); self._enableQuickCreate();
var record = self.model.get(self.handle); var record = self.model.get(self.handle);
self.model.unsetDirty(self.handle);
self.trigger_up("create_quick_record", { self.trigger_up("create_quick_record", {
id: record.id, id: record.id,
}); });
@ -157,6 +173,7 @@ odoo.define("web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm
this.getParent().destroy(); this.getParent().destroy();
} }
} else { } else {
// If is a "normal" record, update it // If is a "normal" record, update it
var record = this.model.get(this.handle); var record = this.model.get(this.handle);
this.trigger_up("update_quick_record", { this.trigger_up("update_quick_record", {

View File

@ -1,3 +1,4 @@
/* global py */
// Copyright 2020 Tecnativa - Alexandre Díaz // Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", function ( odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", function (
@ -9,12 +10,12 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
var Widget = require("web.Widget"); var Widget = require("web.Widget");
var Domain = require("web.Domain"); var Domain = require("web.Domain");
var widgetRegistry = require("web.widget_registry"); var widgetRegistry = require("web.widget_registry");
var core = require("web.core");
var tools = require("web_widget_one2many_product_picker.tools"); var tools = require("web_widget_one2many_product_picker.tools");
var ProductPickerQuickModifPriceForm = require( var ProductPickerQuickModifPriceForm = require(
"web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm"); "web_widget_one2many_product_picker.ProductPickerQuickModifPriceForm");
var qweb = core.qweb; var qweb = core.qweb;
var _t = core._t;
/* This represent a record (a card) */ /* This represent a record (a card) */
var One2ManyProductPickerRecord = Widget.extend({ var One2ManyProductPickerRecord = Widget.extend({
@ -74,7 +75,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
* @override * @override
*/ */
update: function (record) { update: function (record) {
// detach the widgets because the record will empty its $el, which
// Detach the widgets because the record will empty its $el, which
// will remove all event handlers on its descendants, and we want // will remove all event handlers on its descendants, and we want
// to keep those handlers alive as we will re-use these widgets // to keep those handlers alive as we will re-use these widgets
_.invoke(_.pluck(this.subWidgets, "$el"), "detach"); _.invoke(_.pluck(this.subWidgets, "$el"), "detach");
@ -100,8 +102,9 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
* Generates the URL for the given product using the selected field * Generates the URL for the given product using the selected field
* *
* @private * @private
* @param {string} field * @param {Number} product_id
* @returns {string} * @param {String} field_name
* @returns {String}
*/ */
_getImageUrl: function (product_id, field_name) { _getImageUrl: function (product_id, field_name) {
return _.str.sprintf( return _.str.sprintf(
@ -128,8 +131,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
/** /**
* @private * @private
* @param {string} d a stringified domain * @param {String} d a stringified domain
* @returns {boolean} the domain evaluted with the current values * @returns {Boolean} the domain evaluted with the current values
*/ */
_computeDomain: function (d) { _computeDomain: function (d) {
return new Domain(d).compute( return new Domain(d).compute(
@ -160,6 +163,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
* @returns {Object} * @returns {Object}
*/ */
_getQWebContext: function () { _getQWebContext: function () {
// Using directly the 'model record' instead of the state because // Using directly the 'model record' instead of the state because
// the state it's a parsed version of this record that doesn't // the state it's a parsed version of this record that doesn't
// contains the '_virtual' attribute. // contains the '_virtual' attribute.
@ -186,7 +190,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
*/ */
_getInternalVirtualRecordContext: function () { _getInternalVirtualRecordContext: function () {
var context = {}; var context = {};
context["default_" + this.options.basicFieldParams.relation_field] = this.options.basicFieldParams.state.id || null; context["default_" + this.options.basicFieldParams.relation_field] =
this.options.basicFieldParams.state.id || null;
return context; return context;
}, },
@ -199,7 +204,10 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
*/ */
_getInternalVirtualRecordData: function () { _getInternalVirtualRecordData: function () {
var data = {}; var data = {};
data[this.options.fieldMap.product] = {operation: 'ADD', id: this.recordSearch.id}; data[this.options.fieldMap.product] = {
operation: 'ADD',
id: this.recordSearch.id,
};
return data; return data;
}, },
@ -211,9 +219,11 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
*/ */
_generateVirtualState: function (data, context) { _generateVirtualState: function (data, context) {
var model = this.options.basicFieldParams.model; var model = this.options.basicFieldParams.model;
var scontext = _.extend({}, this._getInternalVirtualRecordContext(), context); var scontext = _.extend(
{}, this._getInternalVirtualRecordContext(), context);
var sdata = _.extend({}, this._getInternalVirtualRecordData(), data); var sdata = _.extend({}, this._getInternalVirtualRecordData(), data);
return model.createVirtualRecord(this.options.basicFieldParams.value.id, { return model.createVirtualRecord(
this.options.basicFieldParams.value.id, {
data: sdata, data: sdata,
context: scontext, context: scontext,
}); });
@ -225,7 +235,10 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
_render: function () { _render: function () {
this.defs = []; this.defs = [];
this._replaceElement( this._replaceElement(
qweb.render("One2ManyProductPicker.FlipCard", this._getQWebContext()) qweb.render(
"One2ManyProductPicker.FlipCard",
this._getQWebContext()
)
); );
this.$card = this.$(".oe_flip_card"); this.$card = this.$(".oe_flip_card");
this.$front = this.$(".oe_flip_card_front"); this.$front = this.$(".oe_flip_card_front");
@ -241,6 +254,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
* any, or directly by the formatted value * any, or directly by the formatted value
* *
* @private * @private
* @param {jQueryElement} $container
*/ */
_processWidgetFields: function ($container) { _processWidgetFields: function ($container) {
var self = this; var self = this;
@ -257,13 +271,21 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
// even if it is not specified in the view. // even if it is not specified in the view.
if (field_widget || self.fields[field_name].type === "many2many") { if (field_widget || self.fields[field_name].type === "many2many") {
var widget = self.subWidgets[field_name]; var widget = self.subWidgets[field_name];
if (!widget) { if (widget) {
// a widget already exists for that field, so reset it
// with the new state
widget.reset(self.state);
$field.replaceWith(widget.$el);
} else {
// the widget doesn't exist yet, so instanciate it // the widget doesn't exist yet, so instanciate it
var Widget = self.fieldsInfo[field_name].Widget; var Widget = self.fieldsInfo[field_name].Widget;
if (Widget) { if (Widget) {
widget = self._processWidget($field, field_name, Widget); widget = self._processWidget($field, field_name, Widget);
self.subWidgets[field_name] = widget; self.subWidgets[field_name] = widget;
} else if (config.debug) { } else if (config.debug) {
// the widget is not implemented // the widget is not implemented
$field.replaceWith( $field.replaceWith(
$("<span>", { $("<span>", {
@ -274,11 +296,6 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
}) })
); );
} }
} else {
// a widget already exists for that field, so reset it
// with the new state
widget.reset(self.state);
$field.replaceWith(widget.$el);
} }
} }
}); });
@ -294,6 +311,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
* @returns {Widget} the widget instance * @returns {Widget} the widget instance
*/ */
_processWidget: function ($field, field_name, Widget) { _processWidget: function ($field, field_name, Widget) {
// some field's attrs might be record dependent (they start with // some field's attrs might be record dependent (they start with
// 't-att-') and should thus be evaluated, which is done by qweb // 't-att-') and should thus be evaluated, which is done by qweb
// we here replace those attrs in the dict of attrs of the state // we here replace those attrs in the dict of attrs of the state
@ -301,7 +319,6 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
// field's widgets point of view // field's widgets point of view
// that dict being shared between records, we don't modify it // that dict being shared between records, we don't modify it
// in place // in place
var self = this;
var attrs = Object.create(null); var attrs = Object.create(null);
_.each(this.fieldsInfo[field_name], function (value, key) { _.each(this.fieldsInfo[field_name], function (value, key) {
if (_.str.startsWith(key, "t-att-")) { if (_.str.startsWith(key, "t-att-")) {
@ -310,8 +327,14 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
} }
attrs[key] = value; attrs[key] = value;
}); });
var options = _.extend({}, this.options, {attrs: attrs, data: this.state.data}); var options = _.extend({}, this.options, {
var widget = new Widget(this, field_name, this.getParent().state, options); attrs: attrs,
data: this.state.data,
});
var widget = new Widget(
this, field_name,
this.getParent().state,
options);
var def = widget.replace($field); var def = widget.replace($field);
if (def.state() === "pending") { if (def.state() === "pending") {
this.defs.push(def); this.defs.push(def);
@ -329,8 +352,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
var self = this; var self = this;
$container.find("widget").each(function () { $container.find("widget").each(function () {
var $field = $(this); var $field = $(this);
var Widget = widgetRegistry.get($field.attr("name")); var FieldWidget = widgetRegistry.get($field.attr("name"));
var widget = new Widget(self, { var widget = new FieldWidget(self, {
fieldsInfo: self.fieldsInfo, fieldsInfo: self.fieldsInfo,
fields: self.fields, fields: self.fields,
main_state: self.getParent().state, main_state: self.getParent().state,
@ -346,8 +369,9 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
self.widgets.push(widget); self.widgets.push(widget);
var def = widget var def = widget
._widgetRenderAndInsert(function () {}) ._widgetRenderAndInsert(function () {
.then(function () { // Do nothing
}).then(function () {
widget.$el.addClass("o_widget"); widget.$el.addClass("o_widget");
$field.replaceWith(widget.$el); $field.replaceWith(widget.$el);
}); });
@ -384,7 +408,9 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
var to_find = []; var to_find = [];
if (!_.isEmpty(fields)) { if (!_.isEmpty(fields)) {
to_find = _.map(fields, function(field){ return _.str.sprintf("[data-field=%s]", [field]); }); to_find = _.map(fields, function (field) {
return _.str.sprintf("[data-field=%s]", [field]);
});
} else { } else {
to_find = ["[data-field]"]; to_find = ["[data-field]"];
} }
@ -401,11 +427,13 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
var field_map = this.options.fieldMap; var field_map = this.options.fieldMap;
if (state_data) { if (state_data) {
var has_discount = state_data[field_map.discount] > 0.0; var has_discount = state_data[field_map.discount] > 0.0;
this.$el.find(".original_price,.discount_price").toggleClass("d-none", !has_discount); this.$el.find(".original_price,.discount_price")
.toggleClass("d-none", !has_discount);
if (has_discount) { if (has_discount) {
this.$el.find(".price_unit").html(this._calcPriceReduced()); this.$el.find(".price_unit").html(this._calcPriceReduced());
} else { } else {
this.$el.find(".price_unit").html(this._getMonetaryFieldValue("price_unit")); this.$el.find(".price_unit").html(
this._getMonetaryFieldValue("price_unit"));
} }
} }
} }
@ -420,7 +448,9 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
var field_map = this.options.fieldMap; var field_map = this.options.fieldMap;
var state_data = this.state.data; var state_data = this.state.data;
if (state_data && state_data[field_map.discount]) { if (state_data && state_data[field_map.discount]) {
price_reduce = tools.priceReduce(state_data[field_map.price_unit], state_data[field_map.discount]); price_reduce = tools.priceReduce(
state_data[field_map.price_unit],
state_data[field_map.discount]);
} }
return price_reduce && tools.monetary( return price_reduce && tools.monetary(
price_reduce, price_reduce,
@ -451,7 +481,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
canEditDiscount: this.options.editDiscount, canEditDiscount: this.options.editDiscount,
currencyField: this.options.currencyField, currencyField: this.options.currencyField,
}); });
this.$modifPricePopup = $(qweb.render("One2ManyProductPicker.QuickModifPricePopup")); this.$modifPricePopup = $(
qweb.render("One2ManyProductPicker.QuickModifPricePopup"));
this.$modifPricePopup.appendTo($(".o_main_content")); this.$modifPricePopup.appendTo($(".o_main_content"));
modif_price_form.attachTo(this.$modifPricePopup); modif_price_form.attachTo(this.$modifPricePopup);
}, },
@ -463,12 +494,15 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
* @param {ClickEvent} evt * @param {ClickEvent} evt
*/ */
_onClickFlipCard: function (evt) { _onClickFlipCard: function (evt) {
// Avoid clicks on form elements // Avoid clicks on form elements
if (['INPUT', 'BUTTON', 'A'].indexOf(evt.target.tagName) !== -1) { if (['INPUT', 'BUTTON', 'A'].indexOf(evt.target.tagName) !== -1) {
return; return;
} }
if (!this._clickFlipCardDelayed) { if (!this._clickFlipCardDelayed) {
this._clickFlipCardDelayed = setTimeout(this._onClickDelayedFlipCard.bind(this, evt), this._click_card_delayed_time); this._clickFlipCardDelayed = setTimeout(
this._onClickDelayedFlipCard.bind(this, evt),
this._click_card_delayed_time);
} }
++this._clickFlipCardCount; ++this._clickFlipCardCount;
if (this._clickFlipCardCount >= 2) { if (this._clickFlipCardCount >= 2) {
@ -481,9 +515,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
/** /**
* @private * @private
* @param {MouseEvent} evt
*/ */
_onClickDelayedFlipCard: function (evt) { _onClickDelayedFlipCard: function () {
this._clickFlipCardDelayed = false; this._clickFlipCardDelayed = false;
this._clickFlipCardCount = 0; this._clickFlipCardCount = 0;
@ -504,7 +537,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
$actived_card.removeClass("active"); $actived_card.removeClass("active");
$actived_card.find('.oe_flip_card_front').removeClass("d-none"); $actived_card.find('.oe_flip_card_front').removeClass("d-none");
self.$card.addClass("active"); self.$card.addClass("active");
setTimeout(() => { setTimeout(function () {
self.$('.oe_flip_card_front').addClass("d-none"); self.$('.oe_flip_card_front').addClass("d-none");
}, 200); }, 200);
}); });
@ -517,7 +550,10 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
*/ */
_onDblClickDelayedFlipCard: function (evt) { _onDblClickDelayedFlipCard: function (evt) {
var $target = $(evt.target); var $target = $(evt.target);
if ($target.hasClass('badge_price') || $target.parents('.badge_price').length) { if (
$target.hasClass('badge_price') ||
$target.parents('.badge_price').length
) {
this._openPriceModifier(); this._openPriceModifier();
} else { } else {
var $currentTarget = $(evt.currentTarget); var $currentTarget = $(evt.currentTarget);
@ -540,7 +576,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
var $actived_card = this.$el.parent().find(".active"); var $actived_card = this.$el.parent().find(".active");
if ($actived_card[0] !== $currentTarget[0]) { if ($actived_card[0] !== $currentTarget[0]) {
$actived_card.removeClass("active"); $actived_card.removeClass("active");
$actived_card.find('.oe_flip_card_front').removeClass("d-none"); $actived_card.find('.oe_flip_card_front')
.removeClass("d-none");
} }
var offset = $currentTarget.offset(); var offset = $currentTarget.offset();
$currentTarget.css({ $currentTarget.css({
@ -562,9 +599,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
/** /**
* @private * @private
* @param {CustomEvent} evt
*/ */
_onRestoreFlipCard: function (evt) { _onRestoreFlipCard: function () {
this.$(".oe_flip_card").removeClass("active"); this.$(".oe_flip_card").removeClass("active");
this.$('.oe_flip_card_front').removeClass("d-none"); this.$('.oe_flip_card_front').removeClass("d-none");
}, },
@ -584,8 +620,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRecord", fu
* @private * @private
* @param {CustomEvent} evt * @param {CustomEvent} evt
*/ */
_onQuickRecordUpdated: function (ev) { _onQuickRecordUpdated: function (evt) {
this._processDynamicFields(Object.keys(ev.data.changes)); this._processDynamicFields(Object.keys(evt.data.changes));
this.trigger_up("update_subtotal"); this.trigger_up("update_subtotal");
}, },
}); });

View File

@ -1,11 +1,14 @@
// Copyright 2020 Tecnativa - Alexandre Díaz // Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", function (require) { odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer", function (
require
) {
"use strict"; "use strict";
var core = require("web.core"); var core = require("web.core");
var BasicRenderer = require("web.BasicRenderer"); var BasicRenderer = require("web.BasicRenderer");
var One2ManyProductPickerRecord = require("web_widget_one2many_product_picker.One2ManyProductPickerRecord"); var One2ManyProductPickerRecord = require(
"web_widget_one2many_product_picker.One2ManyProductPickerRecord");
var qweb = core.qweb; var qweb = core.qweb;
@ -14,7 +17,6 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
className: 'oe_one2many_product_picker_view', className: 'oe_one2many_product_picker_view',
events: { events: {
//'scroll': '_lazyOnScrollView',
'click #productPickerLoadMore': '_onClickLoadMore', 'click #productPickerLoadMore': '_onClickLoadMore',
}, },
@ -30,13 +32,16 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
this.recordOptions = _.extend({}, params.record_options, { this.recordOptions = _.extend({}, params.record_options, {
viewType: 'One2ManyProductPicker', viewType: 'One2ManyProductPicker',
}); });
// Workaraound: Odoo initilize this class so we need do this to
// Workaround: Odoo initilize this class so we need do this to
// 'receive' more arguments. // 'receive' more arguments.
this.options = parent.options; this.options = parent.options;
this.mode = parent.mode; this.mode = parent.mode;
this.search_data = parent._searchRecords; this.search_data = parent._searchRecords;
this.last_search_data_count = parent._lastSearchRecordsCount; this.last_search_data_count = parent._lastSearchRecordsCount;
this._lazyOnScrollView = _.debounce(this._onScrollView.bind(this), this.DELAY_GET_RECORDS); this._lazyOnScrollView = _.debounce(
this._onScrollView.bind(this),
this.DELAY_GET_RECORDS);
}, },
/** /**
@ -67,12 +72,11 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
* @override * @override
*/ */
start: function () { start: function () {
//this.$el.addClass("row");
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
}, },
/** /**
* @param {Object} searchState * @param {Object} search_data
*/ */
updateSearchData: function (search_data, count) { updateSearchData: function (search_data, count) {
this.search_data = search_data; this.search_data = search_data;
@ -99,7 +103,10 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
} }
var old_state = _.clone(this.state.data); var old_state = _.clone(this.state.data);
return this._super(state, _.extend({}, params, {noRender: true})).then(function() { return this._super(
state,
_.extend({}, params, {noRender: true})
).then(function () {
self._updateStateRecords(old_state); self._updateStateRecords(old_state);
}); });
}, },
@ -112,8 +119,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
_removeRecords: function (states) { _removeRecords: function (states) {
var defs = []; var defs = [];
var to_destroy = []; var to_destroy = [];
for (var index in states) { for (var index_state in states) {
var state = states[index]; var state = states[index_state];
for (var e = this.widgets.length-1; e>=0; --e) { for (var e = this.widgets.length-1; e>=0; --e) {
var widget = this.widgets[e]; var widget = this.widgets[e];
if (widget && widget.state.id === state.id) { if (widget && widget.state.id === state.id) {
@ -126,22 +133,32 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
// If doesn't exists other records with the same product, we need // If doesn't exists other records with the same product, we need
// create a 'pure virtual' record again. // create a 'pure virtual' record again.
for (var index in to_destroy) { for (var index_destroy in to_destroy) {
var widget_destroyed = to_destroy[index]; var widget_destroyed = to_destroy[index_destroy];
var widget_product_id = widget_destroyed.state.data[this.options.field_map.product].data.id; var widget_product_id = widget_destroyed.state
.data[this.options.field_map.product].data.id;
var found = false; var found = false;
for (var e = this.widgets.length-1; e>=0; --e) { for (var eb = this.widgets.length-1; eb>=0; --eb) {
var widget = this.widgets[e]; var widget = this.widgets[eb];
if (widget.state.data[this.options.field_map.product].data.id === widget_product_id) { if (
widget.state.data[this.options.field_map.product].data.id === widget_product_id
) {
found = true; found = true;
break; break;
} }
} }
if (!found) { if (!found) {
var search_record = _.find(this.search_data, {id: widget_product_id}) var search_record = _.find(this.search_data, {id: widget_product_id});
var new_search_record = _.extend({}, search_record, {__id: state.id}); var new_search_record = _.extend({}, search_record, {__id: state.id});
var search_record_index = widget_destroyed.$el.index(); var search_record_index = widget_destroyed.$el.index();
defs.push(this.appendSearchRecords([new_search_record], false, true, search_record_index)); defs.push(
this.appendSearchRecords(
[new_search_record],
false,
true,
search_record_index
)
);
} }
} }
@ -155,9 +172,11 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
* Thanks to this we don't need re-render 'pure virtual' records. * Thanks to this we don't need re-render 'pure virtual' records.
* *
* @private * @private
* @param {Object} old_states
* @returns {Deferred} * @returns {Deferred}
*/ */
_updateStateRecords: function (old_states) { _updateStateRecords: function (old_states) {
// States to remove // States to remove
var states_to_destroy = []; var states_to_destroy = [];
for (var index in old_states) { for (var index in old_states) {
@ -187,6 +206,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
for (var e = this.widgets.length-1; e>=0; --e) { for (var e = this.widgets.length-1; e>=0; --e) {
var widget = this.widgets[e]; var widget = this.widgets[e];
if (!widget) { if (!widget) {
// Already processed widget (deleted) // Already processed widget (deleted)
continue; continue;
} }
@ -194,17 +214,25 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
widget.recreate(state); widget.recreate(state);
exists = true; exists = true;
break; break;
} else if (widget.recordSearch.id === state.data[this.options.field_map.product].data.id) { } else if (
widget.recordSearch.id === state.data[this.options.field_map.product].data.id
) {
// Is a new record // Is a new record
search_record_index = widget.$el.index(); search_record_index = widget.$el.index();
search_record = widget.recordSearch; search_record = widget.recordSearch;
} }
// Remove "pure virtual" records that have the same product that the new record // Remove "pure virtual" records that have the same product that the new record
if (widget.is_virtual && widget.state.data[this.options.field_map.product].data.id === state.data[this.options.field_map.product].data.id) { if (
widget.is_virtual &&
widget.state.data[this.options.field_map.product].data.id === state.data[this.options.field_map.product].data.id
) {
to_destroy.push(widget); to_destroy.push(widget);
delete this.widgets[e]; delete this.widgets[e];
} }
} }
// Need add a new one? // Need add a new one?
if (!exists && search_record_index !== -1) { if (!exists && search_record_index !== -1) {
var new_search_record = _.extend({}, search_record, {__id: state.id}); var new_search_record = _.extend({}, search_record, {__id: state.id});
@ -248,6 +276,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
* *
* @private * @private
* @param {Array[Object]} results * @param {Array[Object]} results
* @returns {Array[Object]}
*/ */
_processSearchRecords: function (results) { _processSearchRecords: function (results) {
var field_name = this.options.field_map.product; var field_name = this.options.field_map.product;
@ -256,7 +285,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
var record_search = results[index]; var record_search = results[index];
var state_data_found = false; var state_data_found = false;
for (var state_record of this.state.data) { for (var index_data in this.state.data) {
var state_record = this.state.data[index_data];
var field = state_record.data[field_name]; var field = state_record.data[field_name];
if ( if (
(typeof field === "object" && field.data.id === record_search.id) || (typeof field === "object" && field.data.id === record_search.id) ||
@ -321,7 +351,8 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
*/ */
_appendSearchRecords: function (search_records, no_process_records, position) { _appendSearchRecords: function (search_records, no_process_records, position) {
var self = this; var self = this;
var processed_records = no_process_records?search_records:this._processSearchRecords(search_records); var processed_records =
no_process_records?search_records:this._processSearchRecords(search_records);
_.each(processed_records, function (search_record) { _.each(processed_records, function (search_record) {
var state_data = self._getRecordDataById(search_record.__id); var state_data = self._getRecordDataById(search_record.__id);
var ProductPickerRecord = new One2ManyProductPickerRecord( var ProductPickerRecord = new One2ManyProductPickerRecord(
@ -330,6 +361,7 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
self._getRecordOptions(search_record) self._getRecordOptions(search_record)
); );
self.widgets.push(ProductPickerRecord); self.widgets.push(ProductPickerRecord);
// Simulate new lines to dispatch get_default & onchange's to get the // Simulate new lines to dispatch get_default & onchange's to get the
// relevant data to print. This case increase the TTI time. // relevant data to print. This case increase the TTI time.
if (!state_data) { if (!state_data) {
@ -338,10 +370,12 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
self.defsVirtualState.push(defVirtualState); self.defsVirtualState.push(defVirtualState);
} }
} }
// At this point the widget will use the existing state (line) or // At this point the widget will use the existing state (line) or
// the search data. Using search data instead of waiting for // the search data. Using search data instead of waiting for
// simulated state gives a low FCP time. // simulated state gives a low FCP time.
var def = ProductPickerRecord.appendTo(self.$recordsContainer).then(function(){ var def = ProductPickerRecord.appendTo(self.$recordsContainer)
.then(function () {
if (typeof position !== "undefined") { if (typeof position !== "undefined") {
var $elm = self.$el.find("> div > div:nth("+position+")"); var $elm = self.$el.find("> div > div:nth("+position+")");
ProductPickerRecord.$el.insertAfter($elm); ProductPickerRecord.$el.insertAfter($elm);
@ -415,11 +449,11 @@ odoo.define("web_widget_one2many_product_picker.One2ManyProductPickerRenderer",
/** /**
* @private * @private
*/ */
_onClickLoadMore: function (evt) { _onClickLoadMore: function () {
this.$btnLoadMore.attr("disabled", true); this.$btnLoadMore.attr("disabled", true);
this.trigger_up("load_more"); this.trigger_up("load_more");
this._loadMoreWorking = true; this._loadMoreWorking = true;
} },
}); });

View File

@ -6,6 +6,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
var BasicModel = require("web.BasicModel"); var BasicModel = require("web.BasicModel");
BasicModel.include({ BasicModel.include({
/** /**
* @param {Number/String} handle * @param {Number/String} handle
* @param {Object} context * @param {Object} context
@ -62,7 +63,7 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
var list = this.localData[listID]; var list = this.localData[listID];
var context = _.extend({}, this._getContext(list), options.context); var context = _.extend({}, this._getContext(list), options.context);
var position = (options && options.position) || 'top'; var position = options?options.position:'top';
var params = { var params = {
context: context, context: context,
fields: list.fields, fields: list.fields,
@ -75,10 +76,15 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
}; };
return $.Deferred(function (d) { return $.Deferred(function (d) {
self._makeDefaultRecord(list.model, params).then(function (recordID) { self._makeDefaultRecord(list.model, params)
.then(function (recordID) {
self.setPureVirtual(recordID, true); self.setPureVirtual(recordID, true);
if (options.data) { if (options.data) {
self._applyChangeNoWarnings(recordID, options.data, params).then(function(){ self._applyChangeNoWarnings(
recordID,
options.data,
params
).then(function () {
d.resolve(self.get(recordID)); d.resolve(self.get(recordID));
}); });
} else { } else {
@ -92,9 +98,9 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
* Cloned '_applyChange' but without warning messages * Cloned '_applyChange' but without warning messages
* *
* @private * @private
* @param {Object} record * @param {Number} recordID
* @param {Object} fields * @param {Object} changes
* @param {String} viewType * @param {Object} options
* @returns {Deferred} * @returns {Deferred}
*/ */
_applyChangeNoWarnings: function (recordID, changes, options) { _applyChangeNoWarnings: function (recordID, changes, options) {
@ -112,11 +118,16 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
initialData[elem.id] = $.extend(true, {}, _.pick(elem, 'data', '_changes')); initialData[elem.id] = $.extend(true, {}, _.pick(elem, 'data', '_changes'));
}); });
// apply changes to local data // Apply changes to local data
for (var fieldName in changes) { for (var fieldName in changes) {
field = record.fields[fieldName]; field = record.fields[fieldName];
if (field && (field.type === 'one2many' || field.type === 'many2many')) { if (field && (field.type === 'one2many' || field.type === 'many2many')) {
defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], options.viewType, options.allowWarning)); defs.push(this._applyX2ManyChange(
record,
fieldName,
changes[fieldName],
options.viewType,
options.allowWarning));
} else if (field && (field.type === 'many2one' || field.type === 'reference')) { } else if (field && (field.type === 'many2one' || field.type === 'reference')) {
defs.push(this._applyX2OneChange(record, fieldName, changes[fieldName])); defs.push(this._applyX2OneChange(record, fieldName, changes[fieldName]));
} else { } else {
@ -129,12 +140,25 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
} }
return $.when.apply($, defs).then(function () { return $.when.apply($, defs).then(function () {
var onChangeFields = []; // the fields that have changed and that have an on_change
// The fields that have changed and that have an on_change
var onChangeFields = [];
for (var fieldName in changes) { for (var fieldName in changes) {
field = record.fields[fieldName]; field = record.fields[fieldName];
if (field && field.onChange) { if (field && field.onChange) {
var isX2Many = field.type === 'one2many' || field.type === 'many2many'; var isX2Many = (
if (!isX2Many || (self._isX2ManyValid(record._changes[fieldName] || record.data[fieldName]))) { field.type === 'one2many' ||
field.type === 'many2many'
);
if (
!isX2Many ||
(
self._isX2ManyValid(
record._changes[fieldName] ||
record.data[fieldName]
)
)
) {
onChangeFields.push(fieldName); onChangeFields.push(fieldName);
} }
} }
@ -144,12 +168,15 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
self._performOnChangeNoWarnings(record, onChangeFields, options.viewType) self._performOnChangeNoWarnings(record, onChangeFields, options.viewType)
.then(function (result) { .then(function (result) {
delete record._warning; delete record._warning;
onchangeDef.resolve(_.keys(changes).concat(Object.keys(result && result.value || {}))); onchangeDef.resolve(
_.keys(changes).concat(
Object.keys((result && result.value) || {})));
}).fail(function () { }).fail(function () {
self._visitChildren(record, function (elem) { self._visitChildren(record, function (elem) {
_.extend(elem, initialData[elem.id]); _.extend(elem, initialData[elem.id]);
}); });
// safe fix for stable version, for opw-2267444
// Safe fix for stable version, for opw-2267444
if (!options.force_fail) { if (!options.force_fail) {
onchangeDef.resolve({}); onchangeDef.resolve({});
} else { } else {
@ -161,12 +188,16 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
} }
return onchangeDef.then(function (fieldNames) { return onchangeDef.then(function (fieldNames) {
_.each(fieldNames, function (name) { _.each(fieldNames, function (name) {
if (record._changes && record._changes[name] === record.data[name]) { if (
record._changes &&
record._changes[name] === record.data[name]
) {
delete record._changes[name]; delete record._changes[name];
record._isDirty = !_.isEmpty(record._changes); record._isDirty = !_.isEmpty(record._changes);
} }
}); });
return self._fetchSpecialData(record).then(function (fieldNames2) { return self._fetchSpecialData(record).then(function (fieldNames2) {
// Return the names of the fields that changed (onchange or // Return the names of the fields that changed (onchange or
// associated special data change) // associated special data change)
return _.union(fieldNames, fieldNames2); return _.union(fieldNames, fieldNames2);
@ -196,7 +227,8 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
}; };
if (fields.length === 1) { if (fields.length === 1) {
fields = fields[0]; fields = fields[0];
// if only one field changed, add its context to the RPC context
// If only one field changed, add its context to the RPC context
options.fieldName = fields; options.fieldName = fields;
} }
var context = this._getContext(record, options); var context = this._getContext(record, options);
@ -206,10 +238,10 @@ odoo.define("web_widget_one2many_product_picker.BasicModel", function (require)
model: record.model, model: record.model,
method: 'onchange', method: 'onchange',
args: [idList, currentData, fields, onchangeSpec, context], args: [idList, currentData, fields, onchangeSpec, context],
}) }).then(function (result) {
.then(function (result) {
if (!record._changes) { if (!record._changes) {
// if the _changes key does not exist anymore, it means that
// If the _changes key does not exist anymore, it means that
// it was removed by discarding the changes after the rpc // it was removed by discarding the changes after the rpc
// to onchange. So, in that case, the proper response is to // to onchange. So, in that case, the proper response is to
// ignore the onchange. // ignore the onchange.

View File

@ -1,3 +1,4 @@
/* global py */
// Copyright 2020 Tecnativa - Alexandre Díaz // Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). // 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.BasicView", function (require) {
@ -9,21 +10,31 @@ odoo.define("web_widget_one2many_product_picker.BasicView", function (require) {
var _t = core._t; var _t = core._t;
// py.js _ -> _t() call // Add ref to _() -> _t() call
var PY_t = new py.PY_def.fromJSON(function () { var PY_t = new py.PY_def.fromJSON(function () {
var args = py.PY_parseArgs(arguments, ['str']); var args = py.PY_parseArgs(arguments, ['str']);
return py.str.fromJSON(_t(args.str.toJSON())); return py.str.fromJSON(_t(args.str.toJSON()));
}); });
BasicView.include({ BasicView.include({
/** /**
* @override * @override
*/ */
_processField: function (viewType, field, attrs) { _processField: function (viewType, field, attrs) {
/* We need process 'options' attribute to handle translations and special replacements */
if (attrs.widget === "one2many_product_picker" && !_.isObject(attrs.options)) { /**
* We need process 'options' attribute to handle translations and
* special replacements
*/
if (
attrs.widget === "one2many_product_picker" &&
!_.isObject(attrs.options)
) {
attrs.options = attrs.options ? pyUtils.py_eval(attrs.options, { attrs.options = attrs.options ? pyUtils.py_eval(attrs.options, {
_: PY_t, _: PY_t,
// Hack: This allow use $number_search out of an string
number_search: '$number_search', number_search: '$number_search',
}) : {}; }) : {};
} }

View File

@ -8,7 +8,8 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
var core = require("web.core"); var core = require("web.core");
var field_registry = require("web.field_registry"); var field_registry = require("web.field_registry");
var FieldOne2Many = require("web.relational_fields").FieldOne2Many; var FieldOne2Many = require("web.relational_fields").FieldOne2Many;
var One2ManyProductPickerRenderer = require("web_widget_one2many_product_picker.One2ManyProductPickerRenderer"); var One2ManyProductPickerRenderer = require(
"web_widget_one2many_product_picker.One2ManyProductPickerRenderer");
var tools = require("web_widget_one2many_product_picker.tools"); var tools = require("web_widget_one2many_product_picker.tools");
var _t = core._t; var _t = core._t;
@ -36,7 +37,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
_auto_search_delay: 450, _auto_search_delay: 450,
// product.product fields // Model product.product fields
search_read_fields: [ search_read_fields: [
"id", "id",
"display_name", "display_name",
@ -45,9 +46,12 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
/** /**
* @override * @override
*/ */
init: function (parent, name, record, options) { init: function (parent, name, record) {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.state = record; // This is the parent state
// This is the parent state
this.state = record;
// Use jquery 'extend' to have a 'deep' merge. // Use jquery 'extend' to have a 'deep' merge.
this.options = $.extend(true, this._getDefaultOptions(), this.attrs.options); this.options = $.extend(true, this._getDefaultOptions(), this.attrs.options);
if (!this.options.search) { if (!this.options.search) {
@ -58,6 +62,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
if (!(this.options.search[0] instanceof Array)) { if (!(this.options.search[0] instanceof Array)) {
this._searchCategoryNames = _.map(this.options.search, "name"); this._searchCategoryNames = _.map(this.options.search, "name");
} }
// FIXME: Choose a better way to get the active controller or model objects // FIXME: Choose a better way to get the active controller or model objects
this.parent_controller = parent.getParent(); this.parent_controller = parent.getParent();
if (this.view) { if (this.view) {
@ -72,6 +77,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
if (!this.view) { if (!this.view) {
return $.when(); return $.when();
} }
// Uses to work with searchs, so we can mix properties with the user values. // Uses to work with searchs, so we can mix properties with the user values.
this._searchContext = { this._searchContext = {
domain: this.mode === "readonly" ? this._getLinesDomain() : false, domain: this.mode === "readonly" ? this._getLinesDomain() : false,
@ -96,9 +102,13 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
var prices = []; var prices = [];
var field_map = this.options.field_map; var field_map = this.options.field_map;
var records = this.parent_controller.model.get(this.state.id).data[this.name].data; var records = this.parent_controller.model.get(this.state.id).data[this.name].data;
if (this.options.show_discounts) { if (this.options.show_discount) {
prices = _.map(records, function (line) { prices = _.map(records, function (line) {
return line.data[field_map.product_uom_qty] * tools.priceReduce(line.data[field_map.price_unit], line.data[field_map.discount]); return line.data[field_map.product_uom_qty] *
tools.priceReduce(
line.data[field_map.price_unit],
line.data[field_map.discount]
);
}); });
} else { } else {
prices = _.map(records, function (line) { prices = _.map(records, function (line) {
@ -198,7 +208,9 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
* @override * @override
*/ */
_renderButtons: function () { _renderButtons: function () {
if (!this.isReadonly) { if (this.isReadonly) {
return this._super.apply(this, arguments);
}
this.$buttons = $( this.$buttons = $(
qweb.render("One2ManyProductPicker.ControlPanelButtons", { qweb.render("One2ManyProductPicker.ControlPanelButtons", {
search_category_names: this._searchCategoryNames, search_category_names: this._searchCategoryNames,
@ -215,9 +227,6 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
this.$badgeLines = this.$btnLines.find(".badge"); this.$badgeLines = this.$btnLines.find(".badge");
this.updateBadgeLines(); this.updateBadgeLines();
this.$groups.appendTo(this.$buttons); this.$groups.appendTo(this.$buttons);
} else {
return this._super.apply(this, arguments);
}
}, },
/** /**
@ -226,6 +235,7 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
_render: function () { _render: function () {
var self = this; var self = this;
var def = this._super.apply(this, arguments); var def = this._super.apply(this, arguments);
// Parent implementation can return 'undefined' :( // Parent implementation can return 'undefined' :(
return ( return (
def && def &&
@ -329,7 +339,10 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
self._lastSearchRecordsCount = results.length; self._lastSearchRecordsCount = results.length;
self._searchOffset = offset + limit; self._searchOffset = offset + limit;
if (self.renderer) { if (self.renderer) {
self.renderer.updateSearchData(self._searchRecords, self._lastSearchRecordsCount); self.renderer.updateSearchData(
self._searchRecords,
self._lastSearchRecordsCount
);
} }
d.resolve(results); d.resolve(results);
}); });
@ -341,7 +354,6 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
* @param {MouseEvent} evt * @param {MouseEvent} evt
*/ */
_onClickSearchGroup: function (evt) { _onClickSearchGroup: function (evt) {
var self = this;
var $btn = $(evt.target); var $btn = $(evt.target);
var groupIndex = Number($btn.data("group")) || 0; var groupIndex = Number($btn.data("group")) || 0;
this._activeSearchGroup = this.searchGroups[groupIndex]; this._activeSearchGroup = this.searchGroups[groupIndex];
@ -430,29 +442,36 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
search_domain = search_domain[this._searchMode].domain; search_domain = search_domain[this._searchMode].domain;
} }
var involved_fields = []; var involved_fields = [];
// Iterate domain triplets and logic operators // Iterate domain triplets and logic operators
for (var index in search_domain) { for (var index in search_domain) {
var domain = _.clone(search_domain[index]); var domain_cloned = _.clone(search_domain[index]);
// Is a triplet // Is a triplet
if (domain instanceof Array) { if (domain_cloned instanceof Array) {
// Replace right leaf with the current value of the search input // Replace right leaf with the current value of the search input
if (domain[2] === "$number_search") { if (domain_cloned[2] === "$number_search") {
domain[2] = Number(this._searchContext.text); domain_cloned[2] = Number(this._searchContext.text);
involved_fields.push({ involved_fields.push({
type: 'number', type: 'number',
field: domain[0], field: domain_cloned[0],
oper: domain[1], oper: domain_cloned[1],
}); });
} else if (typeof(domain[2]) === "string" && domain[2].includes("$search")) { } else if (
domain[2] = domain[2].replace(/\$search/, this._searchContext.text); typeof domain_cloned[2] === "string" &&
domain_cloned[2].includes("$search")
) {
domain_cloned[2] = domain_cloned[2]
.replace(/\$search/, this._searchContext.text);
involved_fields.push({ involved_fields.push({
type: 'text', type: 'text',
field: domain[0], field: domain_cloned[0],
oper: domain[1], oper: domain_cloned[1],
}); });
} }
} }
sdomain.push(domain); sdomain.push(domain_cloned);
} }
this._searchContext.involvedFields = involved_fields; this._searchContext.involvedFields = involved_fields;
} }
@ -471,7 +490,8 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
return []; return [];
} }
var field_name = this.options.field_map.product; var field_name = this.options.field_map.product;
var lines = this.parent_controller.model.get(this.state.id).data[this.name].data; var lines = this.parent_controller.model.get(this.state.id)
.data[this.name].data;
var ids = _.map(lines, function (line) { var ids = _.map(lines, function (line) {
return line.data[field_name].data.id; return line.data[field_name].data.id;
}); });
@ -483,13 +503,12 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
* that the search results. Use directy in-memory values. * that the search results. Use directy in-memory values.
*/ */
showLines: function () { showLines: function () {
var self = this;
this._clearSearchInput(); this._clearSearchInput();
this.$btnLines.parent().find(".active").removeClass("active"); this.$btnLines.parent().find(".active").removeClass("active");
this.$btnLines.addClass("active"); this.$btnLines.addClass("active");
this._activeSearchGroup = { this._activeSearchGroup = {
'name': 'main_lines', 'name': 'main_lines',
} };
this._searchContext.domain = this._getLinesDomain(); this._searchContext.domain = this._getLinesDomain();
this._searchContext.order = false; this._searchContext.order = false;
this.doRenderSearchRecords(); this.doRenderSearchRecords();
@ -526,9 +545,13 @@ odoo.define("web_widget_one2many_product_picker.FieldOne2ManyProductPicker", fun
* @param {DropdownEvent} evt * @param {DropdownEvent} evt
*/ */
_onShowSearchDropdown: function (evt) { _onShowSearchDropdown: function (evt) {
// Workaround: This "ensures" a correct dropdown position // Workaround: This "ensures" a correct dropdown position
var offset = $(evt.currentTarget).find(".dropdown-toggle").parent().height(); var offset = $(evt.currentTarget).find(".dropdown-toggle").parent().height();
_.defer(function() { $(evt.currentTarget).find(".dropdown-menu").css("transform", "translate3d(0px, " + offset + "px, 0px)"); }); _.defer(function () {
$(evt.currentTarget).find(".dropdown-menu")
.css("transform", "translate3d(0px, " + offset + "px, 0px)");
});
}, },
/** /**

View File

@ -1,3 +1,4 @@
/* global QUnit */
// Copyright 2020 Tecnativa - Alexandre Díaz // Copyright 2020 Tecnativa - Alexandre Díaz
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define('web_widget_one2many_product_picker.widget_tests', function (require) { odoo.define('web_widget_one2many_product_picker.widget_tests', function (require) {
@ -38,7 +39,7 @@ odoo.define('web_widget_one2many_product_picker.widget_tests', function (require
}, },
records: [ records: [
{id: 1, line_ids: [1, 2], currency_id: 1, display_name: "FT01"}, {id: 1, line_ids: [1, 2], currency_id: 1, display_name: "FT01"},
] ],
}, },
line: { line: {
fields: { fields: {
@ -53,7 +54,7 @@ odoo.define('web_widget_one2many_product_picker.widget_tests', function (require
records: [ records: [
{id: 1, name: "Large Cabinet", product_id: 1, product_uom: 1, product_uom_qty: 3, price_unit: 9.99, price_reduce: 9.00, foo_id: 1}, {id: 1, name: "Large Cabinet", product_id: 1, product_uom: 1, product_uom_qty: 3, price_unit: 9.99, price_reduce: 9.00, foo_id: 1},
{id: 2, name: "Cabinet with Doors", product_id: 2, product_uom: 1, product_uom_qty: 8, price_unit: 42.99, price_reduce: 40.00, foo_id: 1}, {id: 2, name: "Cabinet with Doors", product_id: 2, product_uom: 1, product_uom_qty: 8, price_unit: 42.99, price_reduce: 40.00, foo_id: 1},
] ],
}, },
product: { product: {
fields: { fields: {
@ -94,7 +95,7 @@ odoo.define('web_widget_one2many_product_picker.widget_tests', function (require
], ],
}, },
}; };
} },
}, function () { }, function () {
QUnit.test('Load widget', function (assert) { QUnit.test('Load widget', function (assert) {
assert.expect(4); assert.expect(4);