[MIG] base: migrate base/ir to the new API

This commit is contained in:
Deep Bundela
2016-03-21 13:46:44 +01:00
committed by Raphael Collet
parent 348fa4f958
commit eb04aa1e39
44 changed files with 2347 additions and 2628 deletions
+7 -9
View File
@@ -1,14 +1,12 @@
# -*- coding: utf-8 -*-
from openerp import SUPERUSER_ID
from openerp.osv import osv
from odoo import models
class ir_configparameter(osv.Model):
class IrConfigParameter(models.Model):
_inherit = 'ir.config_parameter'
def init(self, cr, force=False):
super(ir_configparameter, self).init(cr, force=force)
def init(self, force=False):
super(IrConfigParameter, self).init(force=force)
if force:
IMD = self.pool['ir.model.data']
oauth_oe = IMD.xmlid_to_object(cr, SUPERUSER_ID, 'auth_oauth.provider_openerp')
dbuuid = self.get_param(cr, SUPERUSER_ID, 'database.uuid')
oauth_oe.write({'client_id': dbuuid})
oauth_oe = self.env.ref('auth_oauth.provider_openerp')
oauth_oe.write({'client_id': self.sudo().get_param('database.uuid')})
+4 -4
View File
@@ -25,7 +25,7 @@ class ir_actions_report_xml(orm.Model):
" is printed on a separate HTML but memory and disk usage are wider.")
}
def _lookup_report(self, cr, name):
def _lookup_report(self, name):
"""
Look up a report definition.
"""
@@ -41,8 +41,8 @@ class ir_actions_report_xml(orm.Model):
if not isinstance(new_report, WebKitParser):
new_report = None
else:
cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s and report_type=%s", (name, 'webkit'))
r = cr.dictfetchone()
self.env.cr.execute("SELECT * FROM ir_act_report_xml WHERE report_name=%s and report_type=%s", (name, 'webkit'))
r = self.env.cr.dictfetchone()
if r:
if r['parser']:
parser = operator.attrgetter(r['parser'])(openerp.addons)
@@ -59,4 +59,4 @@ class ir_actions_report_xml(orm.Model):
if new_report:
return new_report
else:
return super(ir_actions_report_xml, self)._lookup_report(cr, name)
return super(ir_actions_report_xml, self)._lookup_report(name)
+29 -34
View File
@@ -2,64 +2,59 @@
import urlparse
from openerp.http import request
from openerp.osv import fields, osv
from odoo.http import request
from odoo import api, fields, models
class actions_server(osv.Model):
class actions_server(models.Model):
""" Add website option in server actions. """
_name = 'ir.actions.server'
_inherit = ['ir.actions.server']
_inherit = 'ir.actions.server'
def _compute_website_url(self, cr, uid, id, website_path, xml_id, context=None):
base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url', context=context)
link = website_path or xml_id or (id and '%d' % id) or ''
xml_id = fields.Char(compute=models.Model.get_xml_id, string="External ID",
help="ID of the action if defined in a XML file")
website_path = fields.Char()
website_url = fields.Char(compute='_get_website_url', help='The full URL to access the server action through the website.')
website_published = fields.Boolean(string='Available on the Website', copy=False,
help='A code server action can be executed from the website, using a dedicated '
'controller. The address is <base>/website/action/<website_path>. '
'Set this field as True to allow users to run this action. If it '
'is set to False the action cannot be run through the website.')
def _compute_website_url(self, website_path, xml_id):
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
link = website_path or xml_id or (self.id and '%d' % self.id) or ''
if base_url and link:
path = '%s/%s' % ('/website/action', link)
return '%s' % urlparse.urljoin(base_url, path)
return ''
def _get_website_url(self, cr, uid, ids, name, args, context=None):
res = dict.fromkeys(ids, False)
for action in self.browse(cr, uid, ids, context=context):
@api.depends('website_url')
def _get_website_url(self):
for action in self:
if action.state == 'code' and action.website_published:
res[action.id] = self._compute_website_url(cr, uid, action.id, action.website_path, action.xml_id, context=context)
return res
action.website_url = self._compute_website_url(action.website_path, action.xml_id)
_columns = {
'xml_id': fields.function(
osv.osv.get_xml_id, type='char', string="External ID",
help="ID of the action if defined in a XML file"),
'website_path': fields.char('Website Path'),
'website_url': fields.function(
_get_website_url, type='char', string='Website URL',
help='The full URL to access the server action through the website.'),
'website_published': fields.boolean(
'Available on the Website', copy=False,
help='A code server action can be executed from the website, using a dedicated '
'controller. The address is <base>/website/action/<website_path>. '
'Set this field as True to allow users to run this action. If it '
'is set to False the action cannot be run through the website.'),
}
def on_change_website_path(self, cr, uid, ids, website_path, xml_id, context=None):
@api.onchange('website_path', 'xml_id')
def on_change_website_path(self):
values = {
'website_url': self._compute_website_url(cr, uid, ids and ids[0] or None, website_path, xml_id, context=context)
'website_url': self._compute_website_url(self.website_path, self.xml_id)
}
return {'value': values}
def _get_eval_context(self, cr, uid, action, context=None):
@api.model
def _get_eval_context(self, action):
""" Override to add the request object in eval_context. """
eval_context = super(actions_server, self)._get_eval_context(cr, uid, action, context=context)
eval_context = super(actions_server, self)._get_eval_context(action)
if action.state == 'code':
eval_context['request'] = request
return eval_context
def run_action_code_multi(self, cr, uid, action, eval_context=None, context=None):
def run_action_code_multi(self, action, eval_context=None):
""" Override to allow returning response the same way action is already
returned by the basic server action behavior. Note that response has
priority over action, avoid using both. """
res = super(actions_server, self).run_action_code_multi(cr, uid, action, eval_context, context)
res = super(actions_server, self).run_action_code_multi(action, eval_context)
if 'response' in eval_context:
return eval_context['response']
return res
-1
View File
@@ -15,7 +15,6 @@
<xpath expr="//field[@name='sequence']" position="after">
<field name="xml_id" invisible="1"/>
<field name="website_path"
on_change="on_change_website_path(website_path, xml_id, context)"
attrs="{'invisible': ['|', ('website_published', '!=', True), ('state', '!=', 'code')]}"/>
<field name="website_url" readonly="1" widget="url"
attrs="{'invisible': ['|', ('website_published', '!=', True), ('state', '!=', 'code')]}"/>
+1 -1
View File
@@ -62,7 +62,7 @@
<field name="name">Choose Your Theme Todo Action</field>
<field name="model_id" ref="model_ir_module_module"/>
<field name="code">
self.update_list(cr, uid, context=context)
self.update_list()
action = {
'type': 'ir.actions.act_url',
'url': '/web?reload#action=website_theme_install.theme_install_kanban_action', # the ?reload option is there to fool the webclient into thinking it is a different location and so to force a reload
File diff suppressed because it is too large Load Diff
+8 -21
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<!-- ir.actions -->
@@ -275,8 +274,7 @@
<group>
<group>
<field name="type" invisible="1"/>
<field name="model_id"
on_change="on_change_model_id(model_id, wkf_model_id, crud_model_id)"/>
<field name="model_id"/>
<field name="state"/>
</group>
<group>
@@ -297,15 +295,12 @@
</p>
<group attrs="{'invisible': [('model_id', '=', False)]}">
<field name="use_relational_model" widget="radio"
on_change="on_change_wkf_wonfig(use_relational_model, wkf_field_id, wkf_model_id, model_id)"
attrs="{'readonly': [('model_id', '=', False)]}"/>
<field name="wkf_field_id" context="{'key': ''}"
on_change="on_change_wkf_wonfig(use_relational_model, wkf_field_id, wkf_model_id, model_id)"
attrs="{'required': [('state', '=', 'trigger'), ('use_relational_model', '=', 'relational')],
'invisible': [('use_relational_model', '=', 'base')]}"
domain="[('model_id', '=', model_id), ('ttype', 'in', ['many2one'])]"/>
<field name="wkf_model_id" invisible="1"
on_change="on_change_wkf_model_id(wkf_model_id)"/>
<field name="wkf_model_id" invisible="1"/>
<field name="wkf_model_name" invisible="1"/>
<field name="wkf_transition_id" attrs="{'required': [('state', '=', 'trigger')]}"
domain="[('wkf_id.osv', '=', wkf_model_name)]"/>
@@ -327,11 +322,9 @@
</p>
<group attrs="{'invisible': [('model_id', '=', False)]}">
<field name="use_create" widget="radio"
on_change="on_change_crud_config(state, use_create, use_write, ref_object, crud_model_id, model_id)"
attrs="{'invisible': [('state', '!=', 'object_create')]}"/>
<field name="use_write" widget="radio"
on_change="on_change_crud_config(state, use_create, use_write, ref_object, crud_model_id, model_id)"
attrs="{'invisible': [('state', '!=', 'object_write')]}"/>
<label for="ref_object" string=" "
@@ -342,12 +335,10 @@
attrs="{'invisible': ['&amp;',
'|', ('state', '!=', 'object_write'), ('use_write', '!=', 'other'),
'|', ('state', '!=', 'object_create'), ('use_create', '!=', 'copy_other')]}">
<field name="ref_object" nolabel="1"
on_change="on_change_crud_config(state, use_create, use_write, ref_object, crud_model_id, model_id)"/>
<field name="ref_object" nolabel="1"/>
</div>
<field name="crud_model_id"
on_change="on_change_crud_model_id(crud_model_id)"
attrs="{'invisible': ['|', ('state', '!=', 'object_create'), ('use_create', '!=', 'new_other')]}"/>
<field name="crud_model_name" invisible="1"/>
@@ -376,7 +367,6 @@
<li>object.partner_id.currency_id</li>
</ul>
<field name="write_expression"
on_change="on_change_write_expression(write_expression, model_id)"
attrs="{'required': [('state', '=', 'object_write'), ('use_write', '=', 'expression')]}"/>
</div>
@@ -445,18 +435,16 @@
</p>
<field name="model_object_field"
attrs="{'invisible': [('model_id', '=', False)]}"
domain="[('model_id', '=', model_id), ('ttype', '!=', 'one2many'), ('ttype', '!=', 'many2many')]"
on_change="onchange_sub_model_object_value_field(model_object_field)"/>
domain="[('model_id', '=', model_id), ('ttype', '!=', 'one2many'), ('ttype', '!=', 'many2many')]"/>
<field name="sub_object" readonly="1" attrs="{'invisible': [('model_id', '=', False)]}"/>
<field name="sub_model_object_field"
domain="[('model_id', '=', sub_object), ('ttype', '!=', 'one2many'), ('ttype', '!=', 'many2many')]"
attrs="{'readonly':[('sub_object','=',False)],
'required':[('sub_object','!=',False)],
'invisible': [('model_id', '=', False)]}"
on_change="onchange_sub_model_object_value_field(model_object_field,sub_model_object_field)"/>
'invisible': [('model_id', '=', False)]}"/>
<field name="copyvalue" attrs="{'invisible': [('model_id', '=', False)]}"/>
<h3 colspan="2">Find the ID of a record in the database</h3>
<field name="id_object" on_change="onchange_id_object(id_object)"/>
<field name="id_object"/>
<field name="id_value" />
</group>
</group>
@@ -583,5 +571,4 @@ if config.get('type') not in ('ir.actions.act_window_close',):
</field>
</record>
</data>
</openerp>
</odoo>
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<!-- Attachment -->
<record id="view_attachment_form" model="ir.ui.view">
<field name="model">ir.attachment</field>
@@ -104,5 +103,4 @@
</record>
<menuitem action="action_attachment" id="menu_action_attachment" parent="base.next_id_9"/>
</data>
</openerp>
</odoo>
+12 -10
View File
@@ -3,31 +3,33 @@
import logging
from openerp import models
from odoo import api, models
_logger = logging.getLogger(__name__)
class AutoVacuum(models.TransientModel):
""" Expose the vacuum method to the cron jobs mechanism. """
_name = 'ir.autovacuum'
def _gc_transient_models(self, cr, uid, *args, **kwargs):
@api.model
def _gc_transient_models(self):
for model in self.pool.itervalues():
if model.is_transient():
model._transient_vacuum(cr, uid, force=True)
model._transient_vacuum(self._cr, self._uid, force=True)
def _gc_user_logs(self, cr, uid, *args, **kwargs):
cr.execute("""
def _gc_user_logs(self):
self.env.cr.execute("""
DELETE FROM res_users_log log1 WHERE EXISTS (
SELECT 1 FROM res_users_log log2
WHERE log1.create_uid = log2.create_uid
AND log1.create_date < log2.create_date
)
""")
_logger.info("GC'd %d user log entries", cr.rowcount)
_logger.info("GC'd %d user log entries", self.env.cr.rowcount)
def power_on(self, cr, uid, *args, **kwargs):
self._gc_transient_models(cr, uid, *args, **kwargs)
self._gc_user_logs(cr, uid, *args, **kwargs)
@api.model
def power_on(self):
self._gc_transient_models()
self._gc_user_logs()
return True
+2 -5
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<record model="ir.cron" id="autovacuum_job">
<field name='name'>Auto-vacuum internal data</field>
<field name='interval_number'>1</field>
@@ -11,6 +10,4 @@
<field name="function">power_on</field>
<field name="args">()</field>
</record>
</data>
</openerp>
</odoo>
+31 -32
View File
@@ -7,9 +7,8 @@ Store database-specific configuration parameters
import uuid
import datetime
from openerp import SUPERUSER_ID
from openerp.osv import osv, fields
from openerp.tools import misc, config, ormcache
from odoo import api, fields, models
from odoo.tools import misc, config, ormcache
"""
A dictionary holding some configuration parameters to be initialized when the database is created.
@@ -22,35 +21,34 @@ _default_parameters = {
}
class ir_config_parameter(osv.osv):
class IrConfigParameter(models.Model):
"""Per-database storage of configuration key-value pairs."""
_name = 'ir.config_parameter'
_rec_name = 'key'
_columns = {
'key': fields.char('Key', required=True, select=1),
'value': fields.text('Value', required=True),
'group_ids': fields.many2many('res.groups', 'ir_config_parameter_groups_rel', 'icp_id', 'group_id', string='Groups'),
}
key = fields.Char(required=True, index=True)
value = fields.Text(required=True)
group_ids = fields.Many2many('res.groups', 'ir_config_parameter_groups_rel', 'icp_id', 'group_id', string='Groups')
_sql_constraints = [
('key_uniq', 'unique (key)', 'Key must be unique.')
]
def init(self, cr, force=False):
def init(self, force=False):
"""
Initializes the parameters listed in _default_parameters.
It overrides existing parameters if force is ``True``.
"""
for key, func in _default_parameters.iteritems():
# force=True skips search and always performs the 'if' body (because ids=False)
ids = not force and self.search(cr, SUPERUSER_ID, [('key','=',key)])
if not ids:
self = not force and self.sudo().search([('key', '=', key)])
if not self.ids:
value, groups = func()
self.set_param(cr, SUPERUSER_ID, key, value, groups=groups)
self.sudo().set_param(key, value, groups=groups)
def get_param(self, cr, uid, key, default=False, context=None):
@api.model
def get_param(self, key, default=False):
"""Retrieve the value for a given key.
:param string key: The key of the parameter value to retrieve.
@@ -58,10 +56,10 @@ class ir_config_parameter(osv.osv):
:return: The value of the parameter, or ``default`` if it does not exist.
:rtype: string
"""
result = self._get_param(cr, uid, key)
if result is None:
return default
return result
result = self._get_param(key)
if result:
return result
return default
@ormcache('uid', 'key')
def _get_param(self, cr, uid, key):
@@ -70,7 +68,8 @@ class ir_config_parameter(osv.osv):
return None
return params[0]['value']
def set_param(self, cr, uid, key, value, groups=(), context=None):
@api.model
def set_param(self, key, value, groups=()):
"""Sets the value of a parameter.
:param string key: The key of the parameter value to set.
@@ -81,35 +80,35 @@ class ir_config_parameter(osv.osv):
:rtype: string
"""
self._get_param.clear_cache(self)
ids = self.search(cr, uid, [('key','=',key)], context=context)
self = self.search([('key', '=', key)])
gids = []
for group_xml in groups:
res_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, group_xml)
res_id = self.env['ir.model.data'].xmlid_to_res_id(group_xml)
if res_id:
gids.append((4, res_id))
vals = {'value': value}
if gids:
vals.update(group_ids=gids)
if ids:
param = self.browse(cr, uid, ids[0], context=context)
old = param.value
if self.ids:
if value is not False and value is not None:
self.write(cr, uid, ids, vals, context=context)
self.write(vals)
else:
self.unlink(cr, uid, ids, context=context)
return old
self.unlink()
return self.value
else:
vals.update(key=key)
if value is not False and value is not None:
self.create(cr, uid, vals, context=context)
self.create(vals)
return False
def write(self, cr, uid, ids, vals, context=None):
@api.multi
def write(self, vals):
self._get_param.clear_cache(self)
return super(ir_config_parameter, self).write(cr, uid, ids, vals, context=context)
return super(IrConfigParameter, self).write(vals)
def unlink(self, cr, uid, ids, context=None):
@api.multi
def unlink(self):
self._get_param.clear_cache(self)
return super(ir_config_parameter, self).unlink(cr, uid, ids, context=context)
return super(IrConfigParameter, self).unlink()
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record model="ir.ui.view" id="view_ir_config_search">
<field name="model">ir.config_parameter</field>
<field name="arch" type="xml">
@@ -36,5 +35,4 @@
</record>
<act_window name="System Parameters" res_model="ir.config_parameter" id="ir_config_list_action"/>
<menuitem id="ir_config_menu" name="System Parameters" parent="menu_ir_property" action="ir_config_list_action"/>
</data>
</openerp>
+71 -81
View File
@@ -4,22 +4,20 @@ import logging
import threading
import time
import psycopg2
import pytz
from datetime import datetime
from dateutil.relativedelta import relativedelta
import pytz
import openerp
from openerp import SUPERUSER_ID, netsvc, api
from openerp.osv import fields, osv
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools.translate import _
from openerp.modules import load_information_from_description_file
from openerp.exceptions import UserError
import odoo
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
from odoo.tools.safe_eval import safe_eval as eval
_logger = logging.getLogger(__name__)
BASE_VERSION = load_information_from_description_file('base')['version']
BASE_VERSION = odoo.modules.load_information_from_description_file('base')['version']
def str2tuple(s):
return eval('tuple(%s)' % (s or ''))
@@ -33,58 +31,49 @@ _intervalTypes = {
'minutes': lambda interval: relativedelta(minutes=interval),
}
class ir_cron(osv.osv):
class ir_cron(models.Model):
""" Model describing cron jobs (also called actions or tasks).
"""
# TODO: perhaps in the future we could consider a flag on ir.cron jobs
# that would cause database wake-up even if the database has not been
# loaded yet or was already unloaded (e.g. 'force_db_wakeup' or something)
# See also openerp.cron
# See also odoo.cron
_name = "ir.cron"
_order = 'name'
_columns = {
'name': fields.char('Name', required=True),
'user_id': fields.many2one('res.users', 'User', required=True),
'active': fields.boolean('Active'),
'interval_number': fields.integer('Interval Number',help="Repeat every x."),
'interval_type': fields.selection( [('minutes', 'Minutes'),
('hours', 'Hours'), ('work_days','Work Days'), ('days', 'Days'),('weeks', 'Weeks'), ('months', 'Months')], 'Interval Unit'),
'numbercall': fields.integer('Number of Calls', help='How many times the method is called,\na negative number indicates no limit.'),
'doall' : fields.boolean('Repeat Missed', help="Specify if missed occurrences should be executed when the server restarts."),
'nextcall' : fields.datetime('Next Execution Date', required=True, help="Next planned execution date for this job."),
'model': fields.char('Object', help="Model name on which the method to be called is located, e.g. 'res.partner'."),
'function': fields.char('Method', help="Name of the method to be called when this job is processed."),
'args': fields.text('Arguments', help="Arguments to be passed to the method, e.g. (uid,)."),
'priority': fields.integer('Priority', help='The priority of the job, as an integer: 0 means higher priority, 10 means lower priority.')
}
_defaults = {
'nextcall' : lambda *a: time.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'priority' : 5,
'user_id' : lambda obj,cr,uid,context: uid,
'interval_number' : 1,
'interval_type' : 'months',
'numbercall' : 1,
'active' : 1,
}
name = fields.Char(required=True)
user_id = fields.Many2one('res.users', string='User', default=lambda self: self.env.user, required=True)
active = fields.Boolean(default=True)
interval_number = fields.Integer(default=1, help="Repeat every x.")
interval_type = fields.Selection([('minutes', 'Minutes'),
('hours', 'Hours'),
('work_days', 'Work Days'),
('days', 'Days'),
('weeks', 'Weeks'),
('months', 'Months')], string='Interval Unit', default='months')
numbercall = fields.Integer(string='Number of Calls', default=1, help='How many times the method is called,\na negative number indicates no limit.')
doall = fields.Boolean(string='Repeat Missed', help="Specify if missed occurrences should be executed when the server restarts.")
nextcall = fields.Datetime(string='Next Execution Date', required=True, default=lambda *a: time.strftime(DEFAULT_SERVER_DATETIME_FORMAT), help="Next planned execution date for this job.")
model = fields.Char(string='Object', help="Model name on which the method to be called is located, e.g. 'res.partner'.")
function = fields.Char(string='Method', help="Name of the method to be called when this job is processed.")
args = fields.Text(string='Arguments', help="Arguments to be passed to the method, e.g. (uid,).")
priority = fields.Integer(default=5, help='The priority of the job, as an integer: 0 means higher priority, 10 means lower priority.')
def _check_args(self, cr, uid, ids, context=None):
@api.constrains('args')
def _check_args(self):
try:
for this in self.browse(cr, uid, ids, context):
for this in self:
str2tuple(this.args)
except Exception:
return False
raise Exception('Invalid arguments')
return True
_constraints = [
(_check_args, 'Invalid arguments', ['args']),
]
def method_direct_trigger(self, cr, uid, ids, context=None):
if context is None:
context={}
context = {}
cron_obj = self.browse(cr, uid, ids, context=context)
for cron in cron_obj:
self._callback(cr, uid, cron_obj.model, cron_obj.function, cron_obj.args, cron_obj.id)
@@ -117,20 +106,20 @@ class ir_cron(osv.osv):
"""
try:
args = str2tuple(args)
openerp.modules.registry.RegistryManager.check_registry_signaling(cr.dbname)
registry = openerp.registry(cr.dbname)
odoo.modules.registry.RegistryManager.check_registry_signaling(cr.dbname)
registry = odoo.registry(cr.dbname)
if model_name in registry:
model = registry[model_name]
if hasattr(model, method_name):
log_depth = (None if _logger.isEnabledFor(logging.DEBUG) else 1)
netsvc.log(_logger, logging.DEBUG, 'cron.object.execute', (cr.dbname,uid,'*',model_name,method_name)+tuple(args), depth=log_depth)
odoo.netsvc.log(_logger, logging.DEBUG, 'cron.object.execute', (cr.dbname, uid, '*', model_name, method_name)+tuple(args), depth=log_depth)
if _logger.isEnabledFor(logging.DEBUG):
start_time = time.time()
getattr(model, method_name)(cr, uid, *args)
if _logger.isEnabledFor(logging.DEBUG):
end_time = time.time()
_logger.debug('%.3fs (%s, %s)' % (end_time - start_time, model_name, method_name))
openerp.modules.registry.RegistryManager.signal_caches_change(cr.dbname)
odoo.modules.registry.RegistryManager.signal_caches_change(cr.dbname)
else:
msg = "Method `%s.%s` does not exist." % (model_name, method_name)
_logger.warning(msg)
@@ -150,8 +139,8 @@ class ir_cron(osv.osv):
"""
try:
with api.Environment.manage():
now = fields.datetime.context_timestamp(job_cr, job['user_id'], datetime.now())
nextcall = fields.datetime.context_timestamp(job_cr, job['user_id'], datetime.strptime(job['nextcall'], DEFAULT_SERVER_DATETIME_FORMAT))
now = odoo.osv.fields.datetime.context_timestamp(job_cr, job['user_id'], datetime.now())
nextcall = odoo.osv.fields.datetime.context_timestamp(job_cr, job['user_id'], datetime.strptime(job['nextcall'], DEFAULT_SERVER_DATETIME_FORMAT))
numbercall = job['numbercall']
ok = False
@@ -167,8 +156,8 @@ class ir_cron(osv.osv):
if not numbercall:
addsql = ', active=False'
cron_cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",
(nextcall.astimezone(pytz.UTC).strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))
self.invalidate_cache(job_cr, SUPERUSER_ID)
(nextcall.astimezone(pytz.UTC).strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))
self.invalidate_cache(job_cr, odoo.SUPERUSER_ID)
finally:
job_cr.commit()
@@ -186,7 +175,7 @@ class ir_cron(osv.osv):
If a job was processed, returns True, otherwise returns False.
"""
db = openerp.sql_db.db_connect(db_name)
db = odoo.sql_db.db_connect(db_name)
threading.current_thread().dbname = db_name
cr = db.cursor()
jobs = []
@@ -237,7 +226,7 @@ class ir_cron(osv.osv):
_logger.debug('Starting job `%s`.', job['name'])
job_cr = db.cursor()
try:
registry = openerp.registry(db_name)
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)
@@ -256,48 +245,49 @@ class ir_cron(osv.osv):
# 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
if hasattr(threading.current_thread(), 'dbname'): # cron job could have removed it as side-effect
del threading.current_thread().dbname
def _try_lock(self, cr, uid, ids, context=None):
def _try_lock(self):
"""Try to grab a dummy exclusive write-lock to the rows with the given ids,
to make sure a following write() or unlink() will not block due
to a process currently executing those cron tasks"""
try:
cr.execute("""SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table,
(tuple(ids),), log_exceptions=False)
self.env.cr.execute("""SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table,
(tuple(self.ids),), log_exceptions=False)
except psycopg2.OperationalError:
cr.rollback() # early rollback to allow translations to work for the user feedback
self.env.cr.rollback() # early rollback to allow translations to work for the user feedback
raise UserError(_("Record cannot be modified right now: "
"This cron task is currently being executed and may not be modified "
"Please try again in a few minutes"))
"This cron task is currently being executed and may not be modified "
"Please try again in a few minutes"))
def create(self, cr, uid, vals, context=None):
res = super(ir_cron, self).create(cr, uid, vals, context=context)
return res
@api.model
def create(self, vals):
return super(ir_cron, self).create(vals)
def write(self, cr, uid, ids, vals, context=None):
self._try_lock(cr, uid, ids, context)
res = super(ir_cron, self).write(cr, uid, ids, vals, context=context)
return res
@api.multi
def write(self, vals):
self._try_lock()
return super(ir_cron, self).write(vals)
def unlink(self, cr, uid, ids, context=None):
self._try_lock(cr, uid, ids, context)
res = super(ir_cron, self).unlink(cr, uid, ids, context=context)
return res
@api.multi
def unlink(self):
self._try_lock()
return super(ir_cron, self).unlink()
def try_write(self, cr, uid, ids, values, context=None):
@api.multi
def try_write(self, values):
try:
with cr.savepoint():
cr.execute("""SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table,
(tuple(ids),), log_exceptions=False)
with self.env.cr.savepoint():
self.env.cr.execute("""SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table,
(tuple(self.ids),), log_exceptions=False)
except psycopg2.OperationalError:
pass
else:
return super(ir_cron, self).write(cr, uid, ids, values, context=context)
return super(ir_cron, self).write(values)
return False
def toggle(self, cr, uid, ids, model, domain, context=None):
active = bool(self.pool[model].search_count(cr, uid, domain, context=context))
return self.try_write(cr, uid, ids, {'active': active}, context=context)
@api.model
def toggle(self, model, domain):
active = bool(self.env[model].search_count(domain))
return self.try_write({'active': active})
+2 -4
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<!-- ir.cron -->
<record id="ir_cron_view" model="ir.ui.view">
<field name="model">ir.cron</field>
@@ -90,5 +89,4 @@
</record>
<menuitem id="menu_ir_cron_act" action="ir_cron_act" parent="base.menu_automation"/>
</data>
</openerp>
</odoo>
+10 -13
View File
@@ -1,24 +1,21 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import fields,osv
from odoo import fields, models
class ir_exports(osv.osv):
class IrExports(models.Model):
_name = "ir.exports"
_order = 'name'
_columns = {
'name': fields.char('Export Name'),
'resource': fields.char('Resource', select=True),
'export_fields': fields.one2many('ir.exports.line', 'export_id',
'Export ID', copy=True),
}
name = fields.Char(string='Export Name')
resource = fields.Char(select=True)
export_fields = fields.One2many('ir.exports.line', 'export_id', string='Export ID', copy=True)
class ir_exports_line(osv.osv):
class IrExportsLine(models.Model):
_name = 'ir.exports.line'
_order = 'id'
_columns = {
'name': fields.char('Field Name'),
'export_id': fields.many2one('ir.exports', 'Export', select=True, ondelete='cascade'),
}
name = fields.Char(string='Field Name')
export_id = fields.Many2one('ir.exports', string='Export', select=True, ondelete='cascade')
+7 -8
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import datetime
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import functools
import itertools
import time
@@ -7,8 +8,8 @@ import time
import psycopg2
import pytz
from openerp import models, fields, api, _
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, ustr
from odoo import api, fields, models, _
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, ustr
REFERENCING_FIELDS = {None, 'id', '.id'}
def only_ref_fields(record):
@@ -33,7 +34,7 @@ class ImportWarning(Warning):
class ConversionNotFound(ValueError): pass
class ir_fields_converter(models.AbstractModel):
class IrFieldsConverter(models.AbstractModel):
_name = 'ir.fields.converter'
@api.model
@@ -389,8 +390,7 @@ class ir_fields_converter(models.AbstractModel):
subfield, w1 = self._referencing_subfield(record)
reference = record[subfield]
id, _, w2 = self.db_id_for(model, field, subfield, reference)
id, _, w2 = self.db_id_for(model, field, subfield, record[subfield])
return id, w1 + w2
@api.model
@@ -435,8 +435,7 @@ class ir_fields_converter(models.AbstractModel):
if refs:
subfield, w1 = self._referencing_subfield(refs)
warnings.extend(w1)
reference = record[subfield]
id, _, w2 = self.db_id_for(model, field, subfield, reference)
id, _, w2 = self.db_id_for(model, field, subfield, record[subfield])
warnings.extend(w2)
writable = convert(exclude_ref_fields(record), log)
+68 -83
View File
@@ -1,33 +1,51 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import exceptions
from openerp.osv import osv, fields
from openerp.tools.translate import _
from odoo import exceptions
from odoo import api, fields, models, _
class ir_filters(osv.osv):
class IrFilters(models.Model):
_name = 'ir.filters'
_description = 'Filters'
_order = 'model_id, name, id desc'
def _list_all_models(self, cr, uid, context=None):
cr.execute("SELECT model, name FROM ir_model ORDER BY name")
return cr.fetchall()
def _list_all_models(self):
self.env.cr.execute("SELECT model, name FROM ir_model ORDER BY name")
return self.env.cr.fetchall()
def copy(self, cr, uid, id, default=None, context=None):
name = self.read(cr, uid, [id], ['name'])[0]['name']
default.update({'name':_('%s (copy)') % name})
return super(ir_filters, self).copy(cr, uid, id, default, context)
name = fields.Char(string='Filter Name', translate=True, required=True)
user_id = fields.Many2one('res.users', string='User', ondelete='cascade', default=lambda self: self._uid,
help="The user this filter is private to. When left empty the filter is public "
"and available to all users.")
domain = fields.Text(default='[]', required=True)
context = fields.Text(default='{}', required=True)
sort = fields.Text(default='[]', required=True)
model_id = fields.Selection(selection='_list_all_models', string='Model', required=True)
is_default = fields.Boolean(string='Default filter')
action_id = fields.Many2one('ir.actions.actions', string='Action', ondelete='cascade',
help="The menu action this filter applies to. "
"When left empty the filter applies to all menus "
"for this model.")
active = fields.Boolean(default=True)
def _get_action_domain(self, cr, uid, action_id=None):
@api.multi
def copy(self, default=None):
self.ensure_one()
default.update({'name': _('%s (copy)') % self.name})
return super(IrFilters, self).copy(default)
def _get_action_domain(self, action_id=None):
"""Return a domain component for matching filters that are visible in the
same context (menu/view) as the given action."""
if action_id:
# filters specific to this menu + global ones
return [('action_id', 'in' , [action_id, False])]
return [('action_id', 'in', [action_id, False])]
# only global ones
return [('action_id', '=', False)]
def get_filters(self, cr, uid, model, action_id=None, context=None):
@api.model
def get_filters(self, model, action_id=None):
"""Obtain the list of filters available for the user on the given model.
:param action_id: optional ID of action to restrict filters to this action
@@ -40,15 +58,12 @@ class ir_filters(osv.osv):
"""
# available filters: private filters (user_id=uid) and public filters (uid=NULL),
# and filters for the action (action_id=action_id) or global (action_id=NULL)
context = self.pool['res.users'].context_get(cr, uid)
action_domain = self._get_action_domain(cr, uid, action_id)
filter_ids = self.search(cr, uid, action_domain +
[('model_id','=',model),('user_id','in',[uid, False])])
my_filters = self.read(cr, uid, filter_ids,
['name', 'is_default', 'domain', 'context', 'user_id', 'sort'], context=context)
action_domain = self._get_action_domain(action_id)
filter_ids = self.search(action_domain + [('model_id', '=', model), ('user_id', 'in', [self._uid, False])])
my_filters = filter_ids.with_context(self.env.user.context_get()).read(['name', 'is_default', 'domain', 'context', 'user_id', 'sort'])
return my_filters
def _check_global_default(self, cr, uid, vals, matching_filters, context=None):
def _check_global_default(self, vals, matching_filters):
""" _check_global_default(cursor, UID, dict, list(dict), dict) -> None
Checks if there is a global default for the model_id requested.
@@ -64,96 +79,66 @@ class ir_filters(osv.osv):
:raises openerp.exceptions.Warning: if there is an existing default and
we're not updating it
"""
action_domain = self._get_action_domain(cr, uid, vals.get('action_id'))
existing_default = self.search(cr, uid, action_domain + [
action_domain = self._get_action_domain(vals.get('action_id'))
existing_default = self.search(action_domain + [
('model_id', '=', vals['model_id']),
('user_id', '=', False),
('is_default', '=', True)], context=context)
('is_default', '=', True)])
if not existing_default: return
if matching_filters and \
(matching_filters[0]['id'] == existing_default[0]):
if not existing_default:
return
if matching_filters and (matching_filters[0]['id'] == existing_default.id):
return
raise exceptions.Warning(
_("There is already a shared filter set as default for %(model)s, delete or change it before setting a new default") % {
'model': vals['model_id']
})
raise exceptions.Warning(_("There is already a shared filter set as default for %(model)s, delete or change it before setting a new default") % {'model': vals.get('model_id')})
def create_or_replace(self, cr, uid, vals, context=None):
lower_name = vals['name'].lower()
@api.model
def create_or_replace(self, vals):
action_id = vals.get('action_id')
current_filters = self.get_filters(cr, uid, vals['model_id'], action_id)
current_filters = self.get_filters(vals['model_id'], action_id)
matching_filters = [f for f in current_filters
if f['name'].lower() == lower_name
# next line looks for matching user_ids (specific or global), i.e.
# f.user_id is False and vals.user_id is False or missing,
# or f.user_id.id == vals.user_id
if (f['user_id'] and f['user_id'][0]) == vals.get('user_id', False)]
if f['name'].lower() == vals['name'].lower()
# next line looks for matching user_ids (specific or global), i.e.
# f.user_id is False and vals.user_id is False or missing,
# or f.user_id.id == vals.user_id
if (f['user_id'] and f['user_id'][0]) == vals.get('user_id')]
if vals.get('is_default'):
if vals.get('user_id'):
# Setting new default: any other default that belongs to the user
# should be turned off
action_domain = self._get_action_domain(cr, uid, action_id)
act_ids = self.search(cr, uid, action_domain + [
('model_id', '=', vals['model_id']),
('user_id', '=', vals['user_id']),
('is_default', '=', True),
], context=context)
action_domain = self._get_action_domain(action_id)
act_ids = self.search(action_domain + [
('model_id', '=', vals['model_id']),
('user_id', '=', vals['user_id']),
('is_default', '=', True),
])
if act_ids:
self.write(cr, uid, act_ids, {'is_default': False}, context=context)
act_ids.write({'is_default': False})
else:
self._check_global_default(
cr, uid, vals, matching_filters, context=None)
self._check_global_default(vals, matching_filters)
# When a filter exists for the same (name, model, user) triple, we simply
# replace its definition (considering action_id irrelevant here)
if matching_filters:
self.write(cr, uid, matching_filters[0]['id'], vals, context)
self.browse(matching_filters[0]['id']).write(vals)
return matching_filters[0]['id']
return self.create(cr, uid, vals, context)
return self.create(vals)
_sql_constraints = [
# Partial constraint, complemented by unique index (see below)
# Still useful to keep because it provides a proper error message when a violation
# occurs, as it shares the same prefix as the unique index.
# occurs, as it shares the same prefix as the unique index.
('name_model_uid_unique', 'unique (name, model_id, user_id, action_id)', 'Filter names must be unique'),
]
def _auto_init(self, cr, context=None):
result = super(ir_filters, self)._auto_init(cr, context)
def _auto_init(self):
result = super(IrFilters, self)._auto_init()
# Use unique index to implement unique constraint on the lowercase name (not possible using a constraint)
cr.execute("DROP INDEX IF EXISTS ir_filters_name_model_uid_unique_index") # drop old index w/o action
cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_filters_name_model_uid_unique_action_index'")
if not cr.fetchone():
cr.execute("""CREATE UNIQUE INDEX "ir_filters_name_model_uid_unique_action_index" ON ir_filters
self.env.cr.execute("DROP INDEX IF EXISTS ir_filters_name_model_uid_unique_index") # drop old index w/o action
self.env.cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_filters_name_model_uid_unique_action_index'")
if not self.env.cr.fetchone():
self.env.cr.execute("""CREATE UNIQUE INDEX "ir_filters_name_model_uid_unique_action_index" ON ir_filters
(lower(name), model_id, COALESCE(user_id,-1), COALESCE(action_id,-1))""")
return result
_columns = {
'name': fields.char('Filter Name', translate=True, required=True),
'user_id': fields.many2one('res.users', 'User', ondelete='cascade',
help="The user this filter is private to. When left empty the filter is public "
"and available to all users."),
'domain': fields.text('Domain', required=True),
'context': fields.text('Context', required=True),
'sort': fields.text('Sort', required=True),
'model_id': fields.selection(_list_all_models, 'Model', required=True),
'is_default': fields.boolean('Default filter'),
'action_id': fields.many2one('ir.actions.actions', 'Action', ondelete='cascade',
help="The menu action this filter applies to. "
"When left empty the filter applies to all menus "
"for this model."),
'active': fields.boolean('Active')
}
_defaults = {
'domain': '[]',
'context':'{}',
'sort': '[]',
'user_id': lambda self,cr,uid,context=None: uid,
'is_default': False,
'active': True
}
_order = 'model_id, name, id desc'
+2 -2
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data noupdate="1">
<!-- Restrict modifications on ir.filters to owner only -->
<record id="ir_filters_rule" model="ir.rule">
@@ -73,4 +73,4 @@
<menuitem parent="base.next_id_2" name="User-defined Filters"
id="menu_ir_filters" action="actions_ir_filters_view" sequence="5"/>
</data>
</openerp>
</odoo>
+24 -23
View File
@@ -18,18 +18,17 @@ import werkzeug.routing
import werkzeug.urls
import werkzeug.utils
import openerp
import openerp.exceptions
import openerp.models
from openerp import http
from openerp.http import request, STATIC_CACHE
from openerp.modules.module import get_resource_path, get_module_path
from openerp.osv import osv, orm
import odoo
from odoo import models, tools
from odoo import http
from odoo.http import request, STATIC_CACHE
from odoo.modules.module import get_resource_path, get_module_path
_logger = logging.getLogger(__name__)
UID_PLACEHOLDER = object()
class ModelConverter(werkzeug.routing.BaseConverter):
def __init__(self, url_map, model=False):
@@ -45,6 +44,7 @@ class ModelConverter(werkzeug.routing.BaseConverter):
def to_url(self, value):
return value.id
class ModelsConverter(werkzeug.routing.BaseConverter):
def __init__(self, url_map, model=False):
@@ -59,12 +59,13 @@ class ModelsConverter(werkzeug.routing.BaseConverter):
def to_url(self, value):
return ",".join(i.id for i in value)
class SignedIntConverter(werkzeug.routing.NumberConverter):
regex = r'-?\d+'
num_convert = int
class ir_http(osv.AbstractModel):
class IrHttp(models.AbstractModel):
_name = 'ir.http'
_description = "HTTP routing"
@@ -84,7 +85,7 @@ class ir_http(osv.AbstractModel):
def _auth_method_public(self):
if not request.session.uid:
dummy, request.uid = self.pool['ir.model.data'].get_object_reference(request.cr, openerp.SUPERUSER_ID, 'base', 'public_user')
dummy, request.uid = self.env['ir.model.data'].sudo().get_object_reference(request.cr, 'base', 'public_user')
else:
request.uid = request.session.uid
@@ -96,22 +97,22 @@ class ir_http(osv.AbstractModel):
# what if error in security.check()
# -> res_users.check()
# -> res_users.check_credentials()
except (openerp.exceptions.AccessDenied, openerp.http.SessionExpiredException):
except (odoo.exceptions.AccessDenied, http.SessionExpiredException):
# All other exceptions mean undetermined status (e.g. connection pool full),
# let them bubble up
request.session.logout(keep_db=True)
if request.uid is None:
getattr(self, "_auth_method_%s" % auth_method)()
except (openerp.exceptions.AccessDenied, openerp.http.SessionExpiredException, werkzeug.exceptions.HTTPException):
except (odoo.exceptions.AccessDenied, http.SessionExpiredException, werkzeug.exceptions.HTTPException):
raise
except Exception:
_logger.info("Exception during request Authentication.", exc_info=True)
raise openerp.exceptions.AccessDenied()
raise odoo.exceptions.AccessDenied()
return auth_method
def _serve_attachment(self):
domain = [('type', '=', 'binary'), ('url', '=', request.httprequest.path)]
attach = self.pool['ir.attachment'].search_read(request.cr, openerp.SUPERUSER_ID, domain, ['__last_update', 'datas', 'name', 'mimetype', 'checksum'], context=request.context)
attach = self.pool['ir.attachment'].search_read(request.cr, odoo.SUPERUSER_ID, domain, ['__last_update', 'datas', 'name', 'mimetype', 'checksum'], context=request.context)
if attach:
wdate = attach[0]['__last_update']
datas = attach[0]['datas'] or ''
@@ -123,7 +124,7 @@ class ir_http(osv.AbstractModel):
return werkzeug.utils.redirect(name, 301)
response = werkzeug.wrappers.Response()
server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
server_format = tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
try:
response.last_modified = datetime.datetime.strptime(wdate, server_format + '.%f')
except ValueError:
@@ -151,11 +152,11 @@ class ir_http(osv.AbstractModel):
return attach
# Don't handle exception but use werkeug debugger if server in --dev mode
if openerp.tools.config['dev_mode']:
if tools.config['dev_mode']:
raise
try:
return request._handle_exception(exception)
except openerp.exceptions.AccessDenied:
except odoo.exceptions.AccessDenied:
return werkzeug.exceptions.Forbidden()
def _dispatch(self):
@@ -190,26 +191,26 @@ class ir_http(osv.AbstractModel):
def _postprocess_args(self, arguments, rule):
""" post process arg to set uid on browse records """
for name, arg in arguments.items():
if isinstance(arg, orm.browse_record) and arg._uid is UID_PLACEHOLDER:
if isinstance(arg, odoo.osv.orm.browse_record) and arg._uid is UID_PLACEHOLDER:
arguments[name] = arg.sudo(request.uid)
try:
arg.exists()
except openerp.models.MissingError:
except models.MissingError:
return self._handle_exception(werkzeug.exceptions.NotFound())
def routing_map(self):
if not hasattr(self, '_routing_map'):
_logger.info("Generating routing map")
installed = request.registry._init_modules - {'web'}
if openerp.tools.config['test_enable']:
installed.add(openerp.modules.module.current_test)
mods = [''] + openerp.conf.server_wide_modules + sorted(installed)
if tools.config['test_enable']:
installed.add(odoo.modules.module.current_test)
mods = [''] + odoo.conf.server_wide_modules + sorted(installed)
self._routing_map = http.routing_map(mods, False, converters=self._get_converters())
return self._routing_map
def content_disposition(self, filename):
filename = openerp.tools.ustr(filename)
filename = tools.ustr(filename)
escaped = urllib2.quote(filename.encode('utf8'))
browser = request.httprequest.user_agent.browser
version = int((request.httprequest.user_agent.version or '0').split('.')[0])
@@ -255,7 +256,7 @@ class ir_http(osv.AbstractModel):
# check read access
try:
last_update = obj['__last_update']
except openerp.exceptions.AccessError:
except odoo.exceptions.AccessError:
return (403, [], None)
status, headers, content = None, [], None
+12 -21
View File
@@ -1,27 +1,18 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import fields, models
from openerp.osv import osv, fields
from openerp.tools.translate import _
class ir_logging(osv.Model):
class IrLogging(models.Model):
_name = 'ir.logging'
_order = 'id DESC'
EXCEPTIONS_TYPE = [
('client', 'Client'),
('server', 'Server')
]
_columns = {
'create_date': fields.datetime('Create Date', readonly=True),
'create_uid': fields.integer('Uid', readonly=True), # Integer not m2o is intentionnal
'name': fields.char('Name', required=True),
'type': fields.selection(EXCEPTIONS_TYPE, string='Type', required=True, select=True),
'dbname': fields.char('Database Name', select=True),
'level': fields.char('Level', select=True),
'message': fields.text('Message', required=True),
'path': fields.char('Path', required=True),
'func': fields.char('Function', required=True),
'line': fields.char('Line', required=True),
}
create_date = fields.Datetime(readonly=True)
create_uid = fields.Integer(string='Uid', readonly=True) # Integer not m2o is intentionnal
name = fields.Char(required=True)
type = fields.Selection([('client', 'Client'), ('server', 'Server')], required=True, select=True)
dbname = fields.Char(string='Database Name', select=True)
level = fields.Char(select=True)
message = fields.Text(required=True)
path = fields.Char(required=True)
func = fields.Char(string='Function', required=True)
line = fields.Char(required=True)
+3 -5
View File
@@ -1,6 +1,5 @@
<?xml version="1.0"?>
<openerp>
<data>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.ui.view" id="ir_logging_form_view">
<field name="model">ir.logging</field>
<field name="arch" type="xml">
@@ -62,5 +61,4 @@
<field name="search_view_id" ref="ir_logging_search_view" />
</record>
<menuitem parent="base.next_id_9" id="ir_logging_all_menu" action="ir_logging_all_act" />
</data>
</openerp>
</odoo>
+72 -79
View File
@@ -1,29 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email import Encoders
from email.charset import Charset
from email.header import Header
from email.utils import formatdate, make_msgid, COMMASPACE, getaddresses, formataddr
from email import Encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import COMMASPACE, formataddr, formatdate, getaddresses, make_msgid
import logging
import re
import smtplib
import threading
from openerp import SUPERUSER_ID
from openerp.osv import osv, fields
from openerp.tools.translate import _
from openerp.tools import html2text
import openerp.tools as tools
from openerp.exceptions import UserError
from openerp.exceptions import except_orm
from odoo import api, fields, models, tools, _
from odoo.exceptions import except_orm
from odoo.exceptions import UserError
from odoo.tools import html2text
# ustr was originally from tools.misc.
# it is moved to loglevels until we refactor tools.
from openerp.loglevels import ustr
from odoo.loglevels import ustr
_logger = logging.getLogger(__name__)
_test_logger = logging.getLogger('openerp.tests')
@@ -34,6 +31,7 @@ class MailDeliveryException(except_orm):
def __init__(self, name, value):
super(MailDeliveryException, self).__init__(name, value)
class WriteToLogger(object):
"""debugging helper: behave as a fd and pipe to logger at the given level"""
def __init__(self, logger, level=logging.DEBUG):
@@ -58,6 +56,7 @@ def try_coerce_ascii(string_utf8):
return
return string_utf8
def encode_header(header_text):
"""Returns an appropriate representation of the given header value,
suitable for direct assignment as a header value in an
@@ -72,7 +71,8 @@ def encode_header(header_text):
that will perform the appropriate RFC2047 encoding of
non-ASCII values.
"""
if not header_text: return ""
if not header_text:
return ""
# convert anything to utf-8, suitable for testing ASCIIness, as 7-bit chars are
# encoded as ASCII in utf-8
header_text_utf8 = tools.ustr(header_text).encode('utf-8')
@@ -82,7 +82,8 @@ def encode_header(header_text):
# that will take care of RFC2047-encoding it as
# 7-bit string.
return header_text_ascii if header_text_ascii\
else Header(header_text_utf8, 'utf-8')
else Header(header_text_utf8, 'utf-8')
def encode_header_param(param_text):
"""Returns an appropriate RFC2047 encoded representation of the given
@@ -98,25 +99,29 @@ def encode_header_param(param_text):
ASCII string containing the RFC2047 encoded text.
"""
# For details see the encode_header() method that uses the same logic
if not param_text: return ""
if not param_text:
return ""
param_text_utf8 = tools.ustr(param_text).encode('utf-8')
param_text_ascii = try_coerce_ascii(param_text_utf8)
return param_text_ascii if param_text_ascii\
else Charset('utf8').header_encode(param_text_utf8)
else Charset('utf8').header_encode(param_text_utf8)
# TODO master, remove me, no longer used internaly
name_with_email_pattern = re.compile(r'("[^<@>]+")\s*<([^ ,<@]+@[^> ,]+)>')
address_pattern = re.compile(r'([^ ,<@]+@[^> ,]+)')
def extract_rfc2822_addresses(text):
"""Returns a list of valid RFC2822 addresses
that can be found in ``source``, ignoring
that can be found in ``source``, ignoring
malformed ones and non-ASCII ones.
"""
if not text: return []
if not text:
return []
candidates = address_pattern.findall(tools.ustr(text).encode('utf-8'))
return filter(try_coerce_ascii, candidates)
def encode_rfc2822_address_header(header_text):
"""If ``header_text`` contains non-ASCII characters,
attempts to locate patterns of the form
@@ -134,41 +139,32 @@ def encode_rfc2822_address_header(header_text):
return COMMASPACE.join(map(encode_addr, addresses))
class ir_mail_server(osv.osv):
class IrMailServer(models.Model):
"""Represents an SMTP server, able to send outgoing emails, with SSL and TLS capabilities."""
_name = "ir.mail_server"
NO_VALID_RECIPIENT = ("At least one valid recipient address should be "
"specified for outgoing emails (To/Cc/Bcc)")
_columns = {
'name': fields.char('Description', required=True, select=True),
'smtp_host': fields.char('SMTP Server', required=True, help="Hostname or IP of SMTP server"),
'smtp_port': fields.integer('SMTP Port', size=5, required=True, help="SMTP Port. Usually 465 for SSL, and 25 or 587 for other cases."),
'smtp_user': fields.char('Username', size=64, help="Optional username for SMTP authentication"),
'smtp_pass': fields.char('Password', size=64, help="Optional password for SMTP authentication"),
'smtp_encryption': fields.selection([('none','None'),
('starttls','TLS (STARTTLS)'),
('ssl','SSL/TLS')],
string='Connection Security', required=True,
help="Choose the connection encryption scheme:\n"
"- None: SMTP sessions are done in cleartext.\n"
"- TLS (STARTTLS): TLS encryption is requested at start of SMTP session (Recommended)\n"
"- SSL/TLS: SMTP sessions are encrypted with SSL/TLS through a dedicated port (default: 465)"),
'smtp_debug': fields.boolean('Debugging', help="If enabled, the full output of SMTP sessions will "
"be written to the server log at DEBUG level"
"(this is very verbose and may include confidential info!)"),
'sequence': fields.integer('Priority', help="When no specific mail server is requested for a mail, the highest priority one "
"is used. Default priority is 10 (smaller number = higher priority)"),
'active': fields.boolean('Active')
}
_defaults = {
'smtp_port': 25,
'active': True,
'sequence': 10,
'smtp_encryption': 'none',
}
name = fields.Char(string='Description', required=True, index=True)
smtp_host = fields.Char(string='SMTP Server', required=True, help="Hostname or IP of SMTP server")
smtp_port = fields.Integer(string='SMTP Port', size=5, required=True, default=25, help="SMTP Port. Usually 465 for SSL, and 25 or 587 for other cases.")
smtp_user = fields.Char(string='Username', size=64, help="Optional username for SMTP authentication")
smtp_pass = fields.Char(string='Password', size=64, help="Optional password for SMTP authentication")
smtp_encryption = fields.Selection([('none', 'None'),
('starttls', 'TLS (STARTTLS)'),
('ssl', 'SSL/TLS')],
string='Connection Security', required=True, default='none',
help="Choose the connection encryption scheme:\n"
"- None: SMTP sessions are done in cleartext.\n"
"- TLS (STARTTLS): TLS encryption is requested at start of SMTP session (Recommended)\n"
"- SSL/TLS: SMTP sessions are encrypted with SSL/TLS through a dedicated port (default: 465)")
smtp_debug = fields.Boolean(string='Debugging', help="If enabled, the full output of SMTP sessions will "
"be written to the server log at DEBUG level"
"(this is very verbose and may include confidential info!)")
sequence = fields.Integer(string='Priority', default=10, help="When no specific mail server is requested for a mail, the highest priority one "
"is used. Default priority is 10 (smaller number = higher priority)")
active = fields.Boolean(default=True)
def __init__(self, *args, **kwargs):
# Make sure we pipe the smtplib outputs to our own DEBUG logger
@@ -176,13 +172,14 @@ class ir_mail_server(osv.osv):
logpiper = WriteToLogger(_logger)
smtplib.stderr = logpiper
smtplib.stdout = logpiper
super(ir_mail_server, self).__init__(*args,**kwargs)
super(IrMailServer, self).__init__(*args, **kwargs)
def name_get(self, cr, uid, ids, context=None):
return [(a["id"], "(%s)" % (a['name'])) for a in self.read(cr, uid, ids, ['name'], context=context)]
@api.multi
def name_get(self):
return [(a.id, "(%s)" % (a.name)) for a in self]
def test_smtp_connection(self, cr, uid, ids, context=None):
for smtp_server in self.browse(cr, uid, ids, context=context):
def test_smtp_connection(self):
for smtp_server in self:
smtp = False
try:
smtp = self.connect(smtp_server.smtp_host, smtp_server.smtp_port, user=smtp_server.smtp_user,
@@ -192,7 +189,8 @@ class ir_mail_server(osv.osv):
raise UserError(_("Connection Test Failed! Here is what we got instead:\n %s") % tools.ustr(e))
finally:
try:
if smtp: smtp.quit()
if smtp:
smtp.quit()
except Exception:
# ignored, just a consequence of the previous exception
pass
@@ -214,7 +212,7 @@ class ir_mail_server(osv.osv):
if encryption == 'ssl':
if not 'SMTP_SSL' in smtplib.__all__:
raise UserError(_("Your OpenERP Server does not support SMTP-over-SSL. You could use STARTTLS instead."
"If SSL is needed, an upgrade to Python 2.6 on the server-side should do the trick."))
"If SSL is needed, an upgrade to Python 2.6 on the server-side should do the trick."))
connection = smtplib.SMTP_SSL(host, port)
else:
connection = smtplib.SMTP(host, port)
@@ -234,13 +232,13 @@ class ir_mail_server(osv.osv):
# certain hashing schemes, like HMAC.
# See also bug #597143 and python issue #5285
user = tools.ustr(user).encode('utf-8')
password = tools.ustr(password).encode('utf-8')
password = tools.ustr(password).encode('utf-8')
connection.login(user, password)
return connection
def build_email(self, email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
attachments=None, message_id=None, references=None, object_id=False, subtype='plain', headers=None,
body_alternative=None, subtype_alternative='plain'):
attachments=None, message_id=None, references=None, object_id=False, subtype='plain', headers=None,
body_alternative=None, subtype_alternative='plain'):
"""Constructs an RFC2822 email.message.Message object based on the keyword arguments passed, and returns it.
:param string email_from: sender email address
@@ -277,7 +275,7 @@ class ir_mail_server(osv.osv):
# Note: we must force all strings to to 8-bit utf-8 when crafting message,
# or use encode_header() for headers, which does it automatically.
headers = headers or {} # need valid dict later
headers = headers or {} # need valid dict later
if not email_cc: email_cc = []
if not email_bcc: email_bcc = []
@@ -345,7 +343,7 @@ class ir_mail_server(osv.osv):
msg.attach(part)
return msg
def _get_default_bounce_address(self, cr, uid, context=None):
def _get_default_bounce_address(self):
'''Compute the default bounce address.
The default bounce address is used to set the envelop address if no
@@ -358,17 +356,14 @@ class ir_mail_server(osv.osv):
If "mail.catchall.domain" is not set, return None.
'''
get_param = self.pool['ir.config_parameter'].get_param
postmaster = get_param(cr, SUPERUSER_ID, 'mail.bounce.alias',
default='postmaster-odoo',
context=context,)
domain = get_param(cr, SUPERUSER_ID, 'mail.catchall.domain', context=context)
get_param = self.env['ir.config_parameter'].sudo().get_param
postmaster = get_param('mail.bounce.alias', default='postmaster-odoo')
domain = get_param('mail.catchall.domain')
if postmaster and domain:
return '%s@%s' % (postmaster, domain)
def send_email(self, cr, uid, message, mail_server_id=None, smtp_server=None, smtp_port=None,
smtp_user=None, smtp_password=None, smtp_encryption=None, smtp_debug=False,
context=None):
def send_email(self, message, mail_server_id=None, smtp_server=None, smtp_port=None,
smtp_user=None, smtp_password=None, smtp_encryption=None, smtp_debug=False):
"""Sends an email directly (no queuing).
No retries are done, the caller should handle MailDeliveryException in order to ensure that
@@ -399,7 +394,7 @@ class ir_mail_server(osv.osv):
# Path (VERP) to detect no-longer valid email addresses.
smtp_from = message['Return-Path']
if not smtp_from:
smtp_from = self._get_default_bounce_address(cr, uid, context=context)
smtp_from = self._get_default_bounce_address()
if not smtp_from:
smtp_from = message['From']
assert smtp_from, "The Return-Path or From header is required for any outbound email"
@@ -413,15 +408,15 @@ class ir_mail_server(osv.osv):
email_to = message['To']
email_cc = message['Cc']
email_bcc = message['Bcc']
smtp_to_list = filter(None, tools.flatten(map(extract_rfc2822_addresses,[email_to, email_cc, email_bcc])))
smtp_to_list = filter(None, tools.flatten(map(extract_rfc2822_addresses, [email_to, email_cc, email_bcc])))
assert smtp_to_list, self.NO_VALID_RECIPIENT
x_forge_to = message['X-Forge-To']
if x_forge_to:
# `To:` header forged, e.g. for posting on mail.channels, to avoid confusion
del message['X-Forge-To']
del message['To'] # avoid multiple To: headers!
del message['To'] # avoid multiple To: headers!
message['To'] = x_forge_to
# Do not actually send emails in testing mode!
@@ -432,11 +427,9 @@ class ir_mail_server(osv.osv):
# Get SMTP Server Details from Mail Server
mail_server = None
if mail_server_id:
mail_server = self.browse(cr, SUPERUSER_ID, mail_server_id)
mail_server = self.sudo().browse(mail_server_id)
elif not smtp_server:
mail_server_ids = self.search(cr, SUPERUSER_ID, [], order='sequence', limit=1)
if mail_server_ids:
mail_server = self.browse(cr, SUPERUSER_ID, mail_server_ids[0])
mail_server = self.sudo().search([], order='sequence', limit=1)
if mail_server:
smtp_server = mail_server.smtp_host
@@ -452,10 +445,10 @@ class ir_mail_server(osv.osv):
smtp_user = smtp_user or tools.config.get('smtp_user')
smtp_password = smtp_password or tools.config.get('smtp_password')
if smtp_encryption is None and tools.config.get('smtp_ssl'):
smtp_encryption = 'starttls' # STARTTLS is the new meaning of the smtp_ssl flag as of v7.0
smtp_encryption = 'starttls' # STARTTLS is the new meaning of the smtp_ssl flag as of v7.0
if not smtp_server:
raise UserError(_("Missing SMTP Server")+ "\n" + _("Please define at least one SMTP server, or provide the SMTP parameters explicitly."))
raise UserError(_("Missing SMTP Server") + "\n" + _("Please define at least one SMTP server, or provide the SMTP parameters explicitly."))
try:
message_id = message['Message-Id']
@@ -464,7 +457,7 @@ class ir_mail_server(osv.osv):
if smtp_server.startswith('maildir:/'):
from mailbox import Maildir
maildir_path = smtp_server[8:]
mdir = Maildir(maildir_path, factory=None, create = True)
mdir = Maildir(maildir_path, factory=None, create=True)
mdir.add(message.as_string(True))
return message_id
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<!-- ir.mail.server -->
<record model="ir.ui.view" id="ir_mail_server_form">
<field name="model">ir.mail_server</field>
@@ -58,5 +57,4 @@
<field name="search_view_id" ref="view_ir_mail_server_search"/>
</record>
<menuitem id="menu_mail_servers" parent="menu_email" action="action_ir_mail_server_list" sequence="15" groups="base.group_no_one"/>
</data>
</openerp>
</odoo>
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -1,6 +1,5 @@
<?xml version="1.0"?>
<openerp>
<data>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<report
id="report_ir_model_overview"
@@ -10,6 +9,5 @@
name="base.report_irmodeloverview"
file="base.report_irmodeloverview"/>
</data>
</openerp>
</odoo>
+2 -4
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<!-- menu_create from model form -->
<record id="view_model_menu_create" model="ir.ui.view">
@@ -562,5 +561,4 @@
</record>
<menuitem action="ir_access_act" id="menu_ir_access_act" parent="base.menu_security"/>
</data>
</openerp>
</odoo>
+8 -6
View File
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import osv
from odoo import api, models
class ir_needaction_mixin(osv.AbstractModel):
class IrNeedactionMixin(models.AbstractModel):
"""Mixin class for objects using the need action feature.
Need action feature can be used by models that have to be able to
@@ -28,7 +28,8 @@ class ir_needaction_mixin(osv.AbstractModel):
# Addons API
#------------------------------------------------------
def _needaction_domain_get(self, cr, uid, context=None):
@api.model
def _needaction_domain_get(self):
""" Returns the domain to filter records that require an action
:return: domain or False is no action
"""
@@ -38,10 +39,11 @@ class ir_needaction_mixin(osv.AbstractModel):
# "Need action" API
#------------------------------------------------------
def _needaction_count(self, cr, uid, domain=None, context=None):
@api.model
def _needaction_count(self, domain=None):
""" Get the number of actions uid has to perform. """
dom = self._needaction_domain_get(cr, uid, context=context)
dom = self._needaction_domain_get()
if not dom:
return 0
res = self.search(cr, uid, (domain or []) + dom, limit=100, order='id DESC', context=context)
res = self.search((domain or []) + dom, limit=100, order='id DESC')
return len(res)
+176 -133
View File
@@ -4,8 +4,8 @@ import copy
import cStringIO
import datetime
import hashlib
import json
import itertools
import json
import logging
import math
import os
@@ -18,35 +18,35 @@ from urlparse import urlparse
import babel
import babel.dates
import psycopg2
import werkzeug
from lxml import etree, html
from PIL import Image
import psycopg2
import openerp.http
import openerp.tools
from openerp.tools.func import lazy_property
import openerp.tools.lru
from openerp.exceptions import QWebException
from openerp.fields import Datetime
from openerp.http import request
from openerp.tools.safe_eval import safe_eval as eval
from openerp.osv import osv, orm, fields
from openerp.tools import html_escape as escape
from openerp.tools.misc import find_in_path
from openerp.tools.translate import _
from openerp.modules.module import get_resource_path
import odoo.http
import odoo.tools
import odoo.tools.lru
from odoo import api, models, _
from odoo.exceptions import QWebException
from odoo.http import request
from odoo.modules.module import get_resource_path
from odoo.tools import html_escape as escape
from odoo.tools.func import lazy_property
from odoo.tools.misc import find_in_path
from odoo.tools.safe_eval import safe_eval as eval
_logger = logging.getLogger(__name__)
MAX_CSS_RULES = 4095
#--------------------------------------------------------------------
# QWeb template engine
#--------------------------------------------------------------------
class QWebTemplateNotFound(QWebException):
pass
def raise_qweb_exception(etype=None, **kw):
if etype is None:
etype = QWebException
@@ -60,12 +60,14 @@ def raise_qweb_exception(etype=None, **kw):
e.qweb['cause'] = original
raise
def _build_attribute(name, value):
value = escape(value)
if isinstance(name, unicode): name = name.encode('utf-8')
if isinstance(value, unicode): value = value.encode('utf-8')
return ' %s="%s"' % (name, value)
class FileSystemLoader(object):
def __init__(self, path):
# TODO: support multiple files #add_file() + add cache
@@ -86,6 +88,7 @@ class FileSystemLoader(object):
arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
return arch
class QWebContext(dict):
def __init__(self, cr, uid, data, loader=None, context=None):
self.cr = cr
@@ -114,7 +117,8 @@ class QWebContext(dict):
def __copy__(self):
return self.copy()
class QWeb(orm.AbstractModel):
class QWeb(models.AbstractModel):
""" Base QWeb rendering engine
* to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
@@ -225,6 +229,7 @@ class QWeb(orm.AbstractModel):
def eval_bool(self, expr, qwebcontext):
return int(bool(self.eval(expr, qwebcontext)))
@api.cr_uid_ids_context
def render(self, cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None):
""" render(cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None)
@@ -247,7 +252,7 @@ class QWeb(orm.AbstractModel):
qwebcontext['__caller__'] = stack[-1]
stack.append(id_or_xml_id)
qwebcontext['__stack__'] = stack
qwebcontext['xmlid'] = str(stack[0]) # Temporary fix
qwebcontext['xmlid'] = str(stack[0]) # Temporary fix
element = self.get_template(id_or_xml_id, qwebcontext)
element.attrib.pop("name", False)
@@ -259,7 +264,7 @@ class QWeb(orm.AbstractModel):
debugger = element.get('t-debug')
if debugger is not None:
if openerp.tools.config['dev_mode']:
if odoo.tools.config['dev_mode']:
__import__(debugger).set_trace() # pdb, ipdb, pudb, ...
else:
_logger.warning("@t-debug in template '%s' is only available in --dev mode" % qwebcontext['__template__'])
@@ -378,7 +383,7 @@ class QWeb(orm.AbstractModel):
return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
def _iterate(self, iterable):
if isinstance (iterable, collections.Mapping):
if isinstance(iterable, collections.Mapping):
return iterable.iteritems()
return itertools.izip(*itertools.tee(iterable))
@@ -540,11 +545,12 @@ class QWeb(orm.AbstractModel):
return True
return default
#--------------------------------------------------------------------
# QWeb Fields converters
#--------------------------------------------------------------------
class FieldConverter(osv.AbstractModel):
class FieldConverter(models.AbstractModel):
""" Used to convert a t-field specification into an output HTML field.
:meth:`~.to_html` is the entry point of this conversion from QWeb, it:
@@ -556,9 +562,9 @@ class FieldConverter(osv.AbstractModel):
"""
_name = 'ir.qweb.field'
def attributes(self, cr, uid, field_name, record, options,
source_element, g_att, t_att, qweb_context,
context=None):
@api.model
def attributes(self, field_name, record, options,
source_element, g_att, t_att, qweb_context):
""" attributes(cr, uid, field_name, record, options, source_element, g_att, t_att, qweb_context, context=None)
Generates the metadata attributes (prefixed by ``data-oe-`` for the
@@ -592,7 +598,8 @@ class FieldConverter(osv.AbstractModel):
data.append(('data-oe-readonly', 1))
return data
def value_to_html(self, cr, uid, value, field, options=None, context=None):
@api.model
def value_to_html(self, value, field, options=None):
""" value_to_html(cr, uid, value, field, options=None, context=None)
Converts a single value to its HTML version/output
@@ -600,17 +607,18 @@ class FieldConverter(osv.AbstractModel):
if not value: return ''
return value
def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
@api.model
def record_to_html(self, field_name, record, options=None):
""" record_to_html(cr, uid, field_name, record, options=None, context=None)
Converts the specified field of the browse_record ``record`` to HTML
"""
field = record._fields[field_name]
return self.value_to_html(
cr, uid, record[field_name], field, options=options, context=context)
return self.value_to_html(record[field_name], field, options=options)
def to_html(self, cr, uid, field_name, record, options,
source_element, t_att, g_att, qweb_context, context=None):
@api.model
def to_html(self, field_name, record, options,
source_element, t_att, g_att, qweb_context):
""" to_html(cr, uid, field_name, record, options, source_element, t_att, g_att, qweb_context, context=None)
Converts a ``t-field`` to its HTML output. A ``t-field`` may be
@@ -621,7 +629,7 @@ class FieldConverter(osv.AbstractModel):
field's own ``_type``.
"""
try:
content = self.record_to_html(cr, uid, field_name, record, options, context=context)
content = self.record_to_html(field_name, record, options)
if options.get('html-escape', True):
content = escape(content)
elif hasattr(content, '__html__'):
@@ -631,22 +639,21 @@ class FieldConverter(osv.AbstractModel):
field_name, record._name, exc_info=True)
content = None
inherit_branding = context and context.get('inherit_branding')
if not inherit_branding and context and context.get('inherit_branding_auto'):
inherit_branding = self.pool['ir.model.access'].check(cr, uid, record._name, 'write', False, context=context)
translate = context and context.get('edit_translations') and context.get('translatable') and getattr(record._fields[field_name], 'translate', False)
inherit_branding = self.env.context and self.env.context.get('inherit_branding')
if not inherit_branding and self.env.context and self.env.context.get('inherit_branding_auto'):
inherit_branding = self.env['ir.model.access'].check(record._name, 'write', False)
translate = self.env.context and self.env.context.get('edit_translations') and self.env.context.get('translatable') and getattr(record._fields[field_name], 'translate', False)
if inherit_branding or translate:
# add branding attributes
g_att += ''.join(
_build_attribute(name, value)
for name, value in self.attributes(
cr, uid, field_name, record, options,
source_element, g_att, t_att, qweb_context,
context=context)
field_name, record, options,
source_element, g_att, t_att, qweb_context)
)
return self.render_element(cr, uid, source_element, t_att, g_att,
return self.render_element(source_element, t_att, g_att,
qweb_context, content)
def qweb_object(self):
@@ -661,7 +668,8 @@ class FieldConverter(osv.AbstractModel):
return self.qweb_object().render_element(
source_element, t_att, g_att, qweb_context, content or '')
def user_lang(self, cr, uid, context):
@api.model
def user_lang(self):
""" user_lang(cr, uid, context)
Fetches the res.lang object corresponding to the language code stored
@@ -670,41 +678,41 @@ class FieldConverter(osv.AbstractModel):
:returns: res.lang browse_record
"""
if context is None: context = {}
lang_code = context.get('lang') or 'en_US'
Lang = self.pool['res.lang']
lang_code = self.env.context.get('lang') or 'en_US'
Lang = self.env['res.lang']
return Lang.browse(cr, uid, Lang._lang_get(cr, uid, lang_code), context=context)
return Lang.browse(Lang._lang_get(lang_code))
class IntegerConverter(osv.AbstractModel):
class IntegerConverter(models.AbstractModel):
_name = 'ir.qweb.field.integer'
_inherit = 'ir.qweb.field'
def value_to_html(self, cr, uid, value, field, options=None, context=None):
if context is None:
context = {}
@api.model
def value_to_html(self, value, field, options=None):
lang_code = context.get('lang') or 'en_US'
return self.pool['res.lang'].format(cr, uid, [lang_code], '%d', value, grouping=True)
lang_code = self.env.context.get('lang') or 'en_US'
return self.pool['res.lang'].format(self.env.cr, self.env.uid, [lang_code], '%d', value, grouping=True)
class FloatConverter(osv.AbstractModel):
class FloatConverter(models.AbstractModel):
_name = 'ir.qweb.field.float'
_inherit = 'ir.qweb.field'
def precision(self, cr, uid, field, options=None, context=None):
@api.model
def precision(self, field, options=None):
_, precision = field.digits or (None, None)
return precision
def value_to_html(self, cr, uid, value, field, options=None, context=None):
if context is None:
context = {}
precision = self.precision(cr, uid, field, options=options, context=context)
@api.model
def value_to_html(self, value, field, options=None):
precision = self.precision(field, options=options)
fmt = '%f' if precision is None else '%.{precision}f'
lang_code = context.get('lang') or 'en_US'
lang_code = self.env.context.get('lang') or 'en_US'
lang = self.pool['res.lang']
formatted = lang.format(cr, uid, [lang_code], fmt.format(precision=precision), value, grouping=True)
formatted = lang.format(self.env.cr, self.env.uid, [lang_code], fmt.format(precision=precision), value, grouping=True)
# %f does not strip trailing zeroes. %g does but its precision causes
# it to switch to scientific notation starting at a million *and* to
@@ -714,60 +722,66 @@ class FloatConverter(osv.AbstractModel):
formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted)
return formatted
class DateConverter(osv.AbstractModel):
class DateConverter(models.AbstractModel):
_name = 'ir.qweb.field.date'
_inherit = 'ir.qweb.field'
def value_to_html(self, cr, uid, value, field, options=None, context=None):
if not value or len(value)<10: return ''
lang = self.user_lang(cr, uid, context=context)
@api.model
def value_to_html(self, value, field, options=None):
if not value or len(value) < 10: return ''
lang = self.user_lang()
locale = babel.Locale.parse(lang.code)
if isinstance(value, basestring):
value = datetime.datetime.strptime(
value[:10], openerp.tools.DEFAULT_SERVER_DATE_FORMAT)
value[:10], odoo.tools.DEFAULT_SERVER_DATE_FORMAT)
if options and 'format' in options:
pattern = options['format']
else:
strftime_pattern = lang.date_format
pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
pattern = odoo.tools.posix_to_ldml(strftime_pattern, locale=locale)
return babel.dates.format_date(
value, format=pattern,
locale=locale)
class DateTimeConverter(osv.AbstractModel):
class DateTimeConverter(models.AbstractModel):
_name = 'ir.qweb.field.datetime'
_inherit = 'ir.qweb.field'
def value_to_html(self, cr, uid, value, field, options=None, context=None):
@api.model
def value_to_html(self, value, field, options=None):
if not value: return ''
lang = self.user_lang(cr, uid, context=context)
lang = self.user_lang()
locale = babel.Locale.parse(lang.code)
if isinstance(value, basestring):
value = datetime.datetime.strptime(
value, openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT)
value = fields.datetime.context_timestamp(
cr, uid, timestamp=value, context=context)
value, odoo.tools.DEFAULT_SERVER_DATETIME_FORMAT)
value = odoo.osv.fields.datetime.context_timestamp(self.env.cr, self.env.uid, timestamp=value, context=self.env.context)
if options and 'format' in options:
pattern = options['format']
else:
strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format))
pattern = openerp.tools.posix_to_ldml(strftime_pattern, locale=locale)
pattern = odoo.tools.posix_to_ldml(strftime_pattern, locale=locale)
if options and options.get('hide_seconds'):
pattern = pattern.replace(":ss", "").replace(":s", "")
return babel.dates.format_datetime(value, format=pattern, locale=locale)
class TextConverter(osv.AbstractModel):
class TextConverter(models.AbstractModel):
_name = 'ir.qweb.field.text'
_inherit = 'ir.qweb.field'
def value_to_html(self, cr, uid, value, field, options=None, context=None):
@api.model
def value_to_html(self, value, field, options=None):
"""
Escapes the value and converts newlines to br. This is bullshit.
"""
@@ -775,36 +789,42 @@ class TextConverter(osv.AbstractModel):
return nl2br(value, options=options)
class SelectionConverter(osv.AbstractModel):
class SelectionConverter(models.AbstractModel):
_name = 'ir.qweb.field.selection'
_inherit = 'ir.qweb.field'
def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
@api.model
def record_to_html(self, field_name, record, options=None):
value = record[field_name]
if not value: return ''
field = record._fields[field_name]
selection = dict(field.get_description(record.env)['selection'])
return self.value_to_html(
cr, uid, selection[value], field, options=options)
return self.value_to_html(selection[value], field, options=options)
class ManyToOneConverter(osv.AbstractModel):
class ManyToOneConverter(models.AbstractModel):
_name = 'ir.qweb.field.many2one'
_inherit = 'ir.qweb.field'
def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
@api.model
def record_to_html(self, field_name, record, options=None):
[read] = record.read([field_name])
if not read[field_name]: return ''
_, value = read[field_name]
return nl2br(value, options=options)
class HTMLConverter(osv.AbstractModel):
class HTMLConverter(models.AbstractModel):
_name = 'ir.qweb.field.html'
_inherit = 'ir.qweb.field'
def value_to_html(self, cr, uid, value, field, options=None, context=None):
@api.model
def value_to_html(self, value, field, options=None):
return HTMLSafe(value or '')
class ImageConverter(osv.AbstractModel):
class ImageConverter(models.AbstractModel):
""" ``image`` widget rendering, inserts a data:uri-using image tag in the
document. May be overridden by e.g. the website module to generate links
instead.
@@ -816,18 +836,20 @@ class ImageConverter(osv.AbstractModel):
_name = 'ir.qweb.field.image'
_inherit = 'ir.qweb.field'
def value_to_html(self, cr, uid, value, field, options=None, context=None):
@api.model
def value_to_html(self, value, field, options=None):
try:
image = Image.open(cStringIO.StringIO(value.decode('base64')))
image.verify()
except IOError:
raise ValueError("Non-image binary fields can not be converted to HTML")
except: # image.verify() throws "suitable exceptions", I have no idea what they are
except: # image.verify() throws "suitable exceptions", I have no idea what they are
raise ValueError("Invalid image content")
return HTMLSafe('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value))
class MonetaryConverter(osv.AbstractModel):
class MonetaryConverter(models.AbstractModel):
""" ``monetary`` converter, has a mandatory option
``display_currency`` only if field is not of type Monetary.
Otherwise, if we are in presence of a monetary field, the field definition must
@@ -844,16 +866,16 @@ class MonetaryConverter(osv.AbstractModel):
_name = 'ir.qweb.field.monetary'
_inherit = 'ir.qweb.field'
def to_html(self, cr, uid, field_name, record, options,
source_element, t_att, g_att, qweb_context, context=None):
@api.model
def to_html(self, field_name, record, options,
source_element, t_att, g_att, qweb_context):
options['_qweb_context'] = qweb_context
return super(MonetaryConverter, self).to_html(
cr, uid, field_name, record, options,
source_element, t_att, g_att, qweb_context, context=context)
field_name, record, options,
source_element, t_att, g_att, qweb_context)
def record_to_html(self, cr, uid, field_name, record, options, context=None):
if context is None:
context = {}
@api.model
def record_to_html(self, field_name, record, options):
Currency = self.pool['res.currency']
cur_field = record._fields[field_name]
display_currency = False
@@ -862,7 +884,7 @@ class MonetaryConverter(osv.AbstractModel):
display_currency = record[cur_field.currency_field]
#otherwise fall back to old method
if not display_currency:
display_currency = self.display_currency(cr, uid, options['display_currency'], options)
display_currency = self.display_currency(options['display_currency'], options)
# lang.format mandates a sprintf-style format. These formats are non-
# minimal (they have a default fixed precision instead), and
@@ -874,14 +896,13 @@ class MonetaryConverter(osv.AbstractModel):
from_amount = record[field_name]
if options.get('from_currency'):
from_currency = self.display_currency(cr, uid, options['from_currency'], options)
from_amount = Currency.compute(cr, uid, from_currency.id, display_currency.id, from_amount)
from_currency = self.display_currency(options['from_currency'], options)
from_amount = Currency.compute(self.env.cr, self.env.uid, from_currency.id, display_currency.id, from_amount)
lang_code = context.get('lang') or 'en_US'
lang_code = self.env.context.get('lang') or 'en_US'
lang = self.pool['res.lang']
formatted_amount = lang.format(cr, uid, [lang_code],
fmt, Currency.round(cr, uid, display_currency, from_amount),
grouping=True, monetary=True)
formatted_amount = lang.format(self.env.cr, self.env.uid, [lang_code],
fmt, Currency.round(self.env.cr, self.env.uid, display_currency, from_amount), grouping=True, monetary=True)
pre = post = u''
if display_currency.position == 'before':
@@ -896,9 +917,8 @@ class MonetaryConverter(osv.AbstractModel):
symbol=display_currency.symbol,
))
def display_currency(self, cr, uid, currency, options):
return self.qweb_object().eval_object(
currency, options['_qweb_context'])
def display_currency(self, currency, options):
return self.qweb_object().eval_object(currency, options['_qweb_context'])
TIMEDELTA_UNITS = (
('year', 3600 * 24 * 365),
@@ -909,7 +929,9 @@ TIMEDELTA_UNITS = (
('minute', 60),
('second', 1)
)
class DurationConverter(osv.AbstractModel):
class DurationConverter(models.AbstractModel):
""" ``duration`` converter, to display integral or fractional values as
human-readable time spans (e.g. 1.5 as "1 hour 30 minutes").
@@ -924,15 +946,15 @@ class DurationConverter(osv.AbstractModel):
_name = 'ir.qweb.field.duration'
_inherit = 'ir.qweb.field'
def value_to_html(self, cr, uid, value, field, options=None, context=None):
@api.model
def value_to_html(self, value, field, options=None):
units = dict(TIMEDELTA_UNITS)
if value < 0:
raise ValueError(_("Durations can't be negative"))
if not options or options.get('unit') not in units:
raise ValueError(_("A unit must be provided to duration widgets"))
locale = babel.Locale.parse(
self.user_lang(cr, uid, context=context).code)
locale = babel.Locale.parse(self.user_lang().code)
factor = units[options['unit']]
sections = []
@@ -947,14 +969,14 @@ class DurationConverter(osv.AbstractModel):
return ' '.join(sections)
class RelativeDatetimeConverter(osv.AbstractModel):
class RelativeDatetimeConverter(models.AbstractModel):
_name = 'ir.qweb.field.relative'
_inherit = 'ir.qweb.field'
def value_to_html(self, cr, uid, value, field, options=None, context=None):
parse_format = openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT
locale = babel.Locale.parse(
self.user_lang(cr, uid, context=context).code)
@api.model
def value_to_html(self, value, field, options=None):
parse_format = odoo.tools.DEFAULT_SERVER_DATETIME_FORMAT
locale = babel.Locale.parse(self.user_lang().code)
if isinstance(value, basestring):
value = datetime.datetime.strptime(value, parse_format)
@@ -962,17 +984,15 @@ class RelativeDatetimeConverter(osv.AbstractModel):
# value should be a naive datetime in UTC. So is fields.Datetime.now()
reference = datetime.datetime.strptime(field.now(), parse_format)
return babel.dates.format_timedelta(
value - reference, add_direction=True, locale=locale)
return babel.dates.format_timedelta(value - reference, add_direction=True, locale=locale)
class Contact(orm.AbstractModel):
class Contact(models.AbstractModel):
_name = 'ir.qweb.field.contact'
_inherit = 'ir.qweb.field.many2one'
def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
if context is None:
context = {}
@api.model
def record_to_html(self, field_name, record, options=None):
if options is None:
options = {}
opf = options.get('fields') or ["name", "address", "phone", "mobile", "fax", "email"]
@@ -998,15 +1018,17 @@ class Contact(orm.AbstractModel):
'options': options
}
html = self.pool["ir.ui.view"].render(cr, uid, "base.contact", val, engine='ir.qweb', context=context).decode('utf8')
html = self.env['ir.ui.view']._model.render(self.env.cr, self.env.uid, "base.contact", val, engine='ir.qweb', context=self.env.context).decode('utf8')
return HTMLSafe(html)
class QwebView(orm.AbstractModel):
class QwebView(models.AbstractModel):
_name = 'ir.qweb.field.qweb'
_inherit = 'ir.qweb.field.many2one'
def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
@api.model
def record_to_html(self, field_name, record, options=None):
if not getattr(record, field_name):
return None
@@ -1016,13 +1038,14 @@ class QwebView(orm.AbstractModel):
_logger.warning("%s.%s must be a 'ir.ui.view' model." % (record, field_name))
return None
ctx = (context or {}).copy()
ctx = (self.env.context or {}).copy()
ctx['object'] = record
html = view.render(ctx, engine='ir.qweb', context=ctx).decode('utf8')
html = view._model.render(self.env.cr, self.env.uid, ctx, engine='ir.qweb', context=self.env.ctx).decode('utf8')
return HTMLSafe(html)
class QwebWidget(osv.AbstractModel):
class QwebWidget(models.AbstractModel):
_name = 'ir.qweb.widget'
def _format(self, inner, options, qwebcontext):
@@ -1031,7 +1054,8 @@ class QwebWidget(osv.AbstractModel):
def format(self, inner, options, qwebcontext):
return escape(self._format(inner, options, qwebcontext))
class QwebWidgetMonetary(osv.AbstractModel):
class QwebWidgetMonetary(models.AbstractModel):
_name = 'ir.qweb.widget.monetary'
_inherit = 'ir.qweb.widget'
@@ -1054,6 +1078,7 @@ class QwebWidgetMonetary(osv.AbstractModel):
formatted_amount, pre=pre, post=post
).format(symbol=display.symbol,)
class HTMLSafe(object):
""" HTMLSafe string wrapper, Werkzeug's escape() has special handling for
objects with a ``__html__`` methods but AFAIK does not provide any such
@@ -1062,21 +1087,26 @@ class HTMLSafe(object):
Wrapping a string in HTML will prevent its escaping
"""
__slots__ = ['string']
def __init__(self, string):
self.string = string
def __html__(self):
return self.string
def __str__(self):
s = self.string
if isinstance(s, unicode):
return s.encode('utf-8')
return s
def __unicode__(self):
s = self.string
if isinstance(s, str):
return s.decode('utf-8')
return s
def nl2br(string, options=None):
""" Converts newlines to HTML linebreaks in ``string``. Automatically
escapes content unless options['html-escape'] is set to False, and returns
@@ -1092,15 +1122,20 @@ def nl2br(string, options=None):
string = escape(string)
return HTMLSafe(string.replace('\n', '<br>\n'))
def get_field_type(field, options):
""" Gets a t-field's effective type from the field definition and its options """
return options.get('widget', field.type)
class AssetError(Exception):
pass
class AssetNotFound(AssetError):
pass
class AssetsBundle(object):
rx_css_import = re.compile("(@import[^;{]+;?)", re.M)
rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""")
@@ -1242,14 +1277,14 @@ class AssetsBundle(object):
('url', '=like', '/web/content/%-%/{0}%.{1}'.format(self.xmlid, type)), # The wilcards are id, version and pagination number (if any)
'!', ('url', '=like', '/web/content/%-{}/%'.format(self.version))
]
attachment_ids = ira.search(self.cr, openerp.SUPERUSER_ID, domain, context=self.context)
return ira.unlink(self.cr, openerp.SUPERUSER_ID, attachment_ids, context=self.context)
attachment_ids = ira.search(self.cr, odoo.SUPERUSER_ID, domain, context=self.context)
return ira.unlink(self.cr, odoo.SUPERUSER_ID, attachment_ids, context=self.context)
def get_attachments(self, type, inc=None):
ira = self.registry['ir.attachment']
domain = [('url', '=like', '/web/content/%%-%s/%s%s.%s' % (self.version, self.xmlid, ('%%' if inc is None else '.%s' % inc), type))]
attachment_ids = ira.search(self.cr, openerp.SUPERUSER_ID, domain, order='name asc', context=self.context)
return ira.browse(self.cr, openerp.SUPERUSER_ID, attachment_ids, context=self.context)
attachment_ids = ira.search(self.cr, odoo.SUPERUSER_ID, domain, order='name asc', context=self.context)
return ira.browse(self.cr, odoo.SUPERUSER_ID, attachment_ids, context=self.context)
def save_attachment(self, type, content, inc=None):
ira = self.registry['ir.attachment']
@@ -1264,21 +1299,21 @@ class AssetsBundle(object):
'public': True,
'datas': content.encode('utf8').encode('base64'),
}
attachment_id = ira.create(self.cr, openerp.SUPERUSER_ID, values, context=self.context)
attachment_id = ira.create(self.cr, odoo.SUPERUSER_ID, values, context=self.context)
url = '/web/content/%s-%s/%s' % (attachment_id, self.version, fname)
values = {
'name': url,
'url': url,
}
ira.write(self.cr, openerp.SUPERUSER_ID, attachment_id, values, context=self.context)
ira.write(self.cr, odoo.SUPERUSER_ID, attachment_id, values, context=self.context)
if self.context.get('commit_assetsbundle') is True:
self.cr.commit()
self.clean_attachments(type)
return ira.browse(self.cr, openerp.SUPERUSER_ID, attachment_id, context=self.context)
return ira.browse(self.cr, odoo.SUPERUSER_ID, attachment_id, context=self.context)
def js(self):
attachments = self.get_attachments('js')
@@ -1412,6 +1447,7 @@ class AssetsBundle(object):
def compile_css(self, cmd, source):
"""Sanitizes @import rules, remove duplicates @import rules, then compile"""
imports = []
def sanitize(matchobj):
ref = matchobj.group(2)
line = '@import "%s"%s' % (ref, matchobj.group(3))
@@ -1452,6 +1488,7 @@ class AssetsBundle(object):
error += '\n - %s' % (asset.url if asset.url else '<inline sass>')
return error
class WebAsset(object):
html_url = '%s'
@@ -1483,7 +1520,7 @@ class WebAsset(object):
fields = ['__last_update', 'datas', 'mimetype']
domain = [('type', '=', 'binary'), ('url', '=', self.url)]
ira = self.registry['ir.attachment']
attach = ira.search_read(self.cr, openerp.SUPERUSER_ID, domain, fields, context=self.context)
attach = ira.search_read(self.cr, odoo.SUPERUSER_ID, domain, fields, context=self.context)
self._ir_attach = attach[0]
except Exception:
raise AssetNotFound("Could not find %s" % self.name)
@@ -1498,7 +1535,7 @@ class WebAsset(object):
if self._filename:
return datetime.datetime.fromtimestamp(os.path.getmtime(self._filename))
elif self._ir_attach:
server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
server_format = odoo.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
last_update = self._ir_attach['__last_update']
try:
return datetime.datetime.strptime(last_update, server_format + '.%f')
@@ -1538,6 +1575,7 @@ class WebAsset(object):
content = self.content
return '\n/* %s */\n%s' % (self.name, content)
class JavascriptAsset(WebAsset):
def minify(self):
return self.with_header(rjsmin(self.content))
@@ -1554,6 +1592,7 @@ class JavascriptAsset(WebAsset):
else:
return '<script type="text/javascript" charset="utf-8">%s</script>' % self.with_header()
class StylesheetAsset(WebAsset):
rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
@@ -1615,6 +1654,7 @@ class StylesheetAsset(WebAsset):
else:
return '<style type="text/css"%s>%s</style>' % (media, self.with_header())
class PreprocessedCSS(StylesheetAsset):
html_url = '%s.css'
rx_import = None
@@ -1629,6 +1669,7 @@ class PreprocessedCSS(StylesheetAsset):
def get_command(self):
raise NotImplementedError
class SassStylesheetAsset(PreprocessedCSS):
rx_indent = re.compile(r'^( +|\t+)', re.M)
indent = None
@@ -1661,6 +1702,7 @@ class SassStylesheetAsset(PreprocessedCSS):
return [sass, '--stdin', '-t', 'compressed', '--unix-newlines', '--compass',
'-r', 'bootstrap-sass']
class LessStylesheetAsset(PreprocessedCSS):
def get_command(self):
try:
@@ -1673,6 +1715,7 @@ class LessStylesheetAsset(PreprocessedCSS):
lesspath = get_resource_path('web', 'static', 'lib', 'bootstrap', 'less')
return [lessc, '-', '--clean-css', '--no-js', '--no-color', '--include-path=%s' % lesspath]
def rjsmin(script):
""" Minify js with a clever regex.
Taken from http://opensource.perlig.de/rjsmin
+2 -4
View File
@@ -1,5 +1,4 @@
<openerp>
<data>
<odoo>
<template id="contact">
<address t-ignore="true" class="mb0" itemscope="itemscope" itemtype="http://schema.org/Organization">
<div t-if="not (('name' in fields) or (address and 'address' in fields) or (city and 'city' in fields) or (mobile and 'mobile' in fields) or (fax and 'fax' in fields) or (website and 'website' in fields) or (email and 'email' in fields))" class="css_non_editable_mode_hidden">
@@ -36,5 +35,4 @@
</div>
</address>
</template>
</data>
</openerp>
</odoo>
+71 -75
View File
@@ -2,41 +2,20 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from openerp import SUPERUSER_ID
from openerp import tools
from openerp.osv import fields, osv, expression
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools.misc import unquote as unquote
import odoo
from odoo import api, fields, models, _
from odoo import SUPERUSER_ID
from odoo import tools
from odoo.osv import expression
from odoo.tools.safe_eval import safe_eval as eval
from odoo.tools.misc import unquote as unquote
class ir_rule(osv.osv):
class IrRule(models.Model):
_name = 'ir.rule'
_order = 'name'
_order = 'name, model_id DESC'
_MODES = ['read', 'write', 'create', 'unlink']
def _eval_context_for_combinations(self):
"""Returns a dictionary to use as evaluation context for
ir.rule domains, when the goal is to obtain python lists
that are easier to parse and combine, but not to
actually execute them."""
return {'user': unquote('user'),
'time': unquote('time')}
def _eval_context(self, cr, uid):
"""Returns a dictionary to use as evaluation context for
ir.rule domains."""
return {'user': self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid),
'time':time}
def _domain_force_get(self, cr, uid, ids, field_name, arg, context=None):
res = {}
eval_context = self._eval_context(cr, uid)
for rule in self.browse(cr, uid, ids, context):
if rule.domain_force:
res[rule.id] = expression.normalize_domain(eval(rule.domain_force, eval_context))
else:
res[rule.id] = []
return res
def _get_value(self, cr, uid, ids, field_name, arg, context=None):
res = {}
for rule in self.browse(cr, uid, ids, context):
@@ -46,44 +25,56 @@ class ir_rule(osv.osv):
res[rule.id] = False
return res
def _check_model_obj(self, cr, uid, ids, context=None):
return not any(self.pool[rule.model_id.model].is_transient() for rule in self.browse(cr, uid, ids, context))
_columns = {'global': odoo.osv.fields.function(_get_value, string='Global', type='boolean', store=True, help="If no group is specified the rule is global and applied to everyone")}
def _check_model_name(self, cr, uid, ids, context=None):
# Don't allow rules on rules records (this model).
return not any(rule.model_id.model == self._name for rule in self.browse(cr, uid, ids, context))
name = fields.Char(index=True)
active = fields.Boolean(default=True, help="If you uncheck the active field, it will disable the record rule without deleting it (if you delete a native record rule, it may be re-created when you reload the module).")
model_id = fields.Many2one('ir.model', string='Object', index=True, required=True, ondelete="cascade")
groups = fields.Many2many('res.groups', 'rule_group_rel', 'rule_group_id', 'group_id')
domain_force = fields.Text(string='Domain')
domain = fields.Binary(compute='_domain_force_get', string='Domain')
perm_read = fields.Boolean(string='Apply for Read', default=True)
perm_write = fields.Boolean(string='Apply for Write', default=True)
perm_create = fields.Boolean(string='Apply for Create', default=True)
perm_unlink = fields.Boolean(string='Apply for Delete', default=True)
_columns = {
'name': fields.char('Name', select=1),
'active': fields.boolean('Active', help="If you uncheck the active field, it will disable the record rule without deleting it (if you delete a native record rule, it may be re-created when you reload the module)."),
'model_id': fields.many2one('ir.model', 'Object',select=1, required=True, ondelete="cascade"),
'global': fields.function(_get_value, string='Global', type='boolean', store=True, help="If no group is specified the rule is global and applied to everyone"),
'groups': fields.many2many('res.groups', 'rule_group_rel', 'rule_group_id', 'group_id', 'Groups'),
'domain_force': fields.text('Domain'),
'domain': fields.function(_domain_force_get, string='Domain', type='binary'),
'perm_read': fields.boolean('Apply for Read'),
'perm_write': fields.boolean('Apply for Write'),
'perm_create': fields.boolean('Apply for Create'),
'perm_unlink': fields.boolean('Apply for Delete')
}
_order = 'model_id DESC'
_defaults = {
'active': True,
'perm_read': True,
'perm_write': True,
'perm_create': True,
'perm_unlink': True,
'global': True,
}
_sql_constraints = [
('no_access_rights', 'CHECK (perm_read!=False or perm_write!=False or perm_create!=False or perm_unlink!=False)', 'Rule must have at least one checked access right !'),
]
_constraints = [
(_check_model_obj, 'Rules can not be applied on Transient models.', ['model_id']),
(_check_model_name, 'Rules can not be applied on the Record Rules model.', ['model_id']),
]
def _eval_context_for_combinations(self):
"""Returns a dictionary to use as evaluation context for
ir.rule domains, when the goal is to obtain python lists
that are easier to parse and combine, but not to
actually execute them."""
return {'user': unquote('user'),
'time': unquote('time')}
@api.model
def _eval_context(self):
"""Returns a dictionary to use as evaluation context for
ir.rule domains."""
return {'user': self.env.user, 'time': time}
@api.depends('domain')
def _domain_force_get(self):
eval_context = self._eval_context()
for rule in self:
if rule.domain_force:
rule.domain = expression.normalize_domain(eval(rule.domain_force, eval_context))
else:
rule.domain = []
@api.constrains('model_id')
def _check_model_obj(self):
if any(self.pool[rule.model_id.model].is_transient() for rule in self):
raise UserWarning(_('Rules can not be applied on Transient models.'))
@api.constrains('model_id')
def _check_model_name(self):
# Don't allow rules on rules records (this model).
if any(rule.model_id.model == self._name for rule in self):
raise UserWarning(_('Rules can not be applied on the Record Rules model.'))
@tools.ormcache('uid', 'model_name', 'mode')
def _compute_domain(self, cr, uid, model_name, mode="read"):
@@ -125,32 +116,37 @@ class ir_rule(osv.osv):
return domain
return []
def clear_cache(self, cr, uid):
@api.model
def clear_cache(self):
""" Deprecated, use `clear_caches` instead. """
self.clear_caches()
def domain_get(self, cr, uid, model_name, mode='read', context=None):
dom = self._compute_domain(cr, uid, model_name, mode)
@api.model
def domain_get(self, model_name, mode='read'):
dom = self._compute_domain(model_name, mode)
if dom:
# _where_calc is called as superuser. This means that rules can
# involve objects on which the real uid has no acces rights.
# This means also there is no implicit restriction (e.g. an object
# references another object the user can't see).
query = self.pool[model_name]._where_calc(cr, SUPERUSER_ID, dom, active_test=False)
query = self.env[model_name].sudo()._where_calc(dom, active_test=False)
return query.where_clause, query.where_clause_params, query.tables
return [], [], ['"' + self.pool[model_name]._table + '"']
return [], [], ['"' + self.env[model_name]._table + '"']
def unlink(self, cr, uid, ids, context=None):
res = super(ir_rule, self).unlink(cr, uid, ids, context=context)
@api.multi
def unlink(self):
res = super(IrRule, self).unlink()
self.clear_caches()
return res
def create(self, cr, uid, vals, context=None):
res = super(ir_rule, self).create(cr, uid, vals, context=context)
@api.model
def create(self, vals):
res = super(IrRule, self).create(vals)
self.clear_caches()
return res
def write(self, cr, uid, ids, vals, context=None):
res = super(ir_rule, self).write(cr, uid, ids, vals, context=context)
@api.multi
def write(self, vals):
res = super(IrRule, self).write(vals)
self.clear_caches()
return res
+2 -4
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<!-- Rules -->
<record id="view_rule_form" model="ir.ui.view">
<field name="model">ir.rule</field>
@@ -95,5 +94,4 @@
</record>
</data>
</openerp>
</odoo>
+34 -33
View File
@@ -1,12 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
import logging
import pytz
import time
from datetime import datetime, timedelta
from openerp import _, api, fields, models
from openerp.exceptions import UserError
from odoo import api, fields, models, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
@@ -21,6 +20,7 @@ def _create_sequence(cr, seq_name, number_increment, number_next):
sql = "CREATE SEQUENCE %s INCREMENT BY %%s START WITH %%s" % seq_name
cr.execute(sql, (number_increment, number_next))
def _drop_sequence(cr, seq_names):
""" Drop the PostreSQL sequence if it exists.
@@ -34,6 +34,7 @@ def _drop_sequence(cr, seq_names):
# object depends on it.
cr.execute("DROP SEQUENCE IF EXISTS %s RESTRICT " % names)
def _alter_sequence(cr, seq_name, number_increment=None, number_next=None):
""" Alter a PostreSQL sequence.
@@ -52,10 +53,12 @@ def _alter_sequence(cr, seq_name, number_increment=None, number_next=None):
statement += " RESTART WITH %d" % (number_next, )
cr.execute(statement)
def _select_nextval(cr, seq_name):
cr.execute("SELECT nextval('%s')" % seq_name)
return cr.fetchone()
def _update_nogap(self, number_increment):
number_next = self.number_next
self.env.cr.execute("SELECT number_next FROM %s WHERE id=%s FOR UPDATE NOWAIT" % (self._table, self.id))
@@ -64,7 +67,7 @@ def _update_nogap(self, number_increment):
return number_next
class ir_sequence(models.Model):
class IrSequence(models.Model):
""" Sequence model.
The sequence model allows to define and use so-called sequence objects.
@@ -97,30 +100,29 @@ class ir_sequence(models.Model):
record.write({'number_next': record.number_next_actual or 0})
name = fields.Char(required=True)
code = fields.Char('Sequence Code')
implementation = fields.Selection(
[('standard', 'Standard'), ('no_gap', 'No gap')],
'Implementation', required=True, default='standard',
help="Two sequence object implementations are offered: Standard "
"and 'No gap'. The later is slower than the former but forbids any"
" gap in the sequence (while they are possible in the former).")
code = fields.Char(strring='Sequence Code')
implementation = fields.Selection([('standard', 'Standard'), ('no_gap', 'No gap')],
string='Implementation', required=True, default='standard',
help="Two sequence object implementations are offered: Standard "
"and 'No gap'. The later is slower than the former but forbids any"
"gap in the sequence (while they are possible in the former).")
active = fields.Boolean(default=True)
prefix = fields.Char(help="Prefix value of the record for the sequence")
suffix = fields.Char(help="Suffix value of the record for the sequence")
number_next = fields.Integer('Next Number', required=True, default=1, help="Next number of this sequence")
number_next = fields.Integer(string='Next Number', required=True, default=1, help="Next number of this sequence")
number_next_actual = fields.Integer(compute='_get_number_next_actual', inverse='_set_number_next_actual',
required=True, string='Next Number', default=1,
help="Next number that will be used. This number can be incremented "
"frequently so the displayed value might already be obsolete")
number_increment = fields.Integer('Step', required=True, default=1,
number_increment = fields.Integer(string='Step', required=True, default=1,
help="The next number of the sequence will be incremented by this number")
padding = fields.Integer('Sequence Size', required=True, default=0,
padding = fields.Integer(string='Sequence Size', required=True, default=0,
help="Odoo will automatically adds some '0' on the left of the "
"'Next Number' to get the required padding size.")
company_id = fields.Many2one('res.company', 'Company',
"'Next Number' to get the required padding size.")
company_id = fields.Many2one('res.company', string='Company',
default=lambda s: s.env['res.company']._company_default_get('ir.sequence'))
use_date_range = fields.Boolean('Use subsequences per date_range')
date_range_ids = fields.One2many('ir.sequence.date_range', 'sequence_id', 'Subsequences')
use_date_range = fields.Boolean(string='Use subsequences per date_range')
date_range_ids = fields.One2many('ir.sequence.date_range', 'sequence_id', string='Subsequences')
def init(self, cr):
return # Don't do the following index yet.
@@ -141,7 +143,7 @@ class ir_sequence(models.Model):
def create(self, values):
""" Create a sequence, in implementation == standard a fast gaps-allowed PostgreSQL sequence is used.
"""
seq = super(ir_sequence, self).create(values)
seq = super(IrSequence, self).create(values)
if values.get('implementation', 'standard') == 'standard':
_create_sequence(self.env.cr, "ir_sequence_%03d" % seq.id, values.get('number_increment', 1), values.get('number_next', 1))
return seq
@@ -149,7 +151,7 @@ class ir_sequence(models.Model):
@api.multi
def unlink(self):
_drop_sequence(self.env.cr, ["ir_sequence_%03d" % x.id for x in self])
return super(ir_sequence, self).unlink()
return super(IrSequence, self).unlink()
@api.multi
def write(self, values):
@@ -178,7 +180,7 @@ class ir_sequence(models.Model):
_create_sequence(self.env.cr, "ir_sequence_%03d" % seq.id, i, n)
for sub_seq in seq.date_range_ids:
_create_sequence(self.env.cr, "ir_sequence_%03d_%03d" % (seq.id, sub_seq.id), i, n)
return super(ir_sequence, self).write(values)
return super(IrSequence, self).write(values)
def _next_do(self):
if self.implementation == 'standard':
@@ -308,7 +310,7 @@ class ir_sequence(models.Model):
return self.get_id(code, 'code')
class ir_sequence_date_range(models.Model):
class IrSequenceDateRange(models.Model):
_name = 'ir.sequence.date_range'
_rec_name = "sequence_id"
@@ -321,8 +323,7 @@ class ir_sequence_date_range(models.Model):
else:
# get number from postgres sequence. Cannot use currval, because that might give an error when
# not having used nextval before.
query = "SELECT last_value, increment_by, is_called FROM ir_sequence_%03d_%03d" % (element.sequence_id.id, element.id)
self.env.cr.execute(query)
self.env.cr.execute("SELECT last_value, increment_by, is_called FROM ir_sequence_%03d_%03d" % (element.sequence_id.id, element.id))
(last_value, increment_by, is_called) = self.env.cr.fetchone()
if is_called:
element.number_next_actual = last_value + increment_by
@@ -333,14 +334,14 @@ class ir_sequence_date_range(models.Model):
for record in self:
record.write({'number_next': record.number_next_actual or 0})
date_from = fields.Date('From', required=True)
date_to = fields.Date('To', required=True)
sequence_id = fields.Many2one("ir.sequence", 'Main Sequence', required=True, ondelete='cascade')
number_next = fields.Integer('Next Number', required=True, default=1, help="Next number of this sequence")
date_from = fields.Date(string='From', required=True)
date_to = fields.Date(string='To', required=True)
sequence_id = fields.Many2one("ir.sequence", string='Main Sequence', required=True, ondelete='cascade')
number_next = fields.Integer(string='Next Number', required=True, default=1, help="Next number of this sequence")
number_next_actual = fields.Integer(compute='_get_number_next_actual', inverse='_set_number_next_actual',
required=True, string='Next Number', default=1,
help="Next number that will be used. This number can be incremented "
"frequently so the displayed value might already be obsolete")
"frequently so the displayed value might already be obsolete")
def _next(self):
if self.sequence_id.implementation == 'standard':
@@ -358,7 +359,7 @@ class ir_sequence_date_range(models.Model):
def create(self, values):
""" Create a sequence, in implementation == standard a fast gaps-allowed PostgreSQL sequence is used.
"""
seq = super(ir_sequence_date_range, self).create(values)
seq = super(IrSequenceDateRange, self).create(values)
main_seq = seq.sequence_id
if main_seq.implementation == 'standard':
_create_sequence(self.env.cr, "ir_sequence_%03d_%03d" % (main_seq.id, seq.id), main_seq.number_increment, values.get('number_next_actual', 1))
@@ -367,11 +368,11 @@ class ir_sequence_date_range(models.Model):
@api.multi
def unlink(self):
_drop_sequence(self.env.cr, ["ir_sequence_%03d_%03d" % (x.sequence_id.id, x.id) for x in self])
return super(ir_sequence_date_range, self).unlink()
return super(IrSequenceDateRange, self).unlink()
@api.multi
def write(self, values):
if values.get('number_next'):
seq_to_alter = self.filtered(lambda seq: seq.sequence_id.implementation == 'standard')
seq_to_alter._alter_sequence(number_next=values.get('number_next'))
return super(ir_sequence_date_range, self).write(values)
return super(IrSequenceDateRange, self).write(values)
+2 -4
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<!-- Sequences -->
<record id="sequence_view" model="ir.ui.view">
<field name="model">ir.sequence</field>
@@ -105,5 +104,4 @@
</record>
<menuitem action="ir_sequence_form" id="menu_ir_sequence_form" parent="next_id_5"/>
</data>
</openerp>
</odoo>
+121 -134
View File
@@ -1,15 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from collections import defaultdict
from difflib import get_close_matches
import logging
from openerp import api, tools, SUPERUSER_ID
import openerp.modules
from openerp.osv import fields, osv
from openerp.tools.translate import _
from openerp.exceptions import AccessError, UserError, ValidationError
import odoo
from odoo import api, fields, models, tools, _
from odoo.exceptions import AccessError, UserError, ValidationError
_logger = logging.getLogger(__name__)
@@ -25,7 +23,8 @@ TRANSLATION_TYPE = [
('sql_constraint', 'SQL Constraint')
]
class ir_translation_import_cursor(object):
class IrTranslationImportCursor(object):
"""Temporary cursor for optimizing mass insert into ir.translation
Open it (attached to a sql cursor), feed it with translation data and
@@ -168,28 +167,48 @@ class ir_translation_import_cursor(object):
cr.execute("DROP TABLE %s" % self._table_name)
return True
class ir_translation(osv.osv):
class IrTranslation(models.Model):
_name = "ir.translation"
_log_access = False
def _get_language(self, cr, uid, context):
lang_model = self.pool.get('res.lang')
lang_ids = lang_model.search(cr, uid, [('translatable', '=', True)], context=context)
lang_data = lang_model.read(cr, uid, lang_ids, ['code', 'name'], context=context)
return [(d['code'], d['name']) for d in lang_data]
@api.model
def _get_language(self):
lang_model = self.env['res.lang']
lang = lang_model.search([('translatable', '=', True)])
return [(data.code, data.name) for data in lang]
def _get_src(self, cr, uid, ids, name, arg, context=None):
name = fields.Char(string='Translated field', required=True)
res_id = fields.Integer(string='Record ID', index=True)
lang = fields.Selection(selection='_get_language', string='Language')
type = fields.Selection(TRANSLATION_TYPE, string='Type', index=True)
src = fields.Text(string='Internal Source') # stored in database, kept for backward compatibility
source = fields.Text(compute='_get_src', inverse='_set_src', search='_search_src', string='Source term')
value = fields.Text(string='Translation Value')
module = fields.Char(index=True, help="Module this term belongs to")
state = fields.Selection([('to_translate', 'To Translate'),
('inprogress', 'Translation in Progress'),
('translated', 'Translated')],
string="Status", default='to_translate',
help="Automatically set to let administators find new terms that might need to be translated")
# aka gettext extracted-comments - we use them to flag openerp-web translation
# cfr: http://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/PO-Files.html
comments = fields.Text(string='Translation comments', index=True)
_sql_constraints = [('lang_fkey_res_lang', 'FOREIGN KEY(lang) REFERENCES res_lang(code)',
'Language code of translation item must be among known languages')]
@api.depends('source')
def _get_src(self):
''' Get source name for the translation. If object type is model then
return the value store in db. Otherwise return value store in src field
'''
if context is None:
context = {}
res = dict.fromkeys(ids, False)
for record in self.browse(cr, uid, ids, context=context):
res[record.id] = record.src
for record in self:
if record.type == 'model':
model_name, field_name = record.name.split(',')
model = self.pool.get(model_name)
model = self.env[model_name]
if model is None:
continue
field = model._fields.get(field_name)
@@ -197,74 +216,41 @@ class ir_translation(osv.osv):
continue
if not callable(field.translate):
# Pass context without lang, need to read real stored field, not translation
context_no_lang = dict(context, lang=None)
result = model.read(cr, uid, [record.res_id], [field_name], context=context_no_lang)
res[record.id] = result[0][field_name] if result else False
return res
context_no_lang = dict(self.env.context, lang=None)
result = model.with_context(context_no_lang).browse(record.res_id).read([field_name])
record.source = result[0][field_name] if result else False
def _set_src(self, cr, uid, id, name, value, args, context=None):
def _set_src(self):
''' When changing source term of a translation, change its value in db for
the associated object, and the src field
'''
if context is None:
context = {}
record = self.browse(cr, uid, id, context=context)
if record.type == 'model':
model_name, field_name = record.name.split(',')
model = self.pool.get(model_name)
if self.type == 'model':
model_name, field_name = self.name.split(',')
model = self.env[model_name]
field = model._fields[field_name]
if not callable(field.translate):
# Make a context without language information, because we want
# to write on the value stored in db and not on the one
# associated with the current language. Also not removing lang
# from context trigger an error when lang is different.
context_wo_lang = context.copy()
context_wo_lang = self.env.context.copy()
context_wo_lang.pop('lang', None)
model.write(cr, uid, [record.res_id], {field_name: value}, context=context_wo_lang)
return self.write(cr, uid, id, {'src': value}, context=context)
model.with_context(context_wo_lang).browse(self.res_id).write({field_name: self.source})
return self.write({'src': self.source})
def _search_src(self, cr, uid, obj, name, args, context):
@api.model
def _search_src(self, operator, operand):
''' the source term is stored on 'src' field '''
res = []
for field, operator, value in args:
for field, operator, value in operand:
res.append(('src', operator, value))
return res
_columns = {
'name': fields.char('Translated field', required=True),
'res_id': fields.integer('Record ID', select=True),
'lang': fields.selection(_get_language, string='Language'),
'type': fields.selection(TRANSLATION_TYPE, string='Type', select=True),
'src': fields.text('Internal Source'), # stored in database, kept for backward compatibility
'source': fields.function(_get_src, fnct_inv=_set_src, fnct_search=_search_src,
type='text', string='Source term'),
'value': fields.text('Translation Value'),
'module': fields.char('Module', help="Module this term belongs to", select=True),
def _auto_init(self):
super(IrTranslation, self)._auto_init()
'state': fields.selection(
[('to_translate','To Translate'),
('inprogress','Translation in Progress'),
('translated','Translated')],
string="Status",
help="Automatically set to let administators find new terms that might need to be translated"),
# aka gettext extracted-comments - we use them to flag openerp-web translation
# cfr: http://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/PO-Files.html
'comments': fields.text('Translation comments', select=True),
}
_defaults = {
'state': 'to_translate',
}
_sql_constraints = [ ('lang_fkey_res_lang', 'FOREIGN KEY(lang) REFERENCES res_lang(code)',
'Language code of translation item must be among known languages' ), ]
def _auto_init(self, cr, context=None):
super(ir_translation, self)._auto_init(cr, context)
cr.execute("SELECT indexname FROM pg_indexes WHERE indexname LIKE 'ir_translation_%'")
indexes = [row[0] for row in cr.fetchall()]
self.env.cr.execute("SELECT indexname FROM pg_indexes WHERE indexname LIKE 'ir_translation_%'")
indexes = [row[0] for row in self.env.cr.fetchall()]
# Removed because there is a size limit on btree indexed values (problem with column src):
# cr.execute('CREATE INDEX ir_translation_ltns ON ir_translation (name, lang, type, src)')
@@ -273,65 +259,65 @@ class ir_translation(osv.osv):
# Removed because hash indexes are not compatible with postgres streaming replication:
# cr.execute('CREATE INDEX ir_translation_src_hash_idx ON ir_translation USING hash (src)')
if set(indexes) & set(['ir_translation_ltns', 'ir_translation_lts', 'ir_translation_src_hash_idx']):
cr.execute('DROP INDEX IF EXISTS ir_translation_ltns, ir_translation_lts, ir_translation_src_hash_idx')
cr.commit()
self.env.cr.execute('DROP INDEX IF EXISTS ir_translation_ltns, ir_translation_lts, ir_translation_src_hash_idx')
self.env.cr.commit()
# Add separate md5 index on src (no size limit on values, and good performance).
if 'ir_translation_src_md5' not in indexes:
cr.execute('CREATE INDEX ir_translation_src_md5 ON ir_translation (md5(src))')
cr.commit()
self.env.cr.execute('CREATE INDEX ir_translation_src_md5 ON ir_translation (md5(src))')
self.env.cr.commit()
if 'ir_translation_ltn' not in indexes:
cr.execute('CREATE INDEX ir_translation_ltn ON ir_translation (name, lang, type)')
cr.commit()
self.env.cr.execute('CREATE INDEX ir_translation_ltn ON ir_translation (name, lang, type)')
self.env.cr.commit()
def _check_selection_field_value(self, cr, uid, field, value, context=None):
@api.model
def _check_selection_field_value(self, field, value):
if field == 'lang':
return
return super(ir_translation, self)._check_selection_field_value(cr, uid, field, value, context=context)
return super(IrTranslation, self)._check_selection_field_value(field, value)
def _get_ids(self, cr, uid, name, tt, lang, ids):
translations = dict.fromkeys(ids, False)
if ids:
cr.execute('select res_id,value '
'from ir_translation '
'where lang=%s '
'and type=%s '
'and name=%s '
'and res_id IN %s',
(lang,tt,name,tuple(ids)))
for res_id, value in cr.fetchall():
translations[res_id] = value
return translations
@api.model
def _get_ids(self, name, tt, lang, in_ids):
if in_ids:
self.env.cr.execute('select res_id,value '
'from ir_translation '
'where lang=%s '
'and type=%s '
'and name=%s '
'and res_id IN %s',
(lang, tt, name, tuple(in_ids)))
for res_id, value in self.env.cr.fetchall():
self.res_id = value
def _set_ids(self, cr, uid, name, tt, lang, ids, value, src=None):
@api.model
def _set_ids(self, name, tt, lang, rec_ids, value, src=None):
self.clear_caches()
cr.execute('update ir_translation '
'set value=%s '
' , src=%s '
' , state=%s '
'where lang=%s '
'and type=%s '
'and name=%s '
'and res_id IN %s '
'returning res_id',
(value,src,'translated',lang,tt,name,tuple(ids),))
self.env.cr.execute('update ir_translation '
'set value=%s '
' , src=%s '
' , state=%s '
'where lang=%s '
'and type=%s '
'and name=%s '
'and res_id IN %s '
'returning res_id',
(value, src, 'translated', lang, tt, name, tuple(rec_ids),))
existing_ids = [x[0] for x in cr.fetchall()]
existing_ids = [x[0] for x in self.env.cr.fetchall()]
for id in list(set(ids) - set(existing_ids)):
self.create(cr, uid, {
'lang':lang,
'type':tt,
'name':name,
'res_id':id,
'value':value,
'src':src,
'state':'translated'
})
return len(ids)
for id in list(set(rec_ids) - set(existing_ids)):
self.create({'lang': lang,
'type': tt,
'name': name,
'res_id': id,
'value': value,
'src': src,
'state': 'translated'}).id
return len(rec_ids)
def _get_source_query(self, cr, uid, name, types, lang, source, res_id):
@api.model
def _get_source_query(self, name, types, lang, source, res_id):
if source:
# Note: the extra test on md5(src) is a hint for postgres to use the
# index ir_translation_src_md5
@@ -356,21 +342,22 @@ class ir_translation(osv.osv):
AND name=%s"""
params = (lang or '', types, tools.ustr(name))
return (query, params)
@tools.ormcache('name', 'types', 'lang', 'source', 'res_id')
def __get_source(self, cr, uid, name, types, lang, source, res_id):
def __get_source(self, name, types, lang, source, res_id):
# res_id is a tuple or None, otherwise ormcache cannot cache it!
query, params = self._get_source_query(cr, uid, name, types, lang, source, res_id)
cr.execute(query, params)
res = cr.fetchone()
query, params = self._get_source_query(name, types, lang, source, res_id)
self.env.cr.execute(query, params)
res = self.env.cr.fetchone()
trad = res and res[0] or u''
if source and not trad:
return tools.ustr(source)
return trad
def _get_source(self, cr, uid, name, types, lang, source=None, res_id=None):
@api.model
def _get_source(self, name, types, lang, source=None, res_id=None):
"""
Returns the translation for the given combination of name, type, language
and source. All values passed to this method should be unicode (not byte strings),
@@ -396,7 +383,7 @@ class ir_translation(osv.osv):
res_id = (res_id,)
else:
res_id = tuple(res_id)
return self.__get_source(cr, uid, name, types, lang, source, res_id)
return self.__get_source(name, types, lang, source, res_id)
@api.model
def _get_terms_query(self, field, records):
@@ -550,7 +537,7 @@ class ir_translation(osv.osv):
@api.model
def create(self, vals):
record = super(ir_translation, self.sudo()).create(vals).with_env(self.env)
record = super(IrTranslation, self.sudo()).create(vals).with_env(self.env)
record.check('create')
self.clear_caches()
return record
@@ -562,7 +549,7 @@ class ir_translation(osv.osv):
elif vals.get('src') or not vals.get('value', True):
vals.setdefault('state', 'to_translate')
self.check('write')
result = super(ir_translation, self.sudo()).write(vals)
result = super(IrTranslation, self.sudo()).write(vals)
self.check('write')
self.clear_caches()
return result
@@ -571,7 +558,7 @@ class ir_translation(osv.osv):
def unlink(self):
self.check('unlink')
self.clear_caches()
return super(ir_translation, self.sudo()).unlink()
return super(IrTranslation, self.sudo()).unlink()
@api.model
def insert_missing(self, field, records):
@@ -675,17 +662,17 @@ class ir_translation(osv.osv):
def _get_import_cursor(self, cr, uid, context=None):
""" Return a cursor-like object for fast inserting translations
"""
return ir_translation_import_cursor(cr, uid, self, context=context)
return IrTranslationImportCursor(cr, uid, self, context=context)
def load_module_terms(self, cr, modules, langs, context=None):
context_template = dict(context or {}) # local copy
# make sure the given languages are active
lang_obj = self.pool['res.lang']
for lang in langs:
lang_obj.load_lang(cr, SUPERUSER_ID, lang)
lang_obj.load_lang(cr, odoo.SUPERUSER_ID, lang)
# load i18n files
for module_name in modules:
modpath = openerp.modules.get_module_path(module_name)
modpath = odoo.modules.get_module_path(module_name)
if not modpath:
continue
for lang in langs:
@@ -697,28 +684,28 @@ class ir_translation(osv.osv):
# Step 1: for sub-languages, load base language first (e.g. es_CL.po is loaded over es.po)
if base_lang_code:
base_trans_file = openerp.modules.get_module_resource(module_name, 'i18n', base_lang_code + '.po')
base_trans_file = odoo.modules.get_module_resource(module_name, 'i18n', base_lang_code + '.po')
if base_trans_file:
_logger.info('module %s: loading base translation file %s for language %s', module_name, base_lang_code, lang)
tools.trans_load(cr, base_trans_file, lang, verbose=False, module_name=module_name, context=context)
context['overwrite'] = True # make sure the requested translation will override the base terms later
context['overwrite'] = True # make sure the requested translation will override the base terms later
# i18n_extra folder is for additional translations handle manually (eg: for l10n_be)
base_trans_extra_file = openerp.modules.get_module_resource(module_name, 'i18n_extra', base_lang_code + '.po')
base_trans_extra_file = odoo.modules.get_module_resource(module_name, 'i18n_extra', base_lang_code + '.po')
if base_trans_extra_file:
_logger.info('module %s: loading extra base translation file %s for language %s', module_name, base_lang_code, lang)
tools.trans_load(cr, base_trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
context['overwrite'] = True # make sure the requested translation will override the base terms later
context['overwrite'] = True # make sure the requested translation will override the base terms later
# Step 2: then load the main translation file, possibly overriding the terms coming from the base language
trans_file = openerp.modules.get_module_resource(module_name, 'i18n', lang_code + '.po')
trans_file = odoo.modules.get_module_resource(module_name, 'i18n', lang_code + '.po')
if trans_file:
_logger.info('module %s: loading translation file (%s) for language %s', module_name, lang_code, lang)
tools.trans_load(cr, trans_file, lang, verbose=False, module_name=module_name, context=context)
elif lang_code != 'en_US':
_logger.info('module %s: no translation for language %s', module_name, lang_code)
trans_extra_file = openerp.modules.get_module_resource(module_name, 'i18n_extra', lang_code + '.po')
trans_extra_file = odoo.modules.get_module_resource(module_name, 'i18n_extra', lang_code + '.po')
if trans_extra_file:
_logger.info('module %s: loading extra translation file (%s) for language %s', module_name, lang_code, lang)
tools.trans_load(cr, trans_extra_file, lang, verbose=False, module_name=module_name, context=context)
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<!-- Translations -->
<record id="view_translation_search" model="ir.ui.view">
<field name="model">ir.translation</field>
@@ -86,5 +85,4 @@
<menuitem action="action_translation" id="menu_action_translation" parent="base.menu_translation_app" />
</data>
</openerp>
</odoo>
+106 -120
View File
@@ -4,25 +4,78 @@
import base64
import operator
import re
import threading
import openerp
from openerp.osv import fields, osv
from openerp import api, tools
from openerp.http import request
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools.translate import _
import odoo
from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError
from odoo.http import request
from odoo.tools.safe_eval import safe_eval as eval
MENU_ITEM_SEPARATOR = "/"
class ir_ui_menu(osv.osv):
class IrUiMenu(models.Model):
_name = 'ir.ui.menu'
_order = "sequence,id"
_parent_store = True
def __init__(self, *args, **kwargs):
super(ir_ui_menu, self).__init__(*args, **kwargs)
super(IrUiMenu, self).__init__(*args, **kwargs)
self.pool['ir.model.access'].register_cache_clearing_method(self._name, 'clear_caches')
name = fields.Char(string='Menu', required=True, translate=True)
sequence = fields.Integer(default=10)
child_id = fields.One2many('ir.ui.menu', 'parent_id', string='Child IDs')
parent_id = fields.Many2one('ir.ui.menu', string='Parent Menu', index=True, ondelete="restrict")
parent_left = fields.Integer(index=True)
parent_right = fields.Integer(index=True)
groups_id = fields.Many2many('res.groups', 'ir_ui_menu_group_rel',
'menu_id', 'gid', string='Groups',
help="If you have groups, the visibility of this menu will be based on these groups. "\
"If this field is empty, Odoo will compute visibility based on the related object's read access.")
complete_name = fields.Char(compute='_get_full_name', string='Full Path')
web_icon = fields.Char(string='Web Icon File')
action = fields.Reference(selection=[('ir.actions.report.xml', 'ir.actions.report.xml'),
('ir.actions.act_window', 'ir.actions.act_window'),
('ir.actions.act_url', 'ir.actions.act_url'),
('ir.actions.server', 'ir.actions.server'),
('ir.actions.client', 'ir.actions.client')])
web_icon_data = fields.Binary(string='Web Icon Image', compute="_compute_web_icon", store=True, attachment=True)
@api.depends('web_icon')
def _compute_web_icon(self):
for menu in self:
menu.web_icon_data = self.read_image(menu.web_icon)
def read_image(self, path):
if not path:
return False
path_info = path.split(',')
icon_path = odoo.modules.get_module_resource(path_info[0], path_info[1])
icon_image = False
if icon_path:
try:
icon_file = tools.file_open(icon_path, 'rb')
icon_image = base64.encodestring(icon_file.read())
finally:
icon_file.close()
return icon_image
@api.depends('complete_name')
def _get_full_name(self):
for element in self:
element.complete_name = self._get_one_full_name(element)
def _get_one_full_name(self, elmt, level=6):
if level <= 0:
return '...'
if elmt.parent_id:
parent_path = self._get_one_full_name(elmt.parent_id, level-1) + MENU_ITEM_SEPARATOR
else:
parent_path = ''
return parent_path + elmt.name
@api.model
@tools.ormcache('frozenset(self.env.user.groups_id.ids)', 'debug')
def _visible_menu_ids(self, debug=False):
@@ -75,7 +128,7 @@ class ir_ui_menu(osv.osv):
if context is None:
context = {}
ids = super(ir_ui_menu, self).search(cr, uid, args, offset=0,
ids = super(IrUiMenu, self).search(cr, uid, args, offset=0,
limit=None, order=order, context=context, count=False)
if not ids:
@@ -98,99 +151,69 @@ class ir_ui_menu(osv.osv):
return len(result)
return result
def name_get(self, cr, uid, ids, context=None):
@api.multi
def name_get(self):
res = []
for id in ids:
elmt = self.browse(cr, uid, id, context=context)
res.append((id, self._get_one_full_name(elmt)))
for element in self:
res.append((element.id, self._get_one_full_name(element)))
return res
def _get_full_name(self, cr, uid, ids, name=None, args=None, context=None):
if context is None:
context = {}
res = {}
for elmt in self.browse(cr, uid, ids, context=context):
res[elmt.id] = self._get_one_full_name(elmt)
return res
def _get_one_full_name(self, elmt, level=6):
if level<=0:
return '...'
if elmt.parent_id:
parent_path = self._get_one_full_name(elmt.parent_id, level-1) + MENU_ITEM_SEPARATOR
else:
parent_path = ''
return parent_path + elmt.name
def create(self, cr, uid, values, context=None):
@api.model
def create(self, values):
self.clear_caches()
return super(ir_ui_menu, self).create(cr, uid, values, context=context)
return super(IrUiMenu, self).create(values)
def write(self, cr, uid, ids, values, context=None):
@api.multi
def write(self, values):
self.clear_caches()
return super(ir_ui_menu, self).write(cr, uid, ids, values, context=context)
return super(IrUiMenu, self).write(values)
def unlink(self, cr, uid, ids, context=None):
@api.multi
def unlink(self):
# Detach children and promote them to top-level, because it would be unwise to
# cascade-delete submenus blindly. We also can't use ondelete=set null because
# that is not supported when _parent_store is used (would silently corrupt it).
# TODO: ideally we should move them under a generic "Orphans" menu somewhere?
if isinstance(ids, (int, long)):
ids = [ids]
local_context = dict(context or {})
local_context = dict(self.env.context or {})
local_context['ir.ui.menu.full_list'] = True
direct_children_ids = self.search(cr, uid, [('parent_id', 'in', ids)], context=local_context)
if direct_children_ids:
self.write(cr, uid, direct_children_ids, {'parent_id': False})
direct_children = self.with_context(local_context).search([('parent_id', 'in', self.ids)])
if direct_children:
direct_children.write({'parent_id': False})
result = super(ir_ui_menu, self).unlink(cr, uid, ids, context=context)
result = super(IrUiMenu, self).unlink()
self.clear_caches()
return result
def copy(self, cr, uid, id, default=None, context=None):
res = super(ir_ui_menu, self).copy(cr, uid, id, default=default, context=context)
datas=self.read(cr,uid,[res],['name'])[0]
rex=re.compile('\([0-9]+\)')
concat=rex.findall(datas['name'])
@api.multi
def copy(self, default=None):
res = super(IrUiMenu, self).copy(default=default)
rex = re.compile('\([0-9]+\)')
concat = rex.findall(res.name)
if concat:
next_num=int(concat[0])+1
datas['name']=rex.sub(('(%d)'%next_num),datas['name'])
next_num = int(concat[0])+1
res.name = rex.sub(('(%d)' % next_num), res.name)
else:
datas['name'] += '(1)'
self.write(cr,uid,[res],{'name':datas['name']})
res.name += '(1)'
res.write({'name': res.name})
return res
def read_image(self, path):
if not path:
return False
path_info = path.split(',')
icon_path = openerp.modules.get_module_resource(path_info[0],path_info[1])
icon_image = False
if icon_path:
try:
icon_file = tools.file_open(icon_path,'rb')
icon_image = base64.encodestring(icon_file.read())
finally:
icon_file.close()
return icon_image
def get_needaction_data(self, cr, uid, ids, context=None):
@api.multi
def get_needaction_data(self):
""" Return for each menu entry of ids :
- if it uses the needaction mechanism (needaction_enabled)
- the needaction counter of the related action, taking into account
the action domain
"""
if context is None:
context = {}
res = {}
menu_ids = set()
for menu in self.browse(cr, uid, ids, context=context):
for menu in self:
menu_ids.add(menu.id)
ctx = None
if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.context:
try:
# use magical UnquoteEvalContext to ignore undefined client-side variables such as `active_id`
eval_ctx = tools.UnquoteEvalContext(**context)
ctxt = dict(self.env.context or {})
eval_ctx = tools.UnquoteEvalContext(**ctxt)
ctx = eval(menu.action.context, locals_dict=eval_ctx, nocopy=True) or None
except Exception:
# if the eval still fails for some reason, we'll simply skip this menu
@@ -199,43 +222,44 @@ class ir_ui_menu(osv.osv):
if menu_ref:
if not isinstance(menu_ref, list):
menu_ref = [menu_ref]
model_data_obj = self.pool.get('ir.model.data')
model_data_obj = self.env['ir.model.data']
for menu_data in menu_ref:
try:
model, id = model_data_obj.get_object_reference(cr, uid, menu_data.split('.')[0], menu_data.split('.')[1])
model, model_id = model_data_obj.get_object_reference(menu_data.split('.')[0], menu_data.split('.')[1])
if (model == 'ir.ui.menu'):
menu_ids.add(id)
menu_ids.add(model_id)
except Exception:
pass
menu_ids = list(menu_ids)
for menu in self.browse(cr, uid, menu_ids, context=context):
for menu in self:
res[menu.id] = {
'needaction_enabled': False,
'needaction_counter': False,
}
if menu.action and menu.action.type in ('ir.actions.act_window', 'ir.actions.client') and menu.action.res_model:
if menu.action.res_model in self.pool:
obj = self.pool[menu.action.res_model]
obj = self.env[menu.action.res_model]
if obj._needaction:
if menu.action.type == 'ir.actions.act_window':
eval_context = self.pool['ir.actions.act_window']._get_eval_context(cr, uid, context=context)
eval_context = self.env['ir.actions.act_window']._get_eval_context()
dom = menu.action.domain and eval(menu.action.domain, eval_context) or []
else:
dom = eval(menu.action.params_store or '{}', {'uid': uid}).get('domain')
dom = eval(menu.action.params_store or '{}', {'uid': self.env.uid}).get('domain')
res[menu.id]['needaction_enabled'] = obj._needaction
res[menu.id]['needaction_counter'] = obj._needaction_count(cr, uid, dom, context=context)
res[menu.id]['needaction_counter'] = obj._needaction_count(dom)
return res
def get_user_roots(self, cr, uid, context=None):
@api.model
@api.returns('self')
def get_user_roots(self):
""" Return all root menu ids visible for the user.
:return: the root menu ids
:rtype: list(int)
"""
menu_domain = [('parent_id', '=', False)]
return self.search(cr, uid, menu_domain, context=context)
return self.search([('parent_id', '=', False)])
@api.cr_uid_context
@tools.ormcache_context('uid', keys=('lang',))
@@ -300,41 +324,3 @@ class ir_ui_menu(osv.osv):
key=operator.itemgetter('sequence'))
return menu_root
_columns = {
'name': fields.char('Menu', required=True, translate=True),
'sequence': fields.integer('Sequence'),
'child_id': fields.one2many('ir.ui.menu', 'parent_id', 'Child IDs'),
'parent_id': fields.many2one('ir.ui.menu', 'Parent Menu', select=True, ondelete="restrict"),
'parent_left': fields.integer('Parent Left', select=True),
'parent_right': fields.integer('Parent Right', select=True),
'groups_id': fields.many2many('res.groups', 'ir_ui_menu_group_rel',
'menu_id', 'gid', 'Groups', help="If you have groups, the visibility of this menu will be based on these groups. "\
"If this field is empty, Odoo will compute visibility based on the related object's read access."),
'complete_name': fields.function(_get_full_name, string='Full Path', type='char'),
'web_icon': fields.char('Web Icon File'),
'action': fields.reference('Action', selection=[
('ir.actions.report.xml', 'ir.actions.report.xml'),
('ir.actions.act_window', 'ir.actions.act_window'),
('ir.actions.act_url', 'ir.actions.act_url'),
('ir.actions.server', 'ir.actions.server'),
('ir.actions.client', 'ir.actions.client'),
]),
}
web_icon_data = openerp.fields.Binary('Web Icon Image',
compute="_compute_web_icon", store=True, attachment=True)
@api.depends('web_icon')
def _compute_web_icon(self):
for menu in self:
menu.web_icon_data = self.read_image(menu.web_icon)
_constraints = [
(osv.osv._check_recursion, 'Error ! You can not create recursive Menu.', ['parent_id'])
]
_defaults = {
'sequence': 10,
}
_order = "sequence,id"
_parent_store = True
+2 -4
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<record id="edit_menu_access" model="ir.ui.view">
<field name="model">ir.ui.menu</field>
@@ -70,5 +69,4 @@
</record>
<menuitem action="grant_menu_access" id="menu_grant_menu_access" parent="base.next_id_2" sequence="1"/>
</data>
</openerp>
</odoo>
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<!-- View -->
<record id="view_view_form" model="ir.ui.view">
<field name="model">ir.ui.view</field>
@@ -136,5 +135,4 @@
</record>
<menuitem id="menu_action_ui_view_custom" action="action_ui_view_custom" parent="base.next_id_2" sequence="3"/>
</data>
</openerp>
</odoo>
+131 -140
View File
@@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import tools
from openerp.osv import osv, fields
from openerp.exceptions import AccessError, MissingError
from openerp.tools.translate import _
from openerp.tools import pickle
from odoo import api, fields, models, tools, _
from odoo.exceptions import AccessError, MissingError
from odoo.tools import pickle
EXCLUDED_FIELDS = set((
'report_sxw_content', 'report_rml_content', 'report_sxw', 'report_rml',
@@ -21,7 +19,7 @@ ACTION_SLOTS = [
]
class ir_values(osv.osv):
class IrValues(models.Model):
"""Holds internal model-specific action bindings and user-defined default
field values. definitions. This is a legacy internal model, mixing
two different concepts, and will likely be updated or replaced in a
@@ -70,124 +68,112 @@ class ir_values(osv.osv):
users, and set by their UI clients calling :meth:`~.set_default`.
These default values are then automatically used by the
ORM every time a new record is about to be created, i.e. when
:meth:`~openerp.osv.osv.osv.default_get`
or :meth:`~openerp.osv.osv.osv.create` are called.
:meth:`~odoo.osv.osv.osv.default_get`
or :meth:`~odoo.osv.osv.osv.create` are called.
.. rubric:: Usage: action bindings
Business applications will usually bind their actions during
installation, and OpenERP UI clients will apply them as defined,
installation, and Odoo UI clients will apply them as defined,
based on the list of actions included in the result of
:meth:`~openerp.osv.osv.osv.fields_view_get`,
:meth:`~odoo.osv.osv.osv.fields_view_get`,
or directly returned by explicit calls to :meth:`~.get_actions`.
"""
_name = 'ir.values'
def _value_unpickle(self, cursor, user, ids, name, arg, context=None):
res = {}
for record in self.browse(cursor, user, ids, context=context):
value = record[name[:-9]]
name = fields.Char(required=True)
model = fields.Char(string='Model Name', select=True, required=True,
help="Model to which this entry applies")
# TODO: model_id and action_id should be read-write function fields
model_id = fields.Many2one('ir.model', string='Model (change only)',
help="Model to which this entry applies - "
"helper field for setting a model, will "
"automatically set the correct model name")
action_id = fields.Many2one('ir.actions.actions', string='Action (change only)',
help="Action bound to this entry - "
"helper field for binding an action, will "
"automatically set the correct reference")
value = fields.Text(help="Default value (pickled) or reference to an action")
value_unpickle = fields.Text(compute='_value_unpickle', inverse='_value_pickle',
string='Default value or action reference')
key = fields.Selection([('action', 'Action'), ('default', 'Default')],
string='Type', index=True, required=True, default='action',
help="- Action: an action attached to one slot of the given model\n"
"- Default: a default value for a model field")
key2 = fields.Char(string='Qualifier', index=True, default='tree_but_open',
help="For actions, one of the possible action slots: \n"
" - client_action_multi\n"
" - client_print_multi\n"
" - client_action_relate\n"
" - tree_but_open\n"
"For defaults, an optional condition")
res_id = fields.Integer(string='Record ID', index=True,
help="Database identifier of the record to which this applies. "
"0 = for all records")
user_id = fields.Many2one('res.users', string='User', ondelete='cascade', index=True,
help="If set, action binding only applies for this user.")
company_id = fields.Many2one('res.company', string='Company', ondelete='cascade', index=True,
help="If set, action binding only applies for this company")
@api.depends('value_unpickle')
def _value_unpickle(self):
for record in self:
value = record.value
if record.key == 'default' and value:
# default values are pickled on the fly
try:
value = str(pickle.loads(value))
except Exception:
pass
res[record.id] = value
return res
record.value_unpickle = value
def _value_pickle(self, cursor, user, id, name, value, arg, context=None):
if context is None:
context = {}
ctx = context.copy()
def _value_pickle(self):
ctx = self.env.context.copy()
if self.CONCURRENCY_CHECK_FIELD in ctx:
del ctx[self.CONCURRENCY_CHECK_FIELD]
record = self.browse(cursor, user, id, context=context)
if record.key == 'default':
if self.key == 'default':
# default values are pickled on the fly
value = pickle.dumps(value)
self.write(cursor, user, id, {name[:-9]: value}, context=ctx)
value = pickle.dumps(self.value)
self.write({value: value})
def onchange_object_id(self, cr, uid, ids, object_id, context=None):
if not object_id: return {}
act = self.pool.get('ir.model').browse(cr, uid, object_id, context=context)
return {
'value': {'model': act.model}
}
@api.onchange('model_id')
def onchange_object_id(self):
if not self.model_id:
return {}
return {'value': {'model': self.env['ir.model'].browse(self.model_id).model}}
def onchange_action_id(self, cr, uid, ids, action_id, context=None):
if not action_id: return {}
act = self.pool.get('ir.actions.actions').browse(cr, uid, action_id, context=context)
return {
'value': {'value_unpickle': act.type+','+str(act.id)}
}
@api.onchange('action_id')
def onchange_action_id(self):
if not self.action_id:
return {}
act = self.env['ir.actions.actions'].browse(self.action_id)
return {'value': {'value_unpickle': act.type + ',' + str(act.id)}}
_columns = {
'name': fields.char('Name', required=True),
'model': fields.char('Model Name', select=True, required=True,
help="Model to which this entry applies"),
def _auto_init(self):
super(IrValues, self)._auto_init()
self.env.cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_values_key_model_key2_res_id_user_id_idx\'')
if not self.env.cr.fetchone():
self.env.cr.execute('CREATE INDEX ir_values_key_model_key2_res_id_user_id_idx ON ir_values (key, model, key2, res_id, user_id)')
# TODO: model_id and action_id should be read-write function fields
'model_id': fields.many2one('ir.model', 'Model (change only)', size=128,
help="Model to which this entry applies - "
"helper field for setting a model, will "
"automatically set the correct model name"),
'action_id': fields.many2one('ir.actions.actions', 'Action (change only)',
help="Action bound to this entry - "
"helper field for binding an action, will "
"automatically set the correct reference"),
'value': fields.text('Value', help="Default value (pickled) or reference to an action"),
'value_unpickle': fields.function(_value_unpickle, fnct_inv=_value_pickle,
type='text',
string='Default value or action reference'),
'key': fields.selection([('action','Action'),('default','Default')],
'Type', select=True, required=True,
help="- Action: an action attached to one slot of the given model\n"
"- Default: a default value for a model field"),
'key2' : fields.char('Qualifier', select=True,
help="For actions, one of the possible action slots: \n"
" - client_action_multi\n"
" - client_print_multi\n"
" - client_action_relate\n"
" - tree_but_open\n"
"For defaults, an optional condition"
,),
'res_id': fields.integer('Record ID', select=True,
help="Database identifier of the record to which this applies. "
"0 = for all records"),
'user_id': fields.many2one('res.users', 'User', ondelete='cascade', select=True,
help="If set, action binding only applies for this user."),
'company_id': fields.many2one('res.company', 'Company', ondelete='cascade', select=True,
help="If set, action binding only applies for this company")
}
_defaults = {
'key': 'action',
'key2': 'tree_but_open',
}
def _auto_init(self, cr, context=None):
super(ir_values, self)._auto_init(cr, context)
cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_values_key_model_key2_res_id_user_id_idx\'')
if not cr.fetchone():
cr.execute('CREATE INDEX ir_values_key_model_key2_res_id_user_id_idx ON ir_values (key, model, key2, res_id, user_id)')
def create(self, cr, uid, vals, context=None):
res = super(ir_values, self).create(cr, uid, vals, context=context)
@api.model
def create(self, vals):
self.clear_caches()
return res
return super(IrValues, self).create(vals)
def write(self, cr, uid, ids, vals, context=None):
res = super(ir_values, self).write(cr, uid, ids, vals, context=context)
@api.multi
def write(self, vals):
self.clear_caches()
return res
return super(IrValues, self).write(vals)
def unlink(self, cr, uid, ids, context=None):
res = super(ir_values, self).unlink(cr, uid, ids, context=context)
@api.multi
def unlink(self):
self.clear_caches()
return res
return super(IrValues, self).unlink()
def set_default(self, cr, uid, model, field_name, value, for_all_users=True, company_id=False, condition=False):
@api.model
def set_default(self, model, field_name, value, for_all_users=True, company_id=False, condition=False):
"""Defines a default value for the given model and field_name. Any previous
default for the same scope (model, field_name, value, for_all_users, company_id, condition)
will be replaced and lost in the process.
@@ -225,8 +211,7 @@ class ir_values(osv.osv):
value = value.encode('utf8')
if company_id is True:
# should be company-specific, need to get company id
user = self.pool.get('res.users').browse(cr, uid, uid)
company_id = user.company_id.id
company_id = self.env.user.company_id.id
# remove existing defaults for the same scope
search_criteria = [
@@ -234,22 +219,23 @@ class ir_values(osv.osv):
('key2', '=', condition and condition[:200]),
('model', '=', model),
('name', '=', field_name),
('user_id', '=', False if for_all_users else uid),
('company_id','=', company_id)
]
self.unlink(cr, uid, self.search(cr, uid, search_criteria))
('user_id', '=', False if for_all_users else self._uid),
('company_id', '=', company_id)
]
self.search(search_criteria).unlink()
return self.create(cr, uid, {
return self.create({
'name': field_name,
'value': pickle.dumps(value),
'model': model,
'key': 'default',
'key2': condition and condition[:200],
'user_id': False if for_all_users else uid,
'user_id': False if for_all_users else self._uid,
'company_id': company_id,
})
def get_default(self, cr, uid, model, field_name, for_all_users=True, company_id=False, condition=False):
@api.model
def get_default(self, model, field_name, for_all_users=True, company_id=False, condition=False):
""" Return the default value defined for model, field_name, users, company and condition.
Return ``None`` if no such default exists.
"""
@@ -258,13 +244,14 @@ class ir_values(osv.osv):
('key2', '=', condition and condition[:200]),
('model', '=', model),
('name', '=', field_name),
('user_id', '=', False if for_all_users else uid),
('company_id','=', company_id)
]
defaults = self.browse(cr, uid, self.search(cr, uid, search_criteria))
return pickle.loads(defaults[0].value.encode('utf-8')) if defaults else None
('user_id', '=', False if for_all_users else self._uid),
('company_id', '=', company_id)
]
defaults = self.search(search_criteria)
return pickle.loads(defaults.value.encode('utf-8')) if defaults else None
def get_defaults(self, cr, uid, model, condition=False):
@api.model
def get_defaults(self, model, condition=False):
"""Returns any default values that are defined for the current model and user,
(and match ``condition``, if specified), previously registered via
:meth:`~.set_default`.
@@ -303,31 +290,33 @@ class ir_values(osv.osv):
)
%s
ORDER BY v.user_id, u.company_id"""
params = ('default', model, uid, uid)
params = ('default', model, self._uid, self._uid)
if condition:
query %= 'AND v.key2 = %s'
params += (condition[:200],)
else:
query %= 'AND v.key2 is NULL'
cr.execute(query, params)
self.env.cr.execute(query, params)
# keep only the highest priority default for each field
defaults = {}
for row in cr.dictfetchall():
for row in self.env.cr.dictfetchall():
defaults.setdefault(row['name'],
(row['id'], row['name'], pickle.loads(row['value'].encode('utf-8'))))
(row['id'], row['name'], pickle.loads(row['value'].encode('utf-8'))))
return defaults.values()
# use ormcache: this is called a lot by BaseModel.default_get()!
@tools.ormcache('uid', 'model', 'condition')
def get_defaults_dict(self, cr, uid, model, condition=False):
@api.model
@tools.ormcache('self.env.uid', 'model', 'condition')
def get_defaults_dict(self, model, condition=False):
""" Returns a dictionary mapping field names with their corresponding
default value. This method simply improves the returned value of
:meth:`~.get_defaults`.
"""
return dict((f, v) for i, f, v in self.get_defaults(cr, uid, model, condition))
return dict((f, v) for i, f, v in self.get_defaults(model, condition))
def set_action(self, cr, uid, name, action_slot, model, action, res_id=False):
@api.model
def set_action(self, name, action_slot, model, action, res_id=False):
"""Binds an the given action to the given model's action slot - for later
retrieval via :meth:`~.get_actions`. Any existing binding of the same action
to the same slot is first removed, allowing an update of the action's name.
@@ -355,22 +344,23 @@ class ir_values(osv.osv):
('key', '=', 'action'),
('key2', '=', action_slot),
('model', '=', model),
('res_id', '=', res_id or 0), # int field -> NULL == 0
('res_id', '=', res_id or 0), # int field -> NULL == 0
('value', '=', action),
]
self.unlink(cr, uid, self.search(cr, uid, search_criteria))
]
self.search(search_criteria).unlink()
return self.create(cr, uid, {
return self.create({
'key': 'action',
'key2': action_slot,
'model': model,
'res_id': res_id,
'name': name,
'value': action,
})
}).id
@tools.ormcache_context('uid', 'action_slot', 'model', 'res_id', keys=('lang',))
def get_actions(self, cr, uid, action_slot, model, res_id=False, context=None):
@api.model
@tools.ormcache_context('self.env.uid', 'action_slot', 'model', 'res_id', keys=('lang',))
def get_actions(self, action_slot, model, res_id=False):
"""Retrieves the list of actions bound to the given model's action slot.
See the class description for more details about the various action
slots: :class:`~.ir_values`.
@@ -398,21 +388,20 @@ class ir_values(osv.osv):
OR v.res_id IS NULL
OR v.res_id = 0)
ORDER BY v.id"""
cr.execute(query, ('action', action_slot, model, res_id or None))
self.env.cr.execute(query, ('action', action_slot, model, res_id or None))
# map values to their corresponding action record
actions = []
for id, name, value in cr.fetchall():
for id, name, value in self.env.cr.fetchall():
if not value:
continue # skip if undefined
action_model, action_id = value.split(',')
if action_model not in self.pool:
continue # unknown model? skip it!
action = self.pool[action_model].browse(cr, uid, int(action_id), context)
action = self.env[action_model].browse(int(action_id))
actions.append((id, name, action))
# process values and their action
user = self.pool['res.users'].browse(cr, uid, uid, context)
results = {}
for id, name, action in actions:
fields = [field for field in action._fields if field not in EXCLUDED_FIELDS]
@@ -423,7 +412,7 @@ class ir_values(osv.osv):
for field in fields
}
if action._name in ('ir.actions.report.xml', 'ir.actions.act_window'):
if action.groups_id and not action.groups_id & user.groups_id:
if action.groups_id and not action.groups_id & self.env.user.groups_id:
if name == 'Menuitem':
raise AccessError(_('You do not have the permission to perform this operation!!!'))
continue
@@ -454,7 +443,8 @@ class ir_values(osv.osv):
return results
# Backards-compatibility adapter layer to retrofit into split API
def set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=False, preserve_user=False, company=False):
@api.model
def set(self, key, key2, name, models, value, replace=True, isobject=False, meta=False, preserve_user=False, company=False):
"""Deprecated legacy method to set default values and bind actions to models' action slots.
Now dispatches to the newer API methods according to the value of ``key``: :meth:`~.set_default`
(``key=='default'``) or :meth:`~.set_action` (``key == 'action'``).
@@ -463,16 +453,17 @@ class ir_values(osv.osv):
"""
assert key in ['default', 'action'], "ir.values entry keys must be in ['default','action']"
if key == 'default':
def do_set(model,res_id):
return self.set_default(cr, uid, model, field_name=name, value=value,
def do_set(model, res_id):
return self.set_default(model, field_name=name, value=value,
for_all_users=(not preserve_user), company_id=company,
condition=key2)
elif key == 'action':
def do_set(model,res_id):
return self.set_action(cr, uid, name, action_slot=key2, model=model, action=value, res_id=res_id)
def do_set(model, res_id):
return self.set_action(name, action_slot=key2, model=model, action=value, res_id=res_id)
return self._map_legacy_model_list(models, do_set)
def get(self, cr, uid, key, key2, models, meta=False, context=None, res_id_req=False, without_user=True, key2_req=True):
@api.model
def get(self, key, key2, models, meta=False, res_id_req=False, without_user=True, key2_req=True):
"""Deprecated legacy method to get the list of default values or actions bound to models' action slots.
Now dispatches to the newer API methods according to the value of ``key``: :meth:`~.get_defaults`
(``key=='default'``) or :meth:`~.get_actions` (``key == 'action'``)
@@ -482,9 +473,9 @@ class ir_values(osv.osv):
"""
assert key in ['default', 'action'], "ir.values entry keys must be in ['default','action']"
if key == 'default':
def do_get(model,res_id):
return self.get_defaults(cr, uid, model, condition=key2)
def do_get(model, res_id):
return self.get_defaults(model, condition=key2)
elif key == 'action':
def do_get(model,res_id):
return self.get_actions(cr, uid, action_slot=key2, model=model, res_id=res_id, context=context)
def do_get(model, res_id):
return self.get_actions(action_slot=key2, model=model, res_id=res_id)
return self._map_legacy_model_list(models, do_get, merge_results=True)
+5 -7
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<!-- Values -->
<record id="values_view_form_action" model="ir.ui.view">
<field name="name">ir.values.form.action</field>
@@ -10,13 +9,13 @@
<group>
<group>
<field name="name"/>
<field name="model_id" on_change="onchange_object_id(model_id)"/>
<field name="model_id"/>
<field name="model"/>
<field name="res_id"/>
<field name="key2"/>
</group>
<group>
<field name="action_id" on_change="onchange_action_id(action_id)"/>
<field name="action_id"/>
<field name="value_unpickle" colspan="4" string="Action Reference"/>
</group>
</group>
@@ -33,7 +32,7 @@
<group>
<field name="name"/>
<field name="model"/>
<field name="model_id" on_change="onchange_object_id(model_id)"/>
<field name="model_id"/>
</group>
<group>
<field name="key2" string="Condition"/>
@@ -123,5 +122,4 @@
</record>
<menuitem action="act_values_form_defaults" id="menu_values_form_defaults" parent="next_id_6"/>
</data>
</openerp>
</odoo>
+2 -4
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<odoo>
<template id="report_irmodeloverview">
<t t-call="report.html_container">
<t t-foreach="docs" t-as="o">
@@ -93,5 +92,4 @@
</t>
</t>
</template>
</data>
</openerp>
</odoo>
@@ -7,7 +7,7 @@
<field name="type">ir.actions.server</field>
<field name="model_id" ref="model_ir_module_module" />
<field name="state">code</field>
<field name="code">self.button_immediate_install(cr, uid, context.get('active_ids', []), context=context)</field>
<field name="code">self._model.button_immediate_install(self._cr, self._uid, self.env.context.get('active_ids', []), context=self.env.context)</field>
</record>
<record model="ir.values" id="action_module_immediate_install">
+1 -1
View File
@@ -128,7 +128,7 @@ class TestQWeb(common.TransactionCase):
ctx.update(params)
result = doc.find('result[@id="{}"]'.format(template)).text
self.assertEqual(
qweb.render(template, qwebcontext=ctx).strip(),
qweb._model.render(self.cr, self.uid, template, qwebcontext=ctx).strip(),
(result or u'').strip().encode('utf-8'),
template
)