summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-11-09 19:16:05 +0100
committerChristophe Simonis <chs@odoo.com>2017-11-09 19:16:05 +0100
commitde93645fe3c85883d2f08677afad51456815fa6e (patch)
tree618ad9d3144a272e082aee1fc4b824f97a13a59f /addons
parentacd26691eef2930b03c2d3fb228110a1a6cc5f12 (diff)
parent82add438f21085a799f1d2d86a44d523c4fd2690 (diff)
[MERGE] forward port branch saas-16 up to 82add438f21
Diffstat (limited to 'addons')
-rw-r--r--addons/crm/models/crm_lead.py3
-rw-r--r--addons/hr/models/mail_alias.py6
-rw-r--r--addons/hr/models/mail_thread.py24
-rw-r--r--addons/hr_expense/models/hr_expense.py4
-rw-r--r--addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py2
-rw-r--r--addons/mail/controllers/bus.py10
-rw-r--r--addons/mail/models/mail_activity.py53
-rw-r--r--addons/mail/models/mail_alias.py21
-rw-r--r--addons/mail/models/mail_message.py37
-rw-r--r--addons/mail/models/mail_thread.py7
-rw-r--r--addons/mail/models/res_partner.py2
-rw-r--r--addons/mail/security/ir.model.access.csv12
-rw-r--r--addons/mail/security/mail_security.xml11
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js8
-rw-r--r--addons/web/static/src/js/services/data_manager.js2
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js8
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js4
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js25
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js132
-rw-r--r--addons/web/static/tests/helpers/mock_server.js5
-rw-r--r--addons/web/static/tests/views/basic_model_tests.js9
-rw-r--r--addons/web/static/tests/views/list_tests.js15
-rw-r--r--addons/web/static/tests/views/view_dialogs_tests.js38
-rw-r--r--addons/website_slides/controllers/main.py8
24 files changed, 341 insertions, 105 deletions
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index d0698de25ca..627d480f242 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -321,6 +321,9 @@ class Lead(models.Model):
# Set date_open to today if it is an opp
default = default or {}
default['date_open'] = fields.Datetime.now() if self.type == 'opportunity' else False
+ # Do not assign to an archived user
+ if not self.user_id.active:
+ default['user_id'] = False
return super(Lead, self.with_context(context)).copy(default=default)
@api.model
diff --git a/addons/hr/models/mail_alias.py b/addons/hr/models/mail_alias.py
index a88a6a4530f..f98fce32f5a 100644
--- a/addons/hr/models/mail_alias.py
+++ b/addons/hr/models/mail_alias.py
@@ -13,8 +13,8 @@ class Alias(models.Model):
class MailAlias(models.AbstractModel):
_inherit = 'mail.alias.mixin'
- def _alias_check_contact(self, message, message_dict, alias):
- if alias.alias_contact == 'employees' and self.ids:
+ def _alias_check_contact_on_record(self, record, message, message_dict, alias):
+ if alias.alias_contact == 'employees' and record.ids:
email_from = tools.decode_message_header(message, 'From')
email_address = tools.email_split(email_from)[0]
employee = self.env['hr.employee'].search([('work_email', 'ilike', email_address)], limit=1)
@@ -26,4 +26,4 @@ class MailAlias(models.AbstractModel):
'error_template': self.env.ref('hr.mail_template_data_unknown_employee_email_address').body_html,
}
return True
- return super(MailAlias, self)._alias_check_contact(message, message_dict, alias)
+ return super(MailAlias, self)._alias_check_contact_on_record(record, message, message_dict, alias)
diff --git a/addons/hr/models/mail_thread.py b/addons/hr/models/mail_thread.py
deleted file mode 100644
index 25ee48bcd6d..00000000000
--- a/addons/hr/models/mail_thread.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from odoo import models, tools
-from odoo.tools import email_split
-
-
-class MailAlias(models.AbstractModel):
- _inherit = 'mail.alias.mixin'
-
- def _alias_check_contact(self, message, message_dict, alias):
- if alias.alias_contact == 'employees' and self.ids:
- email_from = tools.decode_message_header(message, 'From')
- email_address = email_split(email_from)[0]
- employee = self.env['hr.employee'].search([('work_email', 'ilike', email_address)], limit=1)
- if not employee:
- employee = self.env['hr.employee'].search([('user_id.email', 'ilike', email_address)], limit=1)
- if not employee:
- return {
- 'error_message': 'restricted to employees',
- 'error_template': self.env.ref('hr.mail_template_data_unknown_employee_email_address').body_html,
- }
- return True
- return super(MailAlias, self)._alias_check_contact(message, message_dict, alias)
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index 8ee0401b12f..84f8e41c703 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -530,6 +530,8 @@ class HrExpenseSheet(models.Model):
@api.multi
def refuse_sheet(self, reason):
+ if not self.user_has_groups('hr_expense.group_hr_expense_user'):
+ raise UserError(_("Only HR Officers can refuse expenses"))
self.write({'state': 'cancel'})
for sheet in self:
sheet.message_post_with_view('hr_expense.hr_expense_template_refuse_reason',
@@ -537,6 +539,8 @@ class HrExpenseSheet(models.Model):
@api.multi
def approve_expense_sheets(self):
+ if not self.user_has_groups('hr_expense.group_hr_expense_user'):
+ raise UserError(_("Only HR Officers can approve expenses"))
self.write({'state': 'approve', 'responsible_id': self.env.user.id})
@api.multi
diff --git a/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py b/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py
index 49adf3fed7f..d0c980f9f5f 100644
--- a/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py
+++ b/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py
@@ -165,7 +165,7 @@ class hr_timesheet_sheet_sheet_day(models.Model):
ON r.user_id = u.id
LEFT JOIN res_partner p
ON u.partner_id = p.id
- WHERE check_out IS NOT NULL
+ WHERE a.check_out IS NOT NULL
group by (a.check_in AT TIME ZONE 'UTC' AT TIME ZONE coalesce(p.tz, 'UTC'))::date, s.id, timezone
)) AS foo
GROUP BY name, sheet_id, timezone
diff --git a/addons/mail/controllers/bus.py b/addons/mail/controllers/bus.py
index 2c6d4b835b2..50f08d8d8de 100644
--- a/addons/mail/controllers/bus.py
+++ b/addons/mail/controllers/bus.py
@@ -36,21 +36,19 @@ class MailChatController(BusController):
# --------------------------
@route('/mail/chat_post', type="json", auth="none")
def mail_chat_post(self, uuid, message_content, **kwargs):
- request_uid = self._default_request_uid()
# find the author from the user session, which can be None
author_id = False # message_post accept 'False' author_id, but not 'None'
if request.session.uid:
author_id = request.env['res.users'].sudo().browse(request.session.uid).partner_id.id
# post a message without adding followers to the channel. email_from=False avoid to get author from email data
- mail_channel = request.env["mail.channel"].sudo(request_uid).search([('uuid', '=', uuid)], limit=1)
- message = mail_channel.sudo(request_uid).with_context(mail_create_nosubscribe=True).message_post(author_id=author_id, email_from=False, body=message_content, message_type='comment', subtype='mail.mt_comment', content_subtype='plaintext', **kwargs)
+ mail_channel = request.env["mail.channel"].sudo().search([('uuid', '=', uuid)], limit=1)
+ message = mail_channel.sudo().with_context(mail_create_nosubscribe=True).message_post(author_id=author_id, email_from=False, body=message_content, message_type='comment', subtype='mail.mt_comment', content_subtype='plaintext')
return message and message.id or False
@route(['/mail/chat_history'], type="json", auth="none")
def mail_chat_history(self, uuid, last_id=False, limit=20):
- request_uid = self._default_request_uid()
- channel = request.env["mail.channel"].sudo(request_uid).search([('uuid', '=', uuid)], limit=1)
+ channel = request.env["mail.channel"].sudo().search([('uuid', '=', uuid)], limit=1)
if not channel:
return []
else:
- return channel.sudo(request_uid).channel_fetch_message(last_id, limit)
+ return channel.channel_fetch_message(last_id, limit)
diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py
index 1e7c0729ba4..6ed049833d5 100644
--- a/addons/mail/models/mail_activity.py
+++ b/addons/mail/models/mail_activity.py
@@ -3,7 +3,7 @@
from datetime import date, datetime, timedelta
-from odoo import api, fields, models
+from odoo import api, exceptions, fields, models, _
class MailActivityType(models.Model):
@@ -134,21 +134,61 @@ class MailActivity(models.Model):
def _onchange_recommended_activity_type_id(self):
self.activity_type_id = self.recommended_activity_type_id
+ @api.multi
+ def _check_access(self, operation):
+ """ Rule to access activities
+
+ * create: check write rights on related document;
+ * write: rule OR write rights on document;
+ * unlink: rule OR write rights on document;
+ """
+ self.check_access_rights(operation, raise_exception=True) # will raise an AccessError
+
+ if operation in ('write', 'unlink'):
+ try:
+ self.check_access_rule(operation)
+ except exceptions.AccessError:
+ pass
+ else:
+ return
+
+ doc_operation = 'read' if operation == 'read' else 'write'
+ activity_to_documents = dict()
+ for activity in self.sudo():
+ activity_to_documents.setdefault(activity.res_model, list()).append(activity.res_id)
+ for model, res_ids in activity_to_documents.items():
+ self.env[model].check_access_rights(doc_operation, raise_exception=True)
+ try:
+ self.env[model].browse(res_ids).check_access_rule(doc_operation)
+ except exceptions.AccessError:
+ raise exceptions.AccessError(
+ _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') %
+ (self._description, operation))
+
@api.model
def create(self, values):
- activity = super(MailActivity, self).create(values)
- self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[activity.user_id.partner_id.id])
+ # already compute default values to be sure those are computed using the current user
+ values_w_defaults = self.default_get(self._fields.keys())
+ values_w_defaults.update(values)
+
+ # continue as sudo because activities are somewhat protected
+ activity = super(MailActivity, self.sudo()).create(values_w_defaults)
+ activity_user = activity.sudo(self.env.user)
+ activity_user._check_access('create')
+ self.env[activity_user.res_model].browse(activity_user.res_id).message_subscribe(partner_ids=[activity_user.user_id.partner_id.id])
if activity.date_deadline <= fields.Date.today():
self.env['bus.bus'].sendone(
(self._cr.dbname, 'res.partner', activity.user_id.partner_id.id),
{'type': 'activity_updated', 'activity_created': True})
- return activity
+ return activity_user
@api.multi
def write(self, values):
+ self._check_access('write')
if values.get('user_id'):
pre_responsibles = self.mapped('user_id.partner_id')
- res = super(MailActivity, self).write(values)
+ res = super(MailActivity, self.sudo()).write(values)
+
if values.get('user_id'):
for activity in self:
self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[activity.user_id.partner_id.id])
@@ -166,12 +206,13 @@ class MailActivity(models.Model):
@api.multi
def unlink(self):
+ self._check_access('unlink')
for activity in self:
if activity.date_deadline <= fields.Date.today():
self.env['bus.bus'].sendone(
(self._cr.dbname, 'res.partner', activity.user_id.partner_id.id),
{'type': 'activity_updated', 'activity_deleted': True})
- return super(MailActivity, self).unlink()
+ return super(MailActivity, self.sudo()).unlink()
@api.multi
def action_done(self):
diff --git a/addons/mail/models/mail_alias.py b/addons/mail/models/mail_alias.py
index 8bd0b8f19c9..dc09f3be13b 100644
--- a/addons/mail/models/mail_alias.py
+++ b/addons/mail/models/mail_alias.py
@@ -264,16 +264,27 @@ class AliasMixin(models.AbstractModel):
record._name, record.display_name, record.id)
def _alias_check_contact(self, message, message_dict, alias):
+ """ Main mixin method that inheriting models may inherit in order
+ to implement a specifc behavior. """
+ return self._alias_check_contact_on_record(self, message, message_dict, alias)
+
+ def _alias_check_contact_on_record(self, record, message, message_dict, alias):
+ """ Generic method that takes a record not necessarily inheriting from
+ mail.alias.mixin. """
author = self.env['res.partner'].browse(message_dict.get('author_id', False))
- if alias.alias_contact == 'followers' and self.ids:
- if not hasattr(self, "message_partner_ids") or not hasattr(self, "message_channel_ids"):
+ if alias.alias_contact == 'followers':
+ if not record.ids:
+ return {
+ 'error_message': _('incorrectly configured alias (unknown reference record)'),
+ }
+ if not hasattr(record, "message_partner_ids") or not hasattr(record, "message_channel_ids"):
return {
- 'error_mesage': _('incorrectly configured alias'),
+ 'error_message': _('incorrectly configured alias'),
}
- accepted_partner_ids = self.message_partner_ids | self.message_channel_ids.mapped('channel_partner_ids')
+ accepted_partner_ids = record.message_partner_ids | record.message_channel_ids.mapped('channel_partner_ids')
if not author or author not in accepted_partner_ids:
return {
- 'error_mesage': _('restricted to followers'),
+ 'error_message': _('restricted to followers'),
}
elif alias.alias_contact == 'partners' and not author:
return {
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index 47918de4966..86168bf6049 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -97,6 +97,7 @@ class Message(models.Model):
tracking_value_ids = fields.One2many(
'mail.tracking.value', 'mail_message_id',
string='Tracking values',
+ groups="base.group_no_one",
help='Tracked values are stored in a separate model. This field allow to reconstruct '
'the tracking and to generate statistics on the model.')
# mail gateway
@@ -280,8 +281,8 @@ class Message(models.Model):
# 1. Aggregate partners (author_id and partner_ids), attachments and tracking values
partners = self.env['res.partner'].sudo()
attachments = self.env['ir.attachment']
- trackings = self.env['mail.tracking.value']
- for key, message in message_tree.items():
+ message_ids = list(message_tree.keys())
+ for message in message_tree.values():
if message.author_id:
partners |= message.author_id
if message.subtype_id and message.partner_ids: # take notified people of message with a subtype
@@ -292,8 +293,6 @@ class Message(models.Model):
partners |= message.needaction_partner_ids
if message.attachment_ids:
attachments |= message.attachment_ids
- if message.tracking_value_ids:
- trackings |= message.tracking_value_ids
# Read partners as SUPERUSER -> message being browsed as SUPERUSER it is already the case
partners_names = partners.name_get()
partner_tree = dict((partner[0], partner) for partner in partners_names)
@@ -308,13 +307,18 @@ class Message(models.Model):
}) for attachment in attachments_data)
# 3. Tracking values
- tracking_tree = dict((tracking.id, {
- 'id': tracking.id,
- 'changed_field': tracking.field_desc,
- 'old_value': tracking.get_old_display_value()[0],
- 'new_value': tracking.get_new_display_value()[0],
- 'field_type': tracking.field_type,
- }) for tracking in trackings)
+ tracking_values = self.env['mail.tracking.value'].sudo().search([('mail_message_id', 'in', message_ids)])
+ message_to_tracking = dict()
+ tracking_tree = dict.fromkeys(tracking_values.ids, False)
+ for tracking in tracking_values:
+ message_to_tracking.setdefault(tracking.mail_message_id.id, list()).append(tracking.id)
+ tracking_tree[tracking.id] = {
+ 'id': tracking.id,
+ 'changed_field': tracking.field_desc,
+ 'old_value': tracking.get_old_display_value()[0],
+ 'new_value': tracking.get_new_display_value()[0],
+ 'field_type': tracking.field_type,
+ }
# 4. Update message dictionaries
for message_dict in messages:
@@ -341,9 +345,9 @@ class Message(models.Model):
if attachment.id in attachments_tree:
attachment_ids.append(attachments_tree[attachment.id])
tracking_value_ids = []
- for tracking_value in message.tracking_value_ids:
- if tracking_value.id in tracking_tree:
- tracking_value_ids.append(tracking_tree[tracking_value.id])
+ for tracking_value_id in message_to_tracking.get(message_id, list()):
+ if tracking_value_id in tracking_tree:
+ tracking_value_ids.append(tracking_tree[tracking_value_id])
message_dict.update({
'author_id': author,
@@ -746,7 +750,12 @@ class Message(models.Model):
return '%s%s alt="%s"' % (data_to_url[key], match.group(3), name)
values['body'] = _image_dataurl.sub(base64_to_boundary, tools.ustr(values['body']))
+ # delegate creation of tracking after the create as sudo to avoid access rights issues
+ tracking_values_cmd = values.pop('tracking_value_ids', False)
message = super(Message, self).create(values)
+ if tracking_values_cmd:
+ message.sudo().write({'tracking_value_ids': tracking_values_cmd})
+
message._invalidate_documents()
if not self.env.context.get('message_create_from_mail_mail'):
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index 1d7d9681ea1..b681e18d284 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -958,9 +958,10 @@ class MailThread(models.AbstractModel):
obj = self.env[alias.alias_parent_model_id.model].browse(alias.alias_parent_thread_id)
elif model:
obj = self.env[model]
- if not hasattr(obj, '_alias_check_contact'):
- obj = self.env['mail.alias.mixin']
- check_result = obj._alias_check_contact(message, message_dict, alias)
+ if hasattr(obj, '_alias_check_contact'):
+ check_result = obj._alias_check_contact(message, message_dict, alias)
+ else:
+ check_result = self.env['mail.alias.mixin']._alias_check_contact_on_record(obj, message, message_dict, alias)
if check_result is not True:
self._routing_warn(_('alias %s: %s') % (alias.alias_name, check_result.get('error_message', _('unknown error'))), _('skipping'), message_id, route, False)
self._routing_create_bounce_email(email_from, check_result.get('error_template', _generic_bounce_body_html), message)
diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py
index 3a81423a9b2..6114adcce7b 100644
--- a/addons/mail/models/res_partner.py
+++ b/addons/mail/models/res_partner.py
@@ -62,7 +62,7 @@ class Partner(models.Model):
record_name = message.record_name
tracking = []
- for tracking_value in message.tracking_value_ids:
+ for tracking_value in self.env['mail.tracking.value'].sudo().search([('mail_message_id', '=', message.id)]):
tracking.append((tracking_value.field_desc,
tracking_value.get_old_display_value()[0],
tracking_value.get_new_display_value()[0]))
diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv
index 99f811a3634..0a2e9713c4f 100644
--- a/addons/mail/security/ir.model.access.csv
+++ b/addons/mail/security/ir.model.access.csv
@@ -23,9 +23,9 @@ access_mail_alias_user,mail.alias.user,model_mail_alias,base.group_user,1,1,1,1
access_mail_alias_system,mail.alias.system,model_mail_alias,base.group_system,1,1,1,1
access_mail_message_subtype_all,mail.message.subtype.all,model_mail_message_subtype,,1,0,0,0
access_mail_message_subtype_user,mail.message.subtype.user,model_mail_message_subtype,,1,1,1,1
-access_mail_tracking_value_all,mail.tracking.value.all,model_mail_tracking_value,,1,0,0,0
-access_mail_tracking_value_portal,mail.tracking.value.portal,model_mail_tracking_value,base.group_portal,1,1,1,1
-access_mail_tracking_value_user,mail.tracking.value.user,model_mail_tracking_value,base.group_user,1,1,1,1
+access_mail_tracking_value_all,mail.tracking.value.all,model_mail_tracking_value,,0,0,0,0
+access_mail_tracking_value_portal,mail.tracking.value.portal,model_mail_tracking_value,base.group_portal,0,0,0,0
+access_mail_tracking_value_user,mail.tracking.value.user,model_mail_tracking_value,base.group_user,0,0,0,0
access_mail_tracking_value_system,mail.tracking.value.system,model_mail_tracking_value,base.group_system,1,1,1,1
access_mail_thread_all,mail.thread.all,model_mail_thread,,1,1,1,1
access_publisher_warranty_contract_all,publisher.warranty.contract.all,model_publisher_warranty_contract,,1,1,1,1
@@ -36,7 +36,7 @@ access_mail_shortcode_portal,mail.shortcode.portal,model_mail_shortcode,base.gro
access_mail_test_user,mail.test.all,model_mail_test,base.group_user,1,1,1,1
access_mail_test_portal,mail.test.all,model_mail_test,base.group_portal,1,1,0,0
access_mail_test_simple,mail.test.simple.all,model_mail_test_simple,base.group_user,1,1,1,1
-access_mail_activity_all,mail.activity.all,model_mail_activity,,1,0,0,0
+access_mail_activity_all,mail.activity.all,model_mail_activity,,0,0,0,0
access_mail_activity_user,mail.activity.user,model_mail_activity,base.group_user,1,1,1,1
-access_mail_activity_type_all,mail.activity.type.all,model_mail_activity_type,,1,0,0,0
-access_mail_activity_type_user,mail.activity.type.user,model_mail_activity_type,base.group_user,1,1,1,1 \ No newline at end of file
+access_mail_activity_type_all,mail.activity.type.all,model_mail_activity_type,,0,0,0,0
+access_mail_activity_type_user,mail.activity.type.user,model_mail_activity_type,base.group_user,1,1,1,1
diff --git a/addons/mail/security/mail_security.xml b/addons/mail/security/mail_security.xml
index d5a5b5422c9..b9b13d9de3b 100644
--- a/addons/mail/security/mail_security.xml
+++ b/addons/mail/security/mail_security.xml
@@ -40,5 +40,16 @@
<field name="groups" eval="[(4, ref('base.group_portal')), (4, ref('base.group_public'))]"/>
</record>
+ <record id="mail_activity_rule_user" model="ir.rule">
+ <field name="name">mail.activity: user: own only</field>
+ <field name="model_id" ref="model_mail_activity"/>
+ <field name="domain_force">[('user_id', '=', user.id)]</field>
+ <field name="groups" eval="[(4, ref('base.group_user'))]"/>
+ <field name="perm_create" eval="False"/>
+ <field name="perm_read" eval="False"/>
+ <field name="perm_write" eval="True"/>
+ <field name="perm_unlink" eval="True"/>
+ </record>
+
</data>
</odoo>
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 2d2a5d5ea4d..e5a7cc3da2f 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -503,9 +503,11 @@ var FieldMany2One = AbstractField.extend({
title: _t("Open: ") + self.string,
view_id: view_id,
readonly: !self.can_write,
- on_saved: function () {
- self._setValue(self.value.data, {forceChange: true});
- self.trigger_up('reload', {db_id: self.value.id});
+ on_saved: function (record, changed) {
+ if (changed) {
+ self._setValue(self.value.data, {forceChange: true});
+ self.trigger_up('reload', {db_id: self.value.id});
+ }
},
}).open();
});
diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js
index 32d526e160d..e99f9ebb6f2 100644
--- a/addons/web/static/src/js/services/data_manager.js
+++ b/addons/web/static/src/js/services/data_manager.js
@@ -424,7 +424,7 @@ return core.Class.extend({
var deps = fieldsInfo[node.attrs.name].fieldDependencies;
for (var dependency_name in deps) {
if (!(dependency_name in fieldsInfo)) {
- fieldsInfo[dependency_name] = {'name': dependency_name, 'type': deps[dependency_name].type};
+ fieldsInfo[dependency_name] = {'name': dependency_name, 'type': deps[dependency_name].type, 'options': deps[dependency_name].options || {}};
}
}
}
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 4412d29c455..4028bff322a 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -896,6 +896,9 @@ var BasicModel = AbstractModel.extend({
return def.resolve(changedFields);
}
+ def.then(function () {
+ record._isDirty = false;
+ });
// in the case of a write, only perform the RPC if there are changes to save
if (method === 'create' || changedFields.length) {
var args = method === 'write' ? [[record.data.id], changes] : [changes];
@@ -917,7 +920,6 @@ var BasicModel = AbstractModel.extend({
// Erase changes as they have been applied
record._changes = {};
- record._isDirty = false;
// Update the data directly or reload them
if (shouldReload) {
@@ -947,8 +949,8 @@ var BasicModel = AbstractModel.extend({
*/
addFieldsInfo: function (recordID, viewInfo) {
var record = this.localData[recordID];
- record.fields = _.defaults(record.fields, viewInfo.fields);
- record.fieldsInfo = _.defaults(record.fieldsInfo, viewInfo.fieldsInfo);
+ record.fields = _.extend({}, record.fields, viewInfo.fields);
+ record.fieldsInfo = _.extend({}, record.fieldsInfo, viewInfo.fieldsInfo);
},
/**
* Manually sets a resource as dirty. This is used to notify that a field
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 460ad5de100..67cc6271509 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -355,8 +355,8 @@ var ListController = BasicController.extend({
// trigger a click on the main bus, which would be then caught by the
// list editable renderer and would unselect the newly created row
event.stopPropagation();
-
- if (this.editable) {
+ var state = this.model.get(this.handle, {raw: true});
+ if (this.editable && !state.groupedBy.length) {
this._addRecord();
} else {
this.trigger_up('switch_view', {view_type: 'form', res_id: undefined});
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index fe38fae753e..a33f6f740b8 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -81,9 +81,9 @@ var FormViewDialog = ViewDialog.extend({
* @param {Object} [options.fields_view] optional form fields_view
* @param {boolean} [options.readonly=false] only applicable when not in
* creation mode
- * @param {function} [options.on_save] callback to execute when clicking on
- * 'Save' (form view's 'saveRecord' by default)
- * @param {function} [options.on_saved] callback executed after on_save
+ * @param {function} [options.on_saved] callback executed after saving a
+ * record. It will be called with the record data, and a boolean which
+ * indicates if something was changed
* @param {BasicModel} [options.model] if given, it will be used instead of
* a new form view model
* @param {string} [options.recordID] if given, the model has to be given as
@@ -214,24 +214,16 @@ var FormViewDialog = ViewDialog.extend({
_save: function () {
var self = this;
- var def;
- if (this.options.on_save) {
- if (this.form_view.canBeSaved()) {
- return $.Deferred().reject();
- }
- def = this.options.on_save(this.form_view.model.get(this.form_view.handle));
- } else {
- def = this.form_view.saveRecord(this.form_view.handle, {
+ return this.form_view.saveRecord(this.form_view.handle, {
stayInEdit: true,
reload: false,
savePoint: this.shouldSaveLocally,
viewType: 'form',
- });
- }
- return $.when(def).then(function () {
+ }).then(function (changedFields) {
// record might have been changed by the save (e.g. if this was a new record, it has an
// id now), so don't re-use the copy obtained before the save
- self.on_saved(self.form_view.model.get(self.form_view.handle));
+ var record = self.form_view.model.get(self.form_view.handle);
+ self.on_saved(record, !!changedFields.length);
});
},
});
@@ -402,7 +394,8 @@ var SelectCreateDialog = ViewDialog.extend({
var results = pyeval.eval_domains_and_contexts({
domains: [this.domain].concat(domains),
contexts: [this.context].concat(contexts),
- group_by_seq: groupbys || []
+ group_by_seq: groupbys || [],
+ eval_context: this.getSession().user_context,
});
var context = _.omit(results.context, function (value, key) { return key.indexOf('search_default_') === 0; });
return {
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index f665fa160e4..2e8499b31e1 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -137,6 +137,7 @@ QUnit.module('relational_fields', {
partner_ids: [],
turtle_ref: 'product,37',
}],
+ onchanges: {},
},
user: {
fields: {
@@ -212,6 +213,98 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('editing a many2one, but not changing anything', function (assert) {
+ assert.expect(2);
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="trululu"/>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'partner,false,form': '<form string="Partners"><field name="display_name"/></form>',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'get_formview_id') {
+ assert.deepEqual(args.args[0], [4], "should call get_formview_id with correct id");
+ return $.when(false);
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ ids: [1, 2],
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ // click on the external button (should do an RPC)
+ form.$('.o_external_button').click();
+ // save and close modal
+ $('.modal .modal-footer .btn-primary:first').click();
+ // save form
+ form.$buttons.find('.o_form_button_save').click();
+ // click next on pager
+ form.pager.$('.o_pager_next').click();
+
+ // this checks that the view did not ask for confirmation that the
+ // record is dirty
+ assert.strictEqual(form.pager.$el.text().trim(), '2 / 2',
+ 'pager should be at second page');
+ form.destroy();
+ });
+
+ QUnit.test('editing a many2one (with form view opened with external button)', function (assert) {
+ assert.expect(1);
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="trululu"/>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'partner,false,form': '<form string="Partners"><field name="foo"/></form>',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'get_formview_id') {
+ return $.when(false);
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ ids: [1, 2],
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ // click on the external button (should do an RPC)
+ form.$('.o_external_button').click();
+
+ $('.modal input[name="foo"]').val('brandon').trigger('input');
+
+ // save and close modal
+ $('.modal .modal-footer .btn-primary:first').click();
+ // save form
+ form.$buttons.find('.o_form_button_save').click();
+ // click next on pager
+ form.pager.$('.o_pager_next').click();
+
+ // this checks that the view did not ask for confirmation that the
+ // record is dirty
+ assert.strictEqual(form.pager.$el.text().trim(), '2 / 2',
+ 'pager should be at second page');
+ form.destroy();
+ });
+
QUnit.test('many2ones in form views with show_adress', function (assert) {
assert.expect(4);
var form = createView({
@@ -2587,6 +2680,45 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('edition of one2many field, with onchange and not inline sub view', function (assert) {
+ assert.expect(2);
+
+ this.data.turtle.onchanges.turtle_int = function (obj) {
+ obj.turtle_foo = String(obj.turtle_int);
+ };
+ this.data.partner.onchanges.turtles = function () {};
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles"/>' +
+ '</form>',
+ archs: {
+ 'turtle,false,list': '<tree><field name="turtle_foo"/></tree>',
+ 'turtle,false,form': '<form><group><field name="turtle_foo"/><field name="turtle_int"/></group></form>',
+ },
+ mockRPC: function (route, args) {
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_x2many_list_row_add a').click();
+ $('input[name="turtle_int"]').val('5').trigger('input');
+ $('.modal-footer button.btn-primary').first().click();
+ assert.strictEqual(form.$('tbody tr:eq(1) td.o_data_cell').text(), '5',
+ 'should display 5 in the foo field');
+ form.$('tbody tr:eq(1) td.o_data_cell').click();
+
+ $('input[name="turtle_int"]').val('3').trigger('input');
+ $('.modal-footer button.btn-primary').first().click();
+ assert.strictEqual(form.$('tbody tr:eq(1) td.o_data_cell').text(), '3',
+ 'should now display 3 in the foo field');
+ form.destroy();
+ });
+
QUnit.test('sorting one2many fields', function (assert) {
assert.expect(4);
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index f7d3f2fb307..3556fb946aa 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -32,9 +32,6 @@ var MockServer = Class.extend({
if (!('name' in model.fields)) {
model.fields.name = {string: "Name", type: "char", default: "name"};
}
- for (var fieldName in model.onchanges) {
- model.fields[fieldName].onChange = "1";
- }
model.records = model.records || [];
for (var i = 0; i < model.records.length; i++) {
@@ -282,7 +279,7 @@ var MockServer = Class.extend({
// add onchanges
if (name in onchanges) {
- field.onChange="1";
+ node.attrs.on_change="1";
}
});
return {
diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js
index 65962870be7..492dfd4224c 100644
--- a/addons/web/static/tests/views/basic_model_tests.js
+++ b/addons/web/static/tests/views/basic_model_tests.js
@@ -203,6 +203,7 @@ QUnit.module('Views', {
QUnit.test('basic onchange', function (assert) {
assert.expect(5);
+ this.data.partner.fields.foo.onChange = true;
this.data.partner.onchanges.foo = function (obj) {
obj.bar = obj.foo.length;
};
@@ -240,6 +241,7 @@ QUnit.module('Views', {
QUnit.test('onchange with a many2one', function (assert) {
assert.expect(5);
+ this.data.partner.fields.product_id.onChange = true;
this.data.partner.onchanges.product_id = function (obj) {
if (obj.product_id === 37) {
obj.foo = "space lollipop";
@@ -280,6 +282,7 @@ QUnit.module('Views', {
QUnit.test('onchange on a one2many not in view (fieldNames)', function (assert) {
assert.expect(6);
+ this.data.partner.fields.foo.onChange = true;
this.data.partner.onchanges.foo = function (obj) {
obj.bar = obj.foo.length;
obj.product_ids = [];
@@ -425,6 +428,7 @@ QUnit.module('Views', {
QUnit.test('onchange on a char with an unchanged many2one', function (assert) {
assert.expect(2);
+ this.data.partner.fields.foo.onChange = true;
this.data.partner.onchanges.foo = function (obj) {
obj.foo = obj.foo + " alligator";
};
@@ -453,6 +457,7 @@ QUnit.module('Views', {
QUnit.test('onchange on a char with another many2one not set to a value', function (assert) {
assert.expect(2);
this.data.partner.records[0].product_id = false;
+ this.data.partner.fields.foo.onChange = true;
this.data.partner.onchanges.foo = function (obj) {
obj.foo = obj.foo + " alligator";
};
@@ -1153,6 +1158,7 @@ QUnit.module('Views', {
assert.expect(4);
this.data.partner.fields.total.default = 50;
+ this.data.partner.fields.product_ids.onChange = true;
this.data.partner.onchanges.product_ids = function (obj) {
obj.total += 100;
};
@@ -1497,6 +1503,7 @@ QUnit.module('Views', {
assert.expect(6);
this.params.fieldNames = ['foo', 'bar'];
+ this.data.partner.fields.foo.onChange = true;
this.data.partner.onchanges.foo = function (obj) {
obj.bar = obj.foo.length;
};
@@ -1562,6 +1569,7 @@ QUnit.module('Views', {
assert.expect(6);
this.params.fieldNames = ['foo', 'bar'];
+ this.data.partner.fields.foo.onChange = true;
this.data.partner.onchanges.foo = function (obj) {
obj.bar = obj.foo.length;
};
@@ -2138,6 +2146,7 @@ QUnit.module('Views', {
};
_.extend(this.data.partner.fields, newFields);
+ this.data.partner.fields.foobool.onChange = true;
this.data.partner.onchanges.foobool = function (obj) {
if (obj.foobool) {
obj.foobool2 = true;
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index 32e3ad0046a..242eb2f62d6 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -2628,7 +2628,7 @@ QUnit.module('Views', {
// Editable grouped list views are not supported, so the purpose of this
// test is to check that when a list view is grouped, its editable
// attribute is ignored
- assert.expect(4);
+ assert.expect(5);
var list = createView({
View: ListView,
@@ -2636,8 +2636,9 @@ QUnit.module('Views', {
data: this.data,
arch: '<tree editable="top"><field name="foo"/><field name="bar"/></tree>',
intercepts: {
- switch_view: function () {
- assert.step('switch view');
+ switch_view: function (event) {
+ var resID = event.data.res_id || false;
+ assert.step('switch view ' + event.data.view_type + ' ' + resID);
},
},
});
@@ -2649,9 +2650,15 @@ QUnit.module('Views', {
// reload with groupBy
list.reload({groupBy: ['bar']});
+
+ // clicking on record should open the form view
list.$('.o_group_header:first').click();
list.$('.o_data_cell:first').click();
- assert.verifySteps(['switch view'], 'one switch view should have been requested');
+
+ // clicking on create button should open the form view
+ list.$buttons.find('.o_list_button_add').click();
+ assert.verifySteps(['switch view form 1', 'switch view form false'],
+ 'two switch view to form should have been requested');
list.destroy();
});
diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js
index 43bf4ee6694..7a107c70346 100644
--- a/addons/web/static/tests/views/view_dialogs_tests.js
+++ b/addons/web/static/tests/views/view_dialogs_tests.js
@@ -140,6 +140,44 @@ QUnit.module('Views', {
dialog.destroy();
});
+ QUnit.test('SelectCreateDialog correctly evaluates domains', function (assert) {
+ assert.expect(1);
+
+ var parent = createParent({
+ data: this.data,
+ archs: {
+ 'partner,false,list':
+ '<tree string="Partner">' +
+ '<field name="display_name"/>' +
+ '<field name="foo"/>' +
+ '</tree>',
+ 'partner,false,search':
+ '<search>' +
+ '<field name="foo"/>' +
+ '</search>',
+ },
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/search_read') {
+ assert.deepEqual(args.domain, [['id', '=', 2]],
+ "should have correctly evaluated the domain");
+ }
+ return this._super.apply(this, arguments);
+ },
+ session: {
+ user_context: {uid: 2},
+ },
+ });
+
+ var dialog = new dialogs.SelectCreateDialog(parent, {
+ no_create: true,
+ readonly: true,
+ res_model: 'partner',
+ domain: "[['id', '=', uid]]",
+ }).open();
+
+ dialog.destroy();
+ });
+
QUnit.test('SelectCreateDialog list view in readonly', function (assert) {
assert.expect(1);
diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py
index 9e934d08ad0..f7062cc031e 100644
--- a/addons/website_slides/controllers/main.py
+++ b/addons/website_slides/controllers/main.py
@@ -258,9 +258,11 @@ class WebsiteSlides(http.Controller):
@http.route(['/slides/add_slide'], type='json', auth='user', methods=['POST'], website=True)
def create_slide(self, *args, **post):
- file_size = len(post['datas']) * 3 / 4; # base64
- if (file_size / 1024.0 / 1024.0) > 25:
- return {'error': _('File is too big. File size cannot exceed 25MB')}
+ # check the size only when we upload a file.
+ if post.get('datas'):
+ file_size = len(post['datas']) * 3 / 4 # base64
+ if (file_size / 1024.0 / 1024.0) > 25:
+ return {'error': _('File is too big. File size cannot exceed 25MB')}
values = dict((fname, post[fname]) for fname in [
'name', 'url', 'tag_ids', 'slide_type', 'channel_id',