diff options
| author | Christophe Simonis <chs@odoo.com> | 2017-11-09 19:16:05 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2017-11-09 19:16:05 +0100 |
| commit | de93645fe3c85883d2f08677afad51456815fa6e (patch) | |
| tree | 618ad9d3144a272e082aee1fc4b824f97a13a59f /addons | |
| parent | acd26691eef2930b03c2d3fb228110a1a6cc5f12 (diff) | |
| parent | 82add438f21085a799f1d2d86a44d523c4fd2690 (diff) | |
[MERGE] forward port branch saas-16 up to 82add438f21
Diffstat (limited to 'addons')
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', |
