summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-07-07 14:30:30 +0200
committerChristophe Simonis <chs@odoo.com>2017-07-07 14:30:30 +0200
commitbf51fe55a149fb642a410f579dc4b6dcbd794eac (patch)
treef4f208f06094bcef6541fd1fc034e0265de4ad21 /addons
parentbf32940142beb879211b7034bb77a7845c9d0555 (diff)
parent0f2b2c4e2b99df8d68456016a4bea86e26c1c80e (diff)
[MERGE] forward port branch saas-16 up to 0f2b2c4e2b9
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account_bank_statement.py11
-rw-r--r--addons/account/models/account_config_settings.py7
-rw-r--r--addons/account/models/account_invoice.py2
-rw-r--r--addons/account/models/account_move.py2
-rw-r--r--addons/account/models/partner.py9
-rw-r--r--addons/account/views/account_invoice_view.xml12
-rw-r--r--addons/account/views/account_view.xml8
-rw-r--r--addons/account/views/partner_view.xml11
-rw-r--r--addons/account/views/product_view.xml4
-rw-r--r--addons/account/views/report_invoice.xml2
-rw-r--r--addons/account_voucher/views/account_voucher_views.xml4
-rw-r--r--addons/base_address_city/models/res_partner.py2
-rw-r--r--addons/base_setup/views/res_partner_views.xml2
-rw-r--r--addons/crm/models/crm_lead.py10
-rw-r--r--addons/crm/views/crm_lead_views.xml9
-rw-r--r--addons/delivery/views/delivery_view.xml4
-rw-r--r--addons/fleet/views/fleet_vehicle_views.xml4
-rw-r--r--addons/google_calendar/static/src/js/google_calendar.js182
-rw-r--r--addons/google_calendar/static/tests/google_calendar_tests.js122
-rw-r--r--addons/google_calendar/views/google_calendar_templates.xml6
-rw-r--r--addons/google_drive/models/google_drive.py2
-rw-r--r--addons/hr/views/hr_views.xml4
-rw-r--r--addons/hr_expense/views/hr_expense_views.xml4
-rw-r--r--addons/hr_expense_check/__manifest__.py1
-rw-r--r--addons/hr_recruitment/views/hr_recruitment_views.xml5
-rw-r--r--addons/hr_timesheet/models/project.py10
-rw-r--r--addons/l10n_fr_certification/__manifest__.py2
-rw-r--r--addons/l10n_fr_certification/models/account.py2
-rw-r--r--addons/mail/static/src/js/client_action.js4
-rw-r--r--addons/maintenance/views/maintenance_views.xml2
-rw-r--r--addons/mass_mailing/models/mass_mailing.py13
-rw-r--r--addons/mass_mailing/views/mass_mailing_views.xml6
-rw-r--r--addons/membership/views/product_views.xml2
-rw-r--r--addons/mrp/models/mrp_workorder.py4
-rw-r--r--addons/mrp/models/stock_quant.py6
-rw-r--r--addons/mrp/views/mrp_bom_views.xml2
-rw-r--r--addons/mrp/wizard/change_production_qty.py2
-rw-r--r--addons/mrp/wizard/change_production_qty_views.xml1
-rw-r--r--addons/mrp_repair/views/mrp_repair_views.xml8
-rw-r--r--addons/note/views/note_views.xml7
-rw-r--r--addons/payment/views/account_payment_views.xml5
-rw-r--r--addons/payment/views/payment_views.xml2
-rw-r--r--addons/payment_authorize/views/payment_views.xml2
-rw-r--r--addons/point_of_sale/models/pos_order.py30
-rw-r--r--addons/point_of_sale/views/pos_order_view.xml8
-rw-r--r--addons/product/models/product_template.py2
-rw-r--r--addons/project/views/project_views.xml5
-rw-r--r--addons/project_issue/views/project_issue_views.xml4
-rw-r--r--addons/purchase/views/purchase_views.xml8
-rw-r--r--addons/rating/controllers/main.py2
-rw-r--r--addons/sale/__init__.py18
-rw-r--r--addons/sale/__manifest__.py1
-rw-r--r--addons/sale/views/sale_views.xml8
-rw-r--r--addons/stock/models/stock_quant.py9
-rw-r--r--addons/stock/views/product_views.xml2
-rw-r--r--addons/web/controllers/main.py2
-rw-r--r--addons/web/static/src/js/apps.js20
-rw-r--r--addons/web/static/src/js/chrome/pager.js2
-rw-r--r--addons/web/static/src/js/chrome/view_manager.js3
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js2
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js47
-rw-r--r--addons/web/static/src/js/fields/field_registry.js1
-rw-r--r--addons/web/static/src/js/fields/field_utils.js52
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js254
-rw-r--r--addons/web/static/src/js/services/data_manager.js5
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js77
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js696
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_controller.js11
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_model.js13
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_renderer.js1
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_view.js1
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js2
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js45
-rw-r--r--addons/web/static/src/less/control_panel.less11
-rw-r--r--addons/web/static/src/less/web_calendar.less3
-rw-r--r--addons/web/static/src/xml/base.xml24
-rw-r--r--addons/web/static/src/xml/web_calendar.xml6
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js101
-rw-r--r--addons/web/static/tests/fields/field_utils_tests.js23
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js830
-rw-r--r--addons/web/static/tests/helpers/mock_server.js5
-rw-r--r--addons/web/static/tests/helpers/test_utils.js9
-rw-r--r--addons/web/static/tests/views/basic_model_tests.js190
-rw-r--r--addons/web/static/tests/views/calendar_tests.js102
-rw-r--r--addons/web/static/tests/views/form_tests.js61
-rw-r--r--addons/web/static/tests/views/kanban_tests.js2
-rw-r--r--addons/web/static/tests/views/list_tests.js70
-rw-r--r--addons/web_editor/models/ir_http.py2
-rw-r--r--addons/web_editor/models/ir_ui_view.py2
-rw-r--r--addons/web_editor/static/src/js/backend.js107
-rw-r--r--addons/web_editor/static/src/js/base.js2
-rw-r--r--addons/web_editor/static/src/less/web_editor.ui.less7
-rw-r--r--addons/web_editor/static/src/xml/backend.xml29
-rw-r--r--addons/web_editor/static/tests/web_editor_tests.js40
-rw-r--r--addons/website/models/ir_http.py2
-rw-r--r--addons/website/static/src/js/website.js5
-rw-r--r--addons/website_blog/controllers/main.py1
-rw-r--r--addons/website_blog/views/website_blog_templates.xml2
-rw-r--r--addons/website_customer/views/res_partner_views.xml2
-rw-r--r--addons/website_event/views/event_templates.xml50
-rw-r--r--addons/website_event_questions/models/event.py16
-rw-r--r--addons/website_event_track/views/event_track_views.xml4
-rw-r--r--addons/website_form/models/models.py14
-rw-r--r--addons/website_forum/views/forum.xml2
-rw-r--r--addons/website_hr_recruitment/__manifest__.py1
-rw-r--r--addons/website_hr_recruitment/static/src/img/ui/s_hr_rating_thumb.jpgbin0 -> 6272 bytes
-rw-r--r--addons/website_hr_recruitment/static/src/less/s_hr_rating.less65
-rw-r--r--addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml75
-rw-r--r--addons/website_hr_recruitment/views/website_hr_recuitment_snippets.xml61
-rw-r--r--addons/website_portal_sale/controllers/main.py1
-rw-r--r--addons/website_portal_sale/views/website_portal_sale_templates.xml2
-rw-r--r--addons/website_sale/controllers/main.py4
-rw-r--r--addons/website_sale/i18n/website_sale.pot4
-rw-r--r--addons/website_sale/views/product_views.xml4
-rw-r--r--addons/website_sale/views/templates.xml4
-rw-r--r--addons/website_sale_digital/controllers/main.py2
-rw-r--r--addons/website_sale_options/views/product_views.xml2
117 files changed, 3054 insertions, 735 deletions
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index 90223d85923..4d7add6879a 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -3,7 +3,7 @@
from odoo import api, fields, models, _
from odoo.osv import expression
from odoo.tools import float_is_zero, pycompat
-from odoo.tools import float_compare, float_round
+from odoo.tools import float_compare, float_round, float_repr
from odoo.tools.misc import formatLang
from odoo.exceptions import UserError, ValidationError
@@ -606,7 +606,8 @@ class AccountBankStatementLine(models.Model):
from_clause = "FROM account_move_line aml JOIN account_account acc ON acc.id = aml.account_id "
where_clause = """WHERE aml.company_id = %(company_id)s
AND (
- (aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s)
+ (aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s
+ AND aml.payment_id IS NOT NULL)
OR
("""+acc_type+""" AND aml.reconciled = false)
)"""
@@ -630,7 +631,7 @@ class AccountBankStatementLine(models.Model):
precision = st_line_currency and st_line_currency.decimal_places or company_currency.decimal_places
params = {'company_id': self.env.user.company_id.id,
'account_payable_receivable': (self.journal_id.default_credit_account_id.id, self.journal_id.default_debit_account_id.id),
- 'amount': float_round(amount, precision_digits=precision),
+ 'amount': float_repr(float_round(amount, precision_digits=precision), precision_digits=precision),
'partner_id': self.partner_id.id,
'excluded_ids': tuple(excluded_ids),
'ref': self.name,
@@ -651,9 +652,9 @@ class AccountBankStatementLine(models.Model):
# Look for a single move line with the same amount
field = currency and 'amount_residual_currency' or 'amount_residual'
liquidity_field = currency and 'amount_currency' or amount > 0 and 'debit' or 'credit'
- liquidity_amt_clause = currency and '%(amount)s' or 'abs(%(amount)s)'
+ liquidity_amt_clause = currency and '%(amount)s::numeric' or 'abs(%(amount)s::numeric)'
sql_query = self._get_common_sql_query(excluded_ids=excluded_ids) + \
- " AND ("+field+" = %(amount)s OR (acc.internal_type = 'liquidity' AND "+liquidity_field+" = " + liquidity_amt_clause + ")) \
+ " AND ("+field+" = %(amount)s::numeric OR (acc.internal_type = 'liquidity' AND "+liquidity_field+" = " + liquidity_amt_clause + ")) \
ORDER BY date_maturity desc, aml.id desc LIMIT 1"
self.env.cr.execute(sql_query, params)
results = self.env.cr.fetchone()
diff --git a/addons/account/models/account_config_settings.py b/addons/account/models/account_config_settings.py
index 21f1b10eeb7..1e64aa013a4 100644
--- a/addons/account/models/account_config_settings.py
+++ b/addons/account/models/account_config_settings.py
@@ -127,7 +127,12 @@ class AccountConfigSettings(models.TransientModel):
# related values, including the currency_id field on res_company. This in turn will trigger the recomputation
# of account_move_line related field company_currency_id which can be slow depending on the number of entries
# in the database. Thus, if we do not explicitely change the currency_id, we should not write it on the company
+ # Same for the field `code_digits` which will trigger a write on all the account.account to complete the
+ # code the missing characters to complete the desired number of digit, leading to a sql_constraint.
if ('company_id' in values and 'currency_id' in values):
- if self.env['res.company'].browse(values.get('company_id')).currency_id.id == values.get('currency_id'):
+ company = self.env['res.company'].browse(values.get('company_id'))
+ if company.currency_id.id == values.get('currency_id'):
values.pop('currency_id')
+ if company.accounts_code_digits == values.get('code_digits'):
+ values.pop('code_digits')
return super(AccountConfigSettings, self).create(values)
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 12c34e659b6..b84aa3513cc 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -489,7 +489,7 @@ class AccountInvoice(models.Model):
@api.onchange('invoice_line_ids')
def _onchange_invoice_line_ids(self):
taxes_grouped = self.get_taxes_values()
- tax_lines = self.tax_line_ids.browse([])
+ tax_lines = self.tax_line_ids.filtered('manual')
for tax in pycompat.values(taxes_grouped):
tax_lines += tax_lines.new(tax)
self.tax_line_ids = tax_lines
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index f50593e32cc..b7f90e49624 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -695,7 +695,7 @@ class AccountMoveLine(models.Model):
company_currency = line.account_id.company_id.currency_id
ret_line = {
'id': line.id,
- 'name': line.name != '/' and line.move_id.name + ': ' + line.name or line.move_id.name,
+ 'name': line.name and line.name != '/' and line.move_id.name + ': ' + line.name or line.move_id.name,
'ref': line.move_id.ref or '',
# For reconciliation between statement transactions and already registered payments (eg. checks)
# NB : we don't use the 'reconciled' field because the line we're selecting is not the one that gets reconciled
diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py
index 72976451e8c..f2f0f85f7c0 100644
--- a/addons/account/models/partner.py
+++ b/addons/account/models/partner.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
+from ast import literal_eval
from operator import itemgetter
import time
@@ -427,3 +428,11 @@ class ResPartner(models.Model):
return super(ResPartner, self)._commercial_fields() + \
['debit_limit', 'property_account_payable_id', 'property_account_receivable_id', 'property_account_position_id',
'property_payment_term_id', 'property_supplier_payment_term_id', 'last_time_entries_checked']
+
+ @api.multi
+ def action_view_partner_invoices(self):
+ self.ensure_one()
+ action = self.env.ref('account.action_invoice_refund_out_tree').read()[0]
+ action['domain'] = literal_eval(action['domain'])
+ action['domain'].append(('partner_id', 'child_of', self.id))
+ return action
diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml
index cf1d70b40c0..fccaea99ecc 100644
--- a/addons/account/views/account_invoice_view.xml
+++ b/addons/account/views/account_invoice_view.xml
@@ -268,13 +268,13 @@
<field name="account_analytic_id" groups="analytic.group_analytic_accounting"
domain="[('company_id', '=', parent.company_id)]"
context="{'default_partner_id': parent.partner_id}"/>
- <field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags"/>
+ <field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="quantity"/>
<field name="uom_id" groups="product.group_uom"/>
<field name="price_unit"/>
<field name="discount" groups="base.group_no_one"/>
- <field name="invoice_line_tax_ids" widget="many2many_tags" context="{'type':parent.type}"
- domain="[('type_tax_use','=','purchase'),('company_id', '=', parent.company_id)]" options="{'no_create': True}"/>
+ <field name="invoice_line_tax_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create': True}" context="{'type':parent.type}"
+ domain="[('type_tax_use','=','purchase'),('company_id', '=', parent.company_id)]"/>
<field name="price_subtotal"/>
<field name="currency_id" invisible="1"/>
</tree>
@@ -404,13 +404,13 @@
<field name="account_analytic_id" groups="analytic.group_analytic_accounting"
domain="[('company_id', '=', parent.company_id)]"
context="{'default_partner_id': parent.partner_id}"/>
- <field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags"/>
+ <field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="quantity"/>
<field name="uom_id" groups="product.group_uom"/>
<field name="price_unit"/>
<field name="discount" groups="base.group_no_one"/>
- <field name="invoice_line_tax_ids" widget="many2many_tags" context="{'type':parent.type}"
- domain="[('type_tax_use','=','sale'),('company_id', '=', parent.company_id)]" options="{'no_create': True}"/>
+ <field name="invoice_line_tax_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create': True}" context="{'type':parent.type}"
+ domain="[('type_tax_use','=','sale'),('company_id', '=', parent.company_id)]"/>
<field name="price_subtotal"/>
<field name="currency_id" invisible="1"/>
</tree>
diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml
index 9757169d898..b71980e0ece 100644
--- a/addons/account/views/account_view.xml
+++ b/addons/account/views/account_view.xml
@@ -304,8 +304,8 @@
<group>
<group string="Control-Access" groups="base.group_no_one">
<div class="text-muted" colspan="2">Keep empty for no control</div>
- <field name="type_control_ids" widget="many2many_tags"/>
- <field name="account_control_ids" widget="many2many_tags"/>
+ <field name="type_control_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
+ <field name="account_control_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</group>
<group string="Miscellaneous">
<field name="inbound_payment_method_ids" widget="many2many_checkboxes" attrs="{'invisible': [('type', 'not in', ['bank', 'cash'])]}"/>
@@ -2037,8 +2037,8 @@
<field name="name"/>
<field name="attribute_value_ids" widget="many2many_tags" groups="product.group_product_variant"/>
<field name="lst_price"/>
- <field name="taxes_id" widget="many2many_tags"/>
- <field name="supplier_taxes_id" widget="many2many_tags"/>
+ <field name="taxes_id" widget="many2many_tags" options="{'color_field': 'color'}"/>
+ <field name="supplier_taxes_id" widget="many2many_tags" options="{'color_field': 'color'}"/>
</tree>
</field>
</record>
diff --git a/addons/account/views/partner_view.xml b/addons/account/views/partner_view.xml
index bd2466bec6e..e1aa1d774c6 100644
--- a/addons/account/views/partner_view.xml
+++ b/addons/account/views/partner_view.xml
@@ -95,6 +95,17 @@
<field name="priority" eval="20"/>
<field name="groups_id" eval="[(4, ref('account.group_account_invoice'))]"/>
<field name="arch" type="xml">
+ <div name="button_box" position="inside">
+ <button type="object" class="oe_stat_button" icon="fa-pencil-square-o" name="action_view_partner_invoices"
+ attrs="{'invisible': [('customer', '=', False)]}">
+ <div class="o_form_field o_stat_info">
+ <span class="o_stat_value">
+ <field name="total_invoiced" widget='monetary' options="{'currency_field': 'currency_id'}"/>
+ </span>
+ <span class="o_stat_text">Invoiced</span>
+ </div>
+ </button>
+ </div>
<group name="container_row_2" position="inside">
<group string="Payments" name="banks">
<field name="currency_id" invisible="1"/>
diff --git a/addons/account/views/product_view.xml b/addons/account/views/product_view.xml
index f8979bd45da..8bbf5a69e0e 100644
--- a/addons/account/views/product_view.xml
+++ b/addons/account/views/product_view.xml
@@ -11,13 +11,13 @@
<page string="Invoicing" name="invoicing">
<group name="properties">
<group string="Receivables">
- <field name="taxes_id" widget="many2many_tags"/>
+ <field name="taxes_id" widget="many2many_tags" options="{'color_field': 'color'}/>
<field name="property_account_income_id"
domain="[('internal_type','=','other'),('deprecated','=',False)]"
groups="account.group_account_user"/>
</group>
<group string="Payables" name="payables">
- <field name="supplier_taxes_id" widget="many2many_tags"/>
+ <field name="supplier_taxes_id" widget="many2many_tags" options="{'color_field': 'color'}/>
<field name="property_account_expense_id"
domain="[('internal_type','=','other'),('deprecated','=',False)]"
groups="account.group_account_user"/>
diff --git a/addons/account/views/report_invoice.xml b/addons/account/views/report_invoice.xml
index 22906ebf10f..bea6aa94588 100644
--- a/addons/account/views/report_invoice.xml
+++ b/addons/account/views/report_invoice.xml
@@ -44,7 +44,7 @@
<strong>Customer Code:</strong>
<p t-field="o.partner_id.ref"/>
</div>
- <div name="reference" class="col-xs-2" t-if="o.reference and o.type == 'in_invoice'">
+ <div name="reference" class="col-xs-2" t-if="o.reference">
<strong>Reference:</strong>
<p t-field="o.reference"/>
</div>
diff --git a/addons/account_voucher/views/account_voucher_views.xml b/addons/account_voucher/views/account_voucher_views.xml
index ced7d16a4ef..ea279a24be2 100644
--- a/addons/account_voucher/views/account_voucher_views.xml
+++ b/addons/account_voucher/views/account_voucher_views.xml
@@ -253,7 +253,7 @@
<field name="account_analytic_id" domain="[('company_id', '=', parent.company_id)]" groups="analytic.group_analytic_accounting"/>
<field name="quantity"/>
<field name="price_unit"/>
- <field name="tax_ids" widget="many2many_tags" domain="[('type_tax_use','=','sale')]"/>
+ <field name="tax_ids" widget="many2many_tags" options="{'color_field': 'color'}" domain="[('type_tax_use','=','sale')]"/>
<field name="price_subtotal"/>
<field name="currency_id" invisible="1"/>
<field name="company_id" invisible="1"/>
@@ -369,7 +369,7 @@
<field name="account_analytic_id" domain="[('company_id', '=', parent.company_id)]" groups="analytic.group_analytic_accounting"/>
<field name="quantity"/>
<field name="price_unit"/>
- <field name="tax_ids" widget="many2many_tags" domain="[('type_tax_use','=','purchase')]"/>
+ <field name="tax_ids" widget="many2many_tags" options="{'color_field': 'color'}" domain="[('type_tax_use','=','purchase')]"/>
<field name="price_subtotal"/>
<field name="currency_id" invisible="1"/>
<field name="company_id" invisible="1"/>
diff --git a/addons/base_address_city/models/res_partner.py b/addons/base_address_city/models/res_partner.py
index afa5cd759e8..f6fcbf61d42 100644
--- a/addons/base_address_city/models/res_partner.py
+++ b/addons/base_address_city/models/res_partner.py
@@ -20,6 +20,8 @@ class Partner(models.Model):
@api.model
def _fields_view_get_address(self, arch):
arch = super(Partner, self)._fields_view_get_address(arch)
+ if not self._context.get('no_address_format'):
+ return arch
# render the partner address accordingly to address_view_id
doc = etree.fromstring(arch)
for city_node in doc.xpath("//field[@name='city']"):
diff --git a/addons/base_setup/views/res_partner_views.xml b/addons/base_setup/views/res_partner_views.xml
index 9eef80f6539..2fd1658d825 100644
--- a/addons/base_setup/views/res_partner_views.xml
+++ b/addons/base_setup/views/res_partner_views.xml
@@ -8,7 +8,7 @@
<field name="arch" type="xml">
<xpath expr="//div[contains(@class, 'oe_kanban_partner_categories')]" position="inside">
<span class="oe_kanban_list_many2many">
- <field name="category_id"/>
+ <field name="category_id" widget="many2many_tags" options="{'color_field': 'color'}"/>
</span>
</xpath>
</field>
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index c3795afafe3..9043091164a 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -254,11 +254,13 @@ class Lead(models.Model):
self.update(values)
@api.constrains('user_id')
+ @api.multi
def _valid_team(self):
- if self.user_id:
- values = self.with_context(team_id=self.team_id.id)._onchange_user_values(self.user_id.id)
- if values:
- self.update(values)
+ for lead in self:
+ if lead.user_id:
+ values = lead.with_context(team_id=lead.team_id.id)._onchange_user_values(lead.user_id.id)
+ if values:
+ lead.update(values)
@api.onchange('state_id')
def _onchange_state(self):
diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml
index 4934fed5bc0..eff22f19721 100644
--- a/addons/crm/views/crm_lead_views.xml
+++ b/addons/crm/views/crm_lead_views.xml
@@ -94,7 +94,7 @@
</group>
<group>
<field name="priority" widget="priority"/>
- <field name="tag_ids" widget="many2many_tags" options="{'no_create_edit': True}"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
</group>
</group>
<notebook>
@@ -243,7 +243,7 @@
<group>
<group>
<field name="name" string="Opportunity Title" placeholder="e.g. Customer Deal"/>
- <field name="partner_id"/>
+ <field name="partner_id" domain="[('customer', '=', True)]" context="{'search_default_customer': 1}"/>
<field name="email_from" invisible="1"/>
<field name="phone" invisible="1"/>
<label for="planned_revenue"/>
@@ -287,7 +287,6 @@
<field name="user_id"/>
<field name="partner_address_email"/>
<field name="message_needaction_counter"/>
- <field name="tag_ids"/>
<field name="partner_id"/>
<field name="activity_summary"/>
<field name="active"/>
@@ -313,7 +312,7 @@
</div>
<div class="oe_kanban_content">
<div>
- <field name="tag_ids"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</div>
<div>
<strong><field name="name"/></strong>
@@ -486,7 +485,7 @@
</group>
<group>
<field name="priority" widget="priority"/>
- <field name="tag_ids" widget="many2many_tags" options="{'no_create_edit': True}"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
<field name="lost_reason" attrs="{'invisible': [('active', '=', True)]}"/>
<field name="date_conversion" invisible="1"/>
</group>
diff --git a/addons/delivery/views/delivery_view.xml b/addons/delivery/views/delivery_view.xml
index c85bf787320..d2e483bdddb 100644
--- a/addons/delivery/views/delivery_view.xml
+++ b/addons/delivery/views/delivery_view.xml
@@ -108,8 +108,8 @@
</group>
<group>
<group>
- <field name="country_ids" widget="many2many_tags"/>
- <field name="state_ids" widget="many2many_tags"/>
+ <field name="country_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
+ <field name="state_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</group>
<group></group>
<group>
diff --git a/addons/fleet/views/fleet_vehicle_views.xml b/addons/fleet/views/fleet_vehicle_views.xml
index 4a5416968d7..699d06a877b 100644
--- a/addons/fleet/views/fleet_vehicle_views.xml
+++ b/addons/fleet/views/fleet_vehicle_views.xml
@@ -66,7 +66,7 @@
<field name="license_plate" class="oe_inline" placeholder="e.g. PAE 326"/>
</h2>
<label for="tag_ids" class="oe_edit_only"/>
- <field name="tag_ids" widget="many2many_tags" options="{'no_create_edit': True}"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
</div>
<group col="2" string="General Properties">
<group >
@@ -180,7 +180,7 @@
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<div class="o_kanban_tags_section">
- <field name="tag_ids"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</div>
<div class="o_kanban_image">
<img t-att-src="kanban_image('fleet.vehicle', 'image_small', record.id.value)"/>
diff --git a/addons/google_calendar/static/src/js/google_calendar.js b/addons/google_calendar/static/src/js/google_calendar.js
index 240b5568c0b..dbb7a308540 100644
--- a/addons/google_calendar/static/src/js/google_calendar.js
+++ b/addons/google_calendar/static/src/js/google_calendar.js
@@ -5,97 +5,143 @@ var core = require('web.core');
var Dialog = require('web.Dialog');
var framework = require('web.framework');
var CalendarRenderer = require('web.CalendarRenderer');
+var CalendarController = require('web.CalendarController');
var _t = core._t;
-CalendarRenderer.include({
- events: _.extend({}, CalendarRenderer.prototype.events, {
- 'click .o_google_sync_button': function () {
- this.syncCalendar(this.fields_view);
- }
+CalendarController.include({
+ custom_events: _.extend({}, CalendarController.prototype.custom_events, {
+ syncCalendar: '_onSyncCalendar',
}),
- syncCalendar: function() {
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Try to sync the calendar with Google Calendar. According to the result
+ * from Google API, this function may require an action of the user by the
+ * mean of a dialog.
+ *
+ * @private
+ * @returns {OdooEvent} event
+ */
+ _onSyncCalendar: function (event) {
var self = this;
var context = this.getSession().user_context;
- this.$google_button.prop('disabled', true);
- this.trigger_up('perform_rpc', {
+ this._rpc({
route: '/google_calendar/sync_data',
- args: {
- model: this.state.model,
+ params: {
+ model: this.modelName,
fromurl: window.location.href,
local_context: context,
- },
- on_success: function (o) {
- if (o.status === "need_auth") {
- Dialog.alert(self, _t("You will be redirected to Google to authorize access to your calendar!"), {
+ }
+ }).then(function (o) {
+ if (o.status === "need_auth") {
+ Dialog.alert(self, _t("You will be redirected to Google to authorize access to your calendar!"), {
+ confirm_callback: function() {
+ framework.redirect(o.url);
+ },
+ title: _t('Redirection'),
+ });
+ } else if (o.status === "need_config_from_admin") {
+ if (!_.isUndefined(o.action) && parseInt(o.action)) {
+ Dialog.confirm(self, _t("The Google Synchronization needs to be configured before you can use it, do you want to do it now?"), {
confirm_callback: function() {
- framework.redirect(o.url);
+ self.do_action(o.action);
},
- title: _t('Redirection'),
+ title: _t('Configuration'),
});
- } else if (o.status === "need_config_from_admin") {
- if (!_.isUndefined(o.action) && parseInt(o.action)) {
- Dialog.confirm(self, _t("The Google Synchronization needs to be configured before you can use it, do you want to do it now?"), {
- confirm_callback: function() {
- self.do_action(o.action);
- },
- title: _t('Configuration'),
- });
- } else {
- Dialog.alert(self, _t("An administrator needs to configure Google Synchronization before you can use it!"), {
- title: _t('Configuration'),
- });
- }
- } else if (o.status === "need_refresh") {
- self.$calendar.fullCalendar('refetchEvents');
- } else if (o.status === "need_reset") {
- var confirm_text1 = _t("The account you are trying to synchronize (%s) is not the same as the last one used (%s)!");
- var confirm_text2 = _t("In order to do this, you first need to disconnect all existing events from the old account.");
- var confirm_text3 = _t("Do you want to do this now?");
- var text = _.str.sprintf(confirm_text1 + "\n" + confirm_text2 + "\n\n" + confirm_text3, o.info.new_name, o.info.old_name);
- Dialog.confirm(self, text, {
- confirm_callback: function() {
- self._rpc({
- route: '/google_calendar/remove_references',
- params: {
- model: self.state.model,
- local_context: context,
- },
- })
- .done(function(o) {
- if (o.status === "OK") {
- Dialog.alert(self, _t("All events have been disconnected from your previous account. You can now restart the synchronization"), {
- title: _t('Event disconnection success'),
- });
- } else if (o.status === "KO") {
- Dialog.alert(self, _t("An error occured while disconnecting events from your previous account. Please retry or contact your administrator."), {
- title: _t('Event disconnection error'),
- });
- } // else NOP
- });
- },
- title: _t('Accounts'),
+ } else {
+ Dialog.alert(self, _t("An administrator needs to configure Google Synchronization before you can use it!"), {
+ title: _t('Configuration'),
});
}
-
- self.$google_button.prop('disabled', false);
- },
+ } else if (o.status === "need_refresh") {
+ self.reload();
+ } else if (o.status === "need_reset") {
+ var confirmText1 = _t("The account you are trying to synchronize (%s) is not the same as the last one used (%s)!");
+ var confirmText2 = _t("In order to do this, you first need to disconnect all existing events from the old account.");
+ var confirmText3 = _t("Do you want to do this now?");
+ var text = _.str.sprintf(confirmText1 + "\n" + confirmText2 + "\n\n" + confirmText3, o.info.new_name, o.info.old_name);
+ Dialog.confirm(self, text, {
+ confirm_callback: function() {
+ self._rpc({
+ route: '/google_calendar/remove_references',
+ params: {
+ model: self.state.model,
+ local_context: context,
+ },
+ })
+ .done(function(o) {
+ if (o.status === "OK") {
+ Dialog.alert(self, _t("All events have been disconnected from your previous account. You can now restart the synchronization"), {
+ title: _t('Event disconnection success'),
+ });
+ } else if (o.status === "KO") {
+ Dialog.alert(self, _t("An error occured while disconnecting events from your previous account. Please retry or contact your administrator."), {
+ title: _t('Event disconnection error'),
+ });
+ } // else NOP
+ });
+ },
+ title: _t('Accounts'),
+ });
+ }
+ }).always(function () {
+ event.data.on_always();
});
- },
- initSidebar: function() {
+ }
+});
+
+CalendarRenderer.include({
+ events: _.extend({}, CalendarRenderer.prototype.events, {
+ 'click .o_google_sync_button': '_onSyncCalendar',
+ }),
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Adds the Sync with Google button in the sidebar
+ *
+ * @private
+ */
+ _initSidebar: function () {
var self = this;
- this._super();
- this.$google_button = $();
- if (this.state.model === "calendar.event") {
- this.$google_button = $('<button/>', {type: 'button', html: _t("Sync with <b>Google</b>")})
+ this._super.apply(this, arguments);
+ this.$googleButton = $();
+ if (this.model === "calendar.event") {
+ this.$googleButton = $('<button/>', {type: 'button', html: _t("Sync with <b>Google</b>")})
.addClass('o_google_sync_button oe_button btn btn-sm btn-default')
.prepend($('<img/>', {
src: "/google_calendar/static/src/img/calendar_32.png",
}))
- .appendTo(self.$('.o_calendar_sidebar'));
+ .appendTo(self.$sidebar);
}
},
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Requests to sync the calendar with Google Calendar
+ *
+ * @private
+ */
+ _onSyncCalendar: function () {
+ var self = this;
+ var context = this.getSession().user_context;
+ this.$googleButton.prop('disabled', true);
+ this.trigger_up('syncCalendar', {
+ on_always: function () {
+ self.$googleButton.prop('disabled', false);
+ },
+ });
+ },
});
});
diff --git a/addons/google_calendar/static/tests/google_calendar_tests.js b/addons/google_calendar/static/tests/google_calendar_tests.js
new file mode 100644
index 00000000000..08176dde839
--- /dev/null
+++ b/addons/google_calendar/static/tests/google_calendar_tests.js
@@ -0,0 +1,122 @@
+odoo.define('google_calendar.calendar_tests', function (require) {
+"use strict";
+
+var CalendarView = require('web.CalendarView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+var initialDate = new Date("2016-12-12T08:00:00Z");
+
+
+QUnit.module('Google Calendar', {
+ beforeEach: function () {
+ this.data = {
+ 'calendar.event': {
+ fields: {
+ id: {string: "ID", type: "integer"},
+ user_id: {string: "user", type: "many2one", relation: 'user'},
+ partner_id: {string: "user", type: "many2one", relation: 'partner', related: 'user_id.partner_id'},
+ name: {string: "name", type: "char"},
+ start_date: {string: "start date", type: "date"},
+ stop_date: {string: "stop date", type: "date"},
+ start: {string: "start datetime", type: "datetime"},
+ stop: {string: "stop datetime", type: "datetime"},
+ allday: {string: "allday", type: "boolean"},
+ partner_ids: {string: "attendees", type: "one2many", relation: 'partner'},
+ type: {string: "type", type: "integer"},
+ },
+ records: [
+ {id: 5, user_id: 4, partner_id: 4, name: "event 1", start: "2016-12-13 15:55:05", stop: "2016-12-15 18:55:05", allday: false, partner_ids: [], type: 2},
+ {id: 6, user_id: 4, partner_id: 4, name: "event 2", start: "2016-12-18 08:00:00", stop: "2016-12-18 09:00:00", allday: false, partner_ids: [], type: 3}
+ ],
+ check_access_rights: function () {
+ return $.when(true);
+ }
+ },
+ user: {
+ fields: {
+ id: {string: "ID", type: "integer"},
+ display_name: {string: "Displayed name", type: "char"},
+ partner_id: {string: "partner", type: "many2one", relation: 'partner'},
+ image: {string: "image", type: "integer"},
+ },
+ records: [
+ {id: 4, display_name: "user 4", partner_id: 4},
+ ]
+ },
+ partner: {
+ fields: {
+ id: {string: "ID", type: "integer"},
+ display_name: {string: "Displayed name", type: "char"},
+ image: {string: "image", type: "integer"},
+ },
+ records: [
+ {id: 4, display_name: "partner 4", image: 'DDD'}
+ ]
+ },
+ filter_partner: {
+ fields: {
+ id: {string: "ID", type: "integer"},
+ user_id: {string: "user", type: "many2one", relation: 'user'},
+ partner_id: {string: "partner", type: "many2one", relation: 'partner'},
+ },
+ records: [
+ {id: 3, user_id: 4, partner_id: 4}
+ ]
+ },
+ };
+ }
+}, function () {
+
+ QUnit.test('sync google calendar', function (assert) {
+ assert.expect(6);
+
+ var calendar = createView({
+ View: CalendarView,
+ model: 'calendar.event',
+ data: this.data,
+ arch:
+ '<calendar class="o_calendar_test" '+
+ 'date_start="start" '+
+ 'date_stop="stop" '+
+ 'mode="month">'+
+ '<field name="name"/>'+
+ '</calendar>',
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ mockRPC: function (route, args) {
+ if (route === '/google_calendar/sync_data') {
+ assert.step(route);
+ this.data['calendar.event'].records.push(
+ {id: 7, user_id: 4, partner_id: 4, name: "event from google calendar", start: "2016-12-28 15:55:05", stop: "2016-12-29 18:55:05", allday: false, partner_ids: [], type: 2}
+ );
+ return $.when({status: 'need_refresh'});
+ } else if (route === '/google_calendar/static/src/img/calendar_32.png') {
+ return $.when();
+ } else if (route === '/web/dataset/call_kw/calendar.event/search_read') {
+ assert.step(route);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the month");
+
+ var $sidebar = calendar.$('.o_calendar_sidebar');
+
+ calendar.$('.o_google_sync_button').click();
+
+ assert.verifySteps([
+ '/web/dataset/call_kw/calendar.event/search_read',
+ '/google_calendar/sync_data',
+ '/web/dataset/call_kw/calendar.event/search_read',
+ ], 'should do a search_read before and after the call to sync_data');
+
+ assert.strictEqual(calendar.$('.fc-event').length, 3, "should now display 3 events on the month");
+
+ calendar.destroy();
+ });
+});
+});
diff --git a/addons/google_calendar/views/google_calendar_templates.xml b/addons/google_calendar/views/google_calendar_templates.xml
index 6f8924e4af8..4ff9df7f453 100644
--- a/addons/google_calendar/views/google_calendar_templates.xml
+++ b/addons/google_calendar/views/google_calendar_templates.xml
@@ -6,4 +6,10 @@
<script type="text/javascript" src="/google_calendar/static/src/js/google_calendar.js"></script>
</xpath>
</template>
+
+ <template id="qunit_suite" name="google_calendar_tests" inherit_id="web.qunit_suite">
+ <xpath expr="//t[@t-set='head']" position="inside">
+ <script type="text/javascript" src="/google_calendar/static/tests/google_calendar_tests.js"></script>
+ </xpath>
+ </template>
</odoo>
diff --git a/addons/google_drive/models/google_drive.py b/addons/google_drive/models/google_drive.py
index e653985f20a..d9200f1e2fd 100644
--- a/addons/google_drive/models/google_drive.py
+++ b/addons/google_drive/models/google_drive.py
@@ -158,7 +158,7 @@ class GoogleDrive(models.Model):
# check if a model is configured with a template
configs = self.search([('model_id', '=', res_model)])
config_values = []
- for config in configs:
+ for config in configs.sudo():
if config.filter_id:
if config.filter_id.user_id and config.filter_id.user_id.id != self.env.user.id:
#Private
diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml
index 0e1125bae84..e910db920e6 100644
--- a/addons/hr/views/hr_views.xml
+++ b/addons/hr/views/hr_views.xml
@@ -52,7 +52,7 @@
<field name="name" placeholder="Employee's Name"/>
</h1>
</div>
- <field name="category_ids" widget="many2many_tags" placeholder="e.g. Part Time" options="{'no_create_edit': True}" groups="hr.group_hr_user"/>
+ <field name="category_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}" placeholder="e.g. Part Time" groups="hr.group_hr_user"/>
<notebook>
<page name="public" string="Work Information">
<group>
@@ -175,7 +175,7 @@
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
- <field name="category_ids"/>
+ <field name="category_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<div class="o_kanban_image">
<img t-att-src="kanban_image('hr.employee', 'image_medium', record.id.value)"/>
<div class="o_employee_summary_icons">
diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml
index f0374c92a05..cc9d9fd1c23 100644
--- a/addons/hr_expense/views/hr_expense_views.xml
+++ b/addons/hr_expense/views/hr_expense_views.xml
@@ -299,7 +299,7 @@
</group>
<group string="Accounting">
<field name="property_account_expense_id"/>
- <field name="supplier_taxes_id" widget="many2many_tags"/>
+ <field name="supplier_taxes_id" widget="many2many_tags" options="{'color_field': 'color'}"/>
</group>
</group>
</sheet>
@@ -464,7 +464,7 @@
<field name="message_unread" invisible="1"/>
<field name="attachment_number" string=" "/>
<button name="action_get_attachment_view" string="View Attachments" type="object" icon="fa-paperclip"/>
- <field name="tax_ids" widget="many2many_tags"/>
+ <field name="tax_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="total_amount" sum="Total Amount" widget="monetary"/>
</tree>
</field>
diff --git a/addons/hr_expense_check/__manifest__.py b/addons/hr_expense_check/__manifest__.py
index 90320ac6f64..8fe469b5900 100644
--- a/addons/hr_expense_check/__manifest__.py
+++ b/addons/hr_expense_check/__manifest__.py
@@ -6,7 +6,6 @@
'description': """
Print amount in words on checks issued for expenses
""",
- 'category': 'Accounting &amp; Finance',
'version': '1.0',
'depends': ['account_check_printing', 'hr_expense'],
'auto_install': True,
diff --git a/addons/hr_recruitment/views/hr_recruitment_views.xml b/addons/hr_recruitment/views/hr_recruitment_views.xml
index 01faec3975a..0403ae64501 100644
--- a/addons/hr_recruitment/views/hr_recruitment_views.xml
+++ b/addons/hr_recruitment/views/hr_recruitment_views.xml
@@ -112,7 +112,7 @@
</div>
<div>
<label for="categ_ids" class="oe_edit_only"/>
- <field name="categ_ids" widget="many2many_tags" options="{'no_create_edit': True}"/>
+ <field name="categ_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
</div>
</h2>
</div>
@@ -255,7 +255,6 @@
<field name="type_id"/>
<field name="job_id"/>
<field name="department_id"/>
- <field name="categ_ids"/>
<field name="message_needaction_counter"/>
<field name="attachment_number"/>
<field name="active"/>
@@ -283,7 +282,7 @@
</ul>
</div>
<div class="oe_kanban_content" tooltip="kanban-tooltip">
- <field name="categ_ids"/>
+ <field name="categ_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<b t-if="record.partner_name.raw_value">
<field name="partner_name"/>
</b><t t-else="1">
diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py
index a2b7a9c25e4..0fc7aa314bf 100644
--- a/addons/hr_timesheet/models/project.py
+++ b/addons/hr_timesheet/models/project.py
@@ -48,3 +48,13 @@ class Task(models.Model):
timesheet_ids = fields.One2many('account.analytic.line', 'task_id', 'Timesheets')
_constraints = [(models.BaseModel._check_recursion, 'Circular references are not permitted between tasks and sub-tasks', ['parent_id'])]
+
+
+ @api.model
+ def create(self, vals):
+ context = dict(self.env.context)
+ # Remove default_parent_id to avoid a confusion in get_record_data
+ if context.get('default_parent_id', False):
+ vals['parent_id'] = context.pop('default_parent_id', None)
+ task = super(Task, self.with_context(context)).create(vals)
+ return task \ No newline at end of file
diff --git a/addons/l10n_fr_certification/__manifest__.py b/addons/l10n_fr_certification/__manifest__.py
index e83e9102914..7e2ea327d54 100644
--- a/addons/l10n_fr_certification/__manifest__.py
+++ b/addons/l10n_fr_certification/__manifest__.py
@@ -9,7 +9,7 @@
""",
'depends': ['l10n_fr'],
'installable': True,
- 'auto_install': True,
+ 'auto_install': False,
'application': True,
'data': [
'views/no_cancel.xml',
diff --git a/addons/l10n_fr_certification/models/account.py b/addons/l10n_fr_certification/models/account.py
index ecb0f5aff8b..bef0880d7ca 100644
--- a/addons/l10n_fr_certification/models/account.py
+++ b/addons/l10n_fr_certification/models/account.py
@@ -102,7 +102,7 @@ class AccountMove(models.Model):
"""
moves = self.search([('state', '=', 'posted'),
('company_id', '=', company_id),
- ('l10n_fr_secure_sequence_number', '!=', False)],
+ ('l10n_fr_secure_sequence_number', '!=', 0)],
order="l10n_fr_secure_sequence_number ASC")
if not moves:
diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js
index 5fd383fee18..691d1ee968d 100644
--- a/addons/mail/static/src/js/client_action.js
+++ b/addons/mail/static/src/js/client_action.js
@@ -221,7 +221,9 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
this.$buttons.on('click', '.o_mail_chat_button_mark_read', function () {
chat_manager.mark_all_as_read(self.channel, self.domain);
});
- this.$buttons.on('click', '.o_mail_chat_button_unstar_all', chat_manager.unstar_all);
+ this.$buttons.on('click', '.o_mail_chat_button_unstar_all', function () {
+ chat_manager.unstar_all();
+ });
this.thread.on('redirect', this, function (res_model, res_id) {
chat_manager.redirect(res_model, res_id, this.set_channel.bind(this));
diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml
index 4bb973aad1c..b66903adabf 100644
--- a/addons/maintenance/views/maintenance_views.xml
+++ b/addons/maintenance/views/maintenance_views.xml
@@ -710,7 +710,7 @@
</div>
<group>
<group>
- <field name="member_ids" widget="many2many_tags"/>
+ <field name="member_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</group>
</group>
</sheet>
diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py
index 37d40718516..7142cb5967c 100644
--- a/addons/mass_mailing/models/mass_mailing.py
+++ b/addons/mass_mailing/models/mass_mailing.py
@@ -434,15 +434,14 @@ class MassMailing(models.Model):
def _compute_next_departure(self):
cron_next_call = self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo().nextcall
+ str2dt = fields.Datetime.from_string
+ cron_time = str2dt(cron_next_call)
for mass_mailing in self:
- schedule_date = mass_mailing.schedule_date
- if schedule_date:
- if datetime.now() > fields.Datetime.from_string(schedule_date):
- mass_mailing.next_departure = cron_next_call
- else:
- mass_mailing.next_departure = schedule_date
+ if mass_mailing.schedule_date:
+ schedule_date = str2dt(mass_mailing.schedule_date)
+ mass_mailing.next_departure = max(schedule_date, cron_time)
else:
- mass_mailing.next_departure = cron_next_call
+ mass_mailing.next_departure = cron_time
@api.onchange('mass_mailing_campaign_id')
def _onchange_mass_mailing_campaign_id(self):
diff --git a/addons/mass_mailing/views/mass_mailing_views.xml b/addons/mass_mailing/views/mass_mailing_views.xml
index 17d9818077c..d9c07aff354 100644
--- a/addons/mass_mailing/views/mass_mailing_views.xml
+++ b/addons/mass_mailing/views/mass_mailing_views.xml
@@ -423,7 +423,7 @@
context="{'search_default_not_opt_out':1}"/>
<div attrs="{'invisible': [('mailing_model', '!=', 'mail.mass_mailing.list')]}">
<label for="contact_list_ids" string="Select mailing lists:" class="oe_edit_only"/>
- <field name="contact_list_ids" widget="many2many_tags"
+ <field name="contact_list_ids" widget="many2many_tags" options="{'color_field': 'color'}"
placeholder="Select mailing lists..." class="oe_inline"/>
</div>
</div>
@@ -758,7 +758,7 @@
<group>
<field name="name" string="Campaign Name"/>
<field name="user_id"/>
- <field name="tag_ids" widget="many2many_tags" options="{'no_create_edit': True}"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
<field name="unique_ab_testing" groups="base.group_no_one"/>
</group>
<notebook>
@@ -817,11 +817,11 @@
</ul>
</div>
<div class="oe_kanban_content">
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<div>
<img t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)"
t-att-title="record.user_id.value" width="24" height="24" class="oe_kanban_avatar pull-right"/>
<h3 class="oe_margin_bottom_8"><field name="name"/></h3>
- <field name="tag_ids"/>
<a name="%(action_view_mass_mailings_from_campaign)d" type="action"
class="oe_mailings">
<h4 class="oe_margin_top_8"><t t-raw="record.mass_mailing_ids.raw_value.length"/> Mailings</h4>
diff --git a/addons/membership/views/product_views.xml b/addons/membership/views/product_views.xml
index 909ed782082..988938a1d8d 100644
--- a/addons/membership/views/product_views.xml
+++ b/addons/membership/views/product_views.xml
@@ -98,7 +98,7 @@
<field name="list_price" string="Membership Fee"/>
<field
name="property_account_income_id"/>
- <field name="taxes_id" widget="many2many_tags" string="Taxes"/>
+ <field name="taxes_id" widget="many2many_tags" options="{'color_field': 'color'}" string="Taxes"/>
</group>
</group>
<label for="description"/>
diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py
index 24bc802ec7b..228bbd19082 100644
--- a/addons/mrp/models/mrp_workorder.py
+++ b/addons/mrp/models/mrp_workorder.py
@@ -282,7 +282,7 @@ class MrpWorkorder(models.Model):
for move_lot in self.active_move_lot_ids:
# Check if move_lot already exists
if move_lot.quantity_done <= 0: # rounding...
- move_lot.unlink()
+ move_lot.sudo().unlink()
continue
if not move_lot.lot_id:
raise UserError(_('You should provide a lot for a component'))
@@ -291,7 +291,7 @@ class MrpWorkorder(models.Model):
if lots:
lots[0].quantity_done += move_lot.quantity_done
lots[0].lot_produced_id = self.final_lot_id.id
- move_lot.unlink()
+ move_lot.sudo().unlink()
else:
move_lot.lot_produced_id = self.final_lot_id.id
move_lot.done_wo = True
diff --git a/addons/mrp/models/stock_quant.py b/addons/mrp/models/stock_quant.py
index e94b786f568..6074735a737 100644
--- a/addons/mrp/models/stock_quant.py
+++ b/addons/mrp/models/stock_quant.py
@@ -11,3 +11,9 @@ class StockQuant(models.Model):
'stock.quant', 'stock_quant_consume_rel', 'produce_quant_id', 'consume_quant_id')
produced_quant_ids = fields.Many2many(
'stock.quant', 'stock_quant_consume_rel', 'consume_quant_id', 'produce_quant_id')
+
+ def _prepare_history(self):
+ vals = super(StockQuant, self)._prepare_history()
+ vals['consumed_quant_ids'] = [(4, quant.id) for quant in self.consumed_quant_ids]
+ vals['produced_quant_ids'] = [(4, quant.id) for quant in self.produced_quant_ids]
+ return vals
diff --git a/addons/mrp/views/mrp_bom_views.xml b/addons/mrp/views/mrp_bom_views.xml
index 4449a383e64..531587446a3 100644
--- a/addons/mrp/views/mrp_bom_views.xml
+++ b/addons/mrp/views/mrp_bom_views.xml
@@ -47,7 +47,7 @@
<button name="action_see_attachments" type="object" icon="fa-file" attrs="{'invisible': [('has_attachments', '=', True)]}"/>
<field name="product_qty"/>
<field name="product_uom_id" options="{'no_open':True,'no_create':True}" groups="product.group_uom"/>
- <field name="attribute_value_ids" widget="many2many_tags" domain="[('product_ids.product_tmpl_id', '=', parent.product_tmpl_id)]" options="{'no_create':True}" groups="product.group_product_variant"/>
+ <field name="attribute_value_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create': True}" domain="[('product_ids.product_tmpl_id', '=', parent.product_tmpl_id)]" groups="product.group_product_variant"/>
<field name="operation_id" groups="mrp.group_mrp_routings" domain="[('routing_id', '=', parent.routing_id)]" options="{'no_quick_create':True,'no_create_edit':True}"/>
</tree>
</field>
diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py
index eebf8d29962..463c30c0a7c 100644
--- a/addons/mrp/wizard/change_production_qty.py
+++ b/addons/mrp/wizard/change_production_qty.py
@@ -22,7 +22,7 @@ class ChangeProductionQty(models.TransientModel):
if 'mo_id' in fields and not res.get('mo_id') and self._context.get('active_model') == 'mrp.production' and self._context.get('active_id'):
res['mo_id'] = self._context['active_id']
if 'product_qty' in fields and not res.get('product_qty') and res.get('mo_id'):
- res['product_qty'] = self.env['mrp.production'].browse(res.get['mo_id']).product_qty
+ res['product_qty'] = self.env['mrp.production'].browse(res['mo_id']).product_qty
return res
@api.model
diff --git a/addons/mrp/wizard/change_production_qty_views.xml b/addons/mrp/wizard/change_production_qty_views.xml
index 0ba2fc8488e..f731d2cecc1 100644
--- a/addons/mrp/wizard/change_production_qty_views.xml
+++ b/addons/mrp/wizard/change_production_qty_views.xml
@@ -10,6 +10,7 @@
<form string="Change Product Qty">
<group>
<field name="product_qty"/>
+ <field name="mo_id" invisible="1"/>
</group>
<footer>
<button name="change_prod_qty" string="Approve"
diff --git a/addons/mrp_repair/views/mrp_repair_views.xml b/addons/mrp_repair/views/mrp_repair_views.xml
index 53cb9e7843a..e62a411098a 100644
--- a/addons/mrp_repair/views/mrp_repair_views.xml
+++ b/addons/mrp_repair/views/mrp_repair_views.xml
@@ -89,7 +89,7 @@
</div>
<field name="type" invisible="1"/>
<field name="price_unit"/>
- <field name="tax_id" widget="many2many_tags" domain="[('type_tax_use','=','sale')]"/>
+ <field name="tax_id" widget="many2many_tags" options="{'color_field': 'color'}" domain="[('type_tax_use','=','sale')]"/>
<field name="invoiced" invisible="1"/>
<field name="price_subtotal" widget="monetary" invisible="1"/>
</group>
@@ -117,7 +117,7 @@
<field name="product_uom_qty" string="Quantity"/>
<field name="product_uom" string="Unit of Measure" groups="product.group_uom"/>
<field name="price_unit"/>
- <field name="tax_id" widget="many2many_tags"/>
+ <field name="tax_id" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="price_subtotal" widget="monetary"/>
</tree>
</field>
@@ -146,7 +146,7 @@
<field name="product_uom" groups="product.group_uom"/>
</div>
<field name="price_unit"/>
- <field widget="many2many_tags" name="tax_id" domain="[('type_tax_use','=','sale')]"/>
+ <field widget="many2many_tags" options="{'color_field': 'color'}" name="tax_id" domain="[('type_tax_use','=','sale')]"/>
<field name="price_subtotal" widget="monetary" invisible="1"/>
</group>
</form>
@@ -156,7 +156,7 @@
<field name="product_uom_qty" string="Quantity"/>
<field name="product_uom" string="Unit of Measure" groups="product.group_uom"/>
<field name="price_unit"/>
- <field name="tax_id" domain="[('type_tax_use', '=', 'sale')]" widget="many2many_tags"/>
+ <field name="tax_id" domain="[('type_tax_use', '=', 'sale')]" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="price_subtotal" widget="monetary"/>
</tree>
</field>
diff --git a/addons/note/views/note_views.xml b/addons/note/views/note_views.xml
index 83f658acd06..0373dd330a5 100644
--- a/addons/note/views/note_views.xml
+++ b/addons/note/views/note_views.xml
@@ -87,7 +87,6 @@
<field name="memo"/>
<field name="date_done"/>
<field name="message_partner_ids"/>
- <field name="tag_ids"/>
<field name="activity_ids" />
<field name="activity_state" />
<templates>
@@ -95,7 +94,7 @@
<div t-attf-class="#{kanban_color(record.color.raw_value)} oe_kanban_global_click_edit oe_semantic_html_override oe_kanban_card">
- <field name="tag_ids"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<div class="o_dropdown_kanban dropdown">
<a class="dropdown-toggle btn" data-toggle="dropdown" href="#" >
@@ -143,7 +142,7 @@
<field name="name"/>
<field name="open"/>
<field name="stage_id"/>
- <field name="tag_ids" widget="many2many_tags"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</tree>
</field>
</record>
@@ -158,7 +157,7 @@
<field name="stage_id" domain="[('user_id','=',uid)]" widget="statusbar" clickable="1"/>
</header>
<sheet>
- <field name="tag_ids" widget="many2many_tags" placeholder="Tags"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" placeholder="Tags"/>
<field name="memo" widget="html" class="oe_memo" editor_height="450px" />
</sheet>
<div class="oe_chatter">
diff --git a/addons/payment/views/account_payment_views.xml b/addons/payment/views/account_payment_views.xml
index a284f69b77f..dd9a5400be3 100644
--- a/addons/payment/views/account_payment_views.xml
+++ b/addons/payment/views/account_payment_views.xml
@@ -19,8 +19,9 @@
<field name="arch" type="xml">
<field name="payment_method_id" position="after">
<field name="payment_method_code" invisible="1"/>
- <field name="payment_token_id" attrs="{'invisible': [('payment_method_code', '!=', 'electronic')],
- 'required': [('payment_method_code', '=', 'electronic')]}"/>
+ <field name="payment_token_id" options="{'no_create': True}"
+ attrs="{'invisible': [('payment_method_code', '!=', 'electronic')],
+ 'required': [('payment_method_code', '=', 'electronic')]}"/>
</field>
</field>
</record>
diff --git a/addons/payment/views/payment_views.xml b/addons/payment/views/payment_views.xml
index deaf1dee0bd..d6bce864840 100644
--- a/addons/payment/views/payment_views.xml
+++ b/addons/payment/views/payment_views.xml
@@ -245,7 +245,7 @@
<group>
<field name="acquirer_id"/>
<field name="provider" invisible="1"/>
- <field name="payment_token_id"/>
+ <field name="payment_token_id" options="{'no_create': True}"/>
<field name="acquirer_reference" readonly="1"/>
<field name="create_date"/>
<field name="date_validate"/>
diff --git a/addons/payment_authorize/views/payment_views.xml b/addons/payment_authorize/views/payment_views.xml
index 7c464bc24f4..7a957e41442 100644
--- a/addons/payment_authorize/views/payment_views.xml
+++ b/addons/payment_authorize/views/payment_views.xml
@@ -9,7 +9,7 @@
<group attrs="{'invisible': [('provider', '!=', 'authorize')]}">
<field name="authorize_login"/>
<field name="authorize_transaction_key" password="True"/>
- <a colspan="2" href="http://welcome.authorize.net/about_authorizenet/" target="_blank">How to configure your Authorize.Net account (look for Getting Started Guide) ?</a>
+ <a colspan="2" href="https://www.odoo.com/documentation/user/online/ecommerce/shopper_experience/authorize.html" target="_blank">How to get paid with Authorize.Net</a>
</group>
</xpath>
</field>
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index ec73843481a..5f94e7280fa 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -164,6 +164,7 @@ class PosOrder(models.Model):
'origin': self.name,
'account_id': self.partner_id.property_account_receivable_id.id,
'journal_id': self.session_id.config_id.invoice_journal_id.id,
+ 'company_id': self.company_id.id,
'type': 'out_invoice',
'reference': self.name,
'partner_id': self.partner_id.id,
@@ -881,21 +882,14 @@ class PosOrderLine(models.Model):
@api.depends('price_unit', 'tax_ids', 'qty', 'discount', 'product_id')
def _compute_amount_line_all(self):
for line in self:
- currency = line.order_id.pricelist_id.currency_id
- taxes = line.tax_ids.filtered(lambda tax: tax.company_id.id == line.order_id.company_id.id)
- fiscal_position_id = line.order_id.fiscal_position_id
- if fiscal_position_id:
- taxes = fiscal_position_id.map_tax(taxes, line.product_id, line.order_id.partner_id)
- price = self.env['account.tax']._fix_tax_included_price(
- line.price_unit * (1 - (line.discount or 0.0) / 100.0), line.product_id.taxes_id, taxes)
- line.price_subtotal = line.price_subtotal_incl = price * line.qty
- if taxes:
- taxes = taxes.compute_all(price, currency, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)
- line.price_subtotal = taxes['total_excluded']
- line.price_subtotal_incl = taxes['total_included']
-
- line.price_subtotal = currency.round(line.price_subtotal)
- line.price_subtotal_incl = currency.round(line.price_subtotal_incl)
+ fpos = line.order_id.fiscal_position_id
+ tax_ids_after_fiscal_position = fpos.map_tax(line.tax_ids, line.product_id, line.order_id.partner_id) if fpos else line.tax_ids
+ price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
+ taxes = tax_ids_after_fiscal_position.compute_all(price, line.order_id.pricelist_id.currency_id, line.qty, product=line.product_id, partner=line.order_id.partner_id)
+ line.update({
+ 'price_subtotal_incl': taxes['total_included'],
+ 'price_subtotal': taxes['total_excluded'],
+ })
@api.onchange('product_id')
def _onchange_product_id(self):
@@ -907,8 +901,10 @@ class PosOrderLine(models.Model):
price = self.order_id.pricelist_id.get_product_price(
self.product_id, self.qty or 1.0, self.order_id.partner_id)
self._onchange_qty()
- self.price_unit = price
- self.tax_ids = self.product_id.taxes_id
+ self.tax_ids = self.product_id.taxes_id.filtered(lambda r: not self.company_id or r.company_id == self.company_id)
+ fpos = self.order_id.fiscal_position_id
+ tax_ids_after_fiscal_position = fpos.map_tax(self.tax_ids, line.product_id, line.order_id.partner_id) if fpos else self.tax_ids
+ self.price_unit = self.env['account.tax']._fix_tax_included_price(price, self.product_id.taxes_id, tax_ids_after_fiscal_position)
@api.onchange('qty', 'discount', 'price_unit', 'tax_ids')
def _onchange_qty(self):
diff --git a/addons/point_of_sale/views/pos_order_view.xml b/addons/point_of_sale/views/pos_order_view.xml
index 8dac1cf74fd..bc4a3fa8df8 100644
--- a/addons/point_of_sale/views/pos_order_view.xml
+++ b/addons/point_of_sale/views/pos_order_view.xml
@@ -37,8 +37,8 @@
<field name="qty"/>
<field name="price_unit" widget="monetary"/>
<field name="discount" widget="monetary"/>
- <field name="tax_ids_after_fiscal_position" widget="many2many_tags"/>
- <field name="tax_ids" widget="many2many_tags" invisible="1"/>
+ <field name="tax_ids_after_fiscal_position" widget="many2many_tags" options="{'color_field': 'color'}"/>
+ <field name="tax_ids" widget="many2many_tags" options="{'color_field': 'color'}" invisible="1"/>
<field name="price_subtotal" widget="monetary"/>
<field name="price_subtotal_incl" widget="monetary"/>
</tree>
@@ -50,8 +50,8 @@
<field name="price_unit" widget="monetary"/>
<field name="price_subtotal" invisible="1" widget="monetary"/>
<field name="price_subtotal_incl" invisible="1" widget="monetary"/>
- <field name="tax_ids_after_fiscal_position" widget="many2many_tags"/>
- <field name="tax_ids" widget="many2many_tags" invisible="1"/>
+ <field name="tax_ids_after_fiscal_position" widget="many2many_tags" options="{'color_field': 'color'}"/>
+ <field name="tax_ids" widget="many2many_tags" options="{'color_field': 'color'}" invisible="1"/>
<field name="notice"/>
</group>
</form>
diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py
index 100b59c272b..1564cf80c07 100644
--- a/addons/product/models/product_template.py
+++ b/addons/product/models/product_template.py
@@ -21,7 +21,7 @@ class ProductTemplate(models.Model):
if self._context.get('categ_id') or self._context.get('default_categ_id'):
return self._context.get('categ_id') or self._context.get('default_categ_id')
category = self.env.ref('product.product_category_all', raise_if_not_found=False)
- return category and category.id or False
+ return category.id if category.type == 'normal' else False
def _get_default_uom_id(self):
return self.env["product.uom"].search([], limit=1, order='id').id
diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml
index 34908f9255f..0ebe971be03 100644
--- a/addons/project/views/project_views.xml
+++ b/addons/project/views/project_views.xml
@@ -406,7 +406,7 @@ action = model.activate_sample_project()
</group>
<group>
<field name="date_deadline"/>
- <field name="tag_ids" widget="many2many_tags" options="{'no_create_edit': True}"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
</group>
</group>
<notebook>
@@ -458,7 +458,6 @@ action = model.activate_sample_project()
<field name="remaining_hours" groups="project.group_time_work_estimation_tasks"/>
<field name="date_deadline"/>
<field name="message_needaction_counter"/>
- <field name="tag_ids"/>
<field name="attachment_ids"/>
<field name="displayed_image_id"/>
<field name="active"/>
@@ -484,7 +483,7 @@ action = model.activate_sample_project()
</div>
<div class="oe_kanban_content">
<div>
- <field name="tag_ids"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</div>
<div>
<strong><field name="name"/></strong>
diff --git a/addons/project_issue/views/project_issue_views.xml b/addons/project_issue/views/project_issue_views.xml
index a0d49f1ab75..93b34d49a59 100644
--- a/addons/project_issue/views/project_issue_views.xml
+++ b/addons/project_issue/views/project_issue_views.xml
@@ -25,7 +25,7 @@
<group>
<field name="user_id"/>
<field name="priority" groups="base.group_user" widget="priority"/>
- <field name="tag_ids" widget="many2many_tags"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</group>
<group>
<field name="partner_id"/>
@@ -200,7 +200,7 @@
</ul>
</div>
<div class="oe_kanban_content" tooltip="kanban-tooltip">
- <field name="tag_ids"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<div>
<b><field name="name"/></b> <br/>
<field name="partner_id"/> <br/>
diff --git a/addons/purchase/views/purchase_views.xml b/addons/purchase/views/purchase_views.xml
index db703d92698..58daee24fdf 100644
--- a/addons/purchase/views/purchase_views.xml
+++ b/addons/purchase/views/purchase_views.xml
@@ -218,13 +218,13 @@
<field name="date_planned"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="account_analytic_id" context="{'default_partner_id':parent.partner_id}" groups="purchase.group_analytic_accounting"/>
- <field name="analytic_tag_ids" groups="purchase.group_analytic_accounting" widget="many2many_tags"/>
+ <field name="analytic_tag_ids" groups="purchase.group_analytic_accounting" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="product_qty"/>
<field name="qty_received" invisible="not context.get('show_purchase', True)"/>
<field name="qty_invoiced" invisible="not context.get('show_purchase', True)"/>
<field name="product_uom" groups="product.group_uom" attrs="{'readonly': [('state', 'in', ('purchase', 'done', 'cancel'))]}"/>
<field name="price_unit"/>
- <field name="taxes_id" widget="many2many_tags" domain="[('type_tax_use','=','purchase')]" context="{'default_type_tax_use': 'purchase'}" options="{'no_create': True'}"/>
+ <field name="taxes_id" widget="many2many_tags" domain="[('type_tax_use','=','purchase')]" context="{'default_type_tax_use': 'purchase'}" options="{'no_create': True', 'color_field': 'color'}"/>
<field name="price_subtotal" widget="monetary"/>
</tree>
<form string="Purchase Order Line">
@@ -242,7 +242,7 @@
<field name="price_unit"/>
</group>
<group>
- <field name="taxes_id" widget="many2many_tags" domain="[('type_tax_use', '=', 'purchase')]" options="{'no_create': True'}"/>
+ <field name="taxes_id" widget="many2many_tags" domain="[('type_tax_use', '=', 'purchase')]" options="{'no_create': True', 'color_field': 'color'}"/>
<field name="date_planned" widget="date"/>
<field name="account_analytic_id" colspan="2" groups="purchase.group_analytic_accounting"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
@@ -614,7 +614,7 @@
<field name="property_account_expense_id" domain="[('deprecated','=',False)]" attrs="{'readonly': [('purchase_ok', '=', 0)]}"/>
</field>
<field name='supplier_taxes_id' position="replace" >
- <field name="supplier_taxes_id" colspan="2" widget="many2many_tags" attrs="{'readonly':[('purchase_ok','=',0)]}"/>
+ <field name="supplier_taxes_id" colspan="2" widget="many2many_tags" options="{'color_field': 'color'}" attrs="{'readonly':[('purchase_ok','=',0)]}"/>
</field>
</field>
</record>
diff --git a/addons/rating/controllers/main.py b/addons/rating/controllers/main.py
index c167a980e72..817f26aa79e 100644
--- a/addons/rating/controllers/main.py
+++ b/addons/rating/controllers/main.py
@@ -28,7 +28,7 @@ class Rating(http.Controller):
'rate_name': rate_names[rate], 'rate': rate
})
- @http.route(['/rating/<string:token>/<int:rate>/submit_feedback'], type="http", auth="public", method=['post'])
+ @http.route(['/rating/<string:token>/<int:rate>/submit_feedback'], type="http", auth="public", methods=['post'])
def submit_rating(self, token, rate, **kwargs):
rating = request.env['rating.rating'].sudo().search([('access_token', '=', token)])
if not rating:
diff --git a/addons/sale/__init__.py b/addons/sale/__init__.py
index de3c67453a7..42f44d32a03 100644
--- a/addons/sale/__init__.py
+++ b/addons/sale/__init__.py
@@ -4,3 +4,21 @@
from . import models
from . import report
from . import wizard
+
+from functools import partial
+import odoo
+from odoo import api, SUPERUSER_ID
+
+
+
+def uninstall_hook(cr, registry):
+ def update_dashboard_graph_model(dbname):
+ db_registry = odoo.modules.registry.Registry.new(dbname)
+ with api.Environment.manage(), db_registry.cursor() as cr:
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ if 'crm.team' in env:
+ recs = env['crm.team'].search([])
+ for rec in recs:
+ rec._onchange_team_type()
+
+ cr.after("commit", partial(update_dashboard_graph_model, cr.dbname)) \ No newline at end of file
diff --git a/addons/sale/__manifest__.py b/addons/sale/__manifest__.py
index 8c92979cbff..cd6d3322e80 100644
--- a/addons/sale/__manifest__.py
+++ b/addons/sale/__manifest__.py
@@ -32,6 +32,7 @@ This module contains all the common features of Sales Management and eCommerce.
'data/sale_demo.xml',
'data/product_product_demo.xml',
],
+ 'uninstall_hook': "uninstall_hook",
'css': ['static/src/css/sale.css'],
'installable': True,
'auto_install': False,
diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml
index 0daf25f7ffe..09d80194186 100644
--- a/addons/sale/views/sale_views.xml
+++ b/addons/sale/views/sale_views.xml
@@ -238,7 +238,7 @@
</div>
</group>
<group>
- <field name="tax_id" widget="many2many_tags" domain="[('type_tax_use','=','sale'),('company_id','=',parent.company_id)]"
+ <field name="tax_id" widget="many2many_tags" options="{'color_field': 'color'}" domain="[('type_tax_use','=','sale'),('company_id','=',parent.company_id)]"
attrs="{'readonly': [('qty_invoiced', '&gt;', 0)]}" options="{'no_create': True}"/>
<label for="customer_lead"/>
<div>
@@ -246,7 +246,7 @@
</div>
<label for="analytic_tag_ids"/>
<div>
- <field name="analytic_tag_ids" widget="many2many_tags"/>
+ <field name="analytic_tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</div>
</group>
</group>
@@ -279,10 +279,10 @@
attrs="{'readonly': [('state', 'in', ('sale','done', 'cancel'))]}"
context="{'company_id': parent.company_id}"
groups="product.group_uom" options='{"no_open": True}'/>
- <field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags"/>
+ <field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="price_unit"
attrs="{'readonly': [('qty_invoiced', '&gt;', 0)]}"/>
- <field name="tax_id" widget="many2many_tags" domain="[('type_tax_use','=','sale'),('company_id','=',parent.company_id)]"
+ <field name="tax_id" widget="many2many_tags" options="{'color_field': 'color'}" domain="[('type_tax_use','=','sale'),('company_id','=',parent.company_id)]"
attrs="{'readonly': [('qty_invoiced', '&gt;', 0)]}" options="{'no_create': True}"/>
<field name="discount" groups="sale.group_discount_per_so_line"/>
<field name="price_subtotal" widget="monetary" groups="sale.group_show_price_subtotal"/>
diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py
index e100d32f3ee..18d35c3bb15 100644
--- a/addons/stock/models/stock_quant.py
+++ b/addons/stock/models/stock_quant.py
@@ -317,12 +317,15 @@ class Quant(models.Model):
# price update + accounting entries adjustments
solved_quants._price_update(solving_quant.cost)
# merge history (and cost?)
- solved_quants.write({
- 'history_ids': [(4, history_move.id) for history_move in solving_quant.history_ids]
- })
+ solved_quants.write(solving_quant._prepare_history())
solving_quant.with_context(force_unlink=True).unlink()
solving_quant = remaining_solving_quant
+ def _prepare_history(self):
+ return {
+ 'history_ids': [(4, history_move.id) for history_move in self.history_ids],
+ }
+
@api.multi
def _price_update(self, newprice):
# TDE note: use ACLs instead of sudoing everything
diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml
index eea0b3a74b2..fcdb6333817 100644
--- a/addons/stock/views/product_views.xml
+++ b/addons/stock/views/product_views.xml
@@ -128,7 +128,7 @@
<group name="inventory">
<group string="Operations" name="operations">
<field name="route_ids" widget="many2many_checkboxes" attrs="{'invisible':[('type', 'in', ['service', 'digital'])]}"/>
- <field name="route_from_categ_ids" widget="many2many_tags" readonly="1" attrs="{'invisible':[('route_from_categ_ids', '=', [])]}"/>
+ <field name="route_from_categ_ids" widget="many2many_tags" options="{'color_field': 'color'}" readonly="1" attrs="{'invisible':[('route_from_categ_ids', '=', [])]}"/>
<label for="sale_delay"/>
<div>
<field name="sale_delay" attrs="{'readonly':[('sale_ok','=',False)]}" class="oe_inline" style="vertical-align:baseline"/> days
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index e3265bd08a2..b06b654aefc 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -1016,6 +1016,8 @@ class Binary(http.Controller):
elif status != 200 and download:
return request.not_found()
+ height = int(height)
+ width = int(width)
if content and (width or height):
# resize maximum 500*500
if width > 500:
diff --git a/addons/web/static/src/js/apps.js b/addons/web/static/src/js/apps.js
index fcf21852d2c..fa513372269 100644
--- a/addons/web/static/src/js/apps.js
+++ b/addons/web/static/src/js/apps.js
@@ -59,7 +59,7 @@ var Apps = Widget.extend({
},
destroy: function() {
- $(window).off("message.apps");
+ $(window).off("message." + this.uniq);
if (this.$ifr) {
this.$ifr.remove();
this.$ifr = null;
@@ -83,13 +83,13 @@ var Apps = Widget.extend({
});
},
'rpc': function(m) {
- self.session.rpc.apply(self.session, m.args).then(function(r) {
+ return self._rpc({route: m.args[0], params: m.args[1]}).then(function(r) {
var w = self.$ifr[0].contentWindow;
w.postMessage({id: m.id, result: r}, client.origin);
});
},
'Model': function(m) {
- return this._rpc({model: m.model, method: m.method, args: m.args})
+ return self._rpc({model: m.model, method: m.args[0], args: m.args[1]})
.then(function(r) {
var w = self.$ifr[0].contentWindow;
w.postMessage({id: m.id, result: r}, client.origin);
@@ -114,10 +114,11 @@ var Apps = Widget.extend({
qs.debug = session.debug;
}
var u = $.param.querystring(client.origin + "/apps/embed/client", qs);
- var css = {width: '100%', height: '400px'};
+ var css = {width: '100%', height: '750px'};
self.$ifr = $('<iframe>').attr('src', u);
- $(window).on("message.apps", self.proxy('_on_message'));
+ self.uniq = _.uniqueId('apps');
+ $(window).on("message." + self.uniq, self.proxy('_on_message'));
self.on('message:ready', self, function(m) {
var w = this.$ifr[0].contentWindow;
@@ -125,8 +126,8 @@ var Apps = Widget.extend({
type: 'ir.actions.client',
tag: this.remote_action_tag,
params: _.extend({}, this.params, {
- db: this.session.db,
- origin: this.session.origin,
+ db: session.db,
+ origin: session.origin,
})
};
w.postMessage({type:'action', action: act}, client.origin);
@@ -145,7 +146,10 @@ var Apps = Widget.extend({
def.resolve();
}, function() {
self.do_warn(_t('Odoo Apps will be available soon'), _t('Showing locally available modules'), true);
- return session.rpc('/web/action/load', {action_id: self.failback_action_id}).then(function(action) {
+ return self._rpc({
+ route: '/web/action/load',
+ params: {action_id: self.failback_action_id},
+ }).then(function(action) {
return self.do_action(action);
}).always(function () {
def.reject();
diff --git a/addons/web/static/src/js/chrome/pager.js b/addons/web/static/src/js/chrome/pager.js
index cd16382be0e..bbe69c6d86a 100644
--- a/addons/web/static/src/js/chrome/pager.js
+++ b/addons/web/static/src/js/chrome/pager.js
@@ -144,7 +144,7 @@ var Pager = Widget.extend({
_edit: function () {
if (this.options.can_edit) {
var self = this;
- var $input = $('<input>', {type: 'text', value: this.$value.html()});
+ var $input = $('<input>', {class: 'o_input', type: 'text', value: this.$value.html()});
this.$value.html($input);
$input.focus();
diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js
index d0beb5bf3dd..11be6fa4e6e 100644
--- a/addons/web/static/src/js/chrome/view_manager.js
+++ b/addons/web/static/src/js/chrome/view_manager.js
@@ -567,11 +567,10 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
// Wrong default_* and search_default_* values will no give the expected result
// Wrong group_by values will simply fail and forbid rendering of the destination view
var ncontext = new Context(
- _.object(_.reject(_.pairs(self.env.context), function(pair) {
+ _.object(_.reject(_.pairs(_.extend({}, action_data.context, self.env.context)), function(pair) {
return pair[0].match('^(?:(?:default_|search_default_|show_).+|.+_view_ref|group_by|group_by_no_leaf|active_id|active_ids)$') !== null;
}))
);
- ncontext.add(action_data.context || {});
ncontext.add({active_model: self.env.modelName});
if (res_ids) {
ncontext.add({
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index 52e0aef77e4..4ae4eec9c05 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -273,7 +273,7 @@ var AbstractField = Widget.extend({
*/
reset: function (record, event) {
this._reset(record, event);
- return this._render();
+ return this._render() || $.when();
},
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 82970255b3e..1868a29a237 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -17,7 +17,6 @@ var dom = require('web.dom');
var Domain = require('web.Domain');
var DomainSelector = require('web.DomainSelector');
var DomainSelectorDialog = require('web.DomainSelectorDialog');
-var field_utils = require('web.field_utils');
var framework = require('web.framework');
var session = require('web.session');
var utils = require('web.utils');
@@ -476,15 +475,14 @@ var FieldMonetary = InputField.extend({
this._setCurrency();
- if (this.mode === 'edit' && this.currency) {
+ if (this.mode === 'edit') {
this.tagName = 'div';
this.className += ' o_input';
- }
- // use the formatFloat function in edit
- if (this.mode === 'edit') {
+ // use the formatFloat function in edit
this.formatType = 'float';
}
+
this.formatOptions.currency = this.currency;
this.formatOptions.digits = [16, 2];
},
@@ -514,20 +512,19 @@ var FieldMonetary = InputField.extend({
* @private
*/
_renderEdit: function () {
- if (!this.currency) {
- this._super.apply(this, arguments);
- return;
- }
-
this.$el.empty();
+
// Prepare and add the input
this._prepareInput().appendTo(this.$el);
- // prepare and add the currency symbol
- var $currencySymbol = $('<span>', {text: this.currency.symbol});
- if (this.currency.position === "after") {
- this.$el.append($currencySymbol);
- } else {
- this.$el.prepend($currencySymbol);
+
+ if (this.currency) {
+ // Prepare and add the currency symbol
+ var $currencySymbol = $('<span>', {text: this.currency.symbol});
+ if (this.currency.position === "after") {
+ this.$el.append($currencySymbol);
+ } else {
+ this.$el.prepend($currencySymbol);
+ }
}
},
/**
@@ -791,6 +788,23 @@ var FieldText = InputField.extend(TranslatableFieldMixin, {
}
return this._super();
},
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Stops the enter navigation in a text area.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onKeydown: function (ev) {
+ if (ev.which === $.ui.keyCode.ENTER) {
+ return;
+ }
+ this._super.apply(this, arguments);
+ },
});
/**
@@ -2237,6 +2251,7 @@ var AceEditor = DebouncedField.extend({
});
return {
+ TranslatableFieldMixin: TranslatableFieldMixin,
DebouncedField: DebouncedField,
FieldEmail: FieldEmail,
FieldBinaryFile: FieldBinaryFile,
diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js
index dcd8f7acd48..efafec6ba6a 100644
--- a/addons/web/static/src/js/fields/field_registry.js
+++ b/addons/web/static/src/js/fields/field_registry.js
@@ -64,6 +64,7 @@ registry
.add('many2many_checkboxes', relational_fields.FieldMany2ManyCheckBoxes)
.add('one2many', relational_fields.FieldOne2Many)
.add('statusbar', relational_fields.FieldStatus)
+ .add('reference', relational_fields.FieldReference)
.add('one2many_list', relational_fields.FieldOne2Many);
// Special fields
diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js
index 9a6734b7baf..ac4c06454ea 100644
--- a/addons/web/static/src/js/fields/field_utils.js
+++ b/addons/web/static/src/js/fields/field_utils.js
@@ -427,6 +427,52 @@ function parseFloat(value) {
return parsed;
}
+/**
+ * Parse a String containing currency symbol and returns amount
+ *
+ * @param {string} value
+ * The string to be parsed
+ * We assume that a monetary is always a pair (symbol, amount) separated
+ * by a non breaking space. A simple float can also be accepted as value
+ * @param {Object} [field]
+ * a description of the field (returned by fields_get for example).
+ * @param {Object} [options] additional options.
+ * @param {Object} [options.currency] - the description of the currency to use
+ * @param {integer} [options.currency_id]
+ * the id of the 'res.currency' to use (ignored if options.currency)
+ * @param {string} [options.currency_field]
+ * the name of the field whose value is the currency id
+ * (ignore if options.currency or options.currency_id)
+ * Note: if not given it will default to the field currency_field value
+ * or to 'currency_id'.
+ * @param {Object} [options.data]
+ * a mapping of field name to field value, required with
+ * options.currency_field
+ *
+ * @returns {float} the float value contained in the string representation
+ * @throws {Error} if no float is found or if parameter does not respect monetary condition
+ */
+function parseMonetary(value, field, options) {
+ var values = value.split('&nbsp;');
+ if (values.length === 1) {
+ return parseFloat(value);
+ }
+ else if (values.length !== 2) {
+ throw new Error(_.str.sprintf(core._t("'%s' is not a correct monetary field"), value));
+ }
+ options = options || {};
+ var currency = options.currency;
+ if (!currency) {
+ var currency_id = options.currency_id;
+ if (!currency_id && options.data) {
+ var currency_field = options.currency_field || field.currency_field || 'currency_id';
+ currency_id = options.data[currency_field] && options.data[currency_field].res_id;
+ }
+ currency = session.get_currency(currency_id);
+ }
+ return parseFloat(values[0] === currency.symbol ? values[1] : values[0]);
+}
+
function parseFloatTime(value) {
var factor = 1;
if (value[0] === '-') {
@@ -494,7 +540,7 @@ return {
many2one: formatMany2one,
monetary: formatMonetary,
one2many: formatX2Many,
- reference: _.identity, // todo
+ reference: formatMany2one,
selection: formatSelection,
text: formatChar,
},
@@ -510,9 +556,9 @@ return {
integer: parseInteger,
many2many: _.identity, // todo
many2one: parseMany2one,
- monetary: parseFloat,
+ monetary: parseMonetary,
one2many: _.identity,
- reference: _.identity, // todo
+ reference: parseMany2one,
selection: _.identity, // todo
text: _.identity, // todo
},
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 419d1f5f646..567c54abcb1 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -657,7 +657,14 @@ var FieldX2Many = AbstractField.extend({
reset: function (record, ev) {
if (ev && ev.target === this && ev.data.changes && this.view.arch.tag === 'tree') {
var command = ev.data.changes[this.name];
- if (command.operation === 'UPDATE') {
+ // Here, we only consider 'UPDATE' commands with data, which occur
+ // with editable list view. In order to keep the current line in
+ // edition, we call confirmChange which will reset the widgets
+ // instead of re-rendering the list. 'UPDATE' commands with no data
+ // can be ignored: they occur in one2manys when the record is
+ // updated from a dialog and in this case, we can re-render the
+ // whole subview.
+ if (command.operation === 'UPDATE' && command.data) {
var state = record.data[this.name];
var fieldNames = state.getFieldNames();
this.renderer.confirmChange(state, command.id, fieldNames, ev.initialEvent);
@@ -677,7 +684,7 @@ var FieldX2Many = AbstractField.extend({
*
* @override
* @private
- * @returns {Deferred}
+ * @returns {Deferred|undefined}
*/
_render: function () {
if (!this.view) {
@@ -1068,9 +1075,15 @@ var FieldOne2Many = FieldX2Many.extend({
// we don't want interference with the components upstream.
ev.stopPropagation();
+ var id = ev.data.id;
+ // trigger an empty 'UPDATE' operation when the user clicks on 'Save' in
+ // the dialog, to notify the main record that a subrecord of this
+ // relational field has changed (those changes will be already stored on
+ // that subrecord, thanks to the 'Save').
+ var onSaved = this._setValue.bind(this, { operation: 'UPDATE', id: id }, {});
this._openFormDialog({
- id: ev.data.id,
- on_saved: this._setValue.bind(this, { operation: 'NOOP' }, {}),
+ id: id,
+ on_saved: onSaved,
readonly: this.mode === 'readonly',
});
},
@@ -1354,7 +1367,6 @@ var FieldMany2ManyTags = AbstractField.extend({
'click .o_delete': '_onDeleteTag',
}),
fieldsToFetch: {
- color: {type: 'integer'},
display_name: {type: 'char'},
},
@@ -1367,6 +1379,8 @@ var FieldMany2ManyTags = AbstractField.extend({
if (this.mode === 'edit') {
this.className += ' o_input';
}
+
+ this.colorField = this.nodeOptions.color_field;
},
//--------------------------------------------------------------------------
@@ -1431,6 +1445,7 @@ var FieldMany2ManyTags = AbstractField.extend({
_getRenderTagsContext: function () {
var elements = this.value ? _.pluck(this.value.data, 'data') : [];
return {
+ colorField: this.colorField,
elements: elements,
readonly: this.mode === "readonly",
};
@@ -1558,7 +1573,7 @@ var FormFieldMany2ManyTags = FieldMany2ManyTags.extend({
_onOpenColorPicker: function (event) {
var tag_id = $(event.currentTarget).data('id');
var tag = _.findWhere(this.value.data, { res_id: tag_id });
- if (tag && 'color' in tag.data) { // if there is a color field on the related model
+ if (tag && this.colorField in tag.data) { // if there is a color field on the related model
this.$color_picker = $(qweb.render('FieldMany2ManyTag.colorpicker', {
'widget': this,
'tag_id': tag_id,
@@ -1583,11 +1598,11 @@ var FormFieldMany2ManyTags = FieldMany2ManyTags.extend({
if (color === current_color) { return; }
+ var changes = {};
+ changes[this.colorField] = color;
this.trigger_up('field_changed', {
dataPointID: _.findWhere(this.value.data, {res_id: id}).id,
- changes: {
- color: color,
- },
+ changes: changes,
force_save: true,
});
},
@@ -1598,12 +1613,19 @@ var KanbanFieldMany2ManyTags = FieldMany2ManyTags.extend({
var self = this;
this.$el.empty().addClass('o_field_many2manytags o_kanban_tags');
_.each(this.value.data, function (m2m) {
- // 10th color is invisible
- if ('color' in m2m.data && m2m.data.color !== 10) {
- $('<span>')
- .addClass('o_tag o_tag_color_' + m2m.data.color)
+ var $tag = $('<span>')
.attr('title', _.str.escapeHTML(m2m.data.display_name))
.appendTo(self.$el);
+ if (self.colorField in m2m.data) {
+ if (m2m.data[self.colorField] === 10) {
+ // 10th color is invisible
+ $tag.hide();
+ } else {
+ $tag.addClass('o_tag o_tag_color_' + m2m.data[self.colorField]);
+ }
+ } else {
+ // display tags in grey by default
+ $tag.addClass('o_tag o_tag_color_0');
}
});
},
@@ -1772,15 +1794,7 @@ var FieldSelection = AbstractField.extend({
*/
init: function () {
this._super.apply(this, arguments);
- if (this.field.type === 'many2one') {
- this.values = this.record.specialData[this.name];
- this.formatType = 'many2one';
- } else {
- this.values = _.reject(this.field.selection, function (v) {
- return v[0] === false && v[1] === '';
- });
- }
- this.values = [[false, this.attrs.placeholder || '']].concat(this.values);
+ this._setValues();
},
//--------------------------------------------------------------------------
@@ -1810,13 +1824,12 @@ var FieldSelection = AbstractField.extend({
* @private
*/
_renderEdit: function () {
- if (!this.$el.children().length) {
- for (var i = 0 ; i < this.values.length ; i++) {
- this.$el.append($('<option/>', {
- value: JSON.stringify(this.values[i][0]),
- html: this.values[i][1]
- }));
- }
+ this.$el.empty();
+ for (var i = 0 ; i < this.values.length ; i++) {
+ this.$el.append($('<option/>', {
+ value: JSON.stringify(this.values[i][0]),
+ html: this.values[i][1]
+ }));
}
var value = this.value;
if (this.field.type === 'many2one' && value) {
@@ -1831,6 +1844,31 @@ var FieldSelection = AbstractField.extend({
_renderReadonly: function () {
this.$el.empty().text(this._formatValue(this.value));
},
+ /**
+ * @override
+ */
+ _reset: function () {
+ this._super.apply(this, arguments);
+ this._setValues();
+ },
+ /**
+ * Sets the possible field values. If the field is a many2one, those values
+ * may change during the lifecycle of the widget if the domain change (an
+ * onchange may change the domain).
+ *
+ * @private
+ */
+ _setValues: function () {
+ if (this.field.type === 'many2one') {
+ this.values = this.record.specialData[this.name];
+ this.formatType = 'many2one';
+ } else {
+ this.values = _.reject(this.field.selection, function (v) {
+ return v[0] === false && v[1] === '';
+ });
+ }
+ this.values = [[false, this.attrs.placeholder || '']].concat(this.values);
+ },
//--------------------------------------------------------------------------
// Handlers
@@ -1856,7 +1894,9 @@ var FieldSelection = AbstractField.extend({
});
var FieldRadio = FieldSelection.extend({
- template: 'FieldRadio',
+ template: null,
+ className: 'o_field_radio',
+ tagName: 'span',
specialData: "_fetchSpecialMany2ones",
supportedFieldTypes: ['selection', 'many2one'],
events: _.extend({}, AbstractField.prototype.events, {
@@ -1867,14 +1907,12 @@ var FieldRadio = FieldSelection.extend({
*/
init: function () {
this._super.apply(this, arguments);
- if (this.field.type === 'selection') {
- this.values = this.field.selection || [];
- } else if (this.field.type === 'many2one') {
- this.values = _.map(this.record.specialData[this.name], function (val) {
- return [val.id, val.display_name];
- });
+ if (this.mode === 'edit') {
+ this.tagName = 'div';
+ this.className += this.nodeOptions.horizontal ? 'o_horizontal' : 'o_vertical';
}
this.unique_id = _.uniqueId("radio");
+ this._setValues();
},
//--------------------------------------------------------------------------
@@ -1898,17 +1936,45 @@ var FieldRadio = FieldSelection.extend({
* @override
*/
_renderEdit: function () {
- var value;
+ var self = this;
+ var currentValue;
if (this.field.type === 'many2one') {
- value = this.value && this.value.data.id;
+ currentValue = this.value && this.value.data.id;
} else {
- value = this.value;
+ currentValue = this.value;
}
- var index = _.findIndex(this.values, function (option) {
- return option[0] === value;
+ this.$el.empty();
+ _.each(this.values, function (value, index) {
+ self.$el.append(qweb.render('FieldRadio.button', {
+ checked: value[0] === currentValue,
+ id: self.unique_id + '_' + value[0],
+ index: index,
+ value: value,
+ }));
});
- this.$("input").prop("checked", false);
- this.$('input[data-index="' + index + '"]').prop('checked', true);
+ },
+ /**
+ * @override
+ */
+ _reset: function () {
+ this._super.apply(this, arguments);
+ this._setValues();
+ },
+ /**
+ * Sets the possible field values. If the field is a many2one, those values
+ * may change during the lifecycle of the widget if the domain change (an
+ * onchange may change the domain).
+ *
+ * @private
+ */
+ _setValues: function () {
+ if (this.field.type === 'selection') {
+ this.values = this.field.selection || [];
+ } else if (this.field.type === 'many2one') {
+ this.values = _.map(this.record.specialData[this.name], function (val) {
+ return [val.id, val.display_name];
+ });
+ }
},
//--------------------------------------------------------------------------
@@ -1930,6 +1996,106 @@ var FieldRadio = FieldSelection.extend({
},
});
+/**
+ * The FieldReference is a combination of a select (for the model) and
+ * a FieldMany2one for its value.
+ * Its intern representation is similar to the many2one (a datapoint with a
+ * `name_get` as data).
+ */
+var FieldReference = FieldMany2One.extend({
+ supportedFieldTypes: ['reference'],
+ template: 'FieldReference',
+ events: _.extend({}, FieldMany2One.prototype.events, {
+ 'change select': '_onSelectionChange',
+ }),
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+
+ // needs to be copied as it is an unmutable object
+ this.field = _.extend({}, this.field);
+ if (this.value) {
+ this._setRelation(this.value.model);
+ }
+ },
+ /**
+ * @override
+ */
+ start: function () {
+ this.$('select').val(this.field.relation);
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Add a select in edit mode (for the model).
+ *
+ * @override
+ */
+ _renderEdit: function () {
+ this._super.apply(this, arguments);
+
+ if (this.$('select').val()) {
+ this.$('.o_input_dropdown').show();
+ this.$el.addClass('o_row'); // this class is used to display the two
+ // components (select & input) on the same line
+ } else {
+ // hide the many2one if the selection is empty
+ this.$('.o_input_dropdown').hide();
+ }
+
+ },
+ /**
+ * @override
+ * @private
+ */
+ _reset: function () {
+ this._super.apply(this, arguments);
+ this._setRelation(this.value && this.value.model);
+ },
+ /**
+ * Set `relation` key in field properties.
+ *
+ * @private
+ * @param {string} model
+ */
+ _setRelation: function (model) {
+ // used to generate the search in many2one
+ this.field.relation = model;
+ },
+ /**
+ * @override
+ * @private
+ */
+ _setValue: function (value, options) {
+ value = value || {};
+ // we need to specify the model for the change in basic_model
+ // the value is then now a dict with id, display_name and model
+ value.model = this.$('select').val();
+ return this._super(value, options);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * When the selection (model) changes, the many2one is reset.
+ *
+ * @private
+ */
+ _onSelectionChange: function () {
+ var value = this.$('select').val();
+ this.reinitialize(false);
+ this._setRelation(value);
+ },
+});
+
return {
FieldMany2One: FieldMany2One,
KanbanFieldMany2One: KanbanFieldMany2One,
@@ -1947,6 +2113,8 @@ return {
FieldRadio: FieldRadio,
FieldSelection: FieldSelection,
FieldStatus: FieldStatus,
+
+ FieldReference: FieldReference,
};
});
diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js
index 41fc293e383..c1e69a72ce3 100644
--- a/addons/web/static/src/js/services/data_manager.js
+++ b/addons/web/static/src/js/services/data_manager.js
@@ -355,6 +355,11 @@ return core.Class.extend({
return {};
}),
};
+ if (attrs.options.color_field) {
+ // used by m2m tags
+ attrs.relatedFields[attrs.options.color_field] = { type: 'integer' };
+ attrs.fieldsInfo.default[attrs.options.color_field] = {};
+ }
}
}
return attrs;
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 67a4fa504d8..82dc6f90d26 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -104,39 +104,15 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
return true;
},
/**
- * Discards the changes made to the record whose ID is given, if necessary.
- * Automatically leaves to default mode for the given record.
+ * Waits for the mutex to be unlocked and then calls _.discardChanges.
+ * This ensures that the confirm dialog isn't displayed directly if there is
+ * a pending 'write' rpc.
*
- * @param {string} [recordID] - default to main recordID
- * @param {Object} [options]
- * @param {boolean} [options.readonlyIfRealDiscard=false]
- * After discarding record changes, the usual option is to make the
- * record readonly. However, the view manager calls this function
- * at inappropriate times in the current code and in that case, we
- * don't want to go back to readonly if there is nothing to discard
- * (e.g. when switching record in edit mode in form view, we expect
- * the new record to be in edit mode too, but the view manager calls
- * this function as the URL changes...) @todo get rid of this when
- * the view manager is improved.
- * @returns {Deferred}
+ * @see _.discardChanges
*/
discardChanges: function (recordID, options) {
- var self = this;
- recordID = recordID || this.handle;
- return this.canBeDiscarded(recordID).then(function (needDiscard) {
- if (options && options.readonlyIfRealDiscard && !needDiscard) {
- return;
- }
-
- if (needDiscard) { // Just some optimization
- self.model.discardChanges(recordID);
- }
- if (self.model.isNew(recordID)) {
- self._abandonRecord(recordID);
- return;
- }
- return self._confirmSave(recordID);
- });
+ return this.mutex.exec(function () {})
+ .then(this._discardChanges.bind(this, recordID || this.handle, options));
},
/**
* Method that will be overriden by the views with the ability to have selected ids
@@ -327,6 +303,42 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
}
},
/**
+ * Discards the changes made to the record whose ID is given, if necessary.
+ * Automatically leaves to default mode for the given record.
+ *
+ * @private
+ * @param {string} [recordID] - default to main recordID
+ * @param {Object} [options]
+ * @param {boolean} [options.readonlyIfRealDiscard=false]
+ * After discarding record changes, the usual option is to make the
+ * record readonly. However, the view manager calls this function
+ * at inappropriate times in the current code and in that case, we
+ * don't want to go back to readonly if there is nothing to discard
+ * (e.g. when switching record in edit mode in form view, we expect
+ * the new record to be in edit mode too, but the view manager calls
+ * this function as the URL changes...) @todo get rid of this when
+ * the view manager is improved.
+ * @returns {Deferred}
+ */
+ _discardChanges: function (recordID, options) {
+ var self = this;
+ recordID = recordID || this.handle;
+ return this.canBeDiscarded(recordID)
+ .then(function (needDiscard) {
+ if (options && options.readonlyIfRealDiscard && !needDiscard) {
+ return;
+ }
+ if (needDiscard) { // Just some optimization
+ self.model.discardChanges(recordID);
+ }
+ if (self.model.isNew(recordID)) {
+ self._abandonRecord(recordID);
+ return;
+ }
+ return self._confirmSave(recordID);
+ });
+ },
+ /**
* Enables buttons so they can be clicked again.
*
* @private
@@ -501,11 +513,8 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
var self = this;
ev.stopPropagation();
var recordID = ev.data.recordID;
- this.discardChanges(recordID)
+ this._discardChanges(recordID)
.done(function () {
- if (self.model.isNew(recordID)) {
- self._abandonRecord(recordID);
- }
// TODO this will tell the renderer to rerender the widget that
// asked for the discard but will unfortunately lose the click
// made on another row if any
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 716624f368b..2f06168fd23 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -146,8 +146,11 @@ var BasicModel = AbstractModel.extend({
//--------------------------------------------------------------------------
/**
- * Add a default record to a list object. This method actually make a new
- * record with the _makeDefaultRecord method, then add it to the list object.
+ * Add a default record to a list object. This method actually makes a new
+ * record with the _makeDefaultRecord method, then adds it to the list object.
+ * The default record is added in the data directly. This is meant to be used
+ * by list or kanban controllers (i.e. not for x2manys in form views, as in
+ * this case, we store changes as commands).
*
* @param {string} listID a valid handle for a list object
* @param {Object} [options]
@@ -156,6 +159,7 @@ var BasicModel = AbstractModel.extend({
* @returns {Deferred<string>} resolves to the id of the new created record
*/
addDefaultRecord: function (listID, options) {
+ var self = this;
var list = this.localData[listID];
var context = this._getContext(list);
@@ -169,12 +173,13 @@ var BasicModel = AbstractModel.extend({
};
return this._makeDefaultRecord(list.model, params).then(function (id) {
list.count++;
- list._changes = list._changes || list.data.slice(0);
if (position === 'top') {
- list._changes.unshift(id);
+ list.data.unshift(id);
} else {
- list._changes.push(id);
+ list.data.push(id);
}
+ var record = self.localData[id];
+ list._cache[record.res_id] = id;
return id;
});
},
@@ -327,15 +332,20 @@ var BasicModel = AbstractModel.extend({
} else {
data[fieldName] = this.get(data[fieldName]) || false;
}
- }
- if (field.type === 'one2many' || field.type === 'many2many') {
+ } else if (field.type === 'reference') {
if (options.raw) {
relDataPoint = this.localData[data[fieldName]];
- var relData = relDataPoint._changes || relDataPoint.data;
- var ids = _.map(relData, function (id) {
- return self.localData[id].res_id;
- });
- data[fieldName] = ids;
+ data[fieldName] = relDataPoint ?
+ relDataPoint.model + ',' + relDataPoint.res_id :
+ false;
+ } else {
+ data[fieldName] = this.get(data[fieldName]) || false;
+ }
+ } else if (field.type === 'one2many' || field.type === 'many2many') {
+ if (options.raw) {
+ relDataPoint = this.localData[data[fieldName]];
+ relDataPoint = this._applyX2ManyOperations(relDataPoint);
+ data[fieldName] = relDataPoint.res_ids;
} else {
data[fieldName] = this.get(data[fieldName]) || [];
}
@@ -376,25 +386,15 @@ var BasicModel = AbstractModel.extend({
}
// here, type === 'list'
- var listData, listCount, resIDs;
- if (element._changes) {
- listData = element._changes;
- listCount = listData.length;
- resIDs = _.map(listData, function (elemID) {
- return self.localData[elemID].res_id;
- });
- } else {
- listData = element.data;
- listCount = element.count;
- resIDs = element.res_ids;
- }
- listData = _.map(listData, function (elemID) {
+ // apply potential changes (only for x2many lists)
+ element = this._applyX2ManyOperations(element);
+ var listData = _.map(element.data, function (elemID) {
return self.get(elemID, options);
});
var list = {
aggregateValues: _.extend({}, element.aggregateValues),
context: _.extend({}, element.context),
- count: listCount,
+ count: element.count,
data: listData,
domain: element.domain.slice(0),
fields: element.fields,
@@ -409,7 +409,7 @@ var BasicModel = AbstractModel.extend({
offset: element.offset,
orderedBy: element.orderedBy,
res_id: element.res_id,
- res_ids: resIDs,
+ res_ids: element.res_ids,
type: 'list',
value: element.value,
viewType: element.viewType,
@@ -417,6 +417,7 @@ var BasicModel = AbstractModel.extend({
if (element.fieldsInfo) {
list.fieldsInfo = element.fieldsInfo;
}
+ this._sortList(list);
return list;
},
/**
@@ -538,6 +539,7 @@ var BasicModel = AbstractModel.extend({
id: id,
display_name: display_name,
},
+ parentID: record.id,
});
record.data[field.name] = dataPoint.id;
if (display_name === undefined) {
@@ -553,6 +555,7 @@ var BasicModel = AbstractModel.extend({
var dpParams = {
fieldsInfo: relatedFieldsInfo,
modelName: field.relation,
+ parentID: record.id,
static: true,
type: 'list',
viewType: 'default',
@@ -570,9 +573,11 @@ var BasicModel = AbstractModel.extend({
var recordDP = self._makeDataPoint({
data: data,
modelName: field.relation,
+ parentID: dataPoint.id,
type: 'record',
});
dataPoint.data.push(recordDP.id);
+ dataPoint._cache[recordDP.res_id] = recordDP.id;
});
} else {
dpParams.res_ids = field.value;
@@ -689,7 +694,16 @@ var BasicModel = AbstractModel.extend({
removeLine: function (elementID) {
var record = this.localData[elementID];
var parent = this.localData[record.parentID];
- parent._changes = _.without(parent._changes, elementID);
+ if (parent.parentID) {
+ // x2Many case: the new record has been stored in _changes, as a
+ // command so we remove the command(s) related to that record
+ parent._changes = _.filter(parent._changes, function (change) {
+ return change.id !== elementID;
+ });
+ } else {
+ // main list view case: the new record is in data
+ parent.data = _.without(parent.data, elementID);
+ }
},
/**
* Save a local resource, if needed. This is a complicated operation,
@@ -809,13 +823,7 @@ var BasicModel = AbstractModel.extend({
record.fieldsInfo = _.defaults(record.fieldsInfo, viewInfo.fieldsInfo);
},
/**
- * For list resources, this changes the orderedBy key, then performs the
- * sort directly, in javascript. This is used for sorting static datasets,
- * such as a one2many in a form view. For dynamic datasets, such as a list
- * view, this method will be called, but then the sort will be ignored since
- * we will reload data.
- *
- * @todo don't sort in js when we reload data anyway
+ * For list resources, this changes the orderedBy key.
*
* @param {string} list_id id for the list resource
* @param {string} fieldName valid field name
@@ -837,7 +845,6 @@ var BasicModel = AbstractModel.extend({
});
list.orderedBy = [{name: fieldName, asc: true}].concat(orderedBy);
}
- this._sortList(list);
return this;
},
/**
@@ -894,6 +901,36 @@ var BasicModel = AbstractModel.extend({
//--------------------------------------------------------------------------
/**
+ * Add a default record to a list object. This method actually makes a new
+ * record with the _makeDefaultRecord method, then adds it to the list object
+ * as a 'ADD' command in its _changes. This is meant to be used x2many lists,
+ * not by list or kanban controllers.
+ *
+ * @private
+ * @param {string} listID a valid handle for a list object
+ * @param {Object} [options]
+ * @param {string} [options.position=top] if the new record should be added
+ * on top or on bottom of the list
+ * @returns {Deferred -> string} resolves to the new record id
+ */
+ _addX2ManyDefaultRecord: function (list, options) {
+ var self = this;
+ var params = {
+ context: this._getContext(list),
+ fields: list.fields,
+ fieldsInfo: list.fieldsInfo,
+ parentID: list.id,
+ viewType: list.viewType,
+ };
+ return this._makeDefaultRecord(list.model, params).then(function (id) {
+ var position = options && options.position || 'top';
+ list._changes.push({operation: 'ADD', id: id, position: position});
+ var record = self.localData[id];
+ list._cache[record.res_id] = id;
+ return id;
+ });
+ },
+ /**
* This method is the private version of notifyChanges. Unlike
* notifyChanges, it is not protected by a mutex. Every changes from the
* user to the model go through this method.
@@ -917,8 +954,8 @@ var BasicModel = AbstractModel.extend({
field = record.fields[fieldName];
if (field.type === 'one2many' || field.type === 'many2many') {
defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], viewType));
- } else if (field.type === 'many2one') {
- defs.push(this._applyMany2OneChange(record, fieldName, changes[fieldName]));
+ } else if (field.type === 'many2one' || field.type === 'reference') {
+ defs.push(this._applyX2OneChange(record, fieldName, changes[fieldName]));
} else {
record._changes[fieldName] = changes[fieldName];
}
@@ -960,19 +997,19 @@ var BasicModel = AbstractModel.extend({
});
},
/**
- * Apply a many2one onchange. There is a need for this function because the
- * server only gives an id when a onchange modifies a many2one field. For
- * this reason, we need (sometimes) to do a /name_get to fetch a
- * display_name.
+ * Apply an x2one (either a many2one or a reference field) change. There is
+ * a need for this function because the server only gives an id when a
+ * onchange modifies a many2one field. For this reason, we need (sometimes)
+ * to do a /name_get to fetch a display_name.
*
* @param {Object} record
* @param {string} fieldName
* @param {Object} [data]
* @returns {Deferred}
*/
- _applyMany2OneChange: function (record, fieldName, data) {
+ _applyX2OneChange: function (record, fieldName, data) {
var self = this;
- if (!data) {
+ if (!data || !data.id) {
record._changes[fieldName] = false;
return $.when();
}
@@ -993,12 +1030,16 @@ var BasicModel = AbstractModel.extend({
return $.when();
}
var rel_data = _.pick(data, 'id', 'display_name');
+ var field = record.fields[fieldName];
+
+ // the reference field doesn't store its co-model in its field metadata
+ // but directly in the data (as the co-model isn't fixed)
+ var coModel = field.type === 'reference' ? data.model : field.relation;
var def;
if (rel_data.display_name === undefined) {
- var field = record.fields[fieldName];
// TODO: refactor this to use _fetchNameGet
def = this._rpc({
- model: field.relation,
+ model: coModel,
method: 'name_get',
args: [data.id],
context: record.context,
@@ -1013,13 +1054,14 @@ var BasicModel = AbstractModel.extend({
data: rel_data,
fields: {},
fieldsInfo: {},
- modelName: record.fields[fieldName].relation,
+ modelName: coModel,
+ parentID: record.id,
});
record._changes[fieldName] = rec.id;
});
},
/**
- * Applies the result on an onchange RPC on a record.
+ * Applies the result of an onchange RPC on a record.
*
* @private
* @param {Object} values the result of the onchange RPC (a mapping of
@@ -1053,6 +1095,7 @@ var BasicModel = AbstractModel.extend({
context: record.context,
data: data,
modelName: field.relation,
+ parentID: record.id,
});
id = rec.id;
}
@@ -1070,12 +1113,15 @@ var BasicModel = AbstractModel.extend({
list = self._makeDataPoint({
fieldsInfo: fieldInfo.fieldsInfo,
modelName: field.relation,
+ parentID: record.id,
+ static: true,
type: 'list',
viewtype: fieldInfo.viewType,
});
}
record._changes[name] = list.id;
var shouldLoad = false;
+ list._changes = list._changes || [];
_.each(val, function (command) {
if (command[0] === 0 || command[0] === 1) {
// CREATE or UPDATE
@@ -1091,23 +1137,25 @@ var BasicModel = AbstractModel.extend({
params.res_id = command[1];
}
rec = self._makeDataPoint(params);
- list._changes.push(rec.id);
+ list._cache[rec.res_id] = rec.id;
+ list._changes.push({operation: 'ADD', id: rec.id});
+ if (command[0] === 1) {
+ list._changes.push({operation: 'UPDATE', id: rec.id});
+ }
defs.push(self._applyOnChange(command[2], rec));
} else if (command[0] === 4) {
// LINK TO
- list.res_ids.push(command[1]);
- list.count++;
+ // the dataPoint id will be set when the record will be fetched (for now,
+ // this dataPoint doesn't exist yet)
+ list._changes.push({operation: 'ADD', id: null, resID: command[1]});
shouldLoad = true;
} else if (command[0] === 5) {
// DELETE ALL
- list._changes = [];
+ list._changes = [{operation: 'REMOVE_ALL'}];
}
});
if (shouldLoad) {
- var def = self._readUngroupedList(list).then(function () {
- list._changes = list.data;
- });
- defs.push(def);
+ defs.push(self._readUngroupedList(list));
}
} else {
record._changes[name] = self._parseServerValue(field, val);
@@ -1143,9 +1191,9 @@ var BasicModel = AbstractModel.extend({
var field = record.fields[fieldName];
var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName];
var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode];
- var rec;
+ var def, rec;
var defs = [];
- list._changes = list._changes || list.data.slice(0);
+ list._changes = list._changes || [];
switch (command.operation) {
case 'ADD':
@@ -1163,8 +1211,8 @@ var BasicModel = AbstractModel.extend({
newRecord.fields = list.fields;
newRecord.fieldsInfo = list.fieldsInfo;
newRecord.viewType = list.viewType;
- list._changes.push(newRecord.id);
- this._sortList(list);
+ list._cache[newRecord.res_id] = newRecord.id;
+ list._changes.push(command);
break;
case 'ADD_M2M':
// force to use link command instead of create command
@@ -1184,8 +1232,8 @@ var BasicModel = AbstractModel.extend({
parentID: list.id,
});
list_records[d.id] = rec;
- list._changes.push(rec.id);
- self._sortList(list);
+ list._cache[rec.res_id] = rec.id;
+ list._changes.push({operation: 'ADD', id: rec.id});
});
// read list's records as we only have their ids and optionally their display_name
// (we can't use function readUngroupedList because those records are only in the
@@ -1194,44 +1242,62 @@ var BasicModel = AbstractModel.extend({
// so that if a record is removed and then re-added, it won't be fetched twice
var fieldNames = list.getFieldNames();
if (fieldNames.length) {
- var def = this._rpc({
+ def = this._rpc({
model: list.model,
method: 'read',
args: [_.pluck(data, 'id'), fieldNames]
}).then(function (records) {
_.each(records, function (record) {
list_records[record.id].data = record;
- self._parseServerData(fieldNames, list.fields, record);
+ self._parseServerData(fieldNames, list, record);
});
- return self._fetchX2ManysBatched(list);
+ return $.when(
+ self._fetchX2ManysBatched(list),
+ self._fetchReferencesBatched(list)
+ );
});
defs.push(def);
}
break;
case 'CREATE':
- if (command.data) {
- defs.push(this.addDefaultRecord(list.id).then(function (id) {
+ var options = {position: command.position};
+ def = this._addX2ManyDefaultRecord(list, options).then(function (id) {
+ // FIXME: hack for lunch widget, which does useless default_get and onchange
+ if (command.data) {
return self._applyChange(id, command.data);
- }));
- } else {
- defs.push(this.addDefaultRecord(list.id, {position: command.position}));
- }
+ }
+ });
+ defs.push(def);
break;
case 'UPDATE':
- defs.push(this._applyChange(command.id, command.data));
+ list._changes.push({operation: 'UPDATE', id: command.id});
+ if (command.data) {
+ defs.push(this._applyChange(command.id, command.data));
+ }
break;
case 'REMOVE':
- list._changes = _.difference(list._changes, command.ids);
+ // filter out existing operations involving the current
+ // dataPoint, and add a 'REMOVE' operation only if there is
+ // no 'ADD' operation for that dataPoint, as it would mean
+ // that the record wasn't in the relation yet
+ var idsToRemove = command.ids;
+ list._changes = _.reject(list._changes, function (change) {
+ var idInCommands = _.contains(command.ids, change.id);
+ if (idInCommands && change.operation === 'ADD') {
+ idsToRemove = _.without(idsToRemove, change.id);
+ }
+ return idInCommands;
+ });
+ _.each(idsToRemove, function (id) {
+ list._changes.push({operation: 'REMOVE', id: id});
+ });
break;
case 'REPLACE_WITH':
- // this is certainFly not optimal... and not sure that it is
+ // this is certainly not optimal... and not sure that it is
// correct if some ids are added and some other are removed
- var currentData = _.map(list._changes, function (localId) {
- return self.localData[localId];
- });
- var currentIds = _.pluck(currentData, 'res_id');
- var newIds = _.difference(command.ids, currentIds);
- var removedIds = _.difference(currentIds, command.ids);
+ list._changes = [];
+ var newIds = _.difference(command.ids, list.res_ids);
+ var removedIds = _.difference(list.res_ids, command.ids);
var addDef, removedDef, values;
if (newIds.length) {
values = _.map(newIds, function (id) {
@@ -1243,18 +1309,87 @@ var BasicModel = AbstractModel.extend({
});
}
if (removedIds.length) {
- values = _.filter(currentData, function (dataPoint) {
- return _.contains(removedIds, dataPoint.res_id);
+ var listData = _.map(list.data, function (localId) {
+ return self.localData[localId];
});
removedDef = this._applyX2ManyChange(record, fieldName, {
operation: 'REMOVE',
- ids: _.pluck(values, 'id'),
+ ids: _.map(removedIds, function (resID) {
+ return _.findWhere(listData, {res_id: resID}).id;
+ }),
});
}
return $.when(addDef, removedDef);
}
- return $.when.apply($, defs);
+ return $.when.apply($, defs).then(function () {
+ // ensure to fetch up to 'limit' records (may be useful if records of
+ // the current page have been removed)
+ return self._readUngroupedList(list);
+ });
+ },
+ /**
+ * In dataPoints of type list for x2manys, the changes are stored as a list
+ * of operations (being of type 'ADD', 'REMOVE', 'UPDATE' or 'REMOVE_ALL').
+ * This function applies the operation of such a dataPoint without altering
+ * the original dataPoint. It returns a copy of the dataPoint in which the
+ * 'count', 'data' and 'res_ids' keys have been updated.
+ *
+ * @private
+ * @param {Object} dataPoint of type list
+ * @returns {Object} element of type list in which the commands have been
+ * applied
+ */
+ _applyX2ManyOperations: function (list) {
+ if (!list.static) {
+ // this function only applies on x2many lists
+ return list;
+ }
+ var self = this;
+ list = _.extend({}, list);
+ list.data = list.data.slice(0);
+ list.res_ids = list.res_ids.slice(0);
+ _.each(list._changes, function (change) {
+ var relRecord;
+ if (change.id) {
+ relRecord = self.localData[change.id];
+ }
+ switch(change.operation) {
+ case 'ADD':
+ list.count++;
+ var resID = relRecord ? relRecord.res_id : change.resID;
+ if (change.position === 'top') {
+ list.res_ids.unshift(resID);
+ } else {
+ list.res_ids.push(resID);
+ }
+ break;
+ case 'REMOVE':
+ list.count--;
+ list.res_ids = _.without(list.res_ids, relRecord.res_id);
+ break;
+ case 'REMOVE_ALL':
+ list.count = 0;
+ list.res_ids = [];
+ break;
+ case 'UPDATE':
+ // nothing to do for UPDATE commands
+ break;
+ }
+ });
+ var idsInRange;
+ if (list.limit) {
+ idsInRange = list.res_ids.slice(list.offset, list.offset + list.limit);
+ } else {
+ idsInRange = list.res_ids;
+ }
+ list.data = [];
+ _.each(idsInRange, function (id) {
+ if (list._cache[id]) {
+ list.data.push(list._cache[id]);
+ }
+ });
+ return list;
},
/**
* Helper method to build a 'spec', that is a description of all fields in
@@ -1368,7 +1503,8 @@ var BasicModel = AbstractModel.extend({
var self = this;
var model;
var records = [];
- var ids = _.map(list._changes || list.data, function (localId) {
+ list = this._applyX2ManyOperations(list);
+ var ids = _.map(list.data, function (localId) {
var record = self.localData[localId];
var data = record._changes || record.data;
var many2oneId = data[fieldName];
@@ -1410,7 +1546,7 @@ var BasicModel = AbstractModel.extend({
model: record.model,
method: 'read',
args: [[record.res_id], fieldNames],
- context: _.extend({}, record.context, {bin_size: true}),
+ context: _.extend({}, record.getContext(), {bin_size: true}),
})
.then(function (result) {
if (result.length === 0) {
@@ -1420,15 +1556,143 @@ var BasicModel = AbstractModel.extend({
record.data = _.extend({}, record.data, result);
})
.then(function () {
- self._parseServerData(fieldNames, record.fields, record.data);
+ self._parseServerData(fieldNames, record, record.data);
})
.then(function () {
- return self._fetchX2Manys(record, options).then(function () {
+ return $.when(
+ self._fetchX2Manys(record, options),
+ self._fetchReferences(record)
+ ).then(function () {
return self._postprocess(record, options);
});
});
},
/**
+ * Fetch the extra data (`name_get`) for the reference fields of the record
+ * model.
+ *
+ * @private
+ * @param {Object} record
+ * @returns {Deferred}
+ */
+ _fetchReferences: function (record) {
+ var self = this;
+ var defs = [];
+ var fieldNames = record.getFieldNames();
+ _.each(fieldNames, function (fieldName) {
+ var field = record.fields[fieldName];
+ if (field.type === 'reference') {
+ var value = record.data[fieldName];
+ if (value) {
+ var model = value.split(',')[0];
+ var resID = parseInt(value.split(',')[1]);
+ var def = self._rpc({
+ model: model,
+ method: 'name_get',
+ args: [resID],
+ context: record.getContext({fieldName: fieldName}),
+ }).then(function (result) {
+ var referenceDp = self._makeDataPoint({
+ data: {
+ id: result[0][0],
+ display_name: result[0][1],
+ },
+ modelName: model,
+ parentID: record.id,
+ });
+ record.data[fieldName] = referenceDp.id;
+ });
+ defs.push(def);
+ }
+ }
+ });
+ return $.when.apply($, defs);
+ },
+ /**
+ * Batch requests for one reference field in list (one request by different
+ * model in the field values).
+ *
+ * @see _fetchReferencesBatched
+ * @param {Object} list
+ * @param {string} fieldName
+ * @returns {Deferred}
+ */
+ _fetchReferenceBatched: function (list, fieldName) {
+ var self = this;
+ list = this._applyX2ManyOperations(list);
+
+ // collect ids by model
+ var toFetch = {};
+ _.each(list.data, function (dataPoint) {
+ var record = self.localData[dataPoint];
+ var value = record.data[fieldName];
+ if (value) {
+ var model = value.split(',')[0];
+ var resID = value.split(',')[1];
+ if (!(model in toFetch)) {
+ toFetch[model] = {};
+ }
+ // there could be multiple datapoints with the same model/resID
+ if (toFetch[model][resID]) {
+ toFetch[model][resID].push(dataPoint);
+ } else {
+ toFetch[model][resID] = [dataPoint];
+ }
+ }
+ });
+
+ var defs = [];
+ var def;
+ // one name_get by model
+ _.each(toFetch, function (datapoints, model) {
+ var ids = _.map(Object.keys(datapoints), function (id) { return parseInt(id); });
+ // we need one parent for the context (they all have the same)
+ var parent = datapoints[ids[0]][0];
+ def = self._rpc({
+ model: model,
+ method: 'name_get',
+ args: [ids],
+ context: self.localData[parent].getContext({fieldName: fieldName}),
+ }).then(function (result) {
+ _.each(result, function (el) {
+ var parentIDs = datapoints[el[0]];
+ _.each(parentIDs, function (parentID) {
+ var parent = self.localData[parentID];
+ var referenceDp = self._makeDataPoint({
+ data: {
+ id: el[0],
+ display_name: el[1],
+ },
+ modelName: model,
+ parentID: parent,
+ });
+ parent.data[fieldName] = referenceDp.id;
+ });
+ });
+ });
+ defs.push(def);
+ });
+
+ return $.when.apply($, defs);
+ },
+ /**
+ * Batch requests for references for datapoint of type list.
+ *
+ * @param {Object} list
+ * @returns {Deferred}
+ */
+ _fetchReferencesBatched: function (list) {
+ var defs = [];
+ var fieldNames = list.getFieldNames();
+ for (var i = 0; i < fieldNames.length; i++) {
+ var field = list.fields[fieldNames[i]];
+ if (field.type === 'reference') {
+ defs.push(this._fetchReferenceBatched(list, fieldNames[i]));
+ }
+ }
+ return $.when.apply($, defs);
+ },
+ /**
* This method is incorrectly named. It should be named something like
* _fetchMany2OneData.
*
@@ -1527,7 +1791,9 @@ var BasicModel = AbstractModel.extend({
var context = record.getContext({fieldName: fieldName});
var domain = record.getDomain({fieldName: fieldName});
if (domain.length) {
- var localID = record._changes && record._changes[fieldName] || record.data[fieldName];
+ var localID = (record._changes && fieldName in record._changes) ?
+ record._changes[fieldName] :
+ record.data[fieldName];
if (localID) {
var element = this.localData[localID];
domain = ["|", ["id", "=", element.data.id]].concat(domain);
@@ -1705,7 +1971,9 @@ var BasicModel = AbstractModel.extend({
def = this._searchReadUngroupedList(list);
}
return def.then(function () {
- return self._fetchX2ManysBatched(list);
+ return $.when(
+ self._fetchX2ManysBatched(list),
+ self._fetchReferencesBatched(list));
}).then(function () {
return list;
});
@@ -1759,7 +2027,10 @@ var BasicModel = AbstractModel.extend({
record.data[fieldName] = list.id;
if (!fieldInfo.__no_fetch) {
var def = self._readUngroupedList(list).then(function () {
- return self._fetchX2ManysBatched(list);
+ return $.when(
+ self._fetchX2ManysBatched(list),
+ self._fetchReferencesBatched(list)
+ );
});
defs.push(def);
}
@@ -1783,12 +2054,12 @@ var BasicModel = AbstractModel.extend({
var fieldsInfo = view ? view.fieldsInfo : fieldInfo.fieldsInfo;
var fields = view ? view.fields : fieldInfo.relatedFields;
var viewType = view ? view.type : fieldInfo.viewType;
- var data = list._changes || list.data;
+ list = this._applyX2ManyOperations(list);
var x2mRecords = [];
// step 1: collect ids
var ids = [];
- _.each(data, function (dataPoint) {
+ _.each(list.data, function (dataPoint) {
var record = self.localData[dataPoint];
if (typeof record.data[fieldName] === 'string') {
// in this case, the value is a local ID, which means that the
@@ -1803,6 +2074,7 @@ var BasicModel = AbstractModel.extend({
fieldsInfo: fieldsInfo,
fields: fields,
modelName: field.relation,
+ parentID: record.id,
res_ids: record.data[fieldName],
static: true,
type: 'list',
@@ -1834,25 +2106,20 @@ var BasicModel = AbstractModel.extend({
}
return def.then(function (results) {
// step 3: assign values to correct datapoints
- var dataPoints = _.map(results, function (result) {
- return self._makeDataPoint({
- modelName: field.relation,
- data: result,
- fields: fields,
- fieldsInfo: fieldsInfo,
- viewType: viewType,
- });
- });
-
_.each(x2mRecords, function (record) {
var m2mList = self.localData[record.data[fieldName]];
-
m2mList.data = [];
_.each(m2mList.res_ids, function (res_id) {
- var dataPoint = _.find(dataPoints, function (d) {
- return d.res_id === res_id;
+ var dataPoint = self._makeDataPoint({
+ modelName: field.relation,
+ data: _.findWhere(results, {id: res_id}),
+ fields: fields,
+ fieldsInfo: fieldsInfo,
+ parentID: m2mList.id,
+ viewType: viewType,
});
m2mList.data.push(dataPoint.id);
+ m2mList._cache[res_id] = dataPoint.id;
});
});
});
@@ -1912,6 +2179,7 @@ var BasicModel = AbstractModel.extend({
// process relational fields and handle the null case
var type = record.fields[fieldName].type;
+ var value;
if (type === 'one2many' || type === 'many2many') {
if (commands[fieldName].length) { // replace localId by commands
changes[fieldName] = commands[fieldName];
@@ -1919,8 +2187,13 @@ var BasicModel = AbstractModel.extend({
delete changes[fieldName];
}
} else if (type === 'many2one' && fieldName in changes) {
- var value = changes[fieldName];
+ value = changes[fieldName];
changes[fieldName] = value ? this.localData[value].res_id : false;
+ } else if (type === 'reference' && fieldName in changes) {
+ value = changes[fieldName];
+ changes[fieldName] = value ?
+ this.localData[value].model + ',' + this.localData[value].res_id :
+ false;
} else if (changes[fieldName] === null) {
changes[fieldName] = false;
}
@@ -1961,6 +2234,8 @@ var BasicModel = AbstractModel.extend({
*
* @param {Object} record
* @param {Object} [options]
+ * @param {string} [options.fieldNames] if given, generates the commands for
+ * these fields only
* @param {boolean} [changesOnly=false] if true, only generates commands for
* fields that have changed
* @param {boolean} [options.withReadonly=false] if false, doesn't generate
@@ -1970,11 +2245,15 @@ var BasicModel = AbstractModel.extend({
_generateX2ManyCommands: function (record, options) {
var self = this;
options = options || {};
+ var fields = record.fields;
+ if (options.fieldNames) {
+ fields = _.pick(fields, options.fieldNames);
+ }
var commands = {};
var data = _.extend({}, record.data, record._changes);
var type;
- for (var fieldName in record.fields) {
- type = record.fields[fieldName].type;
+ for (var fieldName in fields) {
+ type = fields[fieldName].type;
if (type === 'many2many' || type === 'one2many') {
commands[fieldName] = [];
@@ -1983,24 +2262,31 @@ var BasicModel = AbstractModel.extend({
continue;
}
var list = this.localData[data[fieldName]];
- if (options.changesOnly && !list._changes) {
+ if (options.changesOnly && (!list._changes || !list._changes.length)) {
// if only changes are requested, skip if there is no change
continue;
}
- var relData = _.map(list._changes || list.data, function (localId) {
- return self.localData[localId];
+ var oldResIDs = list.res_ids.slice(0);
+ var relRecordAdded = [];
+ var relRecordUpdated = [];
+ _.each(list._changes, function (change) {
+ if (change.operation === 'ADD') {
+ relRecordAdded.push(self.localData[change.id]);
+ } else if (change.operation === 'UPDATE') {
+ relRecordUpdated.push(self.localData[change.id]);
+ }
});
- var relIds = _.pluck(relData, 'res_id');
+ list = this._applyX2ManyOperations(list);
if (type === 'many2many' || list._forceM2MLink) {
// deliberately generate a single 'replace' command instead
// of a 'delete' and a 'link' commands with the exact diff
// because 1) performance-wise it doesn't change anything
// and 2) to guard against concurrent updates (policy: force
// a complete override of the actual value of the m2m)
- commands[fieldName].push(x2ManyCommands.replace_with(relIds));
+ commands[fieldName].push(x2ManyCommands.replace_with(list.res_ids));
// generate update commands for records that have been
// updated (it may happen with editable lists)
- _.each(relData, function (relRecord) {
+ _.each(relRecordUpdated, function (relRecord) {
var changes = self._generateChanges(relRecord, options);
if (!_.isEmpty(changes)) {
var command = x2ManyCommands.update(relRecord.res_id, changes);
@@ -2008,29 +2294,29 @@ var BasicModel = AbstractModel.extend({
}
});
} else if (type === 'one2many') {
- var removedIds = _.difference(list.res_ids, relIds);
- var addedIds = _.difference(relIds, list.res_ids);
- var keptIds = _.intersection(list.res_ids, relIds);
+ var removedIds = _.difference(oldResIDs, list.res_ids);
+ var addedIds = _.difference(list.res_ids, oldResIDs);
+ var keptIds = _.intersection(oldResIDs, list.res_ids);
// the didChange variable keeps track of the fact that at
// least one id was updated
var didChange = false;
var changes, command, relRecord;
- for (var i = 0; i < relIds.length; i++) {
- if (_.contains(keptIds, relIds[i])) {
+ for (var i = 0; i < list.res_ids.length; i++) {
+ if (_.contains(keptIds, list.res_ids[i])) {
// this is an id that already existed
- relRecord = _.findWhere(relData, {res_id: relIds[i]});
- changes = this._generateChanges(relRecord, options);
+ relRecord = _.findWhere(relRecordUpdated, {res_id: list.res_ids[i]});
+ changes = relRecord ? this._generateChanges(relRecord, options) : {};
if (!_.isEmpty(changes)) {
command = x2ManyCommands.update(relRecord.res_id, changes);
didChange = true;
} else {
- command = x2ManyCommands.link_to(relIds[i]);
+ command = x2ManyCommands.link_to(list.res_ids[i]);
}
commands[fieldName].push(command);
- } else if (_.contains(addedIds, relIds[i])) {
+ } else if (_.contains(addedIds, list.res_ids[i])) {
// this is a new id
- relRecord = _.findWhere(relData, {res_id: relIds[i]});
+ relRecord = _.findWhere(relRecordAdded, {res_id: list.res_ids[i]});
changes = this._generateChanges(relRecord, options);
commands[fieldName].push(x2ManyCommands.create(changes));
}
@@ -2118,14 +2404,14 @@ var BasicModel = AbstractModel.extend({
if (fieldInfo && fieldInfo.domain) {
return Domain.prototype.stringToArray(
fieldInfo.domain,
- this._getEvalContext(element)
+ this._getEvalContext(element, true)
);
}
var fieldParams = element.fields[options.fieldName];
if (fieldParams.domain) {
return Domain.prototype.stringToArray(
fieldParams.domain,
- this._getEvalContext(element)
+ this._getEvalContext(element, true)
);
}
return [];
@@ -2133,7 +2419,7 @@ var BasicModel = AbstractModel.extend({
return Domain.prototype.stringToArray(
element.domain,
- this._getEvalContext(element)
+ this._getEvalContext(element, true)
);
},
/**
@@ -2145,10 +2431,12 @@ var BasicModel = AbstractModel.extend({
* sure. This allows some domains to use the uid key for example
*
* @param {Object} element - an element from the localData
+ * @param {boolean} [forDomain=false] if true, evaluates x2manys as a list of
+ * ids instead of a list of commands
* @returns {Object}
*/
- _getEvalContext: function (element) {
- var evalContext = element.type === 'record' ? this._getRecordEvalContext(element) : {};
+ _getEvalContext: function (element, forDomain) {
+ var evalContext = element.type === 'record' ? this._getRecordEvalContext(element, forDomain) : {};
if (element.parentID) {
var parent = this.localData[element.parentID];
@@ -2156,7 +2444,7 @@ var BasicModel = AbstractModel.extend({
parent = this.localData[parent.parentID];
}
if (parent.type === 'record') {
- evalContext.parent = this._getRecordEvalContext(parent);
+ evalContext.parent = this._getRecordEvalContext(parent, forDomain);
}
}
return _.extend({
@@ -2184,12 +2472,21 @@ var BasicModel = AbstractModel.extend({
* current values for the record, with commands for x2manys fields.
*
* @param {Object} record an element of type 'record'
+ * @param {boolean} [forDomain=false] if true, x2many values are a list of
+ * ids instead of a list of commands
* @returns Object
*/
- _getRecordEvalContext: function (record) {
+ _getRecordEvalContext: function (record, forDomain) {
var self = this;
var relDataPoint;
var context = _.extend({}, record.data, record._changes);
+
+ // calls _generateX2ManyCommands for a given field, and returns the array of commands
+ function _generateX2ManyCommands (fieldName) {
+ var commands = self._generateX2ManyCommands(record, {fieldNames: [fieldName]});
+ return commands[fieldName];
+ }
+
for (var fieldName in context) {
var field = record.fields[fieldName];
if (context[fieldName] === null) {
@@ -2210,23 +2507,18 @@ var BasicModel = AbstractModel.extend({
continue;
}
if (field.type === 'one2many' || field.type === 'many2many') {
- relDataPoint = this.localData[context[fieldName]];
- var relData = relDataPoint._changes || relDataPoint.data;
- var ids = _.map(relData, function (id) {
- return self.localData[id].res_id;
- });
-
- ids.toJSON = function () {
- return _.map(relData, function (id) {
- var resID = self.localData[id].res_id;
- if (typeof resID === 'string') {
- var changes = self._generateChanges(self.localData[id]);
- return x2ManyCommands.create(changes);
- } else {
- return x2ManyCommands.link_to(resID);
- }
- });
- };
+ var ids;
+ if (!context[fieldName] || _.isArray(context[fieldName])) { // no dataPoint created yet
+ ids = context[fieldName] || [];
+ } else {
+ relDataPoint = this._applyX2ManyOperations(this.localData[context[fieldName]]);
+ ids = relDataPoint.res_ids.slice(0);
+ }
+ if (!forDomain) {
+ // when sent to the server, the x2manys values must be a list
+ // of commands in a context, but the list of ids in a domain
+ ids.toJSON = _generateX2ManyCommands.bind(null, fieldName);
+ }
context[fieldName] = ids;
}
@@ -2247,15 +2539,12 @@ var BasicModel = AbstractModel.extend({
*/
_isFieldReadonly: function (record, fieldName, viewType) {
var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName];
- var modifiers;
if (fieldInfo) {
var rawModifiers = JSON.parse(fieldInfo.modifiers || "{}");
- modifiers = this._evalModifiers(record, rawModifiers);
- }
- if (modifiers && 'readonly' in modifiers) {
+ var modifiers = this._evalModifiers(record, rawModifiers);
return modifiers.readonly;
} else {
- return record.fields[fieldName].readonly;
+ return false;
}
},
/**
@@ -2285,16 +2574,19 @@ var BasicModel = AbstractModel.extend({
* an onchange.
*
* @param {string} id id for a local resource of type 'list'. This is
- * assumed to be a list element for a one2many
+ * assumed to be a list element for an x2many
* @returns {boolean}
*/
_isX2ManyValid: function (id) {
var self = this;
var isValid = true;
var element = this.localData[id];
- _.each(element._changes || element.data, function (recordID) {
- var recordData = self.get(recordID, {raw: true}).data;
- var record = self.localData[recordID];
+ _.each(element._changes, function (command) {
+ if (command.operation === 'REMOVE' || command.operation === 'REMOVE_ALL') {
+ return;
+ }
+ var recordData = self.get(command.id, {raw: true}).data;
+ var record = self.localData[command.id];
_.each(element.getFieldNames(), function (fieldName) {
var field = element.fields[fieldName];
var fieldInfo = element.fieldsInfo[element.viewType][fieldName];
@@ -2477,6 +2769,7 @@ var BasicModel = AbstractModel.extend({
context: record.context,
data: {id: result[name]},
modelName: field.relation,
+ parentID: record.id,
});
record._changes[name] = rec.id;
} else if (field.type === 'one2many' || field.type === 'many2many') {
@@ -2501,6 +2794,7 @@ var BasicModel = AbstractModel.extend({
viewType: viewType,
});
record._changes[name] = x2manyList.id;
+ x2manyList._changes = [];
var many2ones = {};
var r;
var isCommandList = result[name].length && _.isArray(result[name][0]);
@@ -2516,10 +2810,11 @@ var BasicModel = AbstractModel.extend({
context: x2manyList.context,
fieldsInfo: fieldsInfo,
fields: fields,
+ parentID: x2manyList.id,
viewType: viewType,
});
- x2manyList._changes = x2manyList._changes || [];
- x2manyList._changes.push(r.id);
+ x2manyList._changes.push({operation: 'ADD', id: r.id});
+ x2manyList._cache[r.res_id] = r.id;
// this is necessary so the fields are initialized
_.each(r.getFieldNames(), function (fieldName) {
@@ -2533,7 +2828,8 @@ var BasicModel = AbstractModel.extend({
var rec = self._makeDataPoint({
context: r.context,
modelName: r.fields[name].relation,
- data: {id: r._changes[name]}
+ data: {id: r._changes[name]},
+ parentID: r.id,
});
r._changes[name] = rec.id;
many2ones[name] = true;
@@ -2545,7 +2841,10 @@ var BasicModel = AbstractModel.extend({
x2manyList.res_ids = value[2];
x2manyList.count = x2manyList.res_ids.length;
var def = self._readUngroupedList(x2manyList).then(function () {
- return self._fetchX2ManysBatched(x2manyList);
+ return $.when(
+ self._fetchX2ManysBatched(x2manyList),
+ self._fetchReferencesBatched(x2manyList)
+ );
});
defs.push(def);
}
@@ -2586,14 +2885,15 @@ var BasicModel = AbstractModel.extend({
* parse the server values to javascript framwork
*
* @param {[string]} fieldNames
- * @param {Object} fields
- * @param {Object} record
+ * @param {Object} element the dataPoint used as parent for the created
+ * dataPoints
+ * @param {Object} data the server data to parse
*/
- _parseServerData: function (fieldNames, fields, record) {
+ _parseServerData: function (fieldNames, element, data) {
var self = this;
_.each(fieldNames, function (fieldName) {
- var field = fields[fieldName];
- var val = record[fieldName];
+ var field = element.fields[fieldName];
+ var val = data[fieldName];
if (field.type === 'many2one') {
// process many2one: split [id, nameget] and create corresponding record
if (val !== false) {
@@ -2608,14 +2908,15 @@ var BasicModel = AbstractModel.extend({
display_name: val[1],
id: val[0],
},
+ parentID: element.id,
});
- record[fieldName] = r.id;
+ data[fieldName] = r.id;
} else {
// no value for the many2one
- record[fieldName] = false;
+ data[fieldName] = false;
}
} else {
- record[fieldName] = self._parseServerValue(field, val);
+ data[fieldName] = self._parseServerValue(field, val);
}
});
},
@@ -2692,6 +2993,14 @@ var BasicModel = AbstractModel.extend({
});
record._warning = true;
}
+ if (result.domain) {
+ var fieldsInfo = record.fieldsInfo[viewType || record.viewType];
+ for (var fieldName in result.domain) {
+ if (fieldsInfo[fieldName]) {
+ fieldsInfo[fieldName].domain = result.domain[fieldName];
+ }
+ }
+ }
return self._applyOnChange(result.value, record).then(function () {
return result;
});
@@ -2843,10 +3152,14 @@ var BasicModel = AbstractModel.extend({
var def;
var ids = [];
var missingIds = [];
- var upper_bound = list.limit ? Math.min(list.offset + list.limit, list.count) : list.count;
+ // generate the current count and res_ids list by applying the changes
+ var listWithChanges = this._applyX2ManyOperations(list);
+ var currentCount = listWithChanges.count;
+ var currentResIDs = listWithChanges.res_ids;
+ var upper_bound = list.limit ? Math.min(list.offset + list.limit, currentCount) : currentCount;
var fieldNames = list.getFieldNames();
for (var i = list.offset; i < upper_bound; i++) {
- var id = list.res_ids[i];
+ var id = currentResIDs[i];
ids.push(id);
if (!list._cache[id]) {
missingIds.push(id);
@@ -2858,7 +3171,7 @@ var BasicModel = AbstractModel.extend({
model: list.model,
method: 'read',
args: [missingIds, fieldNames],
- context: {} // FIXME
+ context: list.getContext(),
});
} else {
def = $.when(_.map(missingIds, function (id) {
@@ -2876,6 +3189,7 @@ var BasicModel = AbstractModel.extend({
dataPoint = self.localData[list._cache[id]];
} else {
dataPoint = self._makeDataPoint({
+ context: list.context,
data: _.findWhere(records, {id: id}),
fieldsInfo: list.fieldsInfo,
fields: list.fields,
@@ -2885,12 +3199,17 @@ var BasicModel = AbstractModel.extend({
});
// add many2one records
- self._parseServerData(fieldNames, dataPoint.fields, dataPoint.data);
+ self._parseServerData(fieldNames, dataPoint, dataPoint.data);
list._cache[id] = dataPoint.id;
}
+ // set the dataPoint id in potential 'ADD' operation adding the current record
+ _.each(list._changes, function (change) {
+ if (change.operation === 'ADD' && !change.id && change.resID === id) {
+ change.id = dataPoint.id;
+ }
+ });
list.data.push(dataPoint.id);
});
- self._sortList(list);
return list;
});
},
@@ -2927,7 +3246,7 @@ var BasicModel = AbstractModel.extend({
route: '/web/dataset/search_read',
model: list.model,
fields: fieldNames,
- context: list.context,
+ context: list.getContext(),
domain: list.domain || [],
limit: list.limit,
offset: list.loadMoreOffset + list.offset,
@@ -2938,6 +3257,7 @@ var BasicModel = AbstractModel.extend({
var ids = _.pluck(result.records, 'id');
var data = _.map(result.records, function (record) {
var dataPoint = self._makeDataPoint({
+ context: list.context,
data: record,
fields: list.fields,
fieldsInfo: list.fieldsInfo,
@@ -2947,7 +3267,7 @@ var BasicModel = AbstractModel.extend({
});
// add many2one records
- self._parseServerData(fieldNames, dataPoint.fields, dataPoint.data);
+ self._parseServerData(fieldNames, dataPoint, dataPoint.data);
return dataPoint.id;
});
if (list.loadMoreOffset) {
@@ -2978,22 +3298,20 @@ var BasicModel = AbstractModel.extend({
},
/**
* Do a in-memory sort of a list resource data points. This method assumes
- * that the list data has already been fetched. Its intended use is for
- * static datasets, such as a one2many in a form view.
+ * that the list data has already been fetched, and that the changes have
+ * been applied. Its intended use is for static datasets, such as a one2many
+ * in a form view.
*
- * @param {Object} list
+ * @param {Object} list list dataPoint on which changes have been applied
*/
_sortList: function (list) {
- var self = this;
if (list.orderedBy.length) {
// sort records according to ordered_by[0]
var order = list.orderedBy[0];
- var data = list._changes || list.data;
- data.sort(function (id1, id2) {
- var r1 = self.localData[id1];
- var r2 = self.localData[id2];
- var data1 = r1._changes || r1.data;
- var data2 = r2._changes || r2.data;
+ var data = list.data;
+ data.sort(function (r1, r2) {
+ var data1 = r1.data;
+ var data2 = r2.data;
if (data1[order.name] < data2[order.name]) {
return order.asc ? -1 : 1;
}
@@ -3040,8 +3358,8 @@ var BasicModel = AbstractModel.extend({
}
}
if (element.type === 'list') {
- var listData = element._changes || element.data;
- _.each(listData, function (elemId) {
+ element = this._applyX2ManyOperations(element);
+ _.each(element.data, function (elemId) {
var elem = self.localData[elemId];
self._visitChildren(elem, fn);
});
diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js
index d4c96a15736..db2394ddd1d 100644
--- a/addons/web/static/src/js/views/calendar/calendar_controller.js
+++ b/addons/web/static/src/js/views/calendar/calendar_controller.js
@@ -112,13 +112,18 @@ var CalendarController = AbstractController.extend({
*/
_onChangeDate: function (event) {
var modelData = this.model.get();
- if (modelData.target_date.toString() === event.data.date) {
+ if (modelData.target_date.isSame(event.data.date)) {
+ // When clicking on same date, toggle between the two views
switch (modelData.scale) {
case 'month': this.model.setScale('week'); break;
case 'week': this.model.setScale('day'); break;
case 'day': this.model.setScale('month'); break;
}
+ } else if (modelData.target_date.week() === event.data.date.week()) {
+ // When clicking on a date in the same week, switch to day view
+ this.model.setScale('day');
} else {
+ // When clicking on a random day of a random other week, switch to week view
this.model.setScale('week');
}
this.model.setDate(event.data.date, true);
@@ -151,8 +156,10 @@ var CalendarController = AbstractController.extend({
self.quick.destroy();
self.quick = null;
self.reload(id);
- }, function () {
+ }, function (error, errorEvent) {
// This will occurs if there are some more fields required
+ // Preventdefaulting the error event will prevent the traceback window
+ errorEvent.preventDefault();
event.data.options.disableQuickCreate = true;
event.data.data.on_save = self.quick.destroy.bind(self.quick);
self._onOpenCreate(event.data);
diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js
index cb371bfe6f9..ded7938605f 100644
--- a/addons/web/static/src/js/views/calendar/calendar_model.js
+++ b/addons/web/static/src/js/views/calendar/calendar_model.js
@@ -393,16 +393,19 @@ return AbstractModel.extend({
this.data.fc_options = this._getFullCalendarOptions();
var defs = _.map(this.data.filters, this._loadFilter.bind(this));
-
return $.when.apply($, defs).then(function () {
- return self._rpc({
+ var eventsDef = $.Deferred().resolve([]);
+ var filterDomain = self._getFilterDomain();
+ if (filterDomain.length || _.isEmpty(self.data.filters)) {
+ eventsDef = self._rpc({
model: self.modelName,
method: 'search_read',
context: self.data.context,
fields: self.fieldNames,
- domain: self.data.domain.concat(self._getRangeDomain()).concat(self._getFilterDomain())
- })
- .then(function (events) {
+ domain: self.data.domain.concat(self._getRangeDomain()).concat(filterDomain)
+ });
+ }
+ return eventsDef.then(function (events) {
self.data.data = _.map(events, self._recordToCalendarEvent.bind(self));
return $.when(
self._loadColors(self.data, self.data.data),
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index 23d63aa9044..a4930c87d37 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -164,6 +164,7 @@ return AbstractRenderer.extend({
init: function (parent, state, params) {
this._super.apply(this, arguments);
this.displayFields = params.displayFields;
+ this.model = params.model;
this.filters = [];
this.color_map = {};
diff --git a/addons/web/static/src/js/views/calendar/calendar_view.js b/addons/web/static/src/js/views/calendar/calendar_view.js
index 135224fb0cc..71a6c30b756 100644
--- a/addons/web/static/src/js/views/calendar/calendar_view.js
+++ b/addons/web/static/src/js/views/calendar/calendar_view.js
@@ -119,6 +119,7 @@ var CalendarView = AbstractView.extend({
this.rendererParams.displayFields = displayFields;
this.rendererParams.eventTemplate = _.findWhere(arch.children, {'tag': 'templates'});
+ this.rendererParams.model = viewInfo.model;
this.loadParams.fieldNames = _.uniq(fieldNames);
this.loadParams.mapping = mapping;
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index e9bc8cf2a2a..f741e98bf4d 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -409,7 +409,7 @@ var FormController = BasicController.extend({
* @private
*/
_onDiscard: function () {
- this.discardChanges();
+ this._discardChanges();
},
/**
* Called when the user clicks on 'Duplicate Record' in the sidebar
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index ee0fd323ce1..2ec9cf3181e 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -50,27 +50,6 @@ var ListController = BasicController.extend({
//--------------------------------------------------------------------------
/**
- * To improve performance, list view must not be rerendered if it is asked
- * to discard all its changes. Indeed, only the in-edition row needs to be
- * discarded in that case.
- *
- * @override
- * @param {string} [recordID] - default to main recordID
- * @returns {Deferred}
- */
- discardChanges: function (recordID) {
- if ((recordID || this.handle) === this.handle) {
- recordID = this.renderer.getEditableRecordID();
- if (recordID === null) {
- return $.when();
- }
- }
- var self = this;
- return this._super(recordID).then(function () {
- self._updateButtons('readonly');
- });
- },
- /**
* Calculate the active domain of the list view. This should be done only
* if the header checkbox has been checked. This is done by evaluating the
* search results, and then adding the dataset domain (i.e. action domain).
@@ -246,6 +225,28 @@ var ListController = BasicController.extend({
.then(this._setMode.bind(this, 'readonly', id));
},
/**
+ * To improve performance, list view must not be rerendered if it is asked
+ * to discard all its changes. Indeed, only the in-edition row needs to be
+ * discarded in that case.
+ *
+ * @override
+ * @private
+ * @param {string} [recordID] - default to main recordID
+ * @returns {Deferred}
+ */
+ _discardChanges: function (recordID) {
+ if ((recordID || this.handle) === this.handle) {
+ recordID = this.renderer.getEditableRecordID();
+ if (recordID === null) {
+ return $.when();
+ }
+ }
+ var self = this;
+ return this._super(recordID).then(function () {
+ self._updateButtons('readonly');
+ });
+ },
+ /**
* @override
* @private
*/
@@ -362,7 +363,7 @@ var ListController = BasicController.extend({
*/
_onDiscard: function (ev) {
ev.stopPropagation(); // So that it is not considered as a row leaving
- this.discardChanges();
+ this._discardChanges();
},
/**
* Called when the user asks to edit a row -> Updates the controller buttons
diff --git a/addons/web/static/src/less/control_panel.less b/addons/web/static/src/less/control_panel.less
index 83048112815..4a022112e09 100644
--- a/addons/web/static/src/less/control_panel.less
+++ b/addons/web/static/src/less/control_panel.less
@@ -99,13 +99,16 @@
.o-flex-display();
.o-flex-flow(row, wrap);
- .o_cp_buttons, .o_cp_pager {
+ .o_cp_buttons {
.o-flex-display();
margin-right: auto;
+ > div {
+ margin-top: 5px;
+ }
}
-
- .o_cp_buttons > div {
- margin-top: 5px;
+ .o_cp_pager {
+ .o-flex-display();
+ margin-left: auto;
}
}
diff --git a/addons/web/static/src/less/web_calendar.less b/addons/web/static/src/less/web_calendar.less
index ed25f8f8dd0..d6aab5e0e38 100644
--- a/addons/web/static/src/less/web_calendar.less
+++ b/addons/web/static/src/less/web_calendar.less
@@ -64,9 +64,6 @@
.o_field_type_char:first-child {
font-weight: bold;
}
- .o_field_type_many2many:last-child img {
- float: right;
- }
.fc-view {
overflow-x: auto;
> table {
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 78cc5fd9ad5..b8793053758 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -725,15 +725,11 @@
t-att-autofocus="widget.attrs.autofocus"
t-att-id="widget.idForLabel"/>
</t>
-<t t-name="FieldRadio">
- <span t-if="widget.mode === 'readonly'"/>
-
- <div t-if="widget.mode !== 'readonly'" t-attf-class="o_field_radio #{widget.nodeOptions.horizontal ? 'o_horizontal' : 'o_vertical'}">
- <div t-foreach="widget.values" t-as="value" class="o_radio_item">
- <input type="radio" class="o_radio_input" t-att-id="widget.unique_id + '_' + value[0]"
- t-att-name="widget.unique_id" t-att-data-value="value[0]" t-att-data-index="value_index"/>
- <label t-if="!widget.nodeOptions.no_radiolabel" class="o_form_label" t-att-for="widget.unique_id + '_' + value[0]"><t t-esc="value[1]"/></label>
- </div>
+<t t-name="FieldRadio.button">
+ <div class="o_radio_item">
+ <input type="radio" class="o_radio_input" t-att-checked="checked ? true : undefined"
+ t-att-data-value="value[0]" t-att-data-index="index" t-att-id="id"/>
+ <label class="o_form_label" t-att-for="id"><t t-esc="value[1]"/></label>
</div>
</t>
<t t-name="FieldMany2One">
@@ -754,9 +750,17 @@
<button type="button" t-if="!widget.nodeOptions.no_open" class="fa fa-external-link btn btn-default o_external_button" tabindex="-1" draggable="false"/>
</div>
</t>
+<t t-name="FieldReference" t-extend="FieldMany2One">
+ <t t-jquery=".o_input_dropdown" t-operation="before">
+ <select class="o_input o_field_widget">
+ <option/>
+ <option t-foreach="widget.field.selection" t-as="model" t-att-value="model[0]"><t t-esc="model[1]"/></option>
+ </select>
+ </t>
+</t>
<t t-name="FieldMany2ManyTag">
<t t-foreach="elements" t-as="el">
- <span t-attf-class="badge dropdown o_tag_color_#{el.color || 10}" t-att-data-color="el.color || 10" t-att-data-index="el_index" t-att-data-id="el.id">
+ <span t-attf-class="badge dropdown o_tag_color_#{el[colorField] || 10}" t-att-data-color="el[colorField] || 10" t-att-data-index="el_index" t-att-data-id="el.id">
<span class="o_badge_text"><t t-raw="el.display_name"/></span>
<span t-if="!readonly" class="fa fa-times o_delete"/>
</span>
diff --git a/addons/web/static/src/xml/web_calendar.xml b/addons/web/static/src/xml/web_calendar.xml
index e38d98e46de..430c2d3772c 100644
--- a/addons/web/static/src/xml/web_calendar.xml
+++ b/addons/web/static/src/xml/web_calendar.xml
@@ -20,7 +20,11 @@
<t t-foreach="widget.displayFields" t-as="name">
<div t-attf-class="o_field_#{name} o_field_type_#{fields[name].type}">
<t t-if="widget.displayFields[name].avatar_field">
- <t t-esc="fields[name].string"/>: <t t-foreach="widget.getAvatars(record, name, widget.displayFields[name].avatar_field).slice(0,5)" t-as="image"><t t-raw="image"/></t>
+ <t t-esc="fields[name].string"/>:
+ <div class="o_calendar_avatars pull-right">
+ <t t-foreach="widget.getAvatars(record, name, widget.displayFields[name].avatar_field).slice(0,3)" t-as="image"><t t-raw="image"/></t>
+ <span t-if="record[name].length - 3 > 0">+<t t-esc="record[name].length - 3"/></span>
+ </div>
</t>
<t t-else="">
<t t-esc="format(record, name)"/>
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index c492537c63f..c732fe35ce6 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -1043,6 +1043,40 @@ QUnit.module('basic_fields', {
_t.database.multi_lang = multiLang;
});
+ QUnit.test('go to next line (and not the next row) when pressing enter', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.fields.foo.type = 'text';
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<list editable="top">' +
+ '<field name="int_field"/>' +
+ '<field name="foo"/>' +
+ '<field name="qux"/>' +
+ '</list>',
+ });
+
+ list.$('tbody tr:first .o_list_text').click();
+ var $textarea = list.$('textarea.o_field_text');
+ assert.strictEqual($textarea.length, 1, "should have a text area");
+ assert.strictEqual($textarea.val(), 'yop', 'should still be "yop" in edit');
+
+ assert.strictEqual(list.$('textarea').get(0), document.activeElement,
+ "text area should have the focus");
+
+ // click on enter
+ list.$('textarea')
+ .trigger({type: "keydown", which: $.ui.keyCode.ENTER})
+ .trigger({type: "keyup", which: $.ui.keyCode.ENTER});
+
+ assert.strictEqual(list.$('textarea').first().get(0), document.activeElement,
+ "text area should still have the focus");
+
+ list.destroy();
+ });
+
QUnit.module('FieldBinary');
QUnit.test('binary fields are correctly rendered', function (assert) {
@@ -1973,6 +2007,63 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('monetary field with currency set by an onchange', function (assert) {
+ // this test ensures that the monetary field can be re-rendered with and
+ // without currency (which can happen as the currency can be set by an
+ // onchange)
+ assert.expect(8);
+
+ this.data.partner.onchanges = {
+ int_field: function (obj) {
+ obj.currency_id = obj.int_field ? 2 : null;
+ },
+ };
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="top">' +
+ '<field name="int_field"/>' +
+ '<field name="qux" widget="monetary"/>' +
+ '<field name="currency_id" invisible="1"/>' +
+ '</tree>',
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ list.$buttons.find('.o_list_button_add').click();
+ assert.strictEqual(list.$('div.o_field_widget[name=qux] input').length, 1,
+ "monetary field should have been rendered correctly (without currency)");
+ assert.strictEqual(list.$('.o_field_widget[name=qux] span').length, 0,
+ "monetary field should have been rendered correctly (without currency)");
+
+ // set a value for int_field -> should set the currency and re-render qux
+ list.$('.o_field_widget[name=int_field]').click().val('7').trigger('input');
+ assert.strictEqual(list.$('div.o_field_widget[name=qux] input').length, 1,
+ "monetary field should have been re-rendered correctly (with currency)");
+ assert.strictEqual(list.$('.o_field_widget[name=qux] span:contains(€)').length, 1,
+ "monetary field should have been re-rendered correctly (with currency)");
+ var $quxInput = list.$('.o_field_widget[name=qux] input');
+ $quxInput.click(); // check that the field is focusable
+ assert.strictEqual(document.activeElement, $quxInput[0],
+ "focus should be on the qux field's input");
+
+ // unset the value of int_field -> should unset the currency and re-render qux
+ list.$('.o_field_widget[name=int_field]').click().val('0').trigger('input');
+ $quxInput = list.$('div.o_field_widget[name=qux] input');
+ assert.strictEqual($quxInput.length, 1,
+ "monetary field should have been re-rendered correctly (without currency)");
+ assert.strictEqual(list.$('.o_field_widget[name=qux] span').length, 0,
+ "monetary field should have been re-rendered correctly (without currency)");
+ $quxInput.click(); // check that the field is still focusable
+ assert.strictEqual(document.activeElement, $quxInput[0],
+ "focus should be on the qux field's input");
+
+ list.destroy();
+ });
+
QUnit.module('FieldInteger');
QUnit.test('integer field when unset', function (assert) {
@@ -2024,7 +2115,7 @@ QUnit.module('basic_fields', {
});
QUnit.test('integer field in form view with virtual id', function (assert) {
- assert.expect(2);
+ assert.expect(1);
var params = {
View: FormView,
model: 'partner',
@@ -2036,15 +2127,7 @@ QUnit.module('basic_fields', {
var form = createView(params);
assert.strictEqual(form.$('.o_field_widget').text(), "2-20170808020000",
"Should display virtual id");
- form.destroy();
- params.res_id = this.data.partner.records[1].id = "2.65-20170808020000";
- try {
- form = createView(params);
- } catch (error) {
- assert.strictEqual(error.message, '"2.65-20170808020000" is not an integer or a virtual id',
- "Should throw an exception because it's a wrong value");
- }
form.destroy();
});
diff --git a/addons/web/static/tests/fields/field_utils_tests.js b/addons/web/static/tests/fields/field_utils_tests.js
index e00d45ef91f..a49197f6e65 100644
--- a/addons/web/static/tests/fields/field_utils_tests.js
+++ b/addons/web/static/tests/fields/field_utils_tests.js
@@ -2,6 +2,7 @@ odoo.define('web.field_utils_tests', function (require) {
"use strict";
var core = require('web.core');
+var session = require('web.session');
var fieldUtils = require('web.field_utils');
QUnit.module('fields', {}, function () {
@@ -122,7 +123,20 @@ QUnit.test('parse float', function(assert) {
});
QUnit.test('parse monetary', function(assert) {
- assert.expect(6);
+ assert.expect(11);
+ var originalCurrencies = session.currencies;
+ session.currencies = {
+ 1: {
+ digits: [69, 2],
+ position: "after",
+ symbol: "€"
+ },
+ 3: {
+ digits: [69, 2],
+ position: "before",
+ symbol: "$"
+ }
+ };
assert.strictEqual(fieldUtils.parse.monetary(""), 0);
assert.strictEqual(fieldUtils.parse.monetary("0"), 0);
@@ -130,6 +144,13 @@ QUnit.test('parse monetary', function(assert) {
assert.strictEqual(fieldUtils.parse.monetary("-100.00"), -100);
assert.strictEqual(fieldUtils.parse.monetary("1,000.00"), 1000);
assert.strictEqual(fieldUtils.parse.monetary("1,000,000.00"), 1000000);
+ assert.strictEqual(fieldUtils.parse.monetary("$&nbsp;125.00", {}, {currency_id: 3}), 125);
+ assert.strictEqual(fieldUtils.parse.monetary("1,000.00&nbsp;€", {}, {currency_id: 1}), 1000);
+ assert.throws(function() {fieldUtils.parse.monetary("$ 12.00", {}, {currency_id: 3})}, /is not a correct/);
+ assert.throws(function() {fieldUtils.parse.monetary("$&nbsp;12.00", {}, {currency_id: 1})}, /is not a correct/);
+ assert.throws(function() {fieldUtils.parse.monetary("$&nbsp;12.00&nbsp;34", {}, {currency_id: 3})}, /is not a correct/);
+
+ session.currencies = originalCurrencies;
});
});
});
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 3bf541b097d..d4d3a6307f9 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -34,6 +34,8 @@ QUnit.module('relational_fields', {
date: {string: "Some Date", type: "date"},
datetime: {string: "Datetime Field", type: 'datetime'},
user_id: {string: "User", type: 'many2one', relation: 'user'},
+ reference: {string: "Reference Field", type: 'reference', selection: [
+ ["product", "Product"], ["partner_type", "Partner Type"], ["partner", "Partner"]]},
},
records: [{
id: 1,
@@ -47,6 +49,7 @@ QUnit.module('relational_fields', {
timmy: [],
trululu: 4,
user_id: 17,
+ reference: 'product,37',
}, {
id: 2,
display_name: "second record",
@@ -963,7 +966,9 @@ QUnit.module('relational_fields', {
});
QUnit.test('domain and context are correctly used when doing a name_search in a m2o', function (assert) {
- assert.expect(2);
+ assert.expect(4);
+
+ this.data.partner.records[0].timmy = [12];
var form = createView({
View: FormView,
@@ -975,11 +980,13 @@ QUnit.module('relational_fields', {
'domain="[[\'foo\', \'=\', \'bar\'], [\'foo\', \'=\', foo]]" ' +
'context="{\'hello\': \'world\', \'test\': foo}"/>' +
'<field name="foo"/>' +
+ '<field name="trululu" context="{\'timmy\': timmy}" domain="[[\'id\', \'in\', timmy]]"/>' +
+ '<field name="timmy" widget="many2many_tags" invisible="1"/>' +
'</form>',
res_id: 1,
session: {user_context: {hey: "ho"}},
mockRPC: function (route, args) {
- if (route === '/web/dataset/call_kw/product/name_search') {
+ if (args.method === 'name_search' && args.model === 'product') {
assert.deepEqual(
args.kwargs.args,
[['foo', '=', 'bar'], ['foo', '=', 'yop']],
@@ -990,12 +997,20 @@ QUnit.module('relational_fields', {
'the field attr context should have been used for the '
+ 'RPC (evaluated and merged with the session one)');
}
+ if (args.method === 'name_search' && args.model === 'partner') {
+ assert.deepEqual(args.kwargs.args, [['id', 'in', [12]]],
+ 'the field attr domain should have been used for the RPC (and evaluated)');
+ assert.deepEqual(args.kwargs.context, {hey: 'ho', timmy: [[6, false, [12]]]},
+ 'the field attr context should have been used for the RPC (and evaluated)');
+ }
return this._super.apply(this, arguments);
},
});
form.$buttons.find('.o_form_button_edit').click();
- form.$('input').first().click();
+ form.$('.o_field_widget[name=product_id] input').click();
+
+ form.$('.o_field_widget[name=trululu] input').click();
form.destroy();
});
@@ -1235,6 +1250,56 @@ QUnit.module('relational_fields', {
});
});
+ QUnit.test('many2one: domain updated by an onchange', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.onchanges = {
+ int_field: function () {},
+ };
+
+ var domain = [];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field"/>' +
+ '<field name="trululu"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ domain = [['id', 'in', [10]]];
+ return $.when({
+ domain: {
+ trululu: domain,
+ unexisting_field: domain,
+ }
+ });
+ }
+ if (args.method === 'name_search') {
+ assert.deepEqual(args.kwargs.args, domain,
+ "sent domain should be correct");
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // trigger a name_search (domain should be [])
+ form.$('.o_field_widget[name=trululu] input').click();
+ // close the dropdown
+ form.$('.o_field_widget[name=trululu] input').click();
+ // trigger an onchange that will update the domain
+ form.$('.o_field_widget[name=int_field]').val(2).trigger('input');
+ // trigger a name_search (domain should be [['id', 'in', [10]]])
+ form.$('.o_field_widget[name=trululu] input').click();
+
+ form.destroy();
+ });
+
QUnit.module('FieldOne2Many');
QUnit.test('one2many basic properties', function (assert) {
@@ -1522,6 +1587,178 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('edition of one2many field with pager', function (assert) {
+ assert.expect(31);
+
+ var ids = [];
+ for (var i = 0; i < 45; i++) {
+ var id = 10 + i;
+ ids.push(id);
+ this.data.partner.records.push({
+ id: id,
+ display_name: "relational record " + id,
+ });
+ }
+ this.data.partner.records[0].p = ids;
+
+ var saveCount = 0;
+ var checkRead = false;
+ var readIDs;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<kanban>' +
+ '<field name="display_name"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_global_click">' +
+ '<a t-if="!read_only_mode" type="delete" class="fa fa-times pull-right delete_icon"/>' +
+ '<span><t t-esc="record.display_name.value"/></span>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner,false,form': '<form><field name="display_name"/></form>',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'read' && checkRead) {
+ readIDs = args.args[0];
+ checkRead = false;
+ }
+ if (args.method === 'write') {
+ saveCount++;
+ var nbCommands = args.args[1].p.length;
+ var nbLinkCommands = _.filter(args.args[1].p, function (command) {
+ return command[0] === 4;
+ }).length;
+ switch(saveCount) {
+ case 1:
+ assert.strictEqual(nbCommands, 46,
+ "should send 46 commands (one for each record)");
+ assert.strictEqual(nbLinkCommands, 45,
+ "should send a LINK_TO command for each existing record");
+ assert.deepEqual(args.args[1].p[45], [0, false, {
+ display_name: 'new record',
+ }], "should sent a CREATE command for the new record");
+ break;
+ case 2:
+ assert.strictEqual(nbCommands, 46,
+ "should send 46 commands");
+ assert.strictEqual(nbLinkCommands, 45,
+ "should send a LINK_TO command for each existing record");
+ assert.deepEqual(args.args[1].p[45], [2, 10, false],
+ "should sent a DELETE command for the deleted record");
+ break;
+ case 3:
+ assert.strictEqual(nbCommands, 47,
+ "should send 47 commands");
+ assert.strictEqual(nbLinkCommands, 43,
+ "should send a LINK_TO command for each existing record");
+ assert.deepEqual(args.args[1].p[43],
+ [0, false, {display_name: 'new record page 1'}],
+ "should sent correct CREATE command");
+ assert.deepEqual(args.args[1].p[44],
+ [0, false, {display_name: 'new record page 2'}],
+ "should sent correct CREATE command");
+ assert.deepEqual(args.args[1].p[45],
+ [2, 11, false],
+ "should sent correct DELETE command");
+ assert.deepEqual(args.args[1].p[46],
+ [2, 52, false],
+ "should sent correct DELETE command");
+ break;
+ }
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'there should be 40 records on page 1');
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '1-40 / 45', "pager range should be correct");
+
+ // add a record on page one
+ checkRead = true;
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o-kanban-button-new').click();
+ $('.modal input').val('new record').trigger('input');
+ $('.modal .modal-footer .btn-primary:first').click(); // save and close
+ // checks
+ assert.strictEqual(readIDs, undefined, "should not have read any record");
+ assert.strictEqual(form.$('span:contains(new record)').length, 0,
+ "new record should be on page 2");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'there should be 40 records on page 1');
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '1-40 / 46', "pager range should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:first span:contains(new record)').length,
+ 0, 'new record should not be on page 1');
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+
+ // delete a record on page one
+ checkRead = true;
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 10)').length,
+ 1, 'first record should be the one with id 10 (next checks rely on that)');
+ form.$('.delete_icon:first').click();
+ // checks
+ assert.deepEqual(readIDs, [50],
+ "should have read a record (to display 40 records on page 1)");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'there should be 40 records on page 1');
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '1-40 / 45', "pager range should be correct");
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+
+ // add and delete records in both pages
+ form.$buttons.find('.o_form_button_edit').click();
+ checkRead = true;
+ readIDs = undefined;
+ // add and delete a record in page 1
+ form.$('.o-kanban-button-new').click();
+ $('.modal input').val('new record page 1').trigger('input');
+ $('.modal .modal-footer .btn-primary:first').click(); // save and close
+ assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 11)').length,
+ 1, 'first record should be the one with id 11 (next checks rely on that)');
+ form.$('.delete_icon:first').click();
+ assert.deepEqual(readIDs, [51],
+ "should have read a record (to display 40 records on page 1)");
+ // add and delete a record in page 2
+ form.$('.o_x2m_control_panel .o_pager_next').click();
+ assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 52)').length,
+ 1, 'first record should be the one with id 52 (next checks rely on that)');
+ checkRead = true;
+ readIDs = undefined;
+ form.$('.delete_icon:first').click();
+ form.$('.o-kanban-button-new').click();
+ $('.modal input').val('new record page 2').trigger('input');
+ $('.modal .modal-footer .btn-primary:first').click(); // save and close
+ assert.strictEqual(readIDs, undefined, "should not have read any record");
+ // checks
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5,
+ 'there should be 5 records on page 2');
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '41-45 / 45', "pager range should be correct");
+ assert.strictEqual(form.$('.o_kanban_record span:contains(new record page 1)').length,
+ 1, 'new records should be on page 2');
+ assert.strictEqual(form.$('.o_kanban_record span:contains(new record page 2)').length,
+ 1, 'new records should be on page 2');
+ // save
+ form.$buttons.find('.o_form_button_save').click();
+
+ form.destroy();
+ });
+
QUnit.test('sorting one2many fields', function (assert) {
assert.expect(4);
@@ -2357,6 +2594,45 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('editable o2m with onchange and required field: delete an invalid line', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.onchanges = {
+ turtles: function () {},
+ };
+ this.data.partner.records[0].turtles = [1];
+ this.data.turtle.records[0].product_id = 37;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ form.$('.o_data_cell:first').click();
+ form.$('.o_field_widget[name="product_id"] input').val('').trigger('keyup');
+ assert.verifySteps(['read', 'read'], 'no onchange should be done as line is invalid');
+ form.$('.o_list_record_delete').click();
+ assert.verifySteps(['read', 'read', 'onchange'], 'onchange should have been done');
+
+ form.destroy();
+ });
+
QUnit.test('onchange in a one2many', function (assert) {
assert.expect(1);
@@ -2515,6 +2791,75 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('one2many and onchange (with command DELETE_ALL)', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.p = [[5]];
+ },
+ p: function () {}, // dummy onchange on the o2m to execute _isX2ManyValid()
+ };
+ this.data.partner.records[0].p = [2];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (method, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].p, [
+ [0, false, {display_name: 'z'}],
+ [2, 2, false],
+ ], "correct commands should be sent");
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ "o2m should contain one row");
+
+ // empty o2m by triggering the onchange
+ form.$('.o_field_widget[name=foo]').val('trigger onchange').trigger('input');
+
+ assert.strictEqual(form.$('.o_data_row').length, 0,
+ "rows of the o2m should have been deleted");
+
+ // add two new subrecords
+ form.$('.o_field_x2many_list_row_add a').click();
+ form.$('.o_field_widget[name=display_name]').val('x').trigger('input');
+ form.$('.o_field_x2many_list_row_add a').click();
+ form.$('.o_field_widget[name=display_name]').val('y').trigger('input');
+
+ assert.strictEqual(form.$('.o_data_row').length, 2,
+ "o2m should contain two rows");
+
+ // empty o2m by triggering the onchange
+ form.$('.o_field_widget[name=foo]').val('trigger onchange again').trigger('input');
+
+ assert.strictEqual(form.$('.o_data_row').length, 0,
+ "rows of the o2m should have been deleted");
+
+ form.$('.o_field_x2many_list_row_add a').click();
+ form.$('.o_field_widget[name=display_name]').val('z').trigger('input');
+
+ form.$buttons.find('.o_form_button_save').click();
+ form.destroy();
+ });
+
QUnit.test('one2many list (editable): readonly domain is evaluated', function (assert) {
assert.expect(2);
@@ -2782,6 +3127,41 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('value of invisible x2many fields is correctly evaluated in context', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].timmy = [12];
+ this.data.partner.records[0].p = [2, 3];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<field name="product_id" context="{\'p\': p, \'timmy\': timmy}"/>' +
+ '<field name="p" invisible="1"/>' +
+ '<field name="timmy" invisible="1"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_search') {
+ assert.deepEqual(
+ args.kwargs.context, {
+ p: [[4, 2, false], [4, 3, false]],
+ timmy: [[6, false, [12]]],
+ }, 'values of x2manys should have been correctly evaluated in context');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_widget[name=product_id] input').click();
+
+ form.destroy();
+ });
+
QUnit.test('one2many list, editable, with many2one and with context with parent key', function (assert) {
assert.expect(1);
@@ -4328,6 +4708,93 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('editable list: contexts are correctly sent', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].timmy = [12];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="foo"/>' +
+ '<field name="timmy" context="{\'key\': parent.foo}">' +
+ '<tree editable="top">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'read' && args.model === 'partner') {
+ assert.deepEqual(args.kwargs.context, {
+ active_field: 2,
+ bin_size: true,
+ someKey: 'some value',
+ }, "sent context should be correct");
+ }
+ if (args.method === 'read' && args.model === 'partner_type') {
+ assert.deepEqual(args.kwargs.context, {
+ key: 'yop',
+ someKey: 'some value',
+ }, "sent context should be correct");
+ }
+ if (args.method === 'write') {
+ assert.deepEqual(args.kwargs.context, {
+ active_field: 2,
+ someKey: 'some value',
+ }, "sent context should be correct");
+ }
+ return this._super.apply(this, arguments);
+ },
+ session: {
+ user_context: {someKey: 'some value'},
+ },
+ viewOptions: {
+ mode: 'edit',
+ context: {active_field: 2},
+ },
+ res_id: 1,
+ });
+
+ form.$('.o_data_cell:first').click();
+ form.$('.o_field_widget[name=display_name]').val('abc').trigger('input');
+ form.$buttons.find('.o_form_button_save').click();
+
+ form.destroy();
+ });
+
+ QUnit.test('resetting invisible one2manys', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].turtles = [];
+ this.data.partner.onchanges.foo = function (obj) {
+ obj.turtles = [[5], [4, 1]];
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="foo"/>' +
+ '<field name="turtles" invisible="1"/>' +
+ '</form>',
+ viewOptions: {
+ mode: 'edit',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$('input[name="foo"]').val('abcd').trigger('input');
+ assert.verifySteps(['read', 'onchange']);
+
+ form.destroy();
+ });
+
QUnit.module('FieldMany2Many');
QUnit.test('many2many kanban: edition', function (assert) {
@@ -4987,6 +5454,96 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('onchange with 40+ commands for a many2many', function (assert) {
+ // this test ensures that the basic_model correctly handles more LINK_TO
+ // commands than the limit of the dataPoint (40 for x2many kanban)
+ assert.expect(23);
+
+ // create a lot of partner_types that will be linked by the onchange
+ var commands = [[5]];
+ for (var i = 0; i < 45; i++) {
+ var id = 100 + i;
+ this.data.partner_type.records.push({id: id, display_name: "type " + id});
+ commands.push([4, id]);
+ }
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.timmy = commands;
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="timmy">' +
+ '<kanban>' +
+ '<field name="display_name"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div><t t-esc="record.display_name.value"/></div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].timmy[0][0], 6,
+ "should send a command 6");
+ assert.strictEqual(args.args[1].timmy[0][2].length, 45,
+ "should replace with 45 ids");
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.verifySteps(['read']);
+
+ form.$('.o_field_widget[name=foo]').val('trigger onchange').trigger('input');
+
+ assert.verifySteps(['read', 'onchange', 'read']);
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '1-40 / 45', "pager should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'there should be 40 records displayed on page 1');
+
+ form.$('.o_pager_next').click();
+ assert.verifySteps(['read', 'onchange', 'read', 'read']);
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '41-45 / 45', "pager should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5,
+ 'there should be 5 records displayed on page 2');
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '1-40 / 45', "pager should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'there should be 40 records displayed on page 1');
+
+ form.$('.o_pager_next').click();
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '41-45 / 45', "pager should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5,
+ 'there should be 5 records displayed on page 2');
+
+ form.$('.o_pager_next').click(); // back to page 1
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '1-40 / 45', "pager should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'there should be 40 records displayed on page 1');
+
+ form.destroy();
+ });
+
QUnit.module('FieldStatus');
QUnit.test('static statusbar widget on many2one field', function (assert) {
@@ -5380,10 +5937,117 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('unset selection on a many2one field', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="trululu" widget="selection"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].trululu, false,
+ "should send 'false' as trululu value");
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ form.$('select').val("false").trigger('change');
+ form.$buttons.find('.o_form_button_save').click();
+
+ form.destroy();
+ });
+
+ QUnit.test('widget selection on a many2one: domain updated by an onchange', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.onchanges = {
+ int_field: function () {},
+ };
+
+ var domain = [];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field"/>' +
+ '<field name="trululu" widget="selection"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ domain = [['id', 'in', [10]]];
+ return $.when({
+ domain: {
+ trululu: domain,
+ }
+ });
+ }
+ if (args.method === 'name_search') {
+ assert.deepEqual(args.args[1], domain,
+ "sent domain should be correct");
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_widget[name=trululu] option').length, 4,
+ "should be 4 options in the selection");
+
+ // trigger an onchange that will update the domain
+ form.$('.o_field_widget[name=int_field]').val(2).trigger('input');
+
+ assert.strictEqual(form.$('.o_field_widget[name=trululu] option').length, 1,
+ "should be 1 option in the selection");
+
+ form.destroy();
+ });
+
QUnit.module('FieldMany2ManyTags');
- QUnit.test('fieldmany2many tags: rendering and edition', function (assert) {
- assert.expect(17);
+ QUnit.test('fieldmany2many tags without color', function (assert) {
+ assert.expect(4);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="timmy" widget="many2many_tags"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method ==='read' && args.model === 'partner_type') {
+ assert.deepEqual(args.args , [[12], ['display_name']], "should not read any color field");
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+ var $input = form.$('.o_field_many2manytags input');
+ $input.click(); // opens the dropdown
+ assert.strictEqual($input.autocomplete('widget').find('li').length, 3,
+ "autocomplete dropdown should have 3 entries (2 values + 'Search and Edit...')");
+ $input.autocomplete('widget').find('li:first()').click(); // adds a tag
+ assert.strictEqual(form.$('.o_field_many2manytags > span').length, 1,
+ "should contain 1 tag");
+ assert.ok(form.$('.o_field_many2manytags > span:contains("gold")').length,
+ "should contain newly added tag 'gold'");
+ form.destroy();
+ });
+
+ QUnit.test('fieldmany2many tags with color: rendering and edition', function (assert) {
+ assert.expect(20);
this.data.partner.records[0].timmy = [12, 14];
this.data.partner_type.records.push({id: 13, display_name: "red", color: 8});
@@ -5392,7 +6056,7 @@ QUnit.module('relational_fields', {
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
- '<field name="timmy" widget="many2many_tags" options="{\'no_create_edit\': True}"/>' +
+ '<field name="timmy" widget="many2many_tags" options="{\'color_field\': \'color\', \'no_create_edit\': True}"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
@@ -5403,6 +6067,9 @@ QUnit.module('relational_fields', {
assert.ok(_.isEqual(_.sortBy(commands[0][2], _.identity.bind(_)), [12, 13]),
"new value should be [12, 13]");
}
+ if (args.method ==='read' && args.model === 'partner_type') {
+ assert.deepEqual(args.args[1], ['display_name', 'color'], "should read the color field");
+ }
return this._super.apply(this, arguments);
},
});
@@ -5548,7 +6215,7 @@ QUnit.module('relational_fields', {
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
- '<field name="timmy" widget="many2many_tags"/>' +
+ '<field name="timmy" widget="many2many_tags" options="{\'color_field\': \'color\'}"/>' +
'</form>',
res_id: 1,
});
@@ -5794,6 +6461,58 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('widget radio on a many2one: domain updated by an onchange', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.onchanges = {
+ int_field: function () {},
+ };
+
+ var domain = [];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field"/>' +
+ '<field name="trululu" widget="radio"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ domain = [['id', 'in', [10]]];
+ return $.when({
+ value: {
+ trululu: false,
+ },
+ domain: {
+ trululu: domain,
+ },
+ });
+ }
+ if (args.method === 'search_read') {
+ assert.deepEqual(args.kwargs.domain, domain,
+ "sent domain should be correct");
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_widget[name=trululu] .o_radio_item').length, 3,
+ "should be 3 radio buttons");
+
+ // trigger an onchange that will update the domain
+ form.$('.o_field_widget[name=int_field]').val(2).trigger('input');
+
+ assert.strictEqual(form.$('.o_field_widget[name=trululu] .o_radio_item').length, 0,
+ "should be no more radio button");
+
+ form.destroy();
+ });
+
QUnit.module('FieldMany2ManyCheckBoxes');
QUnit.test('widget many2many_checkboxes', function (assert) {
@@ -5973,6 +6692,103 @@ QUnit.module('relational_fields', {
});
});
+ QUnit.module('FieldReference');
+
+ QUnit.test('reference in form view', function (assert) {
+ assert.expect(15);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="reference" string="custom label"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'product,false,form': '<form string="Product"><field name="display_name"/></form>',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'get_formview_action') {
+ assert.deepEqual(args.args[0], [37], "should call get_formview_action with correct id");
+ return $.when({
+ res_id: 17,
+ type: 'ir.actions.act_window',
+ target: 'current',
+ res_model: 'res.partner'
+ });
+ }
+ if (args.method === 'get_formview_id') {
+ assert.deepEqual(args.args[0], [37], "should call get_formview_id with correct id");
+ return $.when(false);
+ }
+ if (args.method === 'name_search') {
+ assert.strictEqual(args.model, 'partner_type',
+ "the name_search should be done on the newly set model");
+ }
+ if (args.method === 'write') {
+ assert.strictEqual(args.model, 'partner',
+ "should write on the current model");
+ assert.deepEqual(args.args, [[1], {reference: 'partner_type,12'}],
+ "should write the correct value");
+ }
+ return this._super(route, args);
+ },
+ });
+
+ testUtils.intercept(form, 'do_action', function (event) {
+ assert.strictEqual(event.data.action.res_id, 17,
+ "should do a do_action with correct parameters");
+ });
+
+ assert.strictEqual(form.$('a.o_form_uri:contains(xphone)').length, 1,
+ "should contain a link");
+ form.$('a.o_form_uri').click(); // click on the link in readonly mode (should trigger do_action)
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('.o_field_widget').length, 2,
+ "should contain two field widgets (selection and many2one)");
+ assert.strictEqual(form.$('.o_field_many2one').length, 1,
+ "should contain one many2one");
+ assert.strictEqual(form.$('.o_field_widget select').val(), "product",
+ "widget should contain one select with the model");
+ assert.strictEqual(form.$('.o_field_widget input').val(), "xphone",
+ "widget should contain one input with the record");
+
+ var options = _.map(form.$('.o_field_widget select > option'), function (el) {
+ return $(el).val();
+ });
+ assert.deepEqual(options, ['', 'product', 'partner_type', 'partner'],
+ "the options should be correctly set");
+
+
+ form.$('.o_external_button').click(); // click on the external button (should do an RPC)
+
+ assert.strictEqual($('.modal .modal-title').text().trim(), 'Open: custom label',
+ "dialog title should display the custom string label");
+ $('.modal .o_form_button_cancel').click();
+
+ form.$('.o_field_widget select').val('partner_type').trigger('change');
+ assert.strictEqual(form.$('.o_field_widget input').val(), "",
+ "many2one value should be reset after model change");
+
+ var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
+ // change the value of the m2o with a suggestion of the dropdown
+ form.$('.o_field_many2one input').click(); // will trigger a name_search on partner_type model
+ $dropdown.find('li:first()').click();
+
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('a.o_form_uri:contains(gold)').length, 1,
+ "should contain a link with the new value");
+
+ form.destroy();
+ });
+
});
});
});
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index cdd4ff4e523..cab291ec012 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -94,6 +94,7 @@ var MockServer = Class.extend({
args = JSON.parse(JSON.stringify(args));
if (logLevel === 2) {
console.log('%c[rpc] request ' + route, 'color: blue; font-weight: bold;', args);
+ args = JSON.parse(JSON.stringify(args));
}
return this._performRpc(route, args).then(function (result) {
var resultString = JSON.stringify(result || false);
@@ -567,8 +568,10 @@ var MockServer = Class.extend({
} else {
result[fields[i]] = false;
}
+ } else if (field.type === 'one2many' || field.type === 'many2many') {
+ result[fields[i]] = record[fields[i]] || [];
} else {
- result[fields[i]] = record[fields[i]];
+ result[fields[i]] = record[fields[i]] || false;
}
}
return result;
diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js
index fafe2dee1c6..0b47e9d0df8 100644
--- a/addons/web/static/tests/helpers/test_utils.js
+++ b/addons/web/static/tests/helpers/test_utils.js
@@ -356,6 +356,15 @@ function createModel(params) {
addMockEnvironment(widget, params);
+ // override the model's 'destroy' so that it calls 'destroy' on the widget
+ // instead, as the widget is the parent of the model and the mockServer.
+ model.destroy = function () {
+ // remove the override to properly destroy the model when it will be
+ // called the second time (by its parent)
+ delete model.destroy;
+ widget.destroy();
+ };
+
return model;
}
diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js
index 9fbc2acdd88..7f7547262da 100644
--- a/addons/web/static/tests/views/basic_model_tests.js
+++ b/addons/web/static/tests/views/basic_model_tests.js
@@ -20,6 +20,7 @@ QUnit.module('Views', {
product_ids: {string: "Favorite products", type: 'one2many', relation: 'product'},
category: {string: "Category M2M", type: 'many2many', relation: 'partner_type'},
date: {string: "Date Field", type: 'date'},
+ reference: {string: "Reference Field", type: 'reference', selection: [["product", "Product"], ["partner_type", "Partner Type"], ["partner", "Partner"]]},
},
records: [
{id: 1, foo: 'blip', bar: 1, product_id: 37, category: [12], display_name: "first partner", date: "2017-01-25"},
@@ -71,9 +72,15 @@ QUnit.module('Views', {
Model: BasicModel,
data: this.data,
mockRPC: function (route, args) {
- assert.strictEqual(args.kwargs.context.active_field, 2,
- "should have sent the correct context");
+ assert.deepEqual(args.kwargs.context, {
+ active_field: 2,
+ bin_size: true,
+ someKey: 'some value',
+ }, "should have sent the correct context");
return this._super.apply(this, arguments);
+ },
+ session: {
+ user_context: {someKey: 'some value'},
}
});
@@ -302,6 +309,89 @@ QUnit.module('Views', {
model.destroy();
});
+ QUnit.test('notifyChange on a one2many', function (assert) {
+ assert.expect(9);
+
+ this.data.partner.records[1].product_ids = [37];
+ this.params.fieldNames = ['product_ids'];
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ assert.strictEqual(args.model, 'product');
+ }
+ return this._super(route, args);
+ },
+ });
+
+ var o2mParams = {
+ modelName: 'product',
+ fields: this.data.product.fields,
+ fieldNames: ['display_name'],
+ };
+ model.load(this.params).then(function (resultID) {
+ model.load(o2mParams).then(function (newRecordID) {
+ var record = model.get(resultID);
+ var x2mListID = record.data.product_ids.id;
+
+ assert.strictEqual(record.data.product_ids.count, 1,
+ "there should be one record in the relation");
+
+ // trigger a 'ADD' command
+ model.notifyChanges(resultID, {product_ids: {operation: 'ADD', id: newRecordID}});
+
+ assert.deepEqual(model.localData[x2mListID]._changes, [{
+ operation: 'ADD', id: newRecordID,
+ }], "_changes should be correct");
+ record = model.get(resultID);
+ assert.strictEqual(record.data.product_ids.count, 2,
+ "there should be two records in the relation");
+
+ // trigger a 'UPDATE' command
+ model.notifyChanges(resultID, {product_ids: {operation: 'UPDATE', id: newRecordID}});
+
+ assert.deepEqual(model.localData[x2mListID]._changes, [{
+ operation: 'ADD', id: newRecordID,
+ }, {
+ operation: 'UPDATE', id: newRecordID,
+ }], "_changes should be correct");
+ record = model.get(resultID);
+ assert.strictEqual(record.data.product_ids.count, 2,
+ "there should be two records in the relation");
+
+ // trigger a 'REMOVE' command on the existing record
+ var existingRecordID = record.data.product_ids.data[0].id;
+ model.notifyChanges(resultID, {product_ids: {operation: 'REMOVE', ids: [existingRecordID]}});
+
+ assert.deepEqual(model.localData[x2mListID]._changes, [{
+ operation: 'ADD', id: newRecordID,
+ }, {
+ operation: 'UPDATE', id: newRecordID,
+ }, {
+ operation: 'REMOVE', id: existingRecordID,
+ }],
+ "_changes should be correct");
+ record = model.get(resultID);
+ assert.strictEqual(record.data.product_ids.count, 1,
+ "there should be one record in the relation");
+
+ // trigger a 'REMOVE' command on the new record
+ model.notifyChanges(resultID, {product_ids: {operation: 'REMOVE', ids: [newRecordID]}});
+
+ assert.deepEqual(model.localData[x2mListID]._changes, [{
+ operation: 'REMOVE', id: existingRecordID,
+ }], "_changes should be correct");
+ record = model.get(resultID);
+ assert.strictEqual(record.data.product_ids.count, 0,
+ "there should be no record in the relation");
+ });
+
+ });
+ model.destroy();
+ });
+
QUnit.test('notifyChange on a many2one, without display_name', function (assert) {
assert.expect(3);
@@ -1328,7 +1418,14 @@ QUnit.module('Views', {
this.data.partner.onchanges.foo = function (obj) {
obj.bar = obj.foo.length;
};
- this.data.partner.fields.bar.readonly = true;
+ this.params.fieldsInfo = {
+ default: {
+ foo: {},
+ bar: {
+ modifiers:"{\"readonly\": true}",
+ },
+ }
+ };
var model = createModel({
Model: BasicModel,
@@ -1694,10 +1791,97 @@ QUnit.module('Views', {
product_id: 37,
product_ids: [],
qux: false,
+ reference: false,
total: 0
}, "should use the proper eval context");
});
model.destroy();
});
+ QUnit.test('x2manys in contexts and domains are correctly evaluated', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].product_ids = [37, 41];
+ this.params.fieldNames = Object.keys(this.data.partner.fields);
+ this.params.fieldsInfo = {
+ form: {
+ qux: {
+ context: "{'category': category, 'product_ids': product_ids}",
+ domain: "[['id', 'in', category], ['id', 'in', product_ids]]",
+ relatedFields: this.data.partner.fields,
+ },
+ category: {
+ relatedFields: this.data.partner_type.fields,
+ },
+ product_ids: {
+ relatedFields: this.data.product.fields,
+ },
+ },
+ };
+ this.params.viewType = 'form';
+ this.params.res_id = 1;
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ var context = record.getContext({fieldName: 'qux'});
+ var domain = record.getDomain({fieldName: 'qux'});
+
+ assert.deepEqual(context, {
+ category: [12],
+ product_ids: [37, 41],
+ }, "x2many values in context manipulated client-side should be lists of ids");
+ assert.strictEqual(JSON.stringify(context),
+ "{\"category\":[[6,false,[12]]],\"product_ids\":[[4,37,false],[4,41,false]]}",
+ "x2many values in context sent to the server should be commands");
+ assert.deepEqual(domain, [
+ ['id', 'in', [12]],
+ ['id', 'in', [37, 41]],
+ ], "x2many values in domains should be lists of ids");
+ assert.strictEqual(JSON.stringify(domain),
+ "[[\"id\",\"in\",[12]],[\"id\",\"in\",[37,41]]]",
+ "x2many values in domains should be lists of ids");
+ });
+ model.destroy();
+ });
+
+ QUnit.test('fetch references in list, with not too many rpcs', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].reference = 'product,37';
+ this.data.partner.records[1].reference = 'product,41';
+
+ this.params.fieldNames = ['reference'];
+ this.params.domain = [];
+ this.params.groupedBy = [];
+ this.params.res_id = undefined;
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ assert.step(route);
+ if (route === "/web/dataset/call_kw/product/name_get") {
+ assert.deepEqual(args.args, [[37, 41]],
+ "the name_get should contain the product ids");
+ }
+ return this._super(route, args);
+ },
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+
+ assert.strictEqual(record.data[0].data.reference.data.display_name, "xphone",
+ "name_get should have been correctly fetched");
+ assert.verifySteps(["/web/dataset/search_read", "/web/dataset/call_kw/product/name_get"],
+ "should have done 2 rpc (searchread and name_get for product)");
+ });
+ model.destroy();
+ });
+
});});
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index 8aa1f692072..011843eefee 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -143,7 +143,7 @@ QUnit.module('Views', {
};
QUnit.test('simple calendar rendering', function (assert) {
- assert.expect(19);
+ assert.expect(22);
var calendar = createView({
View: CalendarView,
@@ -200,6 +200,13 @@ QUnit.module('Views', {
assert.strictEqual($attendeesFilter.find('.o_calendar_filter_item').length, 3, "should display 3 filter items for 'attendees' who use write_model (2 saved + Everything)");
assert.ok($attendeesFilter.find('.o_field_many2one').length, "should display one2many search bar for 'attendees' filter");
+ assert.strictEqual(calendar.$('.fc-event').length, 6,
+ "should display 6 events ('event 5' counts for 2 because it spans two weeks and thus generate two fc-event elements)");
+ calendar.$('.o_calendar_filter .o_checkbox input').first().click(); // Disable first filter
+ assert.strictEqual(calendar.$('.fc-event').length, 4, "should now only display 4 event");
+ calendar.$('.o_calendar_filter .o_checkbox input').eq(1).click(); // Disable second filter
+ assert.strictEqual(calendar.$('.fc-event').length, 0, "should not display any event anymore");
+
// test search bar in filter
$sidebar.find('input[type="text"]').trigger('click');
@@ -208,7 +215,7 @@ QUnit.module('Views', {
assert.strictEqual($sidebar.find('.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item').length, 4, "should display 4 filter items for 'attendees'");
$sidebar.find('input[type="text"]').trigger('click');
assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').text(), "partner 4", "should display the last choice in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice
- $sidebar.find('.o_calendar_filter_item[data-id="1"] .o_remove').trigger('click');
+ $sidebar.find('.o_calendar_filter_item .o_remove').first().trigger('click');
assert.ok($('.modal button.btn:contains(Ok)').length, "should display the confirm message");
$('.modal button.btn:contains(Ok)').trigger('click');
assert.strictEqual($sidebar.find('.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item').length, 3, "click on remove then should display 3 filter items for 'attendees'");
@@ -349,6 +356,63 @@ QUnit.module('Views', {
calendar.destroy();
});
+ QUnit.test('quickcreate switching to actual create for required fields', function (assert) {
+ assert.expect(4);
+
+ var event = $.Event();
+ var calendar = createView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch:
+ '<calendar class="o_calendar_test" '+
+ 'string="Events" ' +
+ 'event_open_popup="true" '+
+ 'date_start="start" '+
+ 'date_stop="stop" '+
+ 'all_day="allday" '+
+ 'mode="month" '+
+ 'readonly_form_view_id="1">'+
+ '<field name="name"/>'+
+ '</calendar>',
+ archs: archs,
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ mockRPC: function (route, args) {
+ if (args.method === "create") {
+ return $.Deferred().reject({
+ code: 200,
+ data: {},
+ message: "Odoo server error",
+ }, event);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ // create a new event
+ var $cell = calendar.$('.fc-day-grid .fc-row:eq(2) .fc-day:eq(2)');
+ testUtils.triggerMouseEvent($cell, "mousedown");
+ testUtils.triggerMouseEvent($cell, "mouseup");
+
+ assert.strictEqual($('.modal-dialog.modal-sm .modal-title').text(), 'Create',
+ "should open the quick create dialog");
+
+ $('.modal-body input:first').val('new event in quick create').trigger('input');
+ $('.modal button.btn:contains(Create)').trigger('click').trigger('click');
+
+ // If the event is not default-prevented, a traceback will be raised, which we do not want
+ assert.ok(event.isDefaultPrevented(), "fail deferred event should have been default-prevented");
+
+ assert.strictEqual($('.modal-dialog.modal-lg .modal-title').text(), 'Create: Events',
+ "should have switched to a bigger modal for an actual create rather than quickcreate");
+ assert.strictEqual($('.modal-dialog.modal-lg .modal-body .o_form_view.o_form_editable').length, 1,
+ "should open the full event form view in a dialog");
+
+ calendar.destroy();
+ });
+
QUnit.test('create event with timezone in week mode', function (assert) {
assert.expect(5);
@@ -640,7 +704,7 @@ QUnit.module('Views', {
});
QUnit.test('use mini calendar', function (assert) {
- assert.expect(2);
+ assert.expect(12);
var calendar = createView({
View: CalendarView,
@@ -665,17 +729,36 @@ QUnit.module('Views', {
},
});
+ assert.strictEqual(calendar.$('.fc-agendaWeek-view').length, 1, "should be in week mode");
assert.strictEqual(calendar.$('.fc-event').length, 9, "should display 9 events on the week (4 event + 5 days event)");
- $('.o_calendar_mini a:contains(19)').click();
+ calendar.$('.o_calendar_mini a:contains(19)').click();
+ // Clicking on a day in another week should switch to the other week view
+ assert.strictEqual(calendar.$('.fc-agendaWeek-view').length, 1, "should be in week mode");
+ assert.strictEqual(calendar.$('.fc-event').length, 4, "should display 4 events on the week (1 event + 3 days event)");
+ // Clicking on a day in the same week should switch to that particular day view
+ calendar.$('.o_calendar_mini a:contains(18)').click();
+ assert.strictEqual(calendar.$('.fc-agendaDay-view').length, 1, "should be in day mode");
+ assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the day");
+ // Clicking on the same day should toggle between day, month and week views
+ calendar.$('.o_calendar_mini a:contains(18)').click();
+ assert.strictEqual(calendar.$('.fc-month-view').length, 1, "should be in month mode");
+ assert.strictEqual(calendar.$('.fc-event').length, 7, "should display 7 events on the month (event 5 is on multiple weeks and generates to .fc-event)");
+ calendar.$('.o_calendar_mini a:contains(18)').click();
+ assert.strictEqual(calendar.$('.fc-agendaWeek-view').length, 1, "should be in week mode");
assert.strictEqual(calendar.$('.fc-event').length, 4, "should display 4 events on the week (1 event + 3 days event)");
+ calendar.$('.o_calendar_mini a:contains(18)').click();
+ assert.strictEqual(calendar.$('.fc-agendaDay-view').length, 1, "should be in day mode");
+ assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the day");
calendar.destroy();
});
QUnit.test('rendering, with many2many', function (assert) {
- assert.expect(1);
+ assert.expect(5);
this.data.event.fields.partner_ids.type = 'many2many';
+ this.data.event.records[0].partner_ids = [1,2,3,4,5];
+ this.data.partner.records.push({id: 5, display_name: "partner 5", image: 'EEE'});
var calendar = createView({
View: CalendarView,
@@ -698,6 +781,15 @@ QUnit.module('Views', {
assert.strictEqual(calendar.$('.o_calendar_filter_items .o_cal_avatar').length, 3,
"should have 3 avatars in the side bar");
+ var $event1Avatars = calendar.$('.fc-event .o_calendar_avatars').first();
+ assert.strictEqual($event1Avatars.find('img').length, 1, "should have 1 avatar");
+ assert.strictEqual($event1Avatars.find('span').length, 0,
+ "should not have a span for more attendees since there is only one");
+
+ var $event2Avatars = calendar.$('.fc-event:contains(All the day) .o_calendar_avatars');
+ assert.strictEqual($event2Avatars.find('img').length, 3, "should have 3 avatars");
+ assert.strictEqual($event2Avatars.find('span').text(), '+2',
+ "should indicate there are 2 more attendees that we don't show");
calendar.destroy();
});
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 0e53d6814d4..0857cb50038 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -2968,6 +2968,53 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('discarding before save returns', function (assert) {
+ assert.expect(4);
+
+ var def = $.Deferred();
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<group><field name="foo"/></group>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'write') {
+ return def.then(_.constant(result));
+ }
+ return result;
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ form.$('input').val("1234").trigger('input');
+
+ // save the value and discard directly
+ form.$buttons.find('.o_form_button_save').click();
+ form.discardChanges(); // Simulate click on breadcrumb
+
+ assert.strictEqual(form.$('.o_field_widget[name="foo"]').val(), "1234",
+ "field foo should still contain new value");
+ assert.strictEqual($('.modal').length, 0,
+ "Confirm dialog should not be displayed");
+
+ // complete the write
+ def.resolve();
+
+ assert.strictEqual($('.modal').length, 0,
+ "Confirm dialog should not be displayed");
+ assert.strictEqual(form.$('.o_field_widget[name="foo"]').text(), "1234",
+ "value should have been saved and rerendered in readonly");
+
+ form.destroy();
+ });
+
QUnit.test('unchanged relational data is sent for onchanges', function (assert) {
assert.expect(1);
@@ -4361,7 +4408,7 @@ QUnit.module('Views', {
});
QUnit.test('default_order on x2many embedded view', function (assert) {
- assert.expect(8);
+ assert.expect(11);
this.data.partner.fields.display_name.sortable = true;
this.data.partner.records[0].p = [1, 4];
@@ -4421,6 +4468,18 @@ QUnit.module('Views', {
assert.ok(form.$('.o_field_one2many tbody tr:eq(2) td:contains(xop)').length,
"record xop should be third");
+ // client-side sort on edit
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_one2many tbody tr:eq(1) td:contains(yop)').click();
+ $('.modal input[name="foo"]').val('zzz').trigger("input");
+ $('.modal .modal-footer button:first').click(); // Save
+ assert.ok(form.$('.o_field_one2many tbody tr:eq(0) td:contains(zzz)').length,
+ "record zzz should be first");
+ assert.ok(form.$('.o_field_one2many tbody tr:eq(1) td:contains(zop)').length,
+ "record zop should be second");
+ assert.ok(form.$('.o_field_one2many tbody tr:eq(2) td:contains(xop)').length,
+ "record xop should be third");
+
form.destroy();
});
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index 549b724ca68..89ec104fc41 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -457,7 +457,7 @@ QUnit.module('Views', {
arch: '<kanban class="o_kanban_test">' +
'<templates><t t-name="kanban-box">' +
'<div>' +
- '<field name="category_ids"/>' +
+ '<field name="category_ids" widget="many2many_tags" options="{\'color_field\': \'color\'}"/>' +
'<field name="foo"/>' +
'<field name="state" widget="priority"/>' +
'</div>' +
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index c39e6d2b1b0..09109d92b84 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -2,6 +2,7 @@ odoo.define('web.list_tests', function (require) {
"use strict";
var config = require('web.config');
+var FormView = require('web.FormView');
var ListView = require('web.ListView');
var testUtils = require('web.test_utils');
@@ -23,6 +24,8 @@ QUnit.module('Views', {
currency_id: {string: "Currency", type: "many2one",
relation: "res_currency", default: 1},
datetime: {string: "Datetime Field", type: 'datetime'},
+ reference: {string: "Reference Field", type: 'reference', selection: [
+ ["bar", "Bar"], ["res_currency", "Currency"], ["event", "Event"]]},
},
records: [
{
@@ -37,11 +40,12 @@ QUnit.module('Views', {
currency_id: 2,
date: "2017-01-25",
datetime: "2016-12-12 10:55:05",
+ reference: 'bar,1',
},
{id: 2, bar: true, foo: "blip", int_field: 9, qux: 13,
- m2o: 2, m2m: [1, 2, 3], amount: 500},
+ m2o: 2, m2m: [1, 2, 3], amount: 500, reference: 'res_currency,1'},
{id: 3, bar: true, foo: "gnap", int_field: 17, qux: -3,
- m2o: 1, m2m: [], amount: 300},
+ m2o: 1, m2m: [], amount: 300, reference: 'res_currency,2'},
{id: 4, bar: false, foo: "blip", int_field: -4, qux: 9,
m2o: 1, m2m: [1], amount: 0},
]
@@ -2400,6 +2404,68 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('reference field rendering', function (assert) {
+ assert.expect(4);
+
+ this.data.foo.records.push({
+ id: 5,
+ reference: 'res_currency,2',
+ });
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree><field name="reference"/></tree>',
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ assert.step(args.model);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.verifySteps(['bar', 'res_currency'], "should have done 1 name_get by model in reference values");
+ assert.strictEqual(list.$('tbody td').text(), "Value 1USDEUREUR",
+ "should have the display_name of the reference");
+ list.destroy();
+ });
+
+ QUnit.test('editable list view: contexts are correctly sent', function (assert) {
+ assert.expect(6);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>',
+ mockRPC: function (route, args) {
+ var context;
+ if (route === '/web/dataset/search_read') {
+ context = args.context;
+ } else {
+ context = args.kwargs.context;
+ }
+ assert.strictEqual(context.active_field, 2, "context should be correct");
+ assert.strictEqual(context.someKey, 'some value', "context should be correct");
+ return this._super.apply(this, arguments);
+ },
+ session: {
+ user_context: {someKey: 'some value'},
+ },
+ viewOptions: {
+ context: {active_field: 2},
+ },
+ });
+
+ list.$('.o_data_cell:first').click();
+ list.$('.o_field_widget[name=foo]').val('abc').trigger('input');
+ list.$buttons.find('.o_list_button_save').click();
+
+ list.destroy();
+ });
});
});
diff --git a/addons/web_editor/models/ir_http.py b/addons/web_editor/models/ir_http.py
index 04845688ea9..d6e2b2d5ee6 100644
--- a/addons/web_editor/models/ir_http.py
+++ b/addons/web_editor/models/ir_http.py
@@ -15,7 +15,7 @@ class IrHttp(models.AbstractModel):
context['editable'] = True
if 'edit_translations' in request.httprequest.args and 'edit_translations' not in context:
context['edit_translations'] = True
- if context.get('lang') != "en_US" and 'translatable' not in context:
+ if context.get('edit_translations') and 'translatable' not in context:
context['translatable'] = True
request.context = context
return super(IrHttp, cls)._dispatch()
diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py
index 8c60e7923f2..7cf9e2f3a27 100644
--- a/addons/web_editor/models/ir_ui_view.py
+++ b/addons/web_editor/models/ir_ui_view.py
@@ -49,7 +49,7 @@ class IrUiView(models.Model):
if value is not None:
# TODO: batch writes?
- if self.get_default_lang_code() and field != 'arch' and not isinstance(Model._fields[field], fields.Html):
+ if not self.env.context.get('lang') and self.get_default_lang_code():
Model.browse(int(el.get('data-oe-id'))).with_context(lang=self.get_default_lang_code()).write({field: value})
else:
Model.browse(int(el.get('data-oe-id'))).write({field: value})
diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js
index 34ca6c9f5d4..06053dd1cea 100644
--- a/addons/web_editor/static/src/js/backend.js
+++ b/addons/web_editor/static/src/js/backend.js
@@ -10,7 +10,10 @@ var field_registry = require('web.field_registry');
var transcoder = require('web_editor.transcoder');
+var TranslatableFieldMixin = basic_fields.TranslatableFieldMixin;
+
var QWeb = core.qweb;
+var _t = core._t;
/**
@@ -18,11 +21,8 @@ var QWeb = core.qweb;
* Intended to display HTML content. This widget uses the summernote editor
* improved by odoo.
*
- * FIXME: this field has a custom handling of the translation feature, which
- * hasn't been re-introduced yet (because this feature hasn't been introduced
- * yet in the fields in general)
*/
-var FieldTextHtmlSimple = basic_fields.DebouncedField.extend({
+var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMixin, {
className: 'oe_form_field oe_form_field_html_text',
supportedFieldTypes: ['html'],
@@ -133,6 +133,7 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend({
if (history) {
history.reset();
}
+ this.$('.note-toolbar').append(this._renderTranslateButton());
},
/**
* @override
@@ -187,6 +188,19 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend({
}
return value;
},
+ /**
+ * @override
+ * @private
+ * @returns {jQueryElement}
+ */
+ _renderTranslateButton: function () {
+ if (_t.database.multi_lang && this.field.translate && this.res_id) {
+ return $(QWeb.render('web_editor.FieldTextHtml.button.translate', {widget: this}))
+ .on('click', this._onTranslate.bind(this));
+ }
+ return $();
+ },
+
});
var FieldTextHtml = AbstractField.extend({
@@ -196,6 +210,7 @@ var FieldTextHtml = AbstractField.extend({
start: function () {
var self = this;
+ this.loaded = false;
this.callback = _.uniqueId('FieldTextHtml_');
window.odoo[this.callback+"_editor"] = function (EditorBar) {
setTimeout(function () {
@@ -206,8 +221,7 @@ var FieldTextHtml = AbstractField.extend({
self.on_content_loaded();
};
window.odoo[this.callback+"_updown"] = null;
- window.odoo[this.callback+"_downup"] = function (value) {
- self._setValue(value);
+ window.odoo[this.callback+"_downup"] = function () {
self.resize();
};
@@ -233,8 +247,6 @@ var FieldTextHtml = AbstractField.extend({
this.old_initialize_content();
var def = this._super.apply(this, arguments);
- // this.$translate.remove();
- // this.$translate = $();
return def;
},
getDatarecord: function () {
@@ -261,18 +273,13 @@ var FieldTextHtml = AbstractField.extend({
if (this.nodeOptions.template) {
attr.template = this.nodeOptions.template;
}
- if (this. mode === "edit") {
+ if (this.mode === "edit") {
attr.enable_editor = 1;
}
- if (this.field.translate) {
- attr.translatable = 1;
- }
if (session.debug) {
attr.debug = session.debug;
}
- attr.lang = attr.enable_editor ? 'en_US' : this.getSession().user_context.lang;
-
for (k in _attr) {
attr[k] = _attr[k];
}
@@ -306,26 +313,10 @@ var FieldTextHtml = AbstractField.extend({
this.document = this.$iframe.contents()[0];
this.$body = $("body", this.document);
this.$content = this.$body.find("#editable_area");
- // this._toggle_label();
- this.lang = this.$iframe.attr('src').match(/[?&]lang=([^&]+)/);
- this.lang = this.lang ? this.lang[1] : session.user_context.lang;
- this._dirty_flag = false;
this.render();
- setTimeout(function () {
- self.trigger_up('perform_model_rpc', {
- model: 'res.lang',
- method: 'search_read',
- args: [
- [['code', '!=', 'en_US']],
- ["name", "code"]
- ],
- on_success: function (res) {
- self.languages = res;
- self.add_button();
- setTimeout(self.resize,0);
- }
- });
- }, 0);
+ this.add_button();
+ this.loaded = true;
+ setTimeout(self.resize, 0);
},
on_editor_loaded: function (EditorBar) {
var self = this;
@@ -341,19 +332,6 @@ var FieldTextHtml = AbstractField.extend({
var self = this;
var $to = this.$body.find("#web_editor-top-edit, #wrapwrap").first();
- $(QWeb.render('FieldTextHtml.translate', {'widget': this}))
- .appendTo($to)
- .on('change', 'select', function () {
- var lang = $(this).val();
- var edit = self. mode === "edit";
- var trans = lang !== 'en_US';
- self.$iframe.attr("src", self.get_url({
- 'edit_translations': edit && trans,
- 'enable_editor': edit && !trans,
- 'lang': lang
- }));
- });
-
$(QWeb.render('web_editor.FieldTextHtml.fullscreen'))
.appendTo($to)
.on('click', '.o_fullscreen', function () {
@@ -369,9 +347,6 @@ var FieldTextHtml = AbstractField.extend({
});
},
render: function () {
- if (this.lang !== session.user_context.lang || this.$iframe.attr('src').match(/[?&]edit_translations=1/)) {
- return;
- }
var value = (this.value || "").replace(/^<p[^>]*>(\s*|<br\/?>)<\/p>$/, '');
if (!this.$content) {
return;
@@ -392,24 +367,34 @@ var FieldTextHtml = AbstractField.extend({
has_no_value: function () {
return this.value === false || !this.$content.html() || !this.$content.html().match(/\S/);
},
- // FIXME: not sure what this is supposed to do that is still relevant now
- // commit_value: function () {
- // if (this.lang !== 'en_US' && this.$body.find('.o_dirty').length) {
- // this.internal_set_value( this.view.datarecord[this.name] );
- // this._dirty_flag = false;
- // return this.editor.save();
- // } else if (this._dirty_flag && this.editor && this.editor.buildingBlock) {
- // this.editor.buildingBlock.clean_for_save();
- // this.internal_set_value( this.$content.html() );
- // }
- // },
destroy: function () {
$(window).off('resize', this.resize);
delete window.odoo[this.callback+"_editor"];
delete window.odoo[this.callback+"_content"];
delete window.odoo[this.callback+"_updown"];
delete window.odoo[this.callback+"_downup"];
- }
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ commitChanges: function () {
+ if (!this.loaded) {
+ return;
+ }
+ // switch to WYSIWYG mode if currently in code mode to get all changes
+ if (config.debug && this.mode === 'edit' && this.editor.rte) {
+ var layoutInfo = this.editor.rte.editable().data('layoutInfo');
+ $.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
+ }
+ this.editor.buildingBlock.clean_for_save();
+ this._setValue(this.$content.html());
+ return this._super.apply(this, arguments);
+ },
});
field_registry
diff --git a/addons/web_editor/static/src/js/base.js b/addons/web_editor/static/src/js/base.js
index e4e2fcd6cad..4368f6de274 100644
--- a/addons/web_editor/static/src/js/base.js
+++ b/addons/web_editor/static/src/js/base.js
@@ -36,7 +36,7 @@ data.ready = function () {
// TODO find a better way to find if we are in the backend or not
// We don't load translation if we are in the backend since it's already done by the webclient
if (!$(document).find('.o_web_client').length) {
- load_translations();
+ return load_translations();
}
});
} else if(all_ready.state() === "resolved") { // can add async template
diff --git a/addons/web_editor/static/src/less/web_editor.ui.less b/addons/web_editor/static/src/less/web_editor.ui.less
index f83a73984c4..3ba3621db9f 100644
--- a/addons/web_editor/static/src/less/web_editor.ui.less
+++ b/addons/web_editor/static/src/less/web_editor.ui.less
@@ -1090,8 +1090,15 @@ body.editor_enable {
background-color: @odoo-brand-secondary;
#wrapwrap {
+ position: relative;
+ display: table;
+ table-layout: fixed;
+ width: 100%;
height: 100%;
+ > * {
+ display: table-row;
+ }
> main {
height: 100%;
diff --git a/addons/web_editor/static/src/xml/backend.xml b/addons/web_editor/static/src/xml/backend.xml
index 69f6e53d6de..082efdbc5a0 100644
--- a/addons/web_editor/static/src/xml/backend.xml
+++ b/addons/web_editor/static/src/xml/backend.xml
@@ -7,41 +7,14 @@
</div>
</t>
- <t t-name="web_editor.FieldTextHtml.button.reset">
- <div class="btn-group pull-right">
- <button class="o_reset_translation btn btn-default btn-sm btn-small" title="Reset from source"><span class="fa fa-upload"></span></button>
- </div>
- </t>
-
<t t-name="web_editor.FieldTextHtml.button.translate">
<div class="btn-group pull-right">
- <button t-if="widget.field.translate" class="oe_field_translate btn btn-default btn-sm btn-small" style="height: 24px; padding: 1px 3px 0px 5px">
+ <button t-if="widget.field.translate" class="o_field_translate btn btn-default btn-sm btn-small" style="height: 24px; padding: 1px 17px 0px 5px">
<span class="fa fa-language fa-lg oe_input_icon"/>
</button>
</div>
</t>
- <t t-name="web_editor.FieldTextHtml.translate">
- <div t-if="widget.recordData.id &amp;&amp; widget.languages.length" style="margin: 5px; position: fixed; top: 0; left: 0; z-index: 2000;">
- <select style="
- width: 148px;
- height: 22px;
- padding: 2px 4px;
- border: 1px solid #ccc;
- -moz-border-radius: 3px;
- -webkit-border-radius: 3px;
- border-radius: 3px;
- background: white;">
- <option value="en_US" t-att-selected="'en_US' == widget.lang ? 'selected' : null">
- <t t-if="!widget.get('effective_readonly')">English (edit mode)</t>
- <t t-if="widget.get('effective_readonly')">English</t>
- </option>
- <t t-foreach="widget.languages" t-as="lang">
- <option t-att-value="lang.code" t-att-selected="lang.code == widget.lang ? 'selected' : null"><t t-esc="lang.name"/></option>
- </t>
- </select>
- </div>
- </t>
<t t-name="web_editor.FieldTextHtml.fullscreen">
<span style="margin: 5px; position: fixed; top: 0; right: 0; z-index: 2000;">
<button class="o_fullscreen btn btn-primary" style="width: 24px; height: 24px; background-color: #337ab7; border: 1px solid #2e6da4; border-radius: 4px; padding: 0; position: relative;">
diff --git a/addons/web_editor/static/tests/web_editor_tests.js b/addons/web_editor/static/tests/web_editor_tests.js
index a053ad99043..283931e9b0d 100644
--- a/addons/web_editor/static/tests/web_editor_tests.js
+++ b/addons/web_editor/static/tests/web_editor_tests.js
@@ -3,6 +3,9 @@ odoo.define('web_editor.web_editor_tests', function (require) {
var FormView = require('web.FormView');
var testUtils = require('web.test_utils');
+var core = require('web.core');
+
+var _t = core._t;
QUnit.module('web_editor', {
beforeEach: function() {
@@ -87,6 +90,43 @@ QUnit.test('field html widget (with options inline-style)', function (assert) {
}, 0);
});
+QUnit.test('field html translatable', function (assert) {
+ assert.expect(3);
+
+ var multiLang = _t.database.multi_lang;
+ _t.database.multi_lang = true;
+
+ this.data['mass.mailing'].fields.body.translate = true;
+
+ var form = testUtils.createView({
+ View: FormView,
+ model: 'mass.mailing',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="body" widget="html"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_button' && args.method === 'translate_fields') {
+ assert.deepEqual(args.args, ['mass.mailing',1,'body',{}], "should call 'call_button' route");
+ return $.when();
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('.oe_form_field_html_text .o_field_translate').length, 0,
+ "should not have a translate button in readonly mode");
+
+ form.$buttons.find('.o_form_button_edit').click();
+ var $button = form.$('.oe_form_field_html_text .o_field_translate');
+ assert.strictEqual($button.length, 1, "should have a translate button");
+ $button.click();
+
+ form.destroy();
+ _t.database.multi_lang = multiLang;
+});
+
QUnit.test('field html_frame widget', function (assert) {
assert.expect(6);
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index 4349eaa1774..077a4622651 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -208,7 +208,7 @@ class Http(models.AbstractModel):
path.pop(1)
request.context = context
return cls.reroute('/'.join(path) or '/')
- if path[1] == request.website.default_lang_code:
+ if request.lang == request.website.default_lang_code:
context['edit_translations'] = False
if not context.get('tz'):
context['tz'] = request.session.get('geoip', {}).get('time_zone')
diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js
index 6b221b801d1..75350dce58a 100644
--- a/addons/website/static/src/js/website.js
+++ b/addons/website/static/src/js/website.js
@@ -7,6 +7,7 @@ odoo.define('website.website', function (require) {
var Widget = require('web.Widget');
var session = require('web.session');
var base = require('web_editor.base');
+ var utils = require('web.utils');
var qweb = core.qweb;
var _t = core._t;
@@ -244,6 +245,10 @@ odoo.define('website.website', function (require) {
}
});
+ /* Load localizations */
+ var lang = utils.get_cookie('website_lang') || $('html').attr('lang') || 'en_US';
+ ajax.loadJS('/web/webclient/locale/' + lang.replace('-', '_'));
+
/**
* Object who contains all method and bind for the top bar, the template is create server side.
*/
diff --git a/addons/website_blog/controllers/main.py b/addons/website_blog/controllers/main.py
index 3a5ef95c3d4..c400b8b792f 100644
--- a/addons/website_blog/controllers/main.py
+++ b/addons/website_blog/controllers/main.py
@@ -174,6 +174,7 @@ class WebsiteBlog(http.Controller):
v['blog'] = blog
v['base_url'] = request.env['ir.config_parameter'].sudo().get_param('web.base.url')
v['posts'] = request.env['blog.post'].search([('blog_id','=', blog.id)], limit=min(int(limit), 50))
+ v['html2plaintext'] = html2plaintext
r = request.render("website_blog.blog_feed", v, headers=[('Content-Type', 'application/atom+xml')])
return r
diff --git a/addons/website_blog/views/website_blog_templates.xml b/addons/website_blog/views/website_blog_templates.xml
index c2ecb61d5aa..a7877cdc8c1 100644
--- a/addons/website_blog/views/website_blog_templates.xml
+++ b/addons/website_blog/views/website_blog_templates.xml
@@ -555,7 +555,7 @@
<link t-att-href="'%s%s' % (base_url, post.website_url)"/>
<id t-esc="'%s%s' % (base_url, post.website_url)"/>
<author><name t-esc="post.sudo().author_id.name"/></author>
- <summary t-esc="post.subtitle"/>
+ <summary t-esc="html2plaintext(post.teaser)"/>
<updated t-esc="post.write_date.replace(' ', 'T') + 'Z'"/>
</entry>
</feed>
diff --git a/addons/website_customer/views/res_partner_views.xml b/addons/website_customer/views/res_partner_views.xml
index 69c59989879..a756453c4f6 100644
--- a/addons/website_customer/views/res_partner_views.xml
+++ b/addons/website_customer/views/res_partner_views.xml
@@ -7,7 +7,7 @@
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='company_id']" position="after">
- <field name="website_tag_ids" widget="many2many_tags" string="Website Tags"/>
+ <field name="website_tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" string="Website Tags"/>
</xpath>
</data>
</field>
diff --git a/addons/website_event/views/event_templates.xml b/addons/website_event/views/event_templates.xml
index 9b1606fc97e..51346d24171 100644
--- a/addons/website_event/views/event_templates.xml
+++ b/addons/website_event/views/event_templates.xml
@@ -57,7 +57,7 @@
<h4 class="media-heading">
<a itemprop="url" t-att-class="event.state == 'done' and 'text-success'" t-attf-href="/event/#{ slug(event) }/#{(not event.menu_id) and 'register' or ''}"><span itemprop="name" t-field="event.name"> </span></a>
<small t-if="event.is_participating" class="label label-info">Participating</small>
- <small t-if="not event.address_id" class="label label-info">Online</small>
+ <small t-if="not event.is_online" class="label label-info">Online</small>
<small t-if="not event.website_published" class="label label-danger">unpublished</small>
</h4>
<div>
@@ -68,10 +68,12 @@
<div>
<i class="fa fa-clock-o"></i> <span itemprop="startDate" t-field="event.with_context(tz=event.date_tz).date_begin" t-options='{"hide_seconds":"True"}'> </span> <i>to</i> <span itemprop="endDate" t-field="event.with_context(tz=event.date_tz).date_end" t-options='{"hide_seconds":"True"}'> </span>
</div>
- <div itemprop="location" t-field="event.address_id" t-options='{
- "widget": "contact",
- "fields": ["city"]
- }'/>
+ <t t-if="not event.is_online">
+ <div itemprop="location" t-field="event.address_id" t-options='{
+ "widget": "contact",
+ "fields": ["city"]
+ }'/>
+ </t>
<div class="text-muted" t-if="event.event_type_id">
<i class="fa fa-tag"></i> <span t-field="event.event_type_id"/>
</div>
@@ -333,25 +335,27 @@
<div class="col-md-4 css_noprint">
<div class="clearfix"/>
- <div class="panel panel-default" t-if="event.address_id">
- <div class="panel-heading">
- <h4>Where</h4>
- </div>
- <div class="panel-body">
- <t t-if="event.google_map_img()">
- <a t-att-href="event.google_map_link()" target="_BLANK">
- <img t-att-src="event.google_map_img()" width="100%%"/>
- </a>
- </t>
- <t t-else="1">
- <a t-att-href="event.google_map_link()" target="_BLANK">Visualize on Google Maps</a>
- </t>
- <div itemprop="location" class="mt16 mb8" t-field="event.address_id" t-options='{
- "widget": "contact",
- "fields": ["name", "address", "phone", "mobile", "fax", "email"]
- }'/>
+ <t t-if="not event.is_online">
+ <div class="panel panel-default" t-if="event.address_id">
+ <div class="panel-heading">
+ <h4>Where</h4>
+ </div>
+ <div class="panel-body">
+ <t t-if="event.google_map_img()">
+ <a t-att-href="event.google_map_link()" target="_BLANK">
+ <img t-att-src="event.google_map_img()" width="100%%"/>
+ </a>
+ </t>
+ <t t-else="1">
+ <a t-att-href="event.google_map_link()" target="_BLANK">Visualize on Google Maps</a>
+ </t>
+ <div itemprop="location" class="mt16 mb8" t-field="event.address_id" t-options='{
+ "widget": "contact",
+ "fields": ["name", "address", "phone", "mobile", "fax", "email"]
+ }'/>
+ </div>
</div>
- </div>
+ </t>
<div class="panel panel-default">
<div class="panel-heading">
diff --git a/addons/website_event_questions/models/event.py b/addons/website_event_questions/models/event.py
index 4645d73c4d2..e1b95dcee46 100644
--- a/addons/website_event_questions/models/event.py
+++ b/addons/website_event_questions/models/event.py
@@ -33,10 +33,6 @@ class EventEvent(models.Model):
'title': question.title,
'sequence': question.sequence,
'is_individual': question.is_individual,
- 'answer_ids': [(0, 0, {
- 'name': answer.name,
- 'sequence': answer.sequence,
- }) for answer in question.answer_ids]
})
for question in self.event_type_id.question_ids
]
@@ -81,6 +77,18 @@ class EventQuestion(models.Model):
if any(question.event_type_id and question.event_id for question in self):
raise UserError(_('Question should belong to either event category or event but not both'))
+ @api.model
+ def create(self, vals):
+ event_id = vals.get('event_id', False)
+ if event_id:
+ event = self.env['event.event'].browse([event_id])
+ if event.event_type_id.use_questions and event.event_type_id.question_ids:
+ vals['answer_ids'] = vals.get('answer_ids', []) + [(0, 0, {
+ 'name': answer.name,
+ 'sequence': answer.sequence,
+ }) for answer in event.event_type_id.question_ids.filtered(lambda question: question.title == vals.get('title')).mapped('answer_ids')]
+ return super(EventQuestion, self).create(vals)
+
class EventAnswer(models.Model):
_name = 'event.answer'
diff --git a/addons/website_event_track/views/event_track_views.xml b/addons/website_event_track/views/event_track_views.xml
index f071619b3a3..5a679f2f644 100644
--- a/addons/website_event_track/views/event_track_views.xml
+++ b/addons/website_event_track/views/event_track_views.xml
@@ -29,7 +29,7 @@
</ul>
</div>
<div class="oe_kanban_content">
- <field name="tag_ids"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<h4><field name="name"/></h4>
<div>
<t t-if="duration"><field name="duration" widget="float_time"/> hours</t>
@@ -147,7 +147,7 @@
<field name="duration" class="oe_inline" widget="float_time"/> hours
</div>
<field name="location_id"/>
- <field name="tag_ids" widget="many2many_tags"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</group>
</group>
</sheet>
diff --git a/addons/website_form/models/models.py b/addons/website_form/models/models.py
index 75a2ce33fb2..654fbfa9ee4 100644
--- a/addons/website_form/models/models.py
+++ b/addons/website_form/models/models.py
@@ -27,12 +27,6 @@ class website_form_model(models.Model):
website_form_default_field_id = fields.Many2one('ir.model.fields', 'Field for custom form data', domain="[('model', '=', model), ('ttype', '=', 'text')]", help="Specify the field which will contain meta and custom form fields datas.")
website_form_label = fields.Char("Label for form action", help="Form action label. Ex: crm.lead could be 'Send an e-mail' and project.issue could be 'Create an Issue'.")
- def _all_inherited_model_ids(self):
- return list(itertools.chain(
- [self.id],
- *(m._all_inherited_model_ids() for m in self.inherited_model_ids)
- ))
-
def _get_form_writable_fields(self):
"""
Restriction of "authorized fields" (fields which can be used in the
@@ -40,16 +34,16 @@ class website_form_model(models.Model):
builders and are writable. By default no field is writable by the
form builder.
"""
- excluded = {
+ included = {
field.name
for field in self.env['ir.model.fields'].sudo().search([
- ('model_id', 'in', self._all_inherited_model_ids()),
- ('website_form_blacklisted', '=', True)
+ ('model_id', '=', self.id),
+ ('website_form_blacklisted', '=', False)
])
}
return {
k: v for k, v in pycompat.items(self.get_authorized_fields(self.model))
- if k not in excluded
+ if k in included
}
@api.model
diff --git a/addons/website_forum/views/forum.xml b/addons/website_forum/views/forum.xml
index 2221667662d..b2ccc640939 100644
--- a/addons/website_forum/views/forum.xml
+++ b/addons/website_forum/views/forum.xml
@@ -159,7 +159,7 @@
<field name="forum_id"/>
<field name="parent_id"/>
</group><group>
- <field name="tag_ids" widget="many2many_tags"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="state"/>
<field name="closed_reason_id"/>
<field name="closed_uid"/>
diff --git a/addons/website_hr_recruitment/__manifest__.py b/addons/website_hr_recruitment/__manifest__.py
index 0e661b173e2..23d6f2d93b4 100644
--- a/addons/website_hr_recruitment/__manifest__.py
+++ b/addons/website_hr_recruitment/__manifest__.py
@@ -17,6 +17,7 @@ Odoo Contact Form
'security/ir.model.access.csv',
'security/website_hr_recruitment_security.xml',
'data/config_data.xml',
+ 'views/website_hr_recuitment_snippets.xml',
'views/website_hr_recruitment_templates.xml',
'views/hr_recruitment_views.xml',
],
diff --git a/addons/website_hr_recruitment/static/src/img/ui/s_hr_rating_thumb.jpg b/addons/website_hr_recruitment/static/src/img/ui/s_hr_rating_thumb.jpg
new file mode 100644
index 00000000000..1817c270b6e
--- /dev/null
+++ b/addons/website_hr_recruitment/static/src/img/ui/s_hr_rating_thumb.jpg
Binary files differ
diff --git a/addons/website_hr_recruitment/static/src/less/s_hr_rating.less b/addons/website_hr_recruitment/static/src/less/s_hr_rating.less
new file mode 100644
index 00000000000..76b6edb8005
--- /dev/null
+++ b/addons/website_hr_recruitment/static/src/less/s_hr_rating.less
@@ -0,0 +1,65 @@
+.s_hr_rating {
+ @star: "\f005";
+ @star-o: "\f006";
+ @bolt: "\f0e7";
+ @bolt-o: "_";
+ @heart: "\f004";
+ @heart-o: "\f08a";
+
+ .s_hr_rating_generate_icons (@off, @on) {
+ .fa:before {
+ content: @off;
+ }
+ .loop(@counter) when (@counter > 0) {
+ &.s_hr_rating_@{counter} {
+ .fa:nth-of-type(-n+@{counter}):before {
+ content: @on;
+ }
+ }
+ .loop((@counter - 1));
+ }
+ .loop(5);
+ }
+
+ &.s_hr_rating_stars {
+ .s_hr_rating_generate_icons(@star-o, @star);
+ }
+
+ &.s_hr_rating_bolts {
+ .s_hr_rating_generate_icons(@bolt-o, @bolt);
+ }
+
+ &.s_hr_rating_hearts {
+ .s_hr_rating_generate_icons(@heart-o, @heart);
+ }
+
+ &.s_hr_rating_bar {
+ .fa {
+ display: none;
+ }
+
+ .s_hr_rating_bar {
+ height: 15px;
+ padding: 1px;
+ background-color: @gray-lighter;
+ border-radius: 2px;
+ &:before {
+ content: "";
+ border-radius: 2px;
+ background-color: @brand-primary;
+ display: block;
+ .size(0, 100%);
+ }
+ }
+
+ .loop(@counter) when (@counter > 0) {
+ &.s_hr_rating_@{counter} {
+ .s_hr_rating_bar:before {
+ width: percentage(@counter/5);
+ }
+ }
+ .loop((@counter - 1));
+ }
+ .loop(5);
+ }
+} \ No newline at end of file
diff --git a/addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml b/addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml
index c46607c2a4c..7a399265790 100644
--- a/addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml
+++ b/addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml
@@ -242,36 +242,51 @@
<div class="container">
<div class="row">
<div class="col-sm-6 col-md-4 mt16 mb16">
- <h6 class="mb0">Job Complexity:</h6>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
- <h6 class="mb0">Personal Evolution:</h6>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
- <h6 class="mb0">Variability of the Job:</h6>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
- <i class="fa fa-star-o"/>
- <h6 class="mb0">Job Security:</h6>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
- <i class="fa fa-star-o"/>
- <i class="fa fa-star-o"/>
- <h6 class="mb0">Overachieving Possibilities:</h6>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
- <i class="fa fa-star"/>
+ <div class="s_hr_rating s_hr_rating_stars s_hr_rating_3">
+ <h6 class="mb0">Job Complexity:</h6>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <div class="s_hr_rating_bar"/>
+ </div>
+ <div class="s_hr_rating s_hr_rating_stars s_hr_rating_1">
+ <h6 class="mb0">Personal Evolution:</h6>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <div class="s_hr_rating_bar"/>
+ </div>
+ <div class="s_hr_rating s_hr_rating_stars s_hr_rating_4">
+ <h6 class="mb0">Variability of the Job:</h6>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <div class="s_hr_rating_bar"/>
+ </div>
+ <div class="s_hr_rating s_hr_rating_stars s_hr_rating_5">
+ <h6 class="mb0">Job Security:</h6>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <div class="s_hr_rating_bar"/>
+ </div>
+ <div class="s_hr_rating s_hr_rating_stars s_hr_rating_2">
+ <h6 class="mb0">Overachieving Possibilities:</h6>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <div class="s_hr_rating_bar"/>
+ </div>
</div>
<div class="col-sm-6 col-md-4 mt16 mb16">
<h6 class="mb0">Team / Company Size:</h6>
diff --git a/addons/website_hr_recruitment/views/website_hr_recuitment_snippets.xml b/addons/website_hr_recruitment/views/website_hr_recuitment_snippets.xml
new file mode 100644
index 00000000000..3713dfede12
--- /dev/null
+++ b/addons/website_hr_recruitment/views/website_hr_recuitment_snippets.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+<!-- Snippets' Assets -->
+<template id="website_hr_recuitment_snippets_assets" name="Website HR Recruitment Snippets Assets" inherit_id="website.assets_frontend">
+ <xpath expr="//link[last()]" position="after">
+ <link rel="stylesheet" type="text/less" href="/website_hr_recruitment/static/src/less/s_hr_rating.less"/>
+ </xpath>
+</template>
+
+<!-- Snippets' Templates -->
+<template id="s_hr_rating" name="HR Rating">
+ <div class="s_hr_rating s_hr_rating_stars s_hr_rating_3">
+ <h6 class="mb0">Job Security:</h6>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <i class="fa"/>
+ <div class="s_hr_rating_bar"/>
+ </div>
+</template>
+
+<!-- Add Snippets' into library -->
+<template id="website_hr_recuitment_snippets_include" inherit_id="website.snippets" name="Website HR Recruitment Snippets Include">
+ <xpath expr="//div[@id='snippet_content']/div[@class='o_panel_body']/*[1]" position="before">
+ <t t-snippet="website_hr_recruitment.s_hr_rating" t-thumbnail="/website_hr_recruitment/static/src/img/ui/s_hr_rating_thumb.jpg"/>
+ </xpath>
+</template>
+
+<!-- Snippets' Options -->
+<template id="s_hr_rating_options" inherit_id="website.snippet_options" name="Website HR Rectuitment s_hr_rating options">
+ <xpath expr="." position="inside">
+ <div data-js='s_hr_rating_option_type' data-selector=".s_hr_rating" data-drop-in="[class^='col-']">
+ <li class="dropdown-submenu">
+ <a tabindex="-1" href="#">Rating type</a>
+ <ul class="dropdown-menu">
+ <li data-select_class="s_hr_rating_stars"><a href="#"><i class="fa fa-star"/> Stars</a></li>
+ <li data-select_class="s_hr_rating_bolts"><a href="#"><i class="fa fa-bolt"/> Bolts</a></li>
+ <li data-select_class="s_hr_rating_hearts"><a href="#"><i class="fa fa-heart"/> Hearts</a></li>
+ <li data-select_class="s_hr_rating_bar"><a href="#"><i class="fa fa-tasks"/> Bar</a></li>
+ </ul>
+ </li>
+ </div>
+ <div data-js='s_hr_rating_option_value' data-selector=".s_hr_rating">
+ <li class="dropdown-submenu">
+ <a tabindex="-1" href="#">Rating value</a>
+ <ul class="dropdown-menu">
+ <li data-select_class=""><a href="#">0</a></li>
+ <li data-select_class="s_hr_rating_1"><a href="#">1/5</a></li>
+ <li data-select_class="s_hr_rating_2"><a href="#">2/5</a></li>
+ <li data-select_class="s_hr_rating_3"><a href="#">3/5</a></li>
+ <li data-select_class="s_hr_rating_4"><a href="#">4/5</a></li>
+ <li data-select_class="s_hr_rating_5"><a href="#">5/5</a></li>
+ </ul>
+ </li>
+ </div>
+ </xpath>
+</template>
+
+</odoo> \ No newline at end of file
diff --git a/addons/website_portal_sale/controllers/main.py b/addons/website_portal_sale/controllers/main.py
index 88e211bccfd..3103b3037e3 100644
--- a/addons/website_portal_sale/controllers/main.py
+++ b/addons/website_portal_sale/controllers/main.py
@@ -158,3 +158,4 @@ class website_account(website_account):
})
values.update(get_records_pager(history, order_sudo))
return request.render("website_portal_sale.orders_followup", values)
+
diff --git a/addons/website_portal_sale/views/website_portal_sale_templates.xml b/addons/website_portal_sale/views/website_portal_sale_templates.xml
index 2e3598c3f55..7d0098e67c0 100644
--- a/addons/website_portal_sale/views/website_portal_sale_templates.xml
+++ b/addons/website_portal_sale/views/website_portal_sale_templates.xml
@@ -197,7 +197,7 @@
</div>
<div>
<t t-foreach="invoices" t-as="i">
- <t t-set="report_url" t-value="'/my/invoices/%s' % i.id"/>
+ <t t-set="report_url" t-value="'/my/invoices/pdf/%s' % i.id"/>
<div>
<a t-att-href="report_url"><span class="fa fa-download"/></a>
<a t-att-href="report_url"><span t-esc="i.number"/></a>
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index dbedb81ab88..9e135847e19 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -275,7 +275,9 @@ class WebsiteSale(http.Controller):
@http.route(['/shop/product/<model("product.template"):product>'], type='http', auth="public", website=True)
def product(self, product, category='', search='', **kwargs):
- product_context = dict(request.env.context, active_id=product.id)
+ product_context = dict(request.env.context,
+ active_id=product.id,
+ partner=request.env.user.partner_id)
ProductCategory = request.env['product.public.category']
if category:
diff --git a/addons/website_sale/i18n/website_sale.pot b/addons/website_sale/i18n/website_sale.pot
index 1e1a4e53796..37b736949aa 100644
--- a/addons/website_sale/i18n/website_sale.pot
+++ b/addons/website_sale/i18n/website_sale.pot
@@ -1848,12 +1848,12 @@ msgstr ""
#. module: website_sale
#: model:ir.ui.view,arch_db:website_sale.sort
-msgid "Price <i class=\"fa fa-caret-down\" aria-hidden=\"true\"/>"
+msgid "Catalog price: decreasing order"
msgstr ""
#. module: website_sale
#: model:ir.ui.view,arch_db:website_sale.sort
-msgid "Price <i class=\"fa fa-caret-up\" aria-hidden=\"true\"/>"
+msgid "Catalog price: increasing order"
msgstr ""
#. module: website_sale
diff --git a/addons/website_sale/views/product_views.xml b/addons/website_sale/views/product_views.xml
index bc18e1de049..3be9cb503d8 100644
--- a/addons/website_sale/views/product_views.xml
+++ b/addons/website_sale/views/product_views.xml
@@ -70,8 +70,8 @@
<group name="website" string="Website" >
<field name="website_url" invisible="1"/>
<field name="public_categ_ids" widget="many2many_tags" string="Website Categories"/>
- <field name="alternative_product_ids" widget="many2many_tags"/>
- <field name="accessory_product_ids" widget="many2many_tags"/>
+ <field name="alternative_product_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
+ <field name="accessory_product_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="availability" widget="selection"/>
<field name="availability_warning" attrs="{'invisible': [('availability', '!=', 'warning')]}"/>
<field name="website_style_ids" widget="many2many_tags" groups="base.group_no_one"/>
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index cba93b108d6..2d25cc8d4fe 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -223,8 +223,8 @@
<template id="sort" inherit_id="website_sale.products" active="True" customize_show="True" name="Show Sort by">
<xpath expr="//div[@class='products_pager']/div/t[@t-call][last()]" position="after">
- <t t-set="list_price_desc_label">Price <i class="fa fa-caret-up" aria-hidden="true"/></t>
- <t t-set="list_price_asc_label">Price <i class="fa fa-caret-down" aria-hidden="true"/></t>
+ <t t-set="list_price_desc_label">Catalog price: decreasing order</t>
+ <t t-set="list_price_asc_label">Catalog price: increasing order</t>
<t t-set="name_asc_label">Name <i class="fa fa-caret-down" aria-hidden="true"/></t>
<t t-set="website_sale_sortable" t-value="[
(list_price_desc_label, 'list_price desc'),
diff --git a/addons/website_sale_digital/controllers/main.py b/addons/website_sale_digital/controllers/main.py
index 2df86980bc6..ec0aeec82a7 100644
--- a/addons/website_sale_digital/controllers/main.py
+++ b/addons/website_sale_digital/controllers/main.py
@@ -85,7 +85,7 @@ class WebsiteSaleDigital(website_account):
# Also check for attachments in the product templates
elif res_model == 'product.template':
- template_ids = request.env['product.product'].browse(purchased_products).mapped('product_tmpl_id').ids
+ template_ids = request.env['product.product'].sudo().browse(purchased_products).mapped('product_tmpl_id').ids
if res_id not in template_ids:
return redirect(self.orders_page)
diff --git a/addons/website_sale_options/views/product_views.xml b/addons/website_sale_options/views/product_views.xml
index 154a06c1e6a..c75b2d215e3 100644
--- a/addons/website_sale_options/views/product_views.xml
+++ b/addons/website_sale_options/views/product_views.xml
@@ -7,7 +7,7 @@
<field name="inherit_id" ref="website_sale.product_template_form_view"/>
<field name="arch" type="xml">
<field name="accessory_product_ids" position="after">
- <field name="optional_product_ids" widget="many2many_tags"/>
+ <field name="optional_product_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</field>
</field>
</record>