[MERGE] forward port branch saas-15 up to 90ca872071

This commit is contained in:
Christophe Simonis
2017-11-09 18:05:49 +01:00
14 changed files with 213 additions and 102 deletions
+3
View File
@@ -325,6 +325,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
+4
View File
@@ -514,6 +514,8 @@ class HrExpenseSheet(models.Model):
@api.multi
def refuse_expenses(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:
body = (_("Your Expense %s has been refused.<br/><ul class=o_timeline_tracking_value_list><li>Reason<span> : </span><span class=o_timeline_tracking_value>%s</span></li></ul>") % (sheet.name, reason))
@@ -521,6 +523,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
+4 -6
View File
@@ -36,18 +36,16 @@ 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)
return channel.sudo(request_uid).channel_fetch_message(last_id, limit)
channel = request.env["mail.channel"].sudo().search([('uuid', '=', uuid)], limit=1)
return channel.sudo().channel_fetch_message(last_id, limit)
+50 -5
View File
@@ -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):
@@ -128,20 +128,65 @@ 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])
return activity
# 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])
return activity_user
@api.multi
def write(self, values):
res = super(MailActivity, self).write(values)
self._check_access('write')
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])
return res
@api.multi
def unlink(self):
self._check_access('unlink')
return super(MailActivity, self.sudo()).unlink()
@api.multi
def action_done(self):
""" Wrapper without feedback because web button add context as
+22 -13
View File
@@ -98,6 +98,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
@@ -281,7 +282,7 @@ 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']
message_ids = message_tree.keys()
for key, message in message_tree.iteritems():
if message.author_id:
partners |= message.author_id
@@ -293,8 +294,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)
@@ -309,13 +308,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:
@@ -342,9 +346,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,
@@ -745,7 +749,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, 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'):
+1 -1
View File
@@ -63,7 +63,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]))
+6 -6
View File
@@ -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
@@ -35,7 +35,7 @@ access_mail_shortcode,mail.shortcode,model_mail_shortcode,base.group_user,1,1,1,
access_mail_shortcode_portal,mail.shortcode.portal,model_mail_shortcode,base.group_portal,1,0,0,0
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_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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
23 access_mail_alias_system mail.alias.system model_mail_alias base.group_system 1 1 1 1
24 access_mail_message_subtype_all mail.message.subtype.all model_mail_message_subtype 1 0 0 0
25 access_mail_message_subtype_user mail.message.subtype.user model_mail_message_subtype 1 1 1 1
26 access_mail_tracking_value_all mail.tracking.value.all model_mail_tracking_value 1 0 0 0 0
27 access_mail_tracking_value_portal mail.tracking.value.portal model_mail_tracking_value base.group_portal 1 0 1 0 1 0 1 0
28 access_mail_tracking_value_user mail.tracking.value.user model_mail_tracking_value base.group_user 1 0 1 0 1 0 1 0
29 access_mail_tracking_value_system mail.tracking.value.system model_mail_tracking_value base.group_system 1 1 1 1
30 access_mail_thread_all mail.thread.all model_mail_thread 1 1 1 1
31 access_publisher_warranty_contract_all publisher.warranty.contract.all model_publisher_warranty_contract 1 1 1 1
35 access_mail_shortcode_portal mail.shortcode.portal model_mail_shortcode base.group_portal 1 0 0 0
36 access_mail_test_user mail.test.all model_mail_test base.group_user 1 1 1 1
37 access_mail_test_portal mail.test.all model_mail_test base.group_portal 1 1 0 0
38 access_mail_activity_all mail.activity.all model_mail_activity 1 0 0 0 0
39 access_mail_activity_user mail.activity.user model_mail_activity base.group_user 1 1 1 1
40 access_mail_activity_type_all mail.activity.type.all model_mail_activity_type 1 0 0 0 0
41 access_mail_activity_type_user mail.activity.type.user model_mail_activity_type base.group_user 1 1 1 1
+11
View File
@@ -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>
+5 -3
View File
@@ -293,9 +293,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',
+11
View File
@@ -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
+92 -61
View File
@@ -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),
@@ -141,33 +148,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
@@ -178,51 +254,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,
+1 -1
View File
@@ -3,7 +3,7 @@
<template id="report_irmodeloverview">
<t t-call="report.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">
+2 -5
View File
@@ -216,14 +216,11 @@ class ThreadedServer(CommonServer):
registries = odoo.modules.registry.Registry.registries
_logger.debug('cron%d polling for jobs', number)
for db_name, registry in registries.iteritems():
while registry.ready:
if registry.ready:
try:
acquired = odoo.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name)
if not acquired:
break
odoo.addons.base.ir.ir_cron.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.