[MERGE] forward port branch saas-16 up to 82add438f2
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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_mesage': _('incorrectly configured alias'),
|
||||
'error_message': _('incorrectly configured alias (unknown reference record)'),
|
||||
}
|
||||
accepted_partner_ids = self.message_partner_ids | self.message_channel_ids.mapped('channel_partner_ids')
|
||||
if not hasattr(record, "message_partner_ids") or not hasattr(record, "message_channel_ids"):
|
||||
return {
|
||||
'error_message': _('incorrectly configured alias'),
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
|
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 || {}};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
France, 2017-11-06
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Rémi CAZENAVE remi@le-filament.com https://github.com/remi-filament
|
||||
@@ -17,6 +17,13 @@ _logger = logging.getLogger(__name__)
|
||||
BASE_VERSION = odoo.modules.load_information_from_description_file('base')['version']
|
||||
|
||||
|
||||
class BadVersion(Exception):
|
||||
pass
|
||||
|
||||
class BadModuleState(Exception):
|
||||
pass
|
||||
|
||||
|
||||
_intervalTypes = {
|
||||
'days': lambda interval: relativedelta(days=interval),
|
||||
'hours': lambda interval: relativedelta(hours=interval),
|
||||
@@ -142,33 +149,102 @@ class ir_cron(models.Model):
|
||||
cron_cr.commit()
|
||||
|
||||
@classmethod
|
||||
def _acquire_job(cls, db_name):
|
||||
# TODO remove 'check' argument from addons/base_automation/base_automation.py
|
||||
""" Try to process one cron job.
|
||||
def _process_jobs(cls, db_name):
|
||||
""" Try to process all cron jobs.
|
||||
|
||||
This selects in database all the jobs that should be processed. It then
|
||||
tries to lock each of them and, if it succeeds, run the cron job (if it
|
||||
doesn't succeed, it means the job was already locked to be taken care
|
||||
of by another thread) and return.
|
||||
|
||||
If a job was processed, returns True, otherwise returns False.
|
||||
:raise BadVersion: if the version is different from the worker's
|
||||
:raise BadModuleState: if modules are to install/upgrade/remove
|
||||
"""
|
||||
db = odoo.sql_db.db_connect(db_name)
|
||||
threading.current_thread().dbname = db_name
|
||||
jobs = []
|
||||
try:
|
||||
with db.cursor() as cr:
|
||||
# Make sure the database we poll has the same version as the code of base
|
||||
cr.execute("SELECT 1 FROM ir_module_module WHERE name=%s AND latest_version=%s", ('base', BASE_VERSION))
|
||||
if cr.fetchone():
|
||||
# Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1.
|
||||
cr.execute("""SELECT * FROM ir_cron
|
||||
WHERE numbercall != 0
|
||||
AND active AND nextcall <= (now() at time zone 'UTC')
|
||||
ORDER BY priority""")
|
||||
jobs = cr.dictfetchall()
|
||||
else:
|
||||
_logger.warning('Skipping database %s as its base version is not %s.', db_name, BASE_VERSION)
|
||||
# Make sure the database has the same version as the code of
|
||||
# base and that no module must be installed/upgraded/removed
|
||||
cr.execute("SELECT latest_version FROM ir_module_module WHERE name=%s", ['base'])
|
||||
(version,) = cr.fetchone()
|
||||
cr.execute("SELECT COUNT(*) FROM ir_module_module WHERE state LIKE %s", ['to %'])
|
||||
(changes,) = cr.fetchone()
|
||||
if not version or changes:
|
||||
raise BadModuleState()
|
||||
elif version != BASE_VERSION:
|
||||
raise BadVersion()
|
||||
# Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1.
|
||||
cr.execute("""SELECT * FROM ir_cron
|
||||
WHERE numbercall != 0
|
||||
AND active AND nextcall <= (now() at time zone 'UTC')
|
||||
ORDER BY priority""")
|
||||
jobs = cr.dictfetchall()
|
||||
|
||||
for job in jobs:
|
||||
lock_cr = db.cursor()
|
||||
try:
|
||||
# Try to grab an exclusive lock on the job row from within the task transaction
|
||||
# Restrict to the same conditions as for the search since the job may have already
|
||||
# been run by an other thread when cron is running in multi thread
|
||||
lock_cr.execute("""SELECT *
|
||||
FROM ir_cron
|
||||
WHERE numbercall != 0
|
||||
AND active
|
||||
AND nextcall <= (now() at time zone 'UTC')
|
||||
AND id=%s
|
||||
FOR UPDATE NOWAIT""",
|
||||
(job['id'],), log_exceptions=False)
|
||||
|
||||
locked_job = lock_cr.fetchone()
|
||||
if not locked_job:
|
||||
_logger.debug("Job `%s` already executed by another process/thread. skipping it", job['cron_name'])
|
||||
continue
|
||||
# Got the lock on the job row, run its code
|
||||
_logger.debug('Starting job `%s`.', job['cron_name'])
|
||||
job_cr = db.cursor()
|
||||
try:
|
||||
registry = odoo.registry(db_name)
|
||||
registry[cls._name]._process_job(job_cr, job, lock_cr)
|
||||
except Exception:
|
||||
_logger.exception('Unexpected exception while processing cron job %r', job)
|
||||
finally:
|
||||
job_cr.close()
|
||||
|
||||
except psycopg2.OperationalError as e:
|
||||
if e.pgcode == '55P03':
|
||||
# Class 55: Object not in prerequisite state; 55P03: lock_not_available
|
||||
_logger.debug('Another process/thread is already busy executing job `%s`, skipping it.', job['cron_name'])
|
||||
continue
|
||||
else:
|
||||
# Unexpected OperationalError
|
||||
raise
|
||||
finally:
|
||||
# we're exiting due to an exception while acquiring the lock
|
||||
lock_cr.close()
|
||||
|
||||
finally:
|
||||
if hasattr(threading.current_thread(), 'dbname'):
|
||||
del threading.current_thread().dbname
|
||||
|
||||
@classmethod
|
||||
def _acquire_job(cls, db_name):
|
||||
""" Try to process all cron jobs.
|
||||
|
||||
This selects in database all the jobs that should be processed. It then
|
||||
tries to lock each of them and, if it succeeds, run the cron job (if it
|
||||
doesn't succeed, it means the job was already locked to be taken care
|
||||
of by another thread) and return.
|
||||
|
||||
This method hides most exceptions related to the database's version, the
|
||||
modules' state, and such.
|
||||
"""
|
||||
try:
|
||||
cls._process_jobs(db_name)
|
||||
except BadVersion:
|
||||
_logger.warning('Skipping database %s as its base version is not %s.', db_name, BASE_VERSION)
|
||||
except BadModuleState:
|
||||
_logger.warning('Skipping database %s because of modules to install/upgrade/remove.', db_name)
|
||||
except psycopg2.ProgrammingError as e:
|
||||
if e.pgcode == '42P01':
|
||||
# Class 42 — Syntax Error or Access Rule Violation; 42P01: undefined_table
|
||||
@@ -179,51 +255,6 @@ class ir_cron(models.Model):
|
||||
except Exception:
|
||||
_logger.warning('Exception in cron:', exc_info=True)
|
||||
|
||||
for job in jobs:
|
||||
lock_cr = db.cursor()
|
||||
try:
|
||||
# Try to grab an exclusive lock on the job row from within the task transaction
|
||||
# Restrict to the same conditions as for the search since the job may have already
|
||||
# been run by an other thread when cron is running in multi thread
|
||||
lock_cr.execute("""SELECT *
|
||||
FROM ir_cron
|
||||
WHERE numbercall != 0
|
||||
AND active
|
||||
AND nextcall <= (now() at time zone 'UTC')
|
||||
AND id=%s
|
||||
FOR UPDATE NOWAIT""",
|
||||
(job['id'],), log_exceptions=False)
|
||||
|
||||
locked_job = lock_cr.fetchone()
|
||||
if not locked_job:
|
||||
_logger.debug("Job `%s` already executed by another process/thread. skipping it", job['cron_name'])
|
||||
continue
|
||||
# Got the lock on the job row, run its code
|
||||
_logger.debug('Starting job `%s`.', job['cron_name'])
|
||||
job_cr = db.cursor()
|
||||
try:
|
||||
registry = odoo.registry(db_name)
|
||||
registry[cls._name]._process_job(job_cr, job, lock_cr)
|
||||
except Exception:
|
||||
_logger.exception('Unexpected exception while processing cron job %r', job)
|
||||
finally:
|
||||
job_cr.close()
|
||||
|
||||
except psycopg2.OperationalError as e:
|
||||
if e.pgcode == '55P03':
|
||||
# Class 55: Object not in prerequisite state; 55P03: lock_not_available
|
||||
_logger.debug('Another process/thread is already busy executing job `%s`, skipping it.', job['cron_name'])
|
||||
continue
|
||||
else:
|
||||
# Unexpected OperationalError
|
||||
raise
|
||||
finally:
|
||||
# we're exiting due to an exception while acquiring the lock
|
||||
lock_cr.close()
|
||||
|
||||
if hasattr(threading.current_thread(), 'dbname'): # cron job could have removed it as side-effect
|
||||
del threading.current_thread().dbname
|
||||
|
||||
@api.multi
|
||||
def _try_lock(self):
|
||||
"""Try to grab a dummy exclusive write-lock to the rows with the given ids,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<template id="report_irmodeloverview">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<div class="page">
|
||||
<div class="article">
|
||||
<table class="table table-bordered mb64">
|
||||
<tr>
|
||||
<td colspan="12">
|
||||
|
||||
@@ -220,14 +220,11 @@ class ThreadedServer(CommonServer):
|
||||
registries = odoo.modules.registry.Registry.registries
|
||||
_logger.debug('cron%d polling for jobs', number)
|
||||
for db_name, registry in registries.items():
|
||||
while registry.ready:
|
||||
if registry.ready:
|
||||
try:
|
||||
acquired = ir_cron._acquire_job(db_name)
|
||||
if not acquired:
|
||||
break
|
||||
ir_cron._acquire_job(db_name)
|
||||
except Exception:
|
||||
_logger.warning('cron%d encountered an Exception:', number, exc_info=True)
|
||||
break
|
||||
|
||||
def cron_spawn(self):
|
||||
""" Start the above runner function in a daemon thread.
|
||||
|
||||
Reference in New Issue
Block a user