[MERGE] trunk-website-al
bzr revid: al@openerp.com-20140131005207-mn7t6tar8cywe9hz
This commit is contained in:
commit
d6c1346e12
|
@ -172,40 +172,80 @@ is as follows:
|
||||||
</data>
|
</data>
|
||||||
</openerp>
|
</openerp>
|
||||||
|
|
||||||
Record Tag
|
``<record>``
|
||||||
//////////
|
////////////
|
||||||
|
|
||||||
**Description**
|
Defines a new record in a specified OpenERP model.
|
||||||
|
|
||||||
The addition of new data is made with the record tag. This one takes a
|
``@model`` (required)
|
||||||
mandatory attribute : model. Model is the object name where the insertion has
|
|
||||||
to be done. The tag record can also take an optional attribute: id. If this
|
|
||||||
attribute is given, a variable of this name can be used later on, in the same
|
|
||||||
file, to make reference to the newly created resource ID.
|
|
||||||
|
|
||||||
A record tag may contain field tags. They indicate the record's fields value.
|
Name of the model in which this record will be created/inserted.
|
||||||
If a field is not specified the default value will be used.
|
|
||||||
|
|
||||||
The Record Field tag
|
``@id`` (optional)
|
||||||
////////////////////
|
|
||||||
|
|
||||||
The attributes for the field tag are the following:
|
:term:`external ID` for the record, also allows referring to this record in
|
||||||
|
the rest of this file or in other files (through ``field/@ref`` or the
|
||||||
|
:py:func:`ref() <openerp.tools.convert._ref>` function)
|
||||||
|
|
||||||
name : mandatory
|
A record tag generally contains multiple ``field`` tags specifying the values
|
||||||
the field name
|
set on the record's fields when creating it. Fields left out will be set to
|
||||||
|
their default value unless required.
|
||||||
|
|
||||||
eval : optional
|
``<field>``
|
||||||
python expression that indicating the value to add
|
///////////
|
||||||
|
|
||||||
ref
|
|
||||||
reference to an id defined in this file
|
|
||||||
|
|
||||||
model
|
In its most basic use, the ``field`` tag will set its body (as a string) as
|
||||||
model to be looked up in the search
|
the value of the corresponding ``record``'s ``@name`` field.
|
||||||
|
|
||||||
search
|
Extra attributes can either preprocess the body or replace its use entirely:
|
||||||
a query
|
|
||||||
|
|
||||||
|
``@name`` (mandatory)
|
||||||
|
|
||||||
|
Name of the field in the containing ``record``'s model
|
||||||
|
|
||||||
|
``@type`` (optional)
|
||||||
|
|
||||||
|
One of ``char``, ``int``, ``float``, ``list``, ``tuple``, ``xml`` or
|
||||||
|
``html``, ``file`` or ``base64``. Converts the ``field``'s body to the
|
||||||
|
specified type (or validates the body's content)
|
||||||
|
|
||||||
|
* ``xml`` will join multiple XML nodes under a single ``<data>`` root
|
||||||
|
* in ``xml`` and ``html``, external ids can be referenced using
|
||||||
|
``%(id_name)s``
|
||||||
|
* ``list`` and ``tuple``'s element are specified using ``<value>``
|
||||||
|
sub-nodes with the same attributes as ``field``.
|
||||||
|
* ``file`` expects a module-local path and will save the path prefixed with
|
||||||
|
the current module's name, separated by a ``,`` (comma). For use with
|
||||||
|
:py:func:`~openerp.modules.module.get_module_resource`.
|
||||||
|
* ``base64`` expects binary data, encodes it to base64 and sets it. Mostly
|
||||||
|
useful with ``@file``
|
||||||
|
|
||||||
|
``@file``
|
||||||
|
|
||||||
|
Can be used with types ``char`` and ``base64``, sources the field's content
|
||||||
|
from the specified file instead of the field's text body.
|
||||||
|
|
||||||
|
``@model``
|
||||||
|
|
||||||
|
Model used for ``@search``'s search, or registry object put in context for
|
||||||
|
``@eval``. Required if ``@search`` but optional if ``@eval``.
|
||||||
|
|
||||||
|
``@eval`` (optional)
|
||||||
|
|
||||||
|
A Python expression evaluated to obtain the value to set on the record
|
||||||
|
|
||||||
|
``@ref`` (optional)
|
||||||
|
|
||||||
|
Links to an other record through its :term:`external id`. The module prefix
|
||||||
|
may be ommitted to link to a record defined in the same module.
|
||||||
|
|
||||||
|
``@search`` (optional)
|
||||||
|
|
||||||
|
Search domain (evaluated Python expression) into ``@model`` to get the
|
||||||
|
records to set on the field.
|
||||||
|
|
||||||
|
Sets all the matches found for m2m fields, the first id for other field
|
||||||
|
types.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
.. _qweb:
|
||||||
|
|
||||||
|
====
|
||||||
|
QWeb
|
||||||
|
====
|
||||||
|
|
||||||
|
``t-field``
|
||||||
|
===========
|
||||||
|
|
||||||
|
The server version of qweb includes a directive dedicated specifically to
|
||||||
|
formatting and rendering field values from
|
||||||
|
:class:`~openerp.osv.orm.browse_record` objects.
|
||||||
|
|
||||||
|
The directive is implemented through
|
||||||
|
:meth:`~base.ir.ir_qweb.QWeb.render_tag_field` on the ``ir.qweb`` openerp
|
||||||
|
object, and generally delegates to converters for rendering. These converters
|
||||||
|
are obtained through :meth:`~base.ir.ir_qweb.QWeb.get_converter_for`.
|
||||||
|
|
||||||
|
By default, the key for obtaining a converter is the type of the field's
|
||||||
|
column, but this can be overridden by providing a ``widget`` as field option.
|
||||||
|
|
||||||
|
Field options are specified through ``t-field-options``, which must be a JSON
|
||||||
|
object (map). Custom widgets may define their own (possibly mandatory) options.
|
||||||
|
|
||||||
|
Global options
|
||||||
|
--------------
|
||||||
|
|
||||||
|
A global option ``html-escape`` is provided. It defaults to ``True``, and for
|
||||||
|
many (not all) fields it determines whether the field's output will be
|
||||||
|
html-escaped before being output.
|
||||||
|
|
||||||
|
Date and datetime converters
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
The default rendering for ``date`` and ``datetime`` fields. They render the
|
||||||
|
field's value according to the current user's ``lang.date_format`` and
|
||||||
|
``lang.time_format``. The ``datetime`` converter will also localize the value
|
||||||
|
to the user's timezone (as defined by the ``tz`` context key, or the timezone
|
||||||
|
in the user's profile if there is no ``tz`` key in the context).
|
||||||
|
|
||||||
|
A custom format can be provided to use a non-default rendering. The custom
|
||||||
|
format uses the ``format`` options key, and uses the
|
||||||
|
`ldml date format patterns`_ [#ldml]_.
|
||||||
|
|
||||||
|
For instance if one wanted a date field to be rendered as
|
||||||
|
"(month) (day of month)" rather than whatever the default is, one could use:
|
||||||
|
|
||||||
|
.. code-block:: xml
|
||||||
|
|
||||||
|
<span t-field="object.datefield" t-field-options='{"format": "MMMM d"}'/>
|
||||||
|
|
||||||
|
Monetary converter (widget: ``monetary``)
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
Used to format and render monetary value, requires a ``display_currency``
|
||||||
|
options value which is a path from the rendering context to a ``res.currency``
|
||||||
|
object. This object is used to set the right currency symbol, and set it at the
|
||||||
|
right position relative to the formatted value.
|
||||||
|
|
||||||
|
The field itself should be a float field.
|
||||||
|
|
||||||
|
Relative Datetime (widget: ``relative``)
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
Used on a ``datetime`` field, formats it relatively to the current time
|
||||||
|
(``datetime.now()``), e.g. if the field's value is 3 hours before now and the
|
||||||
|
user's lang is english, it will render to *3 hours ago*.
|
||||||
|
|
||||||
|
.. note:: this field uses babel's ``format_timedelta`` more or less directly
|
||||||
|
and will only display the biggest unit and round up at 85% e.g.
|
||||||
|
1 hour 15 minutes will be rendered as *1 hour*, and 55 minutes will
|
||||||
|
also be rendered as *1 hour*.
|
||||||
|
|
||||||
|
.. warning:: this converter *requires* babel 1.0 or more recent.
|
||||||
|
|
||||||
|
Duration (widget: ``duration``)
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
Renders a duration defined as a ``float`` to a human-readable localized string,
|
||||||
|
e.g. ``1.5`` as hours in an english locale will be rendered to
|
||||||
|
*1 hour 30 minutes*.
|
||||||
|
|
||||||
|
Requires a ``unit`` option which may be one of ``second``, ``minute``,
|
||||||
|
``hour``, ``day``, ``week``, ``month`` or ``year``. This specifies the unit in
|
||||||
|
which the value should be interpreted before formatting.
|
||||||
|
|
||||||
|
The duration must be a positive number, and no rounding is applied.
|
||||||
|
|
||||||
|
.. [#ldml] in part because `babel`_ is used for rendering, as ``strftime``
|
||||||
|
would require altering the process's locale on the fly in order to
|
||||||
|
get correctly localized date and time output. Babel uses the CLDR
|
||||||
|
as its core and thus uses LDML date format patterns.
|
||||||
|
|
||||||
|
.. _babel: http://babel.pocoo.org
|
||||||
|
|
||||||
|
.. _ldml date format patterns:
|
||||||
|
http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
|
||||||
|
|
|
@ -11,3 +11,4 @@ Miscellanous
|
||||||
06_misc_user_img_specs.rst
|
06_misc_user_img_specs.rst
|
||||||
06_misc_import.rst
|
06_misc_import.rst
|
||||||
06_misc_auto_join.rst
|
06_misc_auto_join.rst
|
||||||
|
06_ir_qweb.rst
|
||||||
|
|
|
@ -252,5 +252,4 @@ texinfo_documents = [
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
'python': ('http://docs.python.org/', None),
|
'python': ('http://docs.python.org/', None),
|
||||||
'openerpweb': ('http://doc.openerp.com/trunk/developers/web', None),
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,26 +12,20 @@ Server actions
|
||||||
|
|
||||||
.. currentmodule:: openerp.addons.base.ir.ir_actions
|
.. currentmodule:: openerp.addons.base.ir.ir_actions
|
||||||
|
|
||||||
.. autoclass:: actions_server
|
.. autoclass:: ir_actions_server
|
||||||
:noindex:
|
:members: run, _get_states
|
||||||
|
|
||||||
Adding a new sever action
|
Adding a new sever action
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
The ``state`` field holds the various available types of server action. In order
|
The ``state`` field holds the various available types of server action. In order
|
||||||
to add a new server action, the first thing to do is to override the ``_get_states``
|
to add a new server action, the first thing to do is to override the :meth:`~.ir_actions_server._get_states`
|
||||||
method that returns the list of values available for the selection field.
|
method that returns the list of values available for the selection field.
|
||||||
|
|
||||||
.. automethod:: actions_server._get_states
|
The method called when executing the server action is the :meth:`~.ir_actions_server.run` method. This
|
||||||
:noindex:
|
|
||||||
|
|
||||||
The method called when executing the server action is the ``run`` method. This
|
|
||||||
method calls ``run_action_<STATE>``. When adding a new server action type, you
|
method calls ``run_action_<STATE>``. When adding a new server action type, you
|
||||||
have to define the related method that will be called upon execution.
|
have to define the related method that will be called upon execution.
|
||||||
|
|
||||||
.. automethod:: actions_server.run
|
|
||||||
:noindex:
|
|
||||||
|
|
||||||
Changelog
|
Changelog
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
|
|
@ -112,18 +112,6 @@ CREATE TABLE ir_act_client (
|
||||||
)
|
)
|
||||||
INHERITS (ir_actions);
|
INHERITS (ir_actions);
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE ir_ui_view (
|
|
||||||
id serial NOT NULL,
|
|
||||||
name varchar(64) DEFAULT ''::varchar NOT NULL,
|
|
||||||
model varchar(64) DEFAULT ''::varchar NOT NULL,
|
|
||||||
"type" varchar(64) DEFAULT 'form'::varchar NOT NULL,
|
|
||||||
arch text NOT NULL,
|
|
||||||
field_parent varchar(64),
|
|
||||||
priority integer DEFAULT 5 NOT NULL,
|
|
||||||
primary key(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE ir_ui_menu (
|
CREATE TABLE ir_ui_menu (
|
||||||
id serial NOT NULL,
|
id serial NOT NULL,
|
||||||
parent_id int references ir_ui_menu on delete set null,
|
parent_id int references ir_ui_menu on delete set null,
|
||||||
|
@ -409,4 +397,4 @@ insert into ir_model_data (name,module,model,noupdate,res_id) VALUES ('main_comp
|
||||||
select setval('res_company_id_seq', 2);
|
select setval('res_company_id_seq', 2);
|
||||||
select setval('res_users_id_seq', 2);
|
select setval('res_users_id_seq', 2);
|
||||||
select setval('res_partner_id_seq', 2);
|
select setval('res_partner_id_seq', 2);
|
||||||
select setval('res_currency_id_seq', 2);
|
select setval('res_currency_id_seq', 2);
|
||||||
|
|
|
@ -91,6 +91,25 @@ Administrator</field>
|
||||||
<field eval="10" name="sequence"/>
|
<field eval="10" name="sequence"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
A group dedicated to the portal users, making groups
|
||||||
|
restrictions more convenient.
|
||||||
|
-->
|
||||||
|
<record id="group_portal" model="res.groups">
|
||||||
|
<field name="name">Portal</field>
|
||||||
|
<field name="comment">Portal members have specific access rights (such as record rules and restricted menus).
|
||||||
|
They usually do not belong to the usual OpenERP groups.</field>
|
||||||
|
</record>
|
||||||
|
<!--
|
||||||
|
A group dedicated to the public user only, making groups
|
||||||
|
restrictions more convenient.
|
||||||
|
-->
|
||||||
|
<record id="group_public" model="res.groups">
|
||||||
|
<field name="name">Public</field>
|
||||||
|
<field name="comment">Public users have specific access rights (such as record rules and restricted menus).
|
||||||
|
They usually do not belong to the usual OpenERP groups.</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
<!-- Basic fonts family included in PDF standart, will always be in the font list -->
|
<!-- Basic fonts family included in PDF standart, will always be in the font list -->
|
||||||
<record model="res.font" id="base.font_helvetica">
|
<record model="res.font" id="base.font_helvetica">
|
||||||
<field name="name">Helvetica</field>
|
<field name="name">Helvetica</field>
|
||||||
|
@ -111,5 +130,21 @@ Administrator</field>
|
||||||
<field name="mode">all</field>
|
<field name="mode">all</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="public_partner" model="res.partner">
|
||||||
|
<field name="name">Public user</field>
|
||||||
|
<field name="active" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="public_user" model="res.users">
|
||||||
|
<field name="name">Public user</field>
|
||||||
|
<field name="login">public</field>
|
||||||
|
<field name="password"></field>
|
||||||
|
<!-- Avoid auto-including this demo user in any default group -->
|
||||||
|
<field name="groups_id" eval="[(6,0,[ref('base.group_public')])]"/>
|
||||||
|
<field name="image" type="base64" file="base/static/img/public_user-image.png"/>
|
||||||
|
<field name="partner_id" ref="public_partner"/>
|
||||||
|
<field name="active" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
</data>
|
</data>
|
||||||
</openerp>
|
</openerp>
|
||||||
|
|
|
@ -1,17 +1,34 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<openerp>
|
<openerp>
|
||||||
<data noupdate="1">
|
<data noupdate="1">
|
||||||
|
|
||||||
<record id="partner_demo" model="res.partner">
|
<record id="partner_demo" model="res.partner">
|
||||||
<field name="name">Demo User</field>
|
<field name="name">Demo User</field>
|
||||||
<field name="company_id" ref="main_company"/>
|
<field name="company_id" ref="main_company"/>
|
||||||
<field name="customer" eval="False"/>
|
<field name="customer" eval="False"/>
|
||||||
<field name="email">demo@example.com</field>
|
<field name="email">demo@yourcompany.example.com</field>
|
||||||
|
<field name="street">Avenue des Dessus-de-Lives, 2</field>
|
||||||
|
<field name="city">Namur (Loyers)</field>
|
||||||
|
<field name="zip">5101</field>
|
||||||
|
<field name="country_id" ref="be"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="main_partner" model="res.partner">
|
<record id="main_partner" model="res.partner">
|
||||||
|
<field name="name">YourCompany</field>
|
||||||
|
<field name="street">1725 Slough Ave.</field>
|
||||||
|
<field name="city">Scranton</field>
|
||||||
|
<field name="zip">18540</field>
|
||||||
|
<field name="phone">+1 555 123 8069</field>
|
||||||
|
<field name="email">info@yourcompany.example.com</field>
|
||||||
|
<field name="website">www.example.com</field>
|
||||||
<field name="image" type="base64" file="base/static/img/main_partner-image.png"/>
|
<field name="image" type="base64" file="base/static/img/main_partner-image.png"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="main_company" model="res.company">
|
||||||
|
<field name="name">YourCompany</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
<record id="user_demo" model="res.users">
|
<record id="user_demo" model="res.users">
|
||||||
<field name="partner_id" ref="base.partner_demo"/>
|
<field name="partner_id" ref="base.partner_demo"/>
|
||||||
<field name="login">demo</field>
|
<field name="login">demo</field>
|
||||||
|
@ -24,7 +41,7 @@ Mr Demo</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record model="res.partner" id="base.partner_root">
|
<record model="res.partner" id="base.partner_root">
|
||||||
<field name="email">admin@example.com</field>
|
<field name="email">admin@yourcompany.example.com</field>
|
||||||
<field name="tz">Europe/Brussels</field>
|
<field name="tz">Europe/Brussels</field>
|
||||||
<field name="image" type="base64" file="base/static/img/partner_root-image.jpg"/>
|
<field name="image" type="base64" file="base/static/img/partner_root-image.jpg"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
|
@ -37,6 +37,7 @@ import ir_config_parameter
|
||||||
import osv_memory_autovacuum
|
import osv_memory_autovacuum
|
||||||
import ir_mail_server
|
import ir_mail_server
|
||||||
import ir_fields
|
import ir_fields
|
||||||
|
import ir_qweb
|
||||||
import ir_http
|
import ir_http
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
|
@ -911,10 +911,10 @@ class ir_actions_server(osv.osv):
|
||||||
self.pool[action.model_id.model].write(cr, uid, [context.get('active_id')], {action.link_field_id.name: res_id})
|
self.pool[action.model_id.model].write(cr, uid, [context.get('active_id')], {action.link_field_id.name: res_id})
|
||||||
|
|
||||||
def run(self, cr, uid, ids, context=None):
|
def run(self, cr, uid, ids, context=None):
|
||||||
""" Run the server action. For each server action, the condition is
|
""" Runs the server action. For each server action, the condition is
|
||||||
checked. Note that A void (aka False) condition is considered as always
|
checked. Note that a void (``False``) condition is considered as always
|
||||||
valid. If it is verified, the run_action_<STATE> method is called. This
|
valid. If it is verified, the run_action_<STATE> method is called. This
|
||||||
allows easy inheritance of the server actions.
|
allows easy overriding of the server actions.
|
||||||
|
|
||||||
:param dict context: context should contain following keys
|
:param dict context: context should contain following keys
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ class ir_attachment(osv.osv):
|
||||||
The default implementation is the file:dirname location that stores files
|
The default implementation is the file:dirname location that stores files
|
||||||
on the local filesystem using name based on their sha1 hash
|
on the local filesystem using name based on their sha1 hash
|
||||||
"""
|
"""
|
||||||
|
_order = 'id desc'
|
||||||
def _name_get_resname(self, cr, uid, ids, object, method, context):
|
def _name_get_resname(self, cr, uid, ids, object, method, context):
|
||||||
data = {}
|
data = {}
|
||||||
for attachment in self.browse(cr, uid, ids, context=context):
|
for attachment in self.browse(cr, uid, ids, context=context):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import cStringIO
|
||||||
import datetime
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
import operator
|
import operator
|
||||||
|
@ -128,14 +129,17 @@ class ir_fields_converter(orm.Model):
|
||||||
|
|
||||||
:param column: column object to generate a value for
|
:param column: column object to generate a value for
|
||||||
:type column: :class:`fields._column`
|
:type column: :class:`fields._column`
|
||||||
:param type fromtype: type to convert to something fitting for ``column``
|
:param fromtype: type to convert to something fitting for ``column``
|
||||||
|
:type fromtype: type | str
|
||||||
:param context: openerp request context
|
:param context: openerp request context
|
||||||
:return: a function (fromtype -> column.write_type), if a converter is found
|
:return: a function (fromtype -> column.write_type), if a converter is found
|
||||||
:rtype: Callable | None
|
:rtype: Callable | None
|
||||||
"""
|
"""
|
||||||
|
assert isinstance(fromtype, (type, str))
|
||||||
# FIXME: return None
|
# FIXME: return None
|
||||||
|
typename = fromtype.__name__ if isinstance(fromtype, type) else fromtype
|
||||||
converter = getattr(
|
converter = getattr(
|
||||||
self, '_%s_to_%s' % (fromtype.__name__, column._type), None)
|
self, '_%s_to_%s' % (typename, column._type), None)
|
||||||
if not converter: return None
|
if not converter: return None
|
||||||
|
|
||||||
return functools.partial(
|
return functools.partial(
|
||||||
|
|
|
@ -15,9 +15,7 @@ from openerp.osv import osv, orm
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
UID_PLACEHOLDER = object()
|
||||||
# FIXME: replace by proxy on request.uid?
|
|
||||||
_uid = object()
|
|
||||||
|
|
||||||
class ModelConverter(werkzeug.routing.BaseConverter):
|
class ModelConverter(werkzeug.routing.BaseConverter):
|
||||||
|
|
||||||
|
@ -29,7 +27,7 @@ class ModelConverter(werkzeug.routing.BaseConverter):
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
m = re.match(self.regex, value)
|
m = re.match(self.regex, value)
|
||||||
return request.registry[self.model].browse(
|
return request.registry[self.model].browse(
|
||||||
request.cr, _uid, int(m.group(1)), context=request.context)
|
request.cr, UID_PLACEHOLDER, int(m.group(1)), context=request.context)
|
||||||
|
|
||||||
def to_url(self, value):
|
def to_url(self, value):
|
||||||
return value.id
|
return value.id
|
||||||
|
@ -43,10 +41,7 @@ class ModelsConverter(werkzeug.routing.BaseConverter):
|
||||||
self.regex = '([0-9,]+)'
|
self.regex = '([0-9,]+)'
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
# TODO:
|
return request.registry[self.model].browse(request.cr, UID_PLACEHOLDER, [int(i) for i in value.split(',')], context=request.context)
|
||||||
# - raise routing.ValidationError() if no browse record can be createdm
|
|
||||||
# - support slug
|
|
||||||
return request.registry[self.model].browse(request.cr, _uid, [int(i) for i in value.split(',')], context=request.context)
|
|
||||||
|
|
||||||
def to_url(self, value):
|
def to_url(self, value):
|
||||||
return ",".join(i.id for i in value)
|
return ",".join(i.id for i in value)
|
||||||
|
@ -66,15 +61,15 @@ class ir_http(osv.AbstractModel):
|
||||||
if not request.uid:
|
if not request.uid:
|
||||||
raise http.SessionExpiredException("Session expired")
|
raise http.SessionExpiredException("Session expired")
|
||||||
|
|
||||||
def _auth_method_admin(self):
|
|
||||||
if not request.db:
|
|
||||||
raise http.SessionExpiredException("No valid database for request %s" % request.httprequest)
|
|
||||||
request.uid = openerp.SUPERUSER_ID
|
|
||||||
|
|
||||||
def _auth_method_none(self):
|
def _auth_method_none(self):
|
||||||
request.disable_db = True
|
|
||||||
request.uid = None
|
request.uid = None
|
||||||
|
|
||||||
|
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')
|
||||||
|
else:
|
||||||
|
request.uid = request.session.uid
|
||||||
|
|
||||||
def _authenticate(self, auth_method='user'):
|
def _authenticate(self, auth_method='user'):
|
||||||
if request.session.uid:
|
if request.session.uid:
|
||||||
try:
|
try:
|
||||||
|
@ -88,16 +83,8 @@ class ir_http(osv.AbstractModel):
|
||||||
return auth_method
|
return auth_method
|
||||||
|
|
||||||
def _handle_exception(self, exception):
|
def _handle_exception(self, exception):
|
||||||
if isinstance(exception, openerp.exceptions.AccessError):
|
# If handle exception return something different than None, it will be used as a response
|
||||||
code = 403
|
raise
|
||||||
else:
|
|
||||||
code = getattr(exception, 'code', 500)
|
|
||||||
|
|
||||||
fn = getattr(self, '_handle_%d' % code, self._handle_unknown_exception)
|
|
||||||
return fn(exception)
|
|
||||||
|
|
||||||
def _handle_unknown_exception(self, exception):
|
|
||||||
raise exception
|
|
||||||
|
|
||||||
def _dispatch(self):
|
def _dispatch(self):
|
||||||
# locate the controller method
|
# locate the controller method
|
||||||
|
@ -108,17 +95,17 @@ class ir_http(osv.AbstractModel):
|
||||||
|
|
||||||
# check authentication level
|
# check authentication level
|
||||||
try:
|
try:
|
||||||
auth_method = self._authenticate(getattr(func, "auth", None))
|
auth_method = self._authenticate(func.routing["auth"])
|
||||||
except Exception:
|
except Exception:
|
||||||
# force a Forbidden exception with the original traceback
|
# force a Forbidden exception with the original traceback
|
||||||
return self._handle_exception(
|
return self._handle_exception(
|
||||||
convert_exception_to(
|
convert_exception_to(
|
||||||
werkzeug.exceptions.Forbidden))
|
werkzeug.exceptions.Forbidden))
|
||||||
|
|
||||||
# post process arg to set uid on browse records
|
processing = self._postprocess_args(arguments)
|
||||||
for arg in arguments.itervalues():
|
if processing:
|
||||||
if isinstance(arg, orm.browse_record) and arg._uid is _uid:
|
return processing
|
||||||
arg._uid = request.uid
|
|
||||||
|
|
||||||
# set and execute handler
|
# set and execute handler
|
||||||
try:
|
try:
|
||||||
|
@ -131,6 +118,16 @@ class ir_http(osv.AbstractModel):
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _postprocess_args(self, arguments):
|
||||||
|
""" post process arg to set uid on browse records """
|
||||||
|
for arg in arguments.itervalues():
|
||||||
|
if isinstance(arg, orm.browse_record) and arg._uid is UID_PLACEHOLDER:
|
||||||
|
arg._uid = request.uid
|
||||||
|
try:
|
||||||
|
arg[arg._rec_name]
|
||||||
|
except KeyError:
|
||||||
|
return self._handle_exception(werkzeug.exceptions.NotFound())
|
||||||
|
|
||||||
def routing_map(self):
|
def routing_map(self):
|
||||||
if not hasattr(self, '_routing_map'):
|
if not hasattr(self, '_routing_map'):
|
||||||
_logger.info("Generating routing map")
|
_logger.info("Generating routing map")
|
||||||
|
@ -138,7 +135,7 @@ class ir_http(osv.AbstractModel):
|
||||||
m = request.registry.get('ir.module.module')
|
m = request.registry.get('ir.module.module')
|
||||||
ids = m.search(cr, openerp.SUPERUSER_ID, [('state', '=', 'installed'), ('name', '!=', 'web')], context=request.context)
|
ids = m.search(cr, openerp.SUPERUSER_ID, [('state', '=', 'installed'), ('name', '!=', 'web')], context=request.context)
|
||||||
installed = set(x['name'] for x in m.read(cr, 1, ids, ['name'], context=request.context))
|
installed = set(x['name'] for x in m.read(cr, 1, ids, ['name'], context=request.context))
|
||||||
mods = ['', "web"] + sorted(installed)
|
mods = [''] + openerp.conf.server_wide_modules + sorted(installed)
|
||||||
self._routing_map = http.routing_map(mods, False, converters=self._get_converters())
|
self._routing_map = http.routing_map(mods, False, converters=self._get_converters())
|
||||||
|
|
||||||
return self._routing_map
|
return self._routing_map
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
# OpenERP, Open Source Business Applications
|
# OpenERP, Open Source Business Applications
|
||||||
# Copyright (C) 2004-2012 OpenERP S.A. (<http://openerp.com>).
|
# Copyright (C) 2004-2014 OpenERP S.A. (<http://openerp.com>).
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU Affero General Public License as
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
@ -29,7 +29,7 @@ import openerp.modules.registry
|
||||||
from openerp import SUPERUSER_ID
|
from openerp import SUPERUSER_ID
|
||||||
from openerp import tools
|
from openerp import tools
|
||||||
from openerp.osv import fields,osv
|
from openerp.osv import fields,osv
|
||||||
from openerp.osv.orm import Model
|
from openerp.osv.orm import Model, browse_null
|
||||||
from openerp.tools.safe_eval import safe_eval as eval
|
from openerp.tools.safe_eval import safe_eval as eval
|
||||||
from openerp.tools import config
|
from openerp.tools import config
|
||||||
from openerp.tools.translate import _
|
from openerp.tools.translate import _
|
||||||
|
@ -737,7 +737,7 @@ class ir_model_access(osv.osv):
|
||||||
msg_params = (model_name,)
|
msg_params = (model_name,)
|
||||||
_logger.warning('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, uid, model_name)
|
_logger.warning('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, uid, model_name)
|
||||||
msg = '%s %s' % (msg_heads[mode], msg_tail)
|
msg = '%s %s' % (msg_heads[mode], msg_tail)
|
||||||
raise except_orm(_('Access Denied'), msg % msg_params)
|
raise openerp.exceptions.AccessError(msg % msg_params)
|
||||||
return r or False
|
return r or False
|
||||||
|
|
||||||
__cache_clearing_methods = []
|
__cache_clearing_methods = []
|
||||||
|
@ -853,24 +853,58 @@ class ir_model_data(osv.osv):
|
||||||
if not cr.fetchone():
|
if not cr.fetchone():
|
||||||
cr.execute('CREATE INDEX ir_model_data_module_name_index ON ir_model_data (module, name)')
|
cr.execute('CREATE INDEX ir_model_data_module_name_index ON ir_model_data (module, name)')
|
||||||
|
|
||||||
@tools.ormcache()
|
# NEW V8 API
|
||||||
|
@tools.ormcache(skiparg=3)
|
||||||
|
def xmlid_lookup(self, cr, uid, xmlid):
|
||||||
|
"""Low level xmlid lookup
|
||||||
|
Return (id, res_model, res_id) or raise ValueError if not found
|
||||||
|
"""
|
||||||
|
module, name = xmlid.split('.', 1)
|
||||||
|
ids = self.search(cr, uid, [('module','=',module), ('name','=', name)])
|
||||||
|
if not ids:
|
||||||
|
raise ValueError('External ID not found in the system: %s' % (xmlid))
|
||||||
|
# the sql constraints ensure us we have only one result
|
||||||
|
res = self.read(cr, uid, ids[0], ['model', 'res_id'])
|
||||||
|
if not res['res_id']:
|
||||||
|
raise ValueError('External ID not found in the system: %s' % (xmlid))
|
||||||
|
return ids[0], res['model'], res['res_id']
|
||||||
|
|
||||||
|
def xmlid_to_res_model_res_id(self, cr, uid, xmlid, raise_if_not_found=False):
|
||||||
|
""" Return (res_model, res_id)"""
|
||||||
|
try:
|
||||||
|
return self.xmlid_lookup(cr, uid, xmlid)[1:3]
|
||||||
|
except ValueError:
|
||||||
|
if raise_if_not_found:
|
||||||
|
raise
|
||||||
|
return (False, False)
|
||||||
|
|
||||||
|
def xmlid_to_res_id(self, cr, uid, xmlid, raise_if_not_found=False):
|
||||||
|
""" Returns res_id """
|
||||||
|
return self.xmlid_to_res_model_res_id(cr, uid, xmlid, raise_if_not_found)[1]
|
||||||
|
|
||||||
|
def xmlid_to_object(self, cr, uid, xmlid, raise_if_not_found=False, context=None):
|
||||||
|
""" Return a browse_record
|
||||||
|
if not found and raise_if_not_found is True return the browse_null
|
||||||
|
"""
|
||||||
|
t = self.xmlid_to_res_model_res_id(cr, uid, xmlid, raise_if_not_found)
|
||||||
|
res_model, res_id = t
|
||||||
|
|
||||||
|
if res_model and res_id:
|
||||||
|
record = self.pool[res_model].browse(cr, uid, res_id, context=context)
|
||||||
|
if record.exists():
|
||||||
|
return record
|
||||||
|
if raise_if_not_found:
|
||||||
|
raise ValueError('No record found for unique ID %s. It may have been deleted.' % (xml_id))
|
||||||
|
return browse_null()
|
||||||
|
|
||||||
|
# OLD API
|
||||||
def _get_id(self, cr, uid, module, xml_id):
|
def _get_id(self, cr, uid, module, xml_id):
|
||||||
"""Returns the id of the ir.model.data record corresponding to a given module and xml_id (cached) or raise a ValueError if not found"""
|
"""Returns the id of the ir.model.data record corresponding to a given module and xml_id (cached) or raise a ValueError if not found"""
|
||||||
ids = self.search(cr, uid, [('module','=',module), ('name','=', xml_id)])
|
return self.xmlid_lookup(cr, uid, "%s.%s" % (module, xml_id))[0]
|
||||||
if not ids:
|
|
||||||
raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
|
|
||||||
# the sql constraints ensure us we have only one result
|
|
||||||
return ids[0]
|
|
||||||
|
|
||||||
@tools.ormcache()
|
|
||||||
def get_object_reference(self, cr, uid, module, xml_id):
|
def get_object_reference(self, cr, uid, module, xml_id):
|
||||||
"""Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""
|
"""Returns (model, res_id) corresponding to a given module and xml_id (cached) or raise ValueError if not found"""
|
||||||
data_id = self._get_id(cr, uid, module, xml_id)
|
return self.xmlid_lookup(cr, uid, "%s.%s" % (module, xml_id))[1:3]
|
||||||
#assuming data_id is not False, as it was checked upstream
|
|
||||||
res = self.read(cr, uid, data_id, ['model', 'res_id'])
|
|
||||||
if not res['res_id']:
|
|
||||||
raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
|
|
||||||
return res['model'], res['res_id']
|
|
||||||
|
|
||||||
def check_object_reference(self, cr, uid, module, xml_id, raise_on_access_error=False):
|
def check_object_reference(self, cr, uid, module, xml_id, raise_on_access_error=False):
|
||||||
"""Returns (model, res_id) corresponding to a given module and xml_id (cached), if and only if the user has the necessary access rights
|
"""Returns (model, res_id) corresponding to a given module and xml_id (cached), if and only if the user has the necessary access rights
|
||||||
|
@ -885,12 +919,11 @@ class ir_model_data(osv.osv):
|
||||||
return model, False
|
return model, False
|
||||||
|
|
||||||
def get_object(self, cr, uid, module, xml_id, context=None):
|
def get_object(self, cr, uid, module, xml_id, context=None):
|
||||||
"""Returns a browsable record for the given module name and xml_id or raise ValueError if not found"""
|
""" Returns a browsable record for the given module name and xml_id.
|
||||||
res_model, res_id = self.get_object_reference(cr, uid, module, xml_id)
|
If not found, raise a ValueError or return a browse_null, depending
|
||||||
result = self.pool[res_model].browse(cr, uid, res_id, context=context)
|
on the value of `raise_exception`.
|
||||||
if not result.exists():
|
"""
|
||||||
raise ValueError('No record found for unique ID %s.%s. It may have been deleted.' % (module, xml_id))
|
return self.xmlid_to_object(cr, uid, "%s.%s" % (module, xml_id), raise_if_not_found=True, context=context)
|
||||||
return result
|
|
||||||
|
|
||||||
def _update_dummy(self,cr, uid, model, module, xml_id=False, store=True):
|
def _update_dummy(self,cr, uid, model, module, xml_id=False, store=True):
|
||||||
if not xml_id:
|
if not xml_id:
|
||||||
|
@ -907,8 +940,7 @@ class ir_model_data(osv.osv):
|
||||||
|
|
||||||
:returns: itself
|
:returns: itself
|
||||||
"""
|
"""
|
||||||
self._get_id.clear_cache(self)
|
self.xmlid_lookup.clear_cache(self)
|
||||||
self.get_object_reference.clear_cache(self)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def unlink(self, cr, uid, ids, context=None):
|
def unlink(self, cr, uid, ids, context=None):
|
||||||
|
@ -929,15 +961,17 @@ class ir_model_data(osv.osv):
|
||||||
return False
|
return False
|
||||||
action_id = False
|
action_id = False
|
||||||
if xml_id:
|
if xml_id:
|
||||||
cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model
|
cr.execute('''SELECT imd.id, imd.res_id, md.id, imd.model, imd.noupdate
|
||||||
FROM ir_model_data imd LEFT JOIN %s md ON (imd.res_id = md.id)
|
FROM ir_model_data imd LEFT JOIN %s md ON (imd.res_id = md.id)
|
||||||
WHERE imd.module=%%s AND imd.name=%%s''' % model_obj._table,
|
WHERE imd.module=%%s AND imd.name=%%s''' % model_obj._table,
|
||||||
(module, xml_id))
|
(module, xml_id))
|
||||||
results = cr.fetchall()
|
results = cr.fetchall()
|
||||||
for imd_id2,res_id2,real_id2,real_model in results:
|
for imd_id2,res_id2,real_id2,real_model,noupdate_imd in results:
|
||||||
|
# In update mode, do not update a record if it's ir.model.data is flagged as noupdate
|
||||||
|
if mode == 'update' and noupdate_imd:
|
||||||
|
return res_id2
|
||||||
if not real_id2:
|
if not real_id2:
|
||||||
self._get_id.clear_cache(self, uid, module, xml_id)
|
self.clear_caches()
|
||||||
self.get_object_reference.clear_cache(self, uid, module, xml_id)
|
|
||||||
cr.execute('delete from ir_model_data where id=%s', (imd_id2,))
|
cr.execute('delete from ir_model_data where id=%s', (imd_id2,))
|
||||||
res_id = False
|
res_id = False
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -0,0 +1,849 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import collections
|
||||||
|
import cStringIO
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import xml # FIXME use lxml and etree
|
||||||
|
|
||||||
|
import babel
|
||||||
|
import babel.dates
|
||||||
|
import werkzeug.utils
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
import openerp.tools
|
||||||
|
from openerp.tools.safe_eval import safe_eval as eval
|
||||||
|
from openerp.osv import osv, orm, fields
|
||||||
|
from openerp.tools.translate import _
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
# QWeb template engine
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
class QWebException(Exception):
|
||||||
|
def __init__(self, message, **kw):
|
||||||
|
Exception.__init__(self, message)
|
||||||
|
self.qweb = dict(kw)
|
||||||
|
|
||||||
|
class QWebTemplateNotFound(QWebException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def convert_to_qweb_exception(etype=None, **kw):
|
||||||
|
if etype is None:
|
||||||
|
etype = QWebException
|
||||||
|
orig_type, original, tb = sys.exc_info()
|
||||||
|
try:
|
||||||
|
raise etype, original, tb
|
||||||
|
except etype, e:
|
||||||
|
for k, v in kw.items():
|
||||||
|
e.qweb[k] = v
|
||||||
|
e.qweb['inner'] = original
|
||||||
|
return e
|
||||||
|
|
||||||
|
class QWebContext(dict):
|
||||||
|
def __init__(self, cr, uid, data, loader=None, templates=None, context=None):
|
||||||
|
self.cr = cr
|
||||||
|
self.uid = uid
|
||||||
|
self.loader = loader
|
||||||
|
self.templates = templates or {}
|
||||||
|
self.context = context
|
||||||
|
dic = dict(data)
|
||||||
|
super(QWebContext, self).__init__(dic)
|
||||||
|
self['defined'] = lambda key: key in self
|
||||||
|
|
||||||
|
def safe_eval(self, expr):
|
||||||
|
locals_dict = collections.defaultdict(lambda: None)
|
||||||
|
locals_dict.update(self)
|
||||||
|
locals_dict.pop('cr', None)
|
||||||
|
locals_dict.pop('loader', None)
|
||||||
|
return eval(expr, None, locals_dict, nocopy=True, locals_builtins=True)
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
return QWebContext(self.cr, self.uid, dict.copy(self),
|
||||||
|
loader=self.loader,
|
||||||
|
templates=self.templates,
|
||||||
|
context=self.context)
|
||||||
|
|
||||||
|
def __copy__(self):
|
||||||
|
return self.copy()
|
||||||
|
|
||||||
|
class QWeb(orm.AbstractModel):
|
||||||
|
"""QWeb Xml templating engine
|
||||||
|
|
||||||
|
The templating engine use a very simple syntax based "magic" xml
|
||||||
|
attributes, to produce textual output (even non-xml).
|
||||||
|
|
||||||
|
The core magic attributes are:
|
||||||
|
|
||||||
|
flow attributes:
|
||||||
|
t-if t-foreach t-call
|
||||||
|
|
||||||
|
output attributes:
|
||||||
|
t-att t-raw t-esc t-trim
|
||||||
|
|
||||||
|
assignation attribute:
|
||||||
|
t-set
|
||||||
|
|
||||||
|
QWeb can be extended like any OpenERP model and new attributes can be
|
||||||
|
added.
|
||||||
|
|
||||||
|
If you need to customize t-fields rendering, subclass the ir.qweb.field
|
||||||
|
model (and its sub-models) then override :meth:`~.get_converter_for` to
|
||||||
|
fetch the right field converters for your qweb model.
|
||||||
|
|
||||||
|
Beware that if you need extensions or alterations which could be
|
||||||
|
incompatible with other subsystems, you should create a local object
|
||||||
|
inheriting from ``ir.qweb`` and customize that.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_name = 'ir.qweb'
|
||||||
|
|
||||||
|
node = xml.dom.Node
|
||||||
|
_void_elements = frozenset([
|
||||||
|
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
|
||||||
|
'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'])
|
||||||
|
_format_regex = re.compile(
|
||||||
|
'(?:'
|
||||||
|
# ruby-style pattern
|
||||||
|
'#\{(.+?)\}'
|
||||||
|
')|(?:'
|
||||||
|
# jinja-style pattern
|
||||||
|
'\{\{(.+?)\}\}'
|
||||||
|
')')
|
||||||
|
|
||||||
|
def __init__(self, pool, cr):
|
||||||
|
super(QWeb, self).__init__(pool, cr)
|
||||||
|
|
||||||
|
self._render_tag = self.prefixed_methods('render_tag_')
|
||||||
|
self._render_att = self.prefixed_methods('render_att_')
|
||||||
|
|
||||||
|
def prefixed_methods(self, prefix):
|
||||||
|
""" Extracts all methods prefixed by ``prefix``, and returns a mapping
|
||||||
|
of (t-name, method) where the t-name is the method name with prefix
|
||||||
|
removed and underscore converted to dashes
|
||||||
|
|
||||||
|
:param str prefix:
|
||||||
|
:return: dict
|
||||||
|
"""
|
||||||
|
n_prefix = len(prefix)
|
||||||
|
return dict(
|
||||||
|
(name[n_prefix:].replace('_', '-'), getattr(type(self), name))
|
||||||
|
for name in dir(self)
|
||||||
|
if name.startswith(prefix)
|
||||||
|
)
|
||||||
|
|
||||||
|
def register_tag(self, tag, func):
|
||||||
|
self._render_tag[tag] = func
|
||||||
|
|
||||||
|
def add_template(self, qwebcontext, name, node):
|
||||||
|
"""Add a parsed template in the context. Used to preprocess templates."""
|
||||||
|
qwebcontext.templates[name] = node
|
||||||
|
|
||||||
|
def load_document(self, document, qwebcontext):
|
||||||
|
"""
|
||||||
|
Loads an XML document and installs any contained template in the engine
|
||||||
|
"""
|
||||||
|
if hasattr(document, 'documentElement'):
|
||||||
|
dom = document
|
||||||
|
elif document.startswith("<?xml"):
|
||||||
|
dom = xml.dom.minidom.parseString(document)
|
||||||
|
else:
|
||||||
|
dom = xml.dom.minidom.parse(document)
|
||||||
|
|
||||||
|
for node in dom.documentElement.childNodes:
|
||||||
|
if node.nodeType == self.node.ELEMENT_NODE and node.getAttribute('t-name'):
|
||||||
|
name = str(node.getAttribute("t-name"))
|
||||||
|
self.add_template(qwebcontext, name, node)
|
||||||
|
|
||||||
|
def get_template(self, name, qwebcontext):
|
||||||
|
origin_template = qwebcontext.get('__caller__') or qwebcontext['__stack__'][0]
|
||||||
|
if qwebcontext.loader and name not in qwebcontext.templates:
|
||||||
|
try:
|
||||||
|
xml_doc = qwebcontext.loader(name)
|
||||||
|
except ValueError:
|
||||||
|
raise convert_to_qweb_exception(QWebTemplateNotFound, message="Loader could not find template %r" % name, template=origin_template)
|
||||||
|
self.load_document(xml_doc, qwebcontext=qwebcontext)
|
||||||
|
|
||||||
|
if name in qwebcontext.templates:
|
||||||
|
return qwebcontext.templates[name]
|
||||||
|
|
||||||
|
raise convert_to_qweb_exception(QWebTemplateNotFound, message="Template %r not found" % name, template=origin_template)
|
||||||
|
|
||||||
|
def eval(self, expr, qwebcontext):
|
||||||
|
try:
|
||||||
|
return qwebcontext.safe_eval(expr)
|
||||||
|
except Exception:
|
||||||
|
template = qwebcontext.get('__template__')
|
||||||
|
raise convert_to_qweb_exception(message="Could not evaluate expression %r" % expr, expression=expr, template=template)
|
||||||
|
|
||||||
|
def eval_object(self, expr, qwebcontext):
|
||||||
|
return self.eval(expr, qwebcontext)
|
||||||
|
|
||||||
|
def eval_str(self, expr, qwebcontext):
|
||||||
|
if expr == "0":
|
||||||
|
return qwebcontext.get(0, '')
|
||||||
|
val = self.eval(expr, qwebcontext)
|
||||||
|
if isinstance(val, unicode):
|
||||||
|
return val.encode("utf8")
|
||||||
|
if val is False or val is None:
|
||||||
|
return ''
|
||||||
|
return str(val)
|
||||||
|
|
||||||
|
def eval_format(self, expr, qwebcontext):
|
||||||
|
expr, replacements = self._format_regex.subn(
|
||||||
|
lambda m: self.eval_str(m.group(1) or m.group(2), qwebcontext),
|
||||||
|
expr
|
||||||
|
)
|
||||||
|
|
||||||
|
if replacements:
|
||||||
|
return expr
|
||||||
|
|
||||||
|
try:
|
||||||
|
return str(expr % qwebcontext)
|
||||||
|
except Exception:
|
||||||
|
template = qwebcontext.get('__template__')
|
||||||
|
raise convert_to_qweb_exception(message="Format error for expression %r" % expr, expression=expr, template=template)
|
||||||
|
|
||||||
|
def eval_bool(self, expr, qwebcontext):
|
||||||
|
return int(bool(self.eval(expr, qwebcontext)))
|
||||||
|
|
||||||
|
def render(self, cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None):
|
||||||
|
if qwebcontext is None:
|
||||||
|
qwebcontext = {}
|
||||||
|
|
||||||
|
if not isinstance(qwebcontext, QWebContext):
|
||||||
|
qwebcontext = QWebContext(cr, uid, qwebcontext, loader=loader, context=context)
|
||||||
|
|
||||||
|
qwebcontext['__template__'] = id_or_xml_id
|
||||||
|
stack = qwebcontext.get('__stack__', [])
|
||||||
|
if stack:
|
||||||
|
qwebcontext['__caller__'] = stack[-1]
|
||||||
|
stack.append(id_or_xml_id)
|
||||||
|
qwebcontext['__stack__'] = stack
|
||||||
|
qwebcontext['xmlid'] = str(stack[0]) # Temporary fix
|
||||||
|
return self.render_node(self.get_template(id_or_xml_id, qwebcontext), qwebcontext)
|
||||||
|
|
||||||
|
def render_node(self, element, qwebcontext):
|
||||||
|
result = ""
|
||||||
|
if element.nodeType == self.node.TEXT_NODE or element.nodeType == self.node.CDATA_SECTION_NODE:
|
||||||
|
result = element.data.encode("utf8")
|
||||||
|
elif element.nodeType == self.node.ELEMENT_NODE:
|
||||||
|
generated_attributes = ""
|
||||||
|
t_render = None
|
||||||
|
template_attributes = {}
|
||||||
|
for (attribute_name, attribute_value) in element.attributes.items():
|
||||||
|
attribute_name = str(attribute_name)
|
||||||
|
if attribute_name == "groups":
|
||||||
|
cr = qwebcontext.get('request') and qwebcontext['request'].cr or None
|
||||||
|
uid = qwebcontext.get('request') and qwebcontext['request'].uid or None
|
||||||
|
can_see = self.user_has_groups(cr, uid, groups=attribute_value)
|
||||||
|
if not can_see:
|
||||||
|
return ''
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(attribute_value, unicode):
|
||||||
|
attribute_value = attribute_value.encode("utf8")
|
||||||
|
else:
|
||||||
|
attribute_value = attribute_value.nodeValue.encode("utf8")
|
||||||
|
|
||||||
|
if attribute_name.startswith("t-"):
|
||||||
|
for attribute in self._render_att:
|
||||||
|
if attribute_name[2:].startswith(attribute):
|
||||||
|
att, val = self._render_att[attribute](self, element, attribute_name, attribute_value, qwebcontext)
|
||||||
|
generated_attributes += val and ' %s="%s"' % (att, werkzeug.utils.escape(val)) or " "
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if attribute_name[2:] in self._render_tag:
|
||||||
|
t_render = attribute_name[2:]
|
||||||
|
template_attributes[attribute_name[2:]] = attribute_value
|
||||||
|
else:
|
||||||
|
generated_attributes += ' %s="%s"' % (attribute_name, werkzeug.utils.escape(attribute_value))
|
||||||
|
|
||||||
|
if 'debug' in template_attributes:
|
||||||
|
debugger = template_attributes.get('debug', 'pdb')
|
||||||
|
__import__(debugger).set_trace() # pdb, ipdb, pudb, ...
|
||||||
|
if t_render:
|
||||||
|
result = self._render_tag[t_render](self, element, template_attributes, generated_attributes, qwebcontext)
|
||||||
|
else:
|
||||||
|
result = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
|
||||||
|
if isinstance(result, unicode):
|
||||||
|
return result.encode('utf-8')
|
||||||
|
return result
|
||||||
|
|
||||||
|
def render_element(self, element, template_attributes, generated_attributes, qwebcontext, inner=None):
|
||||||
|
# element: element
|
||||||
|
# template_attributes: t-* attributes
|
||||||
|
# generated_attributes: generated attributes
|
||||||
|
# qwebcontext: values
|
||||||
|
# inner: optional innerXml
|
||||||
|
if inner:
|
||||||
|
g_inner = inner
|
||||||
|
else:
|
||||||
|
g_inner = []
|
||||||
|
for current_node in element.childNodes:
|
||||||
|
try:
|
||||||
|
g_inner.append(self.render_node(current_node, qwebcontext))
|
||||||
|
except QWebException:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
template = qwebcontext.get('__template__')
|
||||||
|
raise convert_to_qweb_exception(message="Could not render element %r" % element.nodeName, node=element, template=template)
|
||||||
|
name = str(element.nodeName)
|
||||||
|
inner = "".join(g_inner)
|
||||||
|
trim = template_attributes.get("trim", 0)
|
||||||
|
if trim == 0:
|
||||||
|
pass
|
||||||
|
elif trim == 'left':
|
||||||
|
inner = inner.lstrip()
|
||||||
|
elif trim == 'right':
|
||||||
|
inner = inner.rstrip()
|
||||||
|
elif trim == 'both':
|
||||||
|
inner = inner.strip()
|
||||||
|
if name == "t":
|
||||||
|
return inner
|
||||||
|
elif len(inner) or name not in self._void_elements:
|
||||||
|
return "<%s%s>%s</%s>" % tuple(
|
||||||
|
qwebcontext if isinstance(qwebcontext, str) else qwebcontext.encode('utf-8')
|
||||||
|
for qwebcontext in (name, generated_attributes, inner, name)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return "<%s%s/>" % (name, generated_attributes)
|
||||||
|
|
||||||
|
# Attributes
|
||||||
|
def render_att_att(self, element, attribute_name, attribute_value, qwebcontext):
|
||||||
|
if attribute_name.startswith("t-attf-"):
|
||||||
|
att, val = attribute_name[7:], self.eval_format(attribute_value, qwebcontext)
|
||||||
|
elif attribute_name.startswith("t-att-"):
|
||||||
|
att, val = attribute_name[6:], self.eval(attribute_value, qwebcontext)
|
||||||
|
if isinstance(val, unicode):
|
||||||
|
val = val.encode("utf8")
|
||||||
|
else:
|
||||||
|
att, val = self.eval_object(attribute_value, qwebcontext)
|
||||||
|
return att, val
|
||||||
|
|
||||||
|
# Tags
|
||||||
|
def render_tag_raw(self, element, template_attributes, generated_attributes, qwebcontext):
|
||||||
|
inner = self.eval_str(template_attributes["raw"], qwebcontext)
|
||||||
|
return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
|
||||||
|
|
||||||
|
def render_tag_esc(self, element, template_attributes, generated_attributes, qwebcontext):
|
||||||
|
inner = werkzeug.utils.escape(self.eval_str(template_attributes["esc"], qwebcontext))
|
||||||
|
return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
|
||||||
|
|
||||||
|
def render_tag_foreach(self, element, template_attributes, generated_attributes, qwebcontext):
|
||||||
|
expr = template_attributes["foreach"]
|
||||||
|
enum = self.eval_object(expr, qwebcontext)
|
||||||
|
if enum is not None:
|
||||||
|
var = template_attributes.get('as', expr).replace('.', '_')
|
||||||
|
copy_qwebcontext = qwebcontext.copy()
|
||||||
|
size = -1
|
||||||
|
if isinstance(enum, (list, tuple)):
|
||||||
|
size = len(enum)
|
||||||
|
elif hasattr(enum, 'count'):
|
||||||
|
size = enum.count()
|
||||||
|
copy_qwebcontext["%s_size" % var] = size
|
||||||
|
copy_qwebcontext["%s_all" % var] = enum
|
||||||
|
index = 0
|
||||||
|
ru = []
|
||||||
|
for i in enum:
|
||||||
|
copy_qwebcontext["%s_value" % var] = i
|
||||||
|
copy_qwebcontext["%s_index" % var] = index
|
||||||
|
copy_qwebcontext["%s_first" % var] = index == 0
|
||||||
|
copy_qwebcontext["%s_even" % var] = index % 2
|
||||||
|
copy_qwebcontext["%s_odd" % var] = (index + 1) % 2
|
||||||
|
copy_qwebcontext["%s_last" % var] = index + 1 == size
|
||||||
|
if index % 2:
|
||||||
|
copy_qwebcontext["%s_parity" % var] = 'odd'
|
||||||
|
else:
|
||||||
|
copy_qwebcontext["%s_parity" % var] = 'even'
|
||||||
|
if 'as' in template_attributes:
|
||||||
|
copy_qwebcontext[var] = i
|
||||||
|
elif isinstance(i, dict):
|
||||||
|
copy_qwebcontext.update(i)
|
||||||
|
ru.append(self.render_element(element, template_attributes, generated_attributes, copy_qwebcontext))
|
||||||
|
index += 1
|
||||||
|
return "".join(ru)
|
||||||
|
else:
|
||||||
|
template = qwebcontext.get('__template__')
|
||||||
|
raise QWebException("foreach enumerator %r is not defined while rendering template %r" % (expr, template), template=template)
|
||||||
|
|
||||||
|
def render_tag_if(self, element, template_attributes, generated_attributes, qwebcontext):
|
||||||
|
if self.eval_bool(template_attributes["if"], qwebcontext):
|
||||||
|
return self.render_element(element, template_attributes, generated_attributes, qwebcontext)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def render_tag_call(self, element, template_attributes, generated_attributes, qwebcontext):
|
||||||
|
d = qwebcontext.copy()
|
||||||
|
d[0] = self.render_element(element, template_attributes, generated_attributes, d)
|
||||||
|
cr = d.get('request') and d['request'].cr or None
|
||||||
|
uid = d.get('request') and d['request'].uid or None
|
||||||
|
|
||||||
|
return self.render(cr, uid, self.eval_format(template_attributes["call"], d), d)
|
||||||
|
|
||||||
|
def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext):
|
||||||
|
if "value" in template_attributes:
|
||||||
|
qwebcontext[template_attributes["set"]] = self.eval_object(template_attributes["value"], qwebcontext)
|
||||||
|
elif "valuef" in template_attributes:
|
||||||
|
qwebcontext[template_attributes["set"]] = self.eval_format(template_attributes["valuef"], qwebcontext)
|
||||||
|
else:
|
||||||
|
qwebcontext[template_attributes["set"]] = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def render_tag_field(self, element, template_attributes, generated_attributes, qwebcontext):
|
||||||
|
""" eg: <span t-record="browse_record(res.partner, 1)" t-field="phone">+1 555 555 8069</span>"""
|
||||||
|
node_name = element.nodeName
|
||||||
|
assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td",
|
||||||
|
"li", "ul", "ol", "dl", "dt", "dd"),\
|
||||||
|
"RTE widgets do not work correctly on %r elements" % node_name
|
||||||
|
assert node_name != 't',\
|
||||||
|
"t-field can not be used on a t element, provide an actual HTML node"
|
||||||
|
|
||||||
|
record, field_name = template_attributes["field"].rsplit('.', 1)
|
||||||
|
record = self.eval_object(record, qwebcontext)
|
||||||
|
|
||||||
|
column = record._model._all_columns[field_name].column
|
||||||
|
options = json.loads(template_attributes.get('field-options') or '{}')
|
||||||
|
field_type = get_field_type(column, options)
|
||||||
|
|
||||||
|
converter = self.get_converter_for(field_type)
|
||||||
|
|
||||||
|
return converter.to_html(qwebcontext.cr, qwebcontext.uid, field_name, record, options,
|
||||||
|
element, template_attributes, generated_attributes, qwebcontext, context=qwebcontext.context)
|
||||||
|
|
||||||
|
def get_converter_for(self, field_type):
|
||||||
|
return self.pool.get('ir.qweb.field.' + field_type,
|
||||||
|
self.pool['ir.qweb.field'])
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
# QWeb Fields converters
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
|
||||||
|
class FieldConverter(osv.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:
|
||||||
|
|
||||||
|
* converts the record value to html using :meth:`~.record_to_html`
|
||||||
|
* generates the metadata attributes (``data-oe-``) to set on the root
|
||||||
|
result node
|
||||||
|
* generates the root result node itself through :meth:`~.render_element`
|
||||||
|
"""
|
||||||
|
_name = 'ir.qweb.field'
|
||||||
|
|
||||||
|
def attributes(self, 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
|
||||||
|
root node of the field conversion. Attribute values are escaped by the
|
||||||
|
parent using ``werkzeug.utils.escape``.
|
||||||
|
|
||||||
|
The default attributes are:
|
||||||
|
|
||||||
|
* ``model``, the name of the record's model
|
||||||
|
* ``id`` the id of the record to which the field belongs
|
||||||
|
* ``field`` the name of the converted field
|
||||||
|
* ``type`` the logical field type (widget, may not match the column's
|
||||||
|
``type``, may not be any _column subclass name)
|
||||||
|
* ``translate``, a boolean flag (``0`` or ``1``) denoting whether the
|
||||||
|
column is translatable
|
||||||
|
* ``expression``, the original expression
|
||||||
|
|
||||||
|
:returns: iterable of (attribute name, attribute value) pairs.
|
||||||
|
"""
|
||||||
|
column = record._model._all_columns[field_name].column
|
||||||
|
field_type = get_field_type(column, options)
|
||||||
|
return [
|
||||||
|
('data-oe-model', record._model._name),
|
||||||
|
('data-oe-id', record.id),
|
||||||
|
('data-oe-field', field_name),
|
||||||
|
('data-oe-type', field_type),
|
||||||
|
('data-oe-expression', t_att['field']),
|
||||||
|
]
|
||||||
|
|
||||||
|
def value_to_html(self, cr, uid, value, column, options=None, context=None):
|
||||||
|
""" Converts a single value to its HTML version/output
|
||||||
|
"""
|
||||||
|
if not value: return ''
|
||||||
|
return value
|
||||||
|
|
||||||
|
def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
|
||||||
|
""" Converts the specified field of the browse_record ``record`` to
|
||||||
|
HTML
|
||||||
|
"""
|
||||||
|
return self.value_to_html(
|
||||||
|
cr, uid, record[field_name], column, options=options, context=context)
|
||||||
|
|
||||||
|
def to_html(self, 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
|
||||||
|
extended by a ``t-field-options``, which is a JSON-serialized mapping
|
||||||
|
of configuration values.
|
||||||
|
|
||||||
|
A default configuration key is ``widget`` which can override the
|
||||||
|
field's own ``_type``.
|
||||||
|
"""
|
||||||
|
content = None
|
||||||
|
try:
|
||||||
|
content = self.record_to_html(
|
||||||
|
cr, uid, field_name, record,
|
||||||
|
record._model._all_columns[field_name].column,
|
||||||
|
options, context=context)
|
||||||
|
if options.get('html-escape', True):
|
||||||
|
content = werkzeug.utils.escape(content)
|
||||||
|
elif hasattr(content, '__html__'):
|
||||||
|
content = content.__html__()
|
||||||
|
except Exception:
|
||||||
|
_logger.warning("Could not get field %s for model %s",
|
||||||
|
field_name, record._model._name, exc_info=True)
|
||||||
|
content = None
|
||||||
|
|
||||||
|
g_att += ''.join(
|
||||||
|
' %s="%s"' % (name, werkzeug.utils.escape(value))
|
||||||
|
for name, value in self.attributes(
|
||||||
|
cr, uid, field_name, record, options,
|
||||||
|
source_element, g_att, t_att, qweb_context)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.render_element(cr, uid, source_element, t_att, g_att,
|
||||||
|
qweb_context, content)
|
||||||
|
|
||||||
|
def qweb_object(self):
|
||||||
|
return self.pool['ir.qweb']
|
||||||
|
|
||||||
|
def render_element(self, cr, uid, source_element, t_att, g_att,
|
||||||
|
qweb_context, content):
|
||||||
|
""" Final rendering hook, by default just calls ir.qweb's ``render_element``
|
||||||
|
"""
|
||||||
|
return self.qweb_object().render_element(
|
||||||
|
source_element, t_att, g_att, qweb_context, content or '')
|
||||||
|
|
||||||
|
def user_lang(self, cr, uid, context):
|
||||||
|
"""
|
||||||
|
Fetches the res.lang object corresponding to the language code stored
|
||||||
|
in the user's context. Fallbacks to en_US if no lang is present in the
|
||||||
|
context *or the language code is not valid*.
|
||||||
|
|
||||||
|
:returns: res.lang browse_record
|
||||||
|
"""
|
||||||
|
if context is None: context = {}
|
||||||
|
|
||||||
|
lang_code = context.get('lang') or 'en_US'
|
||||||
|
Lang = self.pool['res.lang']
|
||||||
|
|
||||||
|
lang_ids = Lang.search(cr, uid, [('code', '=', lang_code)], context=context) \
|
||||||
|
or Lang.search(cr, uid, [('code', '=', 'en_US')], context=context)
|
||||||
|
|
||||||
|
return Lang.browse(cr, uid, lang_ids[0], context=context)
|
||||||
|
|
||||||
|
class FloatConverter(osv.AbstractModel):
|
||||||
|
_name = 'ir.qweb.field.float'
|
||||||
|
_inherit = 'ir.qweb.field'
|
||||||
|
|
||||||
|
def precision(self, cr, uid, column, options=None, context=None):
|
||||||
|
_, precision = column.digits or (None, None)
|
||||||
|
return precision
|
||||||
|
|
||||||
|
def value_to_html(self, cr, uid, value, column, options=None, context=None):
|
||||||
|
if context is None:
|
||||||
|
context = {}
|
||||||
|
precision = self.precision(cr, uid, column, options=options, context=context)
|
||||||
|
fmt = '%f' if precision is None else '%.{precision}f'
|
||||||
|
|
||||||
|
lang_code = 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)
|
||||||
|
|
||||||
|
# %f does not strip trailing zeroes. %g does but its precision causes
|
||||||
|
# it to switch to scientific notation starting at a million *and* to
|
||||||
|
# strip decimals. So use %f and if no precision was specified manually
|
||||||
|
# strip trailing 0.
|
||||||
|
if not precision:
|
||||||
|
formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted)
|
||||||
|
return formatted
|
||||||
|
|
||||||
|
class DateConverter(osv.AbstractModel):
|
||||||
|
_name = 'ir.qweb.field.date'
|
||||||
|
_inherit = 'ir.qweb.field'
|
||||||
|
|
||||||
|
def value_to_html(self, cr, uid, value, column, options=None, context=None):
|
||||||
|
if not value: return ''
|
||||||
|
lang = self.user_lang(cr, uid, context=context)
|
||||||
|
locale = babel.Locale.parse(lang.code)
|
||||||
|
|
||||||
|
if isinstance(value, basestring):
|
||||||
|
value = datetime.datetime.strptime(
|
||||||
|
value, openerp.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)
|
||||||
|
|
||||||
|
return babel.dates.format_datetime(
|
||||||
|
value, format=pattern,
|
||||||
|
locale=locale)
|
||||||
|
|
||||||
|
class DateTimeConverter(osv.AbstractModel):
|
||||||
|
_name = 'ir.qweb.field.datetime'
|
||||||
|
_inherit = 'ir.qweb.field'
|
||||||
|
|
||||||
|
def value_to_html(self, cr, uid, value, column, options=None, context=None):
|
||||||
|
if not value: return ''
|
||||||
|
lang = self.user_lang(cr, uid, context=context)
|
||||||
|
locale = babel.Locale.parse(lang.code)
|
||||||
|
|
||||||
|
if isinstance(value, basestring):
|
||||||
|
value = datetime.datetime.strptime(
|
||||||
|
value, openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT)
|
||||||
|
value = column.context_timestamp(
|
||||||
|
cr, uid, timestamp=value, context=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)
|
||||||
|
|
||||||
|
return babel.dates.format_datetime(value, format=pattern, locale=locale)
|
||||||
|
|
||||||
|
class TextConverter(osv.AbstractModel):
|
||||||
|
_name = 'ir.qweb.field.text'
|
||||||
|
_inherit = 'ir.qweb.field'
|
||||||
|
|
||||||
|
def value_to_html(self, cr, uid, value, column, options=None, context=None):
|
||||||
|
"""
|
||||||
|
Escapes the value and converts newlines to br. This is bullshit.
|
||||||
|
"""
|
||||||
|
if not value: return ''
|
||||||
|
|
||||||
|
return nl2br(value, options=options)
|
||||||
|
|
||||||
|
class SelectionConverter(osv.AbstractModel):
|
||||||
|
_name = 'ir.qweb.field.selection'
|
||||||
|
_inherit = 'ir.qweb.field'
|
||||||
|
|
||||||
|
def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
|
||||||
|
value = record[field_name]
|
||||||
|
if not value: return ''
|
||||||
|
selection = dict(fields.selection.reify(
|
||||||
|
cr, uid, record._model, column))
|
||||||
|
return self.value_to_html(
|
||||||
|
cr, uid, selection[value], column, options=options)
|
||||||
|
|
||||||
|
class ManyToOneConverter(osv.AbstractModel):
|
||||||
|
_name = 'ir.qweb.field.many2one'
|
||||||
|
_inherit = 'ir.qweb.field'
|
||||||
|
|
||||||
|
def record_to_html(self, cr, uid, field_name, record, column, options=None, context=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):
|
||||||
|
_name = 'ir.qweb.field.html'
|
||||||
|
_inherit = 'ir.qweb.field'
|
||||||
|
|
||||||
|
def value_to_html(self, cr, uid, value, column, options=None, context=None):
|
||||||
|
return HTMLSafe(value or '')
|
||||||
|
|
||||||
|
class ImageConverter(osv.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.
|
||||||
|
|
||||||
|
.. todo:: what happens if different output need different converters? e.g.
|
||||||
|
reports may need embedded images or FS links whereas website
|
||||||
|
needs website-aware
|
||||||
|
"""
|
||||||
|
_name = 'ir.qweb.field.image'
|
||||||
|
_inherit = 'ir.qweb.field'
|
||||||
|
|
||||||
|
def value_to_html(self, cr, uid, value, column, options=None, context=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
|
||||||
|
raise ValueError("Invalid image content")
|
||||||
|
|
||||||
|
return HTMLSafe('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value))
|
||||||
|
|
||||||
|
class MonetaryConverter(osv.AbstractModel):
|
||||||
|
""" ``monetary`` converter, has a mandatory option
|
||||||
|
``display_currency``.
|
||||||
|
|
||||||
|
The currency is used for formatting *and rounding* of the float value. It
|
||||||
|
is assumed that the linked res_currency has a non-empty rounding value and
|
||||||
|
res.currency's ``round`` method is used to perform rounding.
|
||||||
|
|
||||||
|
.. note:: the monetary converter internally adds the qweb context to its
|
||||||
|
options mapping, so that the context is available to callees.
|
||||||
|
It's set under the ``_qweb_context`` key.
|
||||||
|
"""
|
||||||
|
_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):
|
||||||
|
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)
|
||||||
|
|
||||||
|
def record_to_html(self, cr, uid, field_name, record, column, options, context=None):
|
||||||
|
if context is None:
|
||||||
|
context = {}
|
||||||
|
Currency = self.pool['res.currency']
|
||||||
|
display = self.display_currency(cr, uid, options)
|
||||||
|
|
||||||
|
# lang.format mandates a sprintf-style format. These formats are non-
|
||||||
|
# minimal (they have a default fixed precision instead), and
|
||||||
|
# lang.format will not set one by default. currency.round will not
|
||||||
|
# provide one either. So we need to generate a precision value
|
||||||
|
# (integer > 0) from the currency's rounding (a float generally < 1.0).
|
||||||
|
#
|
||||||
|
# The log10 of the rounding should be the number of digits involved if
|
||||||
|
# negative, if positive clamp to 0 digits and call it a day.
|
||||||
|
# nb: int() ~ floor(), we want nearest rounding instead
|
||||||
|
precision = int(round(math.log10(display.rounding)))
|
||||||
|
fmt = "%.{0}f".format(-precision if precision < 0 else 0)
|
||||||
|
|
||||||
|
lang_code = 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, record[field_name]),
|
||||||
|
grouping=True, monetary=True)
|
||||||
|
|
||||||
|
pre = post = u''
|
||||||
|
if display.position == 'before':
|
||||||
|
pre = u'{symbol} '
|
||||||
|
else:
|
||||||
|
post = u' {symbol}'
|
||||||
|
|
||||||
|
return HTMLSafe(u'{pre}<span class="oe_currency_value">{0}</span>{post}'.format(
|
||||||
|
formatted_amount,
|
||||||
|
pre=pre, post=post,
|
||||||
|
).format(
|
||||||
|
symbol=display.symbol,
|
||||||
|
))
|
||||||
|
|
||||||
|
def display_currency(self, cr, uid, options):
|
||||||
|
return self.qweb_object().eval_object(
|
||||||
|
options['display_currency'], options['_qweb_context'])
|
||||||
|
|
||||||
|
TIMEDELTA_UNITS = (
|
||||||
|
('year', 3600 * 24 * 365),
|
||||||
|
('month', 3600 * 24 * 30),
|
||||||
|
('week', 3600 * 24 * 7),
|
||||||
|
('day', 3600 * 24),
|
||||||
|
('hour', 3600),
|
||||||
|
('minute', 60),
|
||||||
|
('second', 1)
|
||||||
|
)
|
||||||
|
class DurationConverter(osv.AbstractModel):
|
||||||
|
""" ``duration`` converter, to display integral or fractional values as
|
||||||
|
human-readable time spans (e.g. 1.5 as "1 hour 30 minutes").
|
||||||
|
|
||||||
|
Can be used on any numerical field.
|
||||||
|
|
||||||
|
Has a mandatory option ``unit`` which can be one of ``second``, ``minute``,
|
||||||
|
``hour``, ``day``, ``week`` or ``year``, used to interpret the numerical
|
||||||
|
field value before converting it.
|
||||||
|
|
||||||
|
Sub-second values will be ignored.
|
||||||
|
"""
|
||||||
|
_name = 'ir.qweb.field.duration'
|
||||||
|
_inherit = 'ir.qweb.field'
|
||||||
|
|
||||||
|
def value_to_html(self, cr, uid, value, column, options=None, context=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)
|
||||||
|
factor = units[options['unit']]
|
||||||
|
|
||||||
|
sections = []
|
||||||
|
r = value * factor
|
||||||
|
for unit, secs_per_unit in TIMEDELTA_UNITS:
|
||||||
|
v, r = divmod(r, secs_per_unit)
|
||||||
|
if not v: continue
|
||||||
|
section = babel.dates.format_timedelta(
|
||||||
|
v*secs_per_unit, threshold=1, locale=locale)
|
||||||
|
if section:
|
||||||
|
sections.append(section)
|
||||||
|
return u' '.join(sections)
|
||||||
|
|
||||||
|
class RelativeDatetimeConverter(osv.AbstractModel):
|
||||||
|
_name = 'ir.qweb.field.relative'
|
||||||
|
_inherit = 'ir.qweb.field'
|
||||||
|
|
||||||
|
def value_to_html(self, cr, uid, value, column, options=None, context=None):
|
||||||
|
parse_format = openerp.tools.DEFAULT_SERVER_DATETIME_FORMAT
|
||||||
|
locale = babel.Locale.parse(
|
||||||
|
self.user_lang(cr, uid, context=context).code)
|
||||||
|
|
||||||
|
if isinstance(value, basestring):
|
||||||
|
value = datetime.datetime.strptime(value, parse_format)
|
||||||
|
|
||||||
|
# value should be a naive datetime in UTC. So is fields.datetime.now()
|
||||||
|
reference = datetime.datetime.strptime(column.now(), parse_format)
|
||||||
|
|
||||||
|
return babel.dates.format_timedelta(
|
||||||
|
value - reference, add_direction=True, locale=locale)
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
the result wrapped in an HTMLSafe object.
|
||||||
|
|
||||||
|
:param str string:
|
||||||
|
:param dict options:
|
||||||
|
:rtype: HTMLSafe
|
||||||
|
"""
|
||||||
|
if options is None: options = {}
|
||||||
|
|
||||||
|
if options.get('html-escape', True):
|
||||||
|
string = werkzeug.utils.escape(string)
|
||||||
|
return HTMLSafe(string.replace('\n', '<br>\n'))
|
||||||
|
|
||||||
|
def get_field_type(column, options):
|
||||||
|
""" Gets a t-field's effective type from the field's column and its options
|
||||||
|
"""
|
||||||
|
return options.get('widget', column._type)
|
||||||
|
|
||||||
|
# vim:et:
|
|
@ -268,13 +268,8 @@ class ir_translation(osv.osv):
|
||||||
return translations
|
return translations
|
||||||
|
|
||||||
def _set_ids(self, cr, uid, name, tt, lang, ids, value, src=None):
|
def _set_ids(self, cr, uid, name, tt, lang, ids, value, src=None):
|
||||||
# clear the caches
|
self._get_ids.clear_cache(self)
|
||||||
tr = self._get_ids(cr, uid, name, tt, lang, ids)
|
self._get_source.clear_cache(self)
|
||||||
for res_id in tr:
|
|
||||||
if tr[res_id]:
|
|
||||||
self._get_source.clear_cache(self, uid, name, tt, lang, tr[res_id])
|
|
||||||
self._get_ids.clear_cache(self, uid, name, tt, lang, res_id)
|
|
||||||
self._get_source.clear_cache(self, uid, name, tt, lang)
|
|
||||||
|
|
||||||
cr.execute('delete from ir_translation '
|
cr.execute('delete from ir_translation '
|
||||||
'where lang=%s '
|
'where lang=%s '
|
||||||
|
@ -294,7 +289,7 @@ class ir_translation(osv.osv):
|
||||||
return len(ids)
|
return len(ids)
|
||||||
|
|
||||||
@tools.ormcache(skiparg=3)
|
@tools.ormcache(skiparg=3)
|
||||||
def _get_source(self, cr, uid, name, types, lang, source=None):
|
def _get_source(self, cr, uid, name, types, lang, source=None, res_id=None):
|
||||||
"""
|
"""
|
||||||
Returns the translation for the given combination of name, type, language
|
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),
|
and source. All values passed to this method should be unicode (not byte strings),
|
||||||
|
@ -304,6 +299,7 @@ class ir_translation(osv.osv):
|
||||||
:param types: single string defining type of term to translate (see ``type`` field on ir.translation), or sequence of allowed types (strings)
|
:param types: single string defining type of term to translate (see ``type`` field on ir.translation), or sequence of allowed types (strings)
|
||||||
:param lang: language code of the desired translation
|
:param lang: language code of the desired translation
|
||||||
:param source: optional source term to translate (should be unicode)
|
:param source: optional source term to translate (should be unicode)
|
||||||
|
:param res_id: optional resource id to translate (if used, ``source`` should be set)
|
||||||
:rtype: unicode
|
:rtype: unicode
|
||||||
:return: the request translation, or an empty unicode string if no translation was
|
:return: the request translation, or an empty unicode string if no translation was
|
||||||
found and `source` was not passed
|
found and `source` was not passed
|
||||||
|
@ -321,6 +317,9 @@ class ir_translation(osv.osv):
|
||||||
AND type in %s
|
AND type in %s
|
||||||
AND src=%s"""
|
AND src=%s"""
|
||||||
params = (lang or '', types, tools.ustr(source))
|
params = (lang or '', types, tools.ustr(source))
|
||||||
|
if res_id:
|
||||||
|
query += "AND res_id=%s"
|
||||||
|
params += (res_id,)
|
||||||
if name:
|
if name:
|
||||||
query += " AND name=%s"
|
query += " AND name=%s"
|
||||||
params += (tools.ustr(name),)
|
params += (tools.ustr(name),)
|
||||||
|
@ -342,8 +341,9 @@ class ir_translation(osv.osv):
|
||||||
if context is None:
|
if context is None:
|
||||||
context = {}
|
context = {}
|
||||||
ids = super(ir_translation, self).create(cr, uid, vals, context=context)
|
ids = super(ir_translation, self).create(cr, uid, vals, context=context)
|
||||||
self._get_source.clear_cache(self, uid, vals.get('name',0), vals.get('type',0), vals.get('lang',0), vals.get('src',0))
|
self._get_source.clear_cache(self)
|
||||||
self._get_ids.clear_cache(self, uid, vals.get('name',0), vals.get('type',0), vals.get('lang',0), vals.get('res_id',0))
|
self._get_ids.clear_cache(self)
|
||||||
|
self.pool['ir.ui.view'].clear_cache()
|
||||||
return ids
|
return ids
|
||||||
|
|
||||||
def write(self, cursor, user, ids, vals, context=None):
|
def write(self, cursor, user, ids, vals, context=None):
|
||||||
|
@ -356,9 +356,9 @@ class ir_translation(osv.osv):
|
||||||
if vals.get('value'):
|
if vals.get('value'):
|
||||||
vals.update({'state':'translated'})
|
vals.update({'state':'translated'})
|
||||||
result = super(ir_translation, self).write(cursor, user, ids, vals, context=context)
|
result = super(ir_translation, self).write(cursor, user, ids, vals, context=context)
|
||||||
for trans_obj in self.read(cursor, user, ids, ['name','type','res_id','src','lang'], context=context):
|
self._get_source.clear_cache(self)
|
||||||
self._get_source.clear_cache(self, user, trans_obj['name'], trans_obj['type'], trans_obj['lang'], trans_obj['src'])
|
self._get_ids.clear_cache(self)
|
||||||
self._get_ids.clear_cache(self, user, trans_obj['name'], trans_obj['type'], trans_obj['lang'], trans_obj['res_id'])
|
self.pool['ir.ui.view'].clear_cache()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def unlink(self, cursor, user, ids, context=None):
|
def unlink(self, cursor, user, ids, context=None):
|
||||||
|
@ -366,9 +366,9 @@ class ir_translation(osv.osv):
|
||||||
context = {}
|
context = {}
|
||||||
if isinstance(ids, (int, long)):
|
if isinstance(ids, (int, long)):
|
||||||
ids = [ids]
|
ids = [ids]
|
||||||
for trans_obj in self.read(cursor, user, ids, ['name','type','res_id','src','lang'], context=context):
|
|
||||||
self._get_source.clear_cache(self, user, trans_obj['name'], trans_obj['type'], trans_obj['lang'], trans_obj['src'])
|
self._get_source.clear_cache(self)
|
||||||
self._get_ids.clear_cache(self, user, trans_obj['name'], trans_obj['type'], trans_obj['lang'], trans_obj['res_id'])
|
self._get_ids.clear_cache(self)
|
||||||
result = super(ir_translation, self).unlink(cursor, user, ids, context=context)
|
result = super(ir_translation, self).unlink(cursor, user, ids, context=context)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
@ -18,20 +18,28 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
import collections
|
||||||
|
import copy
|
||||||
import logging
|
import logging
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import HTMLParser
|
||||||
|
|
||||||
|
import openerp
|
||||||
from openerp import tools
|
from openerp import tools
|
||||||
from openerp.osv import fields,osv
|
from openerp.osv import fields, osv, orm
|
||||||
from openerp.tools import graph
|
from openerp.tools import graph, SKIPPED_ELEMENT_TYPES
|
||||||
from openerp.tools.safe_eval import safe_eval as eval
|
from openerp.tools.safe_eval import safe_eval as eval
|
||||||
from openerp.tools.view_validation import valid_view
|
from openerp.tools.view_validation import valid_view
|
||||||
|
from openerp.tools import misc
|
||||||
|
from openerp.tools.translate import _
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MOVABLE_BRANDING = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-xpath']
|
||||||
|
|
||||||
class view_custom(osv.osv):
|
class view_custom(osv.osv):
|
||||||
_name = 'ir.ui.view.custom'
|
_name = 'ir.ui.view.custom'
|
||||||
_order = 'create_date desc' # search(limit=1) should return the last customization
|
_order = 'create_date desc' # search(limit=1) should return the last customization
|
||||||
|
@ -50,59 +58,45 @@ class view_custom(osv.osv):
|
||||||
class view(osv.osv):
|
class view(osv.osv):
|
||||||
_name = 'ir.ui.view'
|
_name = 'ir.ui.view'
|
||||||
|
|
||||||
def _type_field(self, cr, uid, ids, name, args, context=None):
|
def _get_model_data(self, cr, uid, ids, *args, **kwargs):
|
||||||
result = {}
|
ir_model_data = self.pool.get('ir.model.data')
|
||||||
for record in self.browse(cr, uid, ids, context):
|
data_ids = ir_model_data.search(cr, uid, [('model', '=', self._name), ('res_id', 'in', ids)])
|
||||||
# Get the type from the inherited view if any.
|
result = dict(zip(ids, data_ids))
|
||||||
if record.inherit_id:
|
|
||||||
result[record.id] = record.inherit_id.type
|
|
||||||
else:
|
|
||||||
result[record.id] = etree.fromstring(record.arch.encode('utf8')).tag
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
_columns = {
|
_columns = {
|
||||||
'name': fields.char('View Name', required=True),
|
'name': fields.char('View Name', required=True),
|
||||||
'model': fields.char('Object', size=64, required=True, select=True),
|
'model': fields.char('Object', select=True),
|
||||||
'priority': fields.integer('Sequence', required=True),
|
'priority': fields.integer('Sequence', required=True),
|
||||||
'type': fields.function(_type_field, type='selection', selection=[
|
'type': fields.selection([
|
||||||
('tree','Tree'),
|
('tree','Tree'),
|
||||||
('form','Form'),
|
('form','Form'),
|
||||||
('mdx','mdx'),
|
|
||||||
('graph', 'Graph'),
|
('graph', 'Graph'),
|
||||||
('calendar', 'Calendar'),
|
('calendar', 'Calendar'),
|
||||||
('diagram','Diagram'),
|
('diagram','Diagram'),
|
||||||
('gantt', 'Gantt'),
|
('gantt', 'Gantt'),
|
||||||
('kanban', 'Kanban'),
|
('kanban', 'Kanban'),
|
||||||
('search','Search')], string='View Type', required=True, select=True, store=True),
|
('search','Search'),
|
||||||
|
('qweb', 'QWeb')], string='View Type'),
|
||||||
'arch': fields.text('View Architecture', required=True),
|
'arch': fields.text('View Architecture', required=True),
|
||||||
'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True),
|
'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True),
|
||||||
'field_parent': fields.char('Child Field',size=64),
|
'inherit_children_ids': fields.one2many('ir.ui.view','inherit_id', 'Inherit Views'),
|
||||||
|
'field_parent': fields.char('Child Field'),
|
||||||
|
'model_data_id': fields.function(_get_model_data, type='many2one', relation='ir.model.data', string="Model Data", store=True),
|
||||||
'xml_id': fields.function(osv.osv.get_xml_id, type='char', size=128, string="External ID",
|
'xml_id': fields.function(osv.osv.get_xml_id, type='char', size=128, string="External ID",
|
||||||
help="ID of the view defined in xml file"),
|
help="ID of the view defined in xml file"),
|
||||||
'groups_id': fields.many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id',
|
'groups_id': fields.many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id',
|
||||||
string='Groups', help="If this field is empty, the view applies to all users. Otherwise, the view applies to the users of those groups only."),
|
string='Groups', help="If this field is empty, the view applies to all users. Otherwise, the view applies to the users of those groups only."),
|
||||||
|
'model_ids': fields.one2many('ir.model.data', 'res_id', domain=[('model','=','ir.ui.view')], auto_join=True),
|
||||||
}
|
}
|
||||||
_defaults = {
|
_defaults = {
|
||||||
'arch': '<?xml version="1.0"?>\n<tree string="My view">\n\t<field name="name"/>\n</tree>',
|
|
||||||
'priority': 16,
|
'priority': 16,
|
||||||
'type': 'tree',
|
|
||||||
}
|
}
|
||||||
_order = "priority,name"
|
_order = "priority,name"
|
||||||
|
|
||||||
# Holds the RNG schema
|
# Holds the RNG schema
|
||||||
_relaxng_validator = None
|
_relaxng_validator = None
|
||||||
|
|
||||||
def create(self, cr, uid, values, context=None):
|
|
||||||
if 'type' in values:
|
|
||||||
_logger.warning("Setting the `type` field is deprecated in the `ir.ui.view` model.")
|
|
||||||
if not values.get('name'):
|
|
||||||
if values.get('inherit_id'):
|
|
||||||
inferred_type = self.browse(cr, uid, values['inherit_id'], context).type
|
|
||||||
else:
|
|
||||||
inferred_type = etree.fromstring(values['arch'].encode('utf8')).tag
|
|
||||||
values['name'] = "%s %s" % (values['model'], inferred_type)
|
|
||||||
return super(view, self).create(cr, uid, values, context)
|
|
||||||
|
|
||||||
def _relaxng(self):
|
def _relaxng(self):
|
||||||
if not self._relaxng_validator:
|
if not self._relaxng_validator:
|
||||||
frng = tools.file_open(os.path.join('base','rng','view.rng'))
|
frng = tools.file_open(os.path.join('base','rng','view.rng'))
|
||||||
|
@ -115,59 +109,37 @@ class view(osv.osv):
|
||||||
frng.close()
|
frng.close()
|
||||||
return self._relaxng_validator
|
return self._relaxng_validator
|
||||||
|
|
||||||
def _check_render_view(self, cr, uid, view, context=None):
|
|
||||||
"""Verify that the given view's hierarchy is valid for rendering, along with all the changes applied by
|
|
||||||
its inherited views, by rendering it using ``fields_view_get()``.
|
|
||||||
|
|
||||||
@param browse_record view: view to validate
|
|
||||||
@return: the rendered definition (arch) of the view, always utf-8 bytestring (legacy convention)
|
|
||||||
if no error occurred, else False.
|
|
||||||
"""
|
|
||||||
if view.model not in self.pool:
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
fvg = self.pool[view.model].fields_view_get(cr, uid, view_id=view.id, view_type=view.type, context=context)
|
|
||||||
return fvg['arch']
|
|
||||||
except Exception:
|
|
||||||
_logger.exception('cannot render view %s', view.xml_id)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _check_xml(self, cr, uid, ids, context=None):
|
def _check_xml(self, cr, uid, ids, context=None):
|
||||||
if context is None:
|
if context is None:
|
||||||
context = {}
|
context = {}
|
||||||
context['check_view_ids'] = ids
|
context = dict(context, check_view_ids=ids)
|
||||||
|
|
||||||
|
# Sanity checks: the view should not break anything upon rendering!
|
||||||
|
# Any exception raised below will cause a transaction rollback.
|
||||||
for view in self.browse(cr, uid, ids, context):
|
for view in self.browse(cr, uid, ids, context):
|
||||||
# Sanity check: the view should not break anything upon rendering!
|
view_def = self.read_combined(cr, uid, view.id, None, context=context)
|
||||||
view_arch_utf8 = self._check_render_view(cr, uid, view, context=context)
|
view_arch_utf8 = view_def['arch']
|
||||||
# always utf-8 bytestring - legacy convention
|
if view.type != 'qweb':
|
||||||
if not view_arch_utf8: return False
|
view_doc = etree.fromstring(view_arch_utf8)
|
||||||
|
# verify that all fields used are valid, etc.
|
||||||
# RNG-based validation is not possible anymore with 7.0 forms
|
self.postprocess_and_fields(cr, uid, view.model, view_doc, view.id, context=context)
|
||||||
# TODO 7.0: provide alternative assertion-based validation of view_arch_utf8
|
# RNG-based validation is not possible anymore with 7.0 forms
|
||||||
view_docs = [etree.fromstring(view_arch_utf8)]
|
view_docs = [view_doc]
|
||||||
if view_docs[0].tag == 'data':
|
if view_docs[0].tag == 'data':
|
||||||
# A <data> element is a wrapper for multiple root nodes
|
# A <data> element is a wrapper for multiple root nodes
|
||||||
view_docs = view_docs[0]
|
view_docs = view_docs[0]
|
||||||
validator = self._relaxng()
|
validator = self._relaxng()
|
||||||
for view_arch in view_docs:
|
for view_arch in view_docs:
|
||||||
if (view_arch.get('version') < '7.0') and validator and not validator.validate(view_arch):
|
if (view_arch.get('version') < '7.0') and validator and not validator.validate(view_arch):
|
||||||
for error in validator.error_log:
|
for error in validator.error_log:
|
||||||
_logger.error(tools.ustr(error))
|
_logger.error(tools.ustr(error))
|
||||||
return False
|
return False
|
||||||
if not valid_view(view_arch):
|
if not valid_view(view_arch):
|
||||||
return False
|
return False
|
||||||
return True
|
|
||||||
|
|
||||||
def _check_model(self, cr, uid, ids, context=None):
|
|
||||||
for view in self.browse(cr, uid, ids, context):
|
|
||||||
if view.model not in self.pool:
|
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
_constraints = [
|
_constraints = [
|
||||||
(_check_model, 'The model name does not exist.', ['model']),
|
(_check_xml, 'Invalid view definition', ['arch'])
|
||||||
(_check_xml, 'The model name does not exist or the view architecture cannot be rendered.', ['arch', 'model']),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def _auto_init(self, cr, context=None):
|
def _auto_init(self, cr, context=None):
|
||||||
|
@ -176,6 +148,73 @@ class view(osv.osv):
|
||||||
if not cr.fetchone():
|
if not cr.fetchone():
|
||||||
cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
|
cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
|
||||||
|
|
||||||
|
def create(self, cr, uid, values, context=None):
|
||||||
|
if 'type' not in values:
|
||||||
|
if values.get('inherit_id'):
|
||||||
|
values['type'] = self.browse(cr, uid, values['inherit_id'], context).type
|
||||||
|
else:
|
||||||
|
values['type'] = etree.fromstring(values['arch']).tag
|
||||||
|
|
||||||
|
if not values.get('name'):
|
||||||
|
values['name'] = "%s %s" % (values['model'], values['type'])
|
||||||
|
|
||||||
|
self.read_template.clear_cache(self)
|
||||||
|
return super(view, self).create(cr, uid, values, context)
|
||||||
|
|
||||||
|
def write(self, cr, uid, ids, vals, context=None):
|
||||||
|
if not isinstance(ids, (list, tuple)):
|
||||||
|
ids = [ids]
|
||||||
|
if context is None:
|
||||||
|
context = {}
|
||||||
|
|
||||||
|
# drop the corresponding view customizations (used for dashboards for example), otherwise
|
||||||
|
# not all users would see the updated views
|
||||||
|
custom_view_ids = self.pool.get('ir.ui.view.custom').search(cr, uid, [('ref_id', 'in', ids)])
|
||||||
|
if custom_view_ids:
|
||||||
|
self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
|
||||||
|
|
||||||
|
self.read_template.clear_cache(self)
|
||||||
|
ret = super(view, self).write(cr, uid, ids, vals, context)
|
||||||
|
|
||||||
|
# if arch is modified views become noupdatable
|
||||||
|
if 'arch' in vals and not context.get('install_mode', False):
|
||||||
|
# TODO: should be doable in a read and a write
|
||||||
|
for view_ in self.browse(cr, uid, ids, context=context):
|
||||||
|
if view_.model_data_id:
|
||||||
|
self.pool.get('ir.model.data').write(cr, openerp.SUPERUSER_ID, view_.model_data_id.id, {'noupdate': True})
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def copy(self, cr, uid, id, default=None, context=None):
|
||||||
|
if not default:
|
||||||
|
default = {}
|
||||||
|
default.update({
|
||||||
|
'model_ids': [],
|
||||||
|
})
|
||||||
|
return super(view, self).copy(cr, uid, id, default, context=context)
|
||||||
|
|
||||||
|
# default view selection
|
||||||
|
def default_view(self, cr, uid, model, view_type, context=None):
|
||||||
|
""" Fetches the default view for the provided (model, view_type) pair:
|
||||||
|
view with no parent (inherit_id=Fase) with the lowest priority.
|
||||||
|
|
||||||
|
:param str model:
|
||||||
|
:param int view_type:
|
||||||
|
:return: id of the default view of False if none found
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
domain = [
|
||||||
|
['model', '=', model],
|
||||||
|
['type', '=', view_type],
|
||||||
|
['inherit_id', '=', False],
|
||||||
|
]
|
||||||
|
ids = self.search(cr, uid, domain, limit=1, order='priority', context=context)
|
||||||
|
if not ids:
|
||||||
|
return False
|
||||||
|
return ids[0]
|
||||||
|
|
||||||
|
#------------------------------------------------------
|
||||||
|
# Inheritance mecanism
|
||||||
|
#------------------------------------------------------
|
||||||
def get_inheriting_views_arch(self, cr, uid, view_id, model, context=None):
|
def get_inheriting_views_arch(self, cr, uid, view_id, model, context=None):
|
||||||
"""Retrieves the architecture of views that inherit from the given view, from the sets of
|
"""Retrieves the architecture of views that inherit from the given view, from the sets of
|
||||||
views that should currently be used in the system. During the module upgrade phase it
|
views that should currently be used in the system. During the module upgrade phase it
|
||||||
|
@ -185,43 +224,595 @@ class view(osv.osv):
|
||||||
after the module initialization phase is completely finished.
|
after the module initialization phase is completely finished.
|
||||||
|
|
||||||
:param int view_id: id of the view whose inheriting views should be retrieved
|
:param int view_id: id of the view whose inheriting views should be retrieved
|
||||||
:param str model: model identifier of the view's related model (for double-checking)
|
:param str model: model identifier of the inheriting views.
|
||||||
:rtype: list of tuples
|
:rtype: list of tuples
|
||||||
:return: [(view_arch,view_id), ...]
|
:return: [(view_arch,view_id), ...]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id)
|
user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id)
|
||||||
|
|
||||||
|
check_view_ids = context and context.get('check_view_ids') or (0,)
|
||||||
|
conditions = [['inherit_id', '=', view_id], ['model', '=', model]]
|
||||||
if self.pool._init:
|
if self.pool._init:
|
||||||
# Module init currently in progress, only consider views from modules whose code was already loaded
|
# Module init currently in progress, only consider views from
|
||||||
check_view_ids = context and context.get('check_view_ids') or (0,)
|
# modules whose code is already loaded
|
||||||
query = """SELECT v.id FROM ir_ui_view v LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
|
conditions.extend([
|
||||||
WHERE v.inherit_id=%s AND v.model=%s AND (md.module in %s OR v.id in %s)
|
'|',
|
||||||
ORDER BY priority"""
|
['model_ids.module', 'in', tuple(self.pool._init_modules)],
|
||||||
query_params = (view_id, model, tuple(self.pool._init_modules), tuple(check_view_ids))
|
['id', 'in', check_view_ids],
|
||||||
else:
|
])
|
||||||
# Modules fully loaded, consider all views
|
view_ids = self.search(cr, uid, conditions, context=context)
|
||||||
query = """SELECT v.id FROM ir_ui_view v
|
|
||||||
WHERE v.inherit_id=%s AND v.model=%s
|
|
||||||
ORDER BY priority"""
|
|
||||||
query_params = (view_id, model)
|
|
||||||
cr.execute(query, query_params)
|
|
||||||
view_ids = [v[0] for v in cr.fetchall()]
|
|
||||||
# filter views based on user groups
|
|
||||||
return [(view.arch, view.id)
|
return [(view.arch, view.id)
|
||||||
for view in self.browse(cr, 1, view_ids, context)
|
for view in self.browse(cr, 1, view_ids, context)
|
||||||
if not (view.groups_id and user_groups.isdisjoint(view.groups_id))]
|
if not (view.groups_id and user_groups.isdisjoint(view.groups_id))]
|
||||||
|
|
||||||
def write(self, cr, uid, ids, vals, context=None):
|
def raise_view_error(self, cr, uid, message, view_id, context=None):
|
||||||
if not isinstance(ids, (list, tuple)):
|
view = self.browse(cr, uid, view_id, context)
|
||||||
ids = [ids]
|
not_avail = _('n/a')
|
||||||
|
message = ("%(msg)s\n\n" +
|
||||||
|
_("Error context:\nView `%(view_name)s`") +
|
||||||
|
"\n[view_id: %(viewid)s, xml_id: %(xmlid)s, "
|
||||||
|
"model: %(model)s, parent_id: %(parent)s]") % \
|
||||||
|
{
|
||||||
|
'view_name': view.name or not_avail,
|
||||||
|
'viewid': view_id or not_avail,
|
||||||
|
'xmlid': view.xml_id or not_avail,
|
||||||
|
'model': view.model or not_avail,
|
||||||
|
'parent': view.inherit_id.id or not_avail,
|
||||||
|
'msg': message,
|
||||||
|
}
|
||||||
|
_logger.error(message)
|
||||||
|
raise AttributeError(message)
|
||||||
|
|
||||||
# drop the corresponding view customizations (used for dashboards for example), otherwise
|
def locate_node(self, arch, spec):
|
||||||
# not all users would see the updated views
|
""" Locate a node in a source (parent) architecture.
|
||||||
custom_view_ids = self.pool.get('ir.ui.view.custom').search(cr, uid, [('ref_id','in',ids)])
|
|
||||||
if custom_view_ids:
|
|
||||||
self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
|
|
||||||
|
|
||||||
return super(view, self).write(cr, uid, ids, vals, context)
|
Given a complete source (parent) architecture (i.e. the field
|
||||||
|
`arch` in a view), and a 'spec' node (a node in an inheriting
|
||||||
|
view that specifies the location in the source view of what
|
||||||
|
should be changed), return (if it exists) the node in the
|
||||||
|
source view matching the specification.
|
||||||
|
|
||||||
|
:param arch: a parent architecture to modify
|
||||||
|
:param spec: a modifying node in an inheriting view
|
||||||
|
:return: a node in the source matching the spec
|
||||||
|
"""
|
||||||
|
if spec.tag == 'xpath':
|
||||||
|
nodes = arch.xpath(spec.get('expr'))
|
||||||
|
return nodes[0] if nodes else None
|
||||||
|
elif spec.tag == 'field':
|
||||||
|
# Only compare the field name: a field can be only once in a given view
|
||||||
|
# at a given level (and for multilevel expressions, we should use xpath
|
||||||
|
# inheritance spec anyway).
|
||||||
|
for node in arch.iter('field'):
|
||||||
|
if node.get('name') == spec.get('name'):
|
||||||
|
return node
|
||||||
|
return None
|
||||||
|
|
||||||
|
for node in arch.iter(spec.tag):
|
||||||
|
if isinstance(node, SKIPPED_ELEMENT_TYPES):
|
||||||
|
continue
|
||||||
|
if all(node.get(attr) == spec.get(attr) for attr in spec.attrib
|
||||||
|
if attr not in ('position','version')):
|
||||||
|
# Version spec should match parent's root element's version
|
||||||
|
if spec.get('version') and spec.get('version') != arch.get('version'):
|
||||||
|
return None
|
||||||
|
return node
|
||||||
|
return None
|
||||||
|
|
||||||
|
def inherit_branding(self, specs_tree, view_id, source_id):
|
||||||
|
for node in specs_tree.iterchildren(tag=etree.Element):
|
||||||
|
xpath = node.getroottree().getpath(node)
|
||||||
|
if node.tag == 'data' or node.tag == 'xpath':
|
||||||
|
self.inherit_branding(node, view_id, source_id)
|
||||||
|
else:
|
||||||
|
node.set('data-oe-id', str(view_id))
|
||||||
|
node.set('data-oe-source-id', str(source_id))
|
||||||
|
node.set('data-oe-xpath', xpath)
|
||||||
|
node.set('data-oe-model', 'ir.ui.view')
|
||||||
|
node.set('data-oe-field', 'arch')
|
||||||
|
|
||||||
|
return specs_tree
|
||||||
|
|
||||||
|
def apply_inheritance_specs(self, cr, uid, source, specs_tree, inherit_id, context=None):
|
||||||
|
""" Apply an inheriting view (a descendant of the base view)
|
||||||
|
|
||||||
|
Apply to a source architecture all the spec nodes (i.e. nodes
|
||||||
|
describing where and what changes to apply to some parent
|
||||||
|
architecture) given by an inheriting view.
|
||||||
|
|
||||||
|
:param Element source: a parent architecture to modify
|
||||||
|
:param Elepect specs_tree: a modifying architecture in an inheriting view
|
||||||
|
:param inherit_id: the database id of specs_arch
|
||||||
|
:return: a modified source where the specs are applied
|
||||||
|
:rtype: Element
|
||||||
|
"""
|
||||||
|
# Queue of specification nodes (i.e. nodes describing where and
|
||||||
|
# changes to apply to some parent architecture).
|
||||||
|
specs = [specs_tree]
|
||||||
|
|
||||||
|
while len(specs):
|
||||||
|
spec = specs.pop(0)
|
||||||
|
if isinstance(spec, SKIPPED_ELEMENT_TYPES):
|
||||||
|
continue
|
||||||
|
if spec.tag == 'data':
|
||||||
|
specs += [ c for c in specs_tree ]
|
||||||
|
continue
|
||||||
|
node = self.locate_node(source, spec)
|
||||||
|
if node is not None:
|
||||||
|
pos = spec.get('position', 'inside')
|
||||||
|
if pos == 'replace':
|
||||||
|
if node.getparent() is None:
|
||||||
|
source = copy.deepcopy(spec[0])
|
||||||
|
else:
|
||||||
|
for child in spec:
|
||||||
|
node.addprevious(child)
|
||||||
|
node.getparent().remove(node)
|
||||||
|
elif pos == 'attributes':
|
||||||
|
for child in spec.getiterator('attribute'):
|
||||||
|
attribute = (child.get('name'), child.text and child.text.encode('utf8') or None)
|
||||||
|
if attribute[1]:
|
||||||
|
node.set(attribute[0], attribute[1])
|
||||||
|
elif attribute[0] in node.attrib:
|
||||||
|
del node.attrib[attribute[0]]
|
||||||
|
else:
|
||||||
|
sib = node.getnext()
|
||||||
|
for child in spec:
|
||||||
|
if pos == 'inside':
|
||||||
|
node.append(child)
|
||||||
|
elif pos == 'after':
|
||||||
|
if sib is None:
|
||||||
|
node.addnext(child)
|
||||||
|
node = child
|
||||||
|
else:
|
||||||
|
sib.addprevious(child)
|
||||||
|
elif pos == 'before':
|
||||||
|
node.addprevious(child)
|
||||||
|
else:
|
||||||
|
self.raise_view_error(cr, uid, _("Invalid position attribute: '%s'") % pos, inherit_id, context=context)
|
||||||
|
else:
|
||||||
|
attrs = ''.join([
|
||||||
|
' %s="%s"' % (attr, spec.get(attr))
|
||||||
|
for attr in spec.attrib
|
||||||
|
if attr != 'position'
|
||||||
|
])
|
||||||
|
tag = "<%s%s>" % (spec.tag, attrs)
|
||||||
|
self.raise_view_error(cr, uid, _("Element '%s' cannot be located in parent view") % tag, inherit_id, context=context)
|
||||||
|
|
||||||
|
return source
|
||||||
|
|
||||||
|
def apply_view_inheritance(self, cr, uid, source, source_id, model, context=None):
|
||||||
|
""" Apply all the (directly and indirectly) inheriting views.
|
||||||
|
|
||||||
|
:param source: a parent architecture to modify (with parent modifications already applied)
|
||||||
|
:param source_id: the database view_id of the parent view
|
||||||
|
:param model: the original model for which we create a view (not
|
||||||
|
necessarily the same as the source's model); only the inheriting
|
||||||
|
views with that specific model will be applied.
|
||||||
|
:return: a modified source where all the modifying architecture are applied
|
||||||
|
"""
|
||||||
|
if context is None: context = {}
|
||||||
|
sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model, context=context)
|
||||||
|
for (specs, view_id) in sql_inherit:
|
||||||
|
specs_tree = etree.fromstring(specs.encode('utf-8'))
|
||||||
|
if context.get('inherit_branding'):
|
||||||
|
self.inherit_branding(specs_tree, view_id, source_id)
|
||||||
|
source = self.apply_inheritance_specs(cr, uid, source, specs_tree, view_id, context=context)
|
||||||
|
source = self.apply_view_inheritance(cr, uid, source, view_id, model, context=context)
|
||||||
|
return source
|
||||||
|
|
||||||
|
def read_combined(self, cr, uid, view_id, fields=None, context=None):
|
||||||
|
"""
|
||||||
|
Utility function to get a view combined with its inherited views.
|
||||||
|
|
||||||
|
* Gets the top of the view tree if a sub-view is requested
|
||||||
|
* Applies all inherited archs on the root view
|
||||||
|
* Returns the view with all requested fields
|
||||||
|
.. note:: ``arch`` is always added to the fields list even if not
|
||||||
|
requested (similar to ``id``)
|
||||||
|
"""
|
||||||
|
if context is None: context = {}
|
||||||
|
|
||||||
|
# if view_id is not a root view, climb back to the top.
|
||||||
|
base = v = self.browse(cr, uid, view_id, context=context)
|
||||||
|
while v.inherit_id:
|
||||||
|
v = v.inherit_id
|
||||||
|
root_id = v.id
|
||||||
|
|
||||||
|
# arch and model fields are always returned
|
||||||
|
if fields:
|
||||||
|
fields = list(set(fields) | set(['arch', 'model']))
|
||||||
|
|
||||||
|
# read the view arch
|
||||||
|
[view] = self.read(cr, uid, [root_id], fields=fields, context=context)
|
||||||
|
arch_tree = etree.fromstring(view['arch'].encode('utf-8'))
|
||||||
|
|
||||||
|
if context.get('inherit_branding'):
|
||||||
|
arch_tree.attrib.update({
|
||||||
|
'data-oe-model': 'ir.ui.view',
|
||||||
|
'data-oe-id': str(root_id),
|
||||||
|
'data-oe-field': 'arch',
|
||||||
|
})
|
||||||
|
|
||||||
|
# and apply inheritance
|
||||||
|
arch = self.apply_view_inheritance(
|
||||||
|
cr, uid, arch_tree, root_id, base.model, context=context)
|
||||||
|
|
||||||
|
return dict(view, arch=etree.tostring(arch, encoding='utf-8'))
|
||||||
|
|
||||||
|
#------------------------------------------------------
|
||||||
|
# Postprocessing: translation, groups and modifiers
|
||||||
|
#------------------------------------------------------
|
||||||
|
# TODO:
|
||||||
|
# - split postprocess so that it can be used instead of translate_qweb
|
||||||
|
# - remove group processing from ir_qweb
|
||||||
|
#------------------------------------------------------
|
||||||
|
def postprocess(self, cr, user, model, node, view_id, in_tree_view, model_fields, context=None):
|
||||||
|
"""Return the description of the fields in the node.
|
||||||
|
|
||||||
|
In a normal call to this method, node is a complete view architecture
|
||||||
|
but it is actually possible to give some sub-node (this is used so
|
||||||
|
that the method can call itself recursively).
|
||||||
|
|
||||||
|
Originally, the field descriptions are drawn from the node itself.
|
||||||
|
But there is now some code calling fields_get() in order to merge some
|
||||||
|
of those information in the architecture.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if context is None:
|
||||||
|
context = {}
|
||||||
|
result = False
|
||||||
|
fields = {}
|
||||||
|
children = True
|
||||||
|
|
||||||
|
modifiers = {}
|
||||||
|
Model = self.pool.get(model)
|
||||||
|
if not Model:
|
||||||
|
self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model),
|
||||||
|
view_id, context)
|
||||||
|
|
||||||
|
def encode(s):
|
||||||
|
if isinstance(s, unicode):
|
||||||
|
return s.encode('utf8')
|
||||||
|
return s
|
||||||
|
|
||||||
|
def check_group(node):
|
||||||
|
"""Apply group restrictions, may be set at view level or model level::
|
||||||
|
* at view level this means the element should be made invisible to
|
||||||
|
people who are not members
|
||||||
|
* at model level (exclusively for fields, obviously), this means
|
||||||
|
the field should be completely removed from the view, as it is
|
||||||
|
completely unavailable for non-members
|
||||||
|
|
||||||
|
:return: True if field should be included in the result of fields_view_get
|
||||||
|
"""
|
||||||
|
if node.tag == 'field' and node.get('name') in Model._all_columns:
|
||||||
|
column = Model._all_columns[node.get('name')].column
|
||||||
|
if column.groups and not self.user_has_groups(
|
||||||
|
cr, user, groups=column.groups, context=context):
|
||||||
|
node.getparent().remove(node)
|
||||||
|
fields.pop(node.get('name'), None)
|
||||||
|
# no point processing view-level ``groups`` anymore, return
|
||||||
|
return False
|
||||||
|
if node.get('groups'):
|
||||||
|
can_see = self.user_has_groups(
|
||||||
|
cr, user, groups=node.get('groups'), context=context)
|
||||||
|
if not can_see:
|
||||||
|
node.set('invisible', '1')
|
||||||
|
modifiers['invisible'] = True
|
||||||
|
if 'attrs' in node.attrib:
|
||||||
|
del(node.attrib['attrs']) #avoid making field visible later
|
||||||
|
del(node.attrib['groups'])
|
||||||
|
return True
|
||||||
|
|
||||||
|
if node.tag in ('field', 'node', 'arrow'):
|
||||||
|
if node.get('object'):
|
||||||
|
attrs = {}
|
||||||
|
views = {}
|
||||||
|
xml = "<form>"
|
||||||
|
for f in node:
|
||||||
|
if f.tag == 'field':
|
||||||
|
xml += etree.tostring(f, encoding="utf-8")
|
||||||
|
xml += "</form>"
|
||||||
|
new_xml = etree.fromstring(encode(xml))
|
||||||
|
ctx = context.copy()
|
||||||
|
ctx['base_model_name'] = model
|
||||||
|
xarch, xfields = self.postprocess_and_fields(cr, user, node.get('object'), new_xml, view_id, ctx)
|
||||||
|
views['form'] = {
|
||||||
|
'arch': xarch,
|
||||||
|
'fields': xfields
|
||||||
|
}
|
||||||
|
attrs = {'views': views}
|
||||||
|
fields = xfields
|
||||||
|
if node.get('name'):
|
||||||
|
attrs = {}
|
||||||
|
try:
|
||||||
|
if node.get('name') in Model._columns:
|
||||||
|
column = Model._columns[node.get('name')]
|
||||||
|
else:
|
||||||
|
column = Model._inherit_fields[node.get('name')][2]
|
||||||
|
except Exception:
|
||||||
|
column = False
|
||||||
|
|
||||||
|
if column:
|
||||||
|
children = False
|
||||||
|
views = {}
|
||||||
|
for f in node:
|
||||||
|
if f.tag in ('form', 'tree', 'graph', 'kanban', 'calendar'):
|
||||||
|
node.remove(f)
|
||||||
|
ctx = context.copy()
|
||||||
|
ctx['base_model_name'] = Model
|
||||||
|
xarch, xfields = self.postprocess_and_fields(cr, user, column._obj or None, f, view_id, ctx)
|
||||||
|
views[str(f.tag)] = {
|
||||||
|
'arch': xarch,
|
||||||
|
'fields': xfields
|
||||||
|
}
|
||||||
|
attrs = {'views': views}
|
||||||
|
fields[node.get('name')] = attrs
|
||||||
|
|
||||||
|
field = model_fields.get(node.get('name'))
|
||||||
|
if field:
|
||||||
|
orm.transfer_field_to_modifiers(field, modifiers)
|
||||||
|
|
||||||
|
elif node.tag in ('form', 'tree'):
|
||||||
|
result = Model.view_header_get(cr, user, False, node.tag, context)
|
||||||
|
if result:
|
||||||
|
node.set('string', result)
|
||||||
|
in_tree_view = node.tag == 'tree'
|
||||||
|
|
||||||
|
elif node.tag == 'calendar':
|
||||||
|
for additional_field in ('date_start', 'date_delay', 'date_stop', 'color', 'all_day', 'attendee'):
|
||||||
|
if node.get(additional_field):
|
||||||
|
fields[node.get(additional_field)] = {}
|
||||||
|
|
||||||
|
if not check_group(node):
|
||||||
|
# node must be removed, no need to proceed further with its children
|
||||||
|
return fields
|
||||||
|
|
||||||
|
# The view architeture overrides the python model.
|
||||||
|
# Get the attrs before they are (possibly) deleted by check_group below
|
||||||
|
orm.transfer_node_to_modifiers(node, modifiers, context, in_tree_view)
|
||||||
|
|
||||||
|
# TODO remove attrs counterpart in modifiers when invisible is true ?
|
||||||
|
|
||||||
|
# translate view
|
||||||
|
if 'lang' in context:
|
||||||
|
Translations = self.pool['ir.translation']
|
||||||
|
if node.text and node.text.strip():
|
||||||
|
trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.text.strip())
|
||||||
|
if trans:
|
||||||
|
node.text = node.text.replace(node.text.strip(), trans)
|
||||||
|
if node.tail and node.tail.strip():
|
||||||
|
trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.tail.strip())
|
||||||
|
if trans:
|
||||||
|
node.tail = node.tail.replace(node.tail.strip(), trans)
|
||||||
|
|
||||||
|
if node.get('string') and not result:
|
||||||
|
trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.get('string'))
|
||||||
|
if trans == node.get('string') and ('base_model_name' in context):
|
||||||
|
# If translation is same as source, perhaps we'd have more luck with the alternative model name
|
||||||
|
# (in case we are in a mixed situation, such as an inherited view where parent_view.model != model
|
||||||
|
trans = Translations._get_source(cr, user, context['base_model_name'], 'view', context['lang'], node.get('string'))
|
||||||
|
if trans:
|
||||||
|
node.set('string', trans)
|
||||||
|
|
||||||
|
for attr_name in ('confirm', 'sum', 'avg', 'help', 'placeholder'):
|
||||||
|
attr_value = node.get(attr_name)
|
||||||
|
if attr_value:
|
||||||
|
trans = Translations._get_source(cr, user, model, 'view', context['lang'], attr_value)
|
||||||
|
if trans:
|
||||||
|
node.set(attr_name, trans)
|
||||||
|
|
||||||
|
for f in node:
|
||||||
|
if children or (node.tag == 'field' and f.tag in ('filter','separator')):
|
||||||
|
fields.update(self.postprocess(cr, user, model, f, view_id, in_tree_view, model_fields, context))
|
||||||
|
|
||||||
|
orm.transfer_modifiers_to_node(modifiers, node)
|
||||||
|
return fields
|
||||||
|
|
||||||
|
def _disable_workflow_buttons(self, cr, user, model, node):
|
||||||
|
""" Set the buttons in node to readonly if the user can't activate them. """
|
||||||
|
if model is None or user == 1:
|
||||||
|
# admin user can always activate workflow buttons
|
||||||
|
return node
|
||||||
|
|
||||||
|
# TODO handle the case of more than one workflow for a model or multiple
|
||||||
|
# transitions with different groups and same signal
|
||||||
|
usersobj = self.pool.get('res.users')
|
||||||
|
buttons = (n for n in node.getiterator('button') if n.get('type') != 'object')
|
||||||
|
for button in buttons:
|
||||||
|
user_groups = usersobj.read(cr, user, [user], ['groups_id'])[0]['groups_id']
|
||||||
|
cr.execute("""SELECT DISTINCT t.group_id
|
||||||
|
FROM wkf
|
||||||
|
INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
|
||||||
|
INNER JOIN wkf_transition t ON (t.act_to = a.id)
|
||||||
|
WHERE wkf.osv = %s
|
||||||
|
AND t.signal = %s
|
||||||
|
AND t.group_id is NOT NULL
|
||||||
|
""", (model, button.get('name')))
|
||||||
|
group_ids = [x[0] for x in cr.fetchall() if x[0]]
|
||||||
|
can_click = not group_ids or bool(set(user_groups).intersection(group_ids))
|
||||||
|
button.set('readonly', str(int(not can_click)))
|
||||||
|
return node
|
||||||
|
|
||||||
|
def postprocess_and_fields(self, cr, user, model, node, view_id, context=None):
|
||||||
|
""" Return an architecture and a description of all the fields.
|
||||||
|
|
||||||
|
The field description combines the result of fields_get() and
|
||||||
|
postprocess().
|
||||||
|
|
||||||
|
:param node: the architecture as as an etree
|
||||||
|
:return: a tuple (arch, fields) where arch is the given node as a
|
||||||
|
string and fields is the description of all the fields.
|
||||||
|
|
||||||
|
"""
|
||||||
|
fields = {}
|
||||||
|
Model = self.pool.get(model)
|
||||||
|
if not Model:
|
||||||
|
self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model), view_id, context)
|
||||||
|
|
||||||
|
if node.tag == 'diagram':
|
||||||
|
if node.getchildren()[0].tag == 'node':
|
||||||
|
node_model = self.pool[node.getchildren()[0].get('object')]
|
||||||
|
node_fields = node_model.fields_get(cr, user, None, context)
|
||||||
|
fields.update(node_fields)
|
||||||
|
if not node.get("create") and not node_model.check_access_rights(cr, user, 'create', raise_exception=False):
|
||||||
|
node.set("create", 'false')
|
||||||
|
if node.getchildren()[1].tag == 'arrow':
|
||||||
|
arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context)
|
||||||
|
fields.update(arrow_fields)
|
||||||
|
else:
|
||||||
|
fields = Model.fields_get(cr, user, None, context)
|
||||||
|
|
||||||
|
fields_def = self.postprocess(cr, user, model, node, view_id, False, fields, context=context)
|
||||||
|
node = self._disable_workflow_buttons(cr, user, model, node)
|
||||||
|
if node.tag in ('kanban', 'tree', 'form', 'gantt'):
|
||||||
|
for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')):
|
||||||
|
if not node.get(action) and not Model.check_access_rights(cr, user, operation, raise_exception=False):
|
||||||
|
node.set(action, 'false')
|
||||||
|
arch = etree.tostring(node, encoding="utf-8").replace('\t', '')
|
||||||
|
for k in fields.keys():
|
||||||
|
if k not in fields_def:
|
||||||
|
del fields[k]
|
||||||
|
for field in fields_def:
|
||||||
|
if field == 'id':
|
||||||
|
# sometime, the view may contain the (invisible) field 'id' needed for a domain (when 2 objects have cross references)
|
||||||
|
fields['id'] = {'readonly': True, 'type': 'integer', 'string': 'ID'}
|
||||||
|
elif field in fields:
|
||||||
|
fields[field].update(fields_def[field])
|
||||||
|
else:
|
||||||
|
message = _("Field `%(field_name)s` does not exist") % \
|
||||||
|
dict(field_name=field)
|
||||||
|
self.raise_view_error(cr, user, message, view_id, context)
|
||||||
|
return arch, fields
|
||||||
|
|
||||||
|
#------------------------------------------------------
|
||||||
|
# QWeb template views
|
||||||
|
#------------------------------------------------------
|
||||||
|
@tools.ormcache_context(accepted_keys=('lang','inherit_branding', 'editable', 'translatable'))
|
||||||
|
def read_template(self, cr, uid, xml_id, context=None):
|
||||||
|
if '.' not in xml_id:
|
||||||
|
raise ValueError('Invalid template id: %r' % (xml_id,))
|
||||||
|
|
||||||
|
view_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xml_id, raise_if_not_found=True)
|
||||||
|
arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch']
|
||||||
|
arch_tree = etree.fromstring(arch)
|
||||||
|
|
||||||
|
if 'lang' in context:
|
||||||
|
arch_tree = self.translate_qweb(cr, uid, view_id, arch_tree, context['lang'], context)
|
||||||
|
|
||||||
|
self.distribute_branding(arch_tree)
|
||||||
|
root = etree.Element('templates')
|
||||||
|
root.append(arch_tree)
|
||||||
|
arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
|
||||||
|
return arch
|
||||||
|
|
||||||
|
def clear_cache(self):
|
||||||
|
self.read_template.clear_cache(self)
|
||||||
|
|
||||||
|
def distribute_branding(self, e, branding=None, parent_xpath='',
|
||||||
|
index_map=misc.ConstantMapping(1)):
|
||||||
|
if e.get('t-ignore') or e.tag == 'head':
|
||||||
|
# TODO: find a better name and check if we have a string to boolean helper
|
||||||
|
return
|
||||||
|
|
||||||
|
node_path = e.get('data-oe-xpath')
|
||||||
|
if node_path is None:
|
||||||
|
node_path = "%s/%s[%d]" % (parent_xpath, e.tag, index_map[e.tag])
|
||||||
|
if branding and not (e.get('data-oe-model') or e.get('t-field')):
|
||||||
|
e.attrib.update(branding)
|
||||||
|
e.set('data-oe-xpath', node_path)
|
||||||
|
if not e.get('data-oe-model'): return
|
||||||
|
|
||||||
|
# if a branded element contains branded elements distribute own
|
||||||
|
# branding to children unless it's t-raw, then just remove branding
|
||||||
|
# on current element
|
||||||
|
if e.tag == 't' or 't-raw' in e.attrib or \
|
||||||
|
any(self.is_node_branded(child) for child in e.iterdescendants()):
|
||||||
|
distributed_branding = dict(
|
||||||
|
(attribute, e.attrib.pop(attribute))
|
||||||
|
for attribute in MOVABLE_BRANDING
|
||||||
|
if e.get(attribute))
|
||||||
|
|
||||||
|
if 't-raw' not in e.attrib:
|
||||||
|
# TODO: collections.Counter if remove p2.6 compat
|
||||||
|
# running index by tag type, for XPath query generation
|
||||||
|
indexes = collections.defaultdict(lambda: 0)
|
||||||
|
for child in e.iterchildren(tag=etree.Element):
|
||||||
|
indexes[child.tag] += 1
|
||||||
|
self.distribute_branding(child, distributed_branding,
|
||||||
|
parent_xpath=node_path,
|
||||||
|
index_map=indexes)
|
||||||
|
|
||||||
|
def is_node_branded(self, node):
|
||||||
|
""" Finds out whether a node is branded or qweb-active (bears a
|
||||||
|
@data-oe-model or a @t-* *which is not t-field* as t-field does not
|
||||||
|
section out views)
|
||||||
|
|
||||||
|
:param node: an etree-compatible element to test
|
||||||
|
:type node: etree._Element
|
||||||
|
:rtype: boolean
|
||||||
|
"""
|
||||||
|
return any(
|
||||||
|
(attr == 'data-oe-model' or (attr != 't-field' and attr.startswith('t-')))
|
||||||
|
for attr in node.attrib
|
||||||
|
)
|
||||||
|
|
||||||
|
def translate_qweb(self, cr, uid, id_, arch, lang, context=None):
|
||||||
|
# TODO: this should be moved in a place before inheritance is applied
|
||||||
|
# but process() is only called on fields_view_get()
|
||||||
|
Translations = self.pool['ir.translation']
|
||||||
|
h = HTMLParser.HTMLParser()
|
||||||
|
def get_trans(text):
|
||||||
|
if not text or not text.strip():
|
||||||
|
return None
|
||||||
|
text = h.unescape(text.strip())
|
||||||
|
if len(text) < 2 or (text.startswith('<!') and text.endswith('>')):
|
||||||
|
return None
|
||||||
|
return Translations._get_source(cr, uid, 'website', 'view', lang, text, id_)
|
||||||
|
|
||||||
|
if arch.tag not in ['script']:
|
||||||
|
text = get_trans(arch.text)
|
||||||
|
if text:
|
||||||
|
arch.text = arch.text.replace(arch.text.strip(), text)
|
||||||
|
tail = get_trans(arch.tail)
|
||||||
|
if tail:
|
||||||
|
arch.tail = arch.tail.replace(arch.tail.strip(), tail)
|
||||||
|
|
||||||
|
for attr_name in ('title', 'alt', 'placeholder'):
|
||||||
|
attr = get_trans(arch.get(attr_name))
|
||||||
|
if attr:
|
||||||
|
arch.set(attr_name, attr)
|
||||||
|
for node in arch.iterchildren("*"):
|
||||||
|
self.translate_qweb(cr, uid, id_, node, lang, context)
|
||||||
|
return arch
|
||||||
|
|
||||||
|
@openerp.tools.ormcache()
|
||||||
|
def get_view_xmlid(self, cr, uid, id):
|
||||||
|
imd = self.pool['ir.model.data']
|
||||||
|
domain = [('model', '=', 'ir.ui.view'), ('res_id', '=', id)]
|
||||||
|
xmlid = imd.search_read(cr, uid, domain, ['module', 'name'])[0]
|
||||||
|
return '%s.%s' % (xmlid['module'], xmlid['name'])
|
||||||
|
|
||||||
|
def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
|
||||||
|
if isinstance(id_or_xml_id, list):
|
||||||
|
id_or_xml_id = id_or_xml_id[0]
|
||||||
|
tname = id_or_xml_id
|
||||||
|
if isinstance(tname, (int, long)):
|
||||||
|
tname = self.get_view_xmlid(cr, uid, tname)
|
||||||
|
|
||||||
|
if not context:
|
||||||
|
context = {}
|
||||||
|
|
||||||
|
def loader(name):
|
||||||
|
return self.read_template(cr, uid, name, context=context)
|
||||||
|
|
||||||
|
return self.pool[engine].render(cr, uid, tname, values, loader=loader, context=context)
|
||||||
|
|
||||||
|
#------------------------------------------------------
|
||||||
|
# Misc
|
||||||
|
#------------------------------------------------------
|
||||||
|
|
||||||
def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
|
def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
|
||||||
nodes=[]
|
nodes=[]
|
||||||
|
@ -305,5 +896,4 @@ class view(osv.osv):
|
||||||
ids = map(itemgetter(0), cr.fetchall())
|
ids = map(itemgetter(0), cr.fetchall())
|
||||||
return self._check_xml(cr, uid, ids)
|
return self._check_xml(cr, uid, ids)
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:et:
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
<group>
|
<group>
|
||||||
<field name="field_parent"/>
|
<field name="field_parent"/>
|
||||||
<field name="inherit_id"/>
|
<field name="inherit_id"/>
|
||||||
|
<field name="model_data_id"/>
|
||||||
<field name="xml_id"/>
|
<field name="xml_id"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
@ -49,17 +50,19 @@
|
||||||
<field name="model">ir.ui.view</field>
|
<field name="model">ir.ui.view</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<search string="Views">
|
<search string="Views">
|
||||||
<field name="name" filter_domain="['|', ('name','ilike',self), ('model','ilike',self)]" string="View"/>
|
<field name="name" filter_domain="['|', '|', ('name','ilike',self), ('model','ilike',self), ('model_data_id','ilike',self)]" string="View"/>
|
||||||
<filter string="Form" domain="[('type', '=','form')]"/>
|
<filter string="Form" domain="[('type', '=','form')]"/>
|
||||||
<filter string="Tree" domain="[('type', '=', 'tree')]"/>
|
<filter string="Tree" domain="[('type', '=', 'tree')]"/>
|
||||||
<filter string="Kanban" domain="[('type', '=', 'kanban')]"/>
|
<filter string="Kanban" domain="[('type', '=', 'kanban')]"/>
|
||||||
<filter string="Search" domain="[('type', '=', 'search')]"/>
|
<filter string="Search" domain="[('type', '=', 'search')]"/>
|
||||||
|
<filter string="QWeb" domain="[('type', '=', 'qweb')]"/>
|
||||||
<field name="model"/>
|
<field name="model"/>
|
||||||
<field name="inherit_id"/>
|
<field name="inherit_id"/>
|
||||||
<field name="type"/>
|
<field name="type"/>
|
||||||
<group expand="0" string="Group By...">
|
<group expand="0" string="Group By...">
|
||||||
<filter string="Object" icon="terp-stock_align_left_24" domain="[]" context="{'group_by':'model'}"/>
|
<filter string="Object" domain="[]" context="{'group_by':'model'}"/>
|
||||||
<filter string="Type" icon="terp-stock_symbol-selection" domain="[]" context="{'group_by':'type'}"/>
|
<filter string="Type" domain="[]" context="{'group_by':'type'}"/>
|
||||||
|
<filter string="Inherit" domain="[]" context="{'group_by':'inherit_id'}"/>
|
||||||
</group>
|
</group>
|
||||||
</search>
|
</search>
|
||||||
</field>
|
</field>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
<field name="visible" eval="0" />
|
<field name="visible" eval="0" />
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
||||||
<record model="ir.module.category" id="module_category_localization">
|
<record model="ir.module.category" id="module_category_localization">
|
||||||
<field name="name">Localization</field>
|
<field name="name">Localization</field>
|
||||||
<field name="visible" eval="0" />
|
<field name="visible" eval="0" />
|
||||||
|
@ -113,6 +114,11 @@
|
||||||
<field name="sequence">15</field>
|
<field name="sequence">15</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record model="ir.module.category" id="module_category_website">
|
||||||
|
<field name="name">Website</field>
|
||||||
|
<field name="sequence">16</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
<record model="ir.module.category" id="module_category_administration">
|
<record model="ir.module.category" id="module_category_administration">
|
||||||
<field name="name">Administration</field>
|
<field name="name">Administration</field>
|
||||||
<field name="sequence">100</field>
|
<field name="sequence">100</field>
|
||||||
|
|
|
@ -141,8 +141,8 @@ class res_company(osv.osv):
|
||||||
'state_id': fields.function(_get_address_data, fnct_inv=_set_address_data, type='many2one', relation='res.country.state', string="Fed. State", multi='address'),
|
'state_id': fields.function(_get_address_data, fnct_inv=_set_address_data, type='many2one', relation='res.country.state', string="Fed. State", multi='address'),
|
||||||
'bank_ids': fields.one2many('res.partner.bank','company_id', 'Bank Accounts', help='Bank accounts related to this company'),
|
'bank_ids': fields.one2many('res.partner.bank','company_id', 'Bank Accounts', help='Bank accounts related to this company'),
|
||||||
'country_id': fields.function(_get_address_data, fnct_inv=_set_address_data, type='many2one', relation='res.country', string="Country", multi='address'),
|
'country_id': fields.function(_get_address_data, fnct_inv=_set_address_data, type='many2one', relation='res.country', string="Country", multi='address'),
|
||||||
'email': fields.function(_get_address_data, fnct_inv=_set_address_data, size=64, type='char', string="Email", multi='address'),
|
'email': fields.related('partner_id', 'email', size=64, type='char', string="Email", store=True),
|
||||||
'phone': fields.function(_get_address_data, fnct_inv=_set_address_data, size=64, type='char', string="Phone", multi='address'),
|
'phone': fields.related('partner_id', 'phone', size=64, type='char', string="Phone", store=True),
|
||||||
'fax': fields.function(_get_address_data, fnct_inv=_set_address_data, size=64, type='char', string="Fax", multi='address'),
|
'fax': fields.function(_get_address_data, fnct_inv=_set_address_data, size=64, type='char', string="Fax", multi='address'),
|
||||||
'website': fields.related('partner_id', 'website', string="Website", type="char", size=64),
|
'website': fields.related('partner_id', 'website', string="Website", type="char", size=64),
|
||||||
'vat': fields.related('partner_id', 'vat', string="Tax ID", type="char", size=32),
|
'vat': fields.related('partner_id', 'vat', string="Tax ID", type="char", size=32),
|
||||||
|
|
|
@ -415,6 +415,7 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
|
||||||
* For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
|
* For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
|
||||||
to/from the implied groups of 'group', depending on the field's value.
|
to/from the implied groups of 'group', depending on the field's value.
|
||||||
By default 'group' is the group Employee. Groups are given by their xml id.
|
By default 'group' is the group Employee. Groups are given by their xml id.
|
||||||
|
The attribute 'group' may contain several xml ids, separated by commas.
|
||||||
|
|
||||||
* For a boolean field like 'module_XXX', ``execute`` triggers the immediate
|
* For a boolean field like 'module_XXX', ``execute`` triggers the immediate
|
||||||
installation of the module named 'XXX' if the field has value ``True``.
|
installation of the module named 'XXX' if the field has value ``True``.
|
||||||
|
@ -437,7 +438,7 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
|
||||||
""" return a dictionary with the fields classified by category::
|
""" return a dictionary with the fields classified by category::
|
||||||
|
|
||||||
{ 'default': [('default_foo', 'model', 'foo'), ...],
|
{ 'default': [('default_foo', 'model', 'foo'), ...],
|
||||||
'group': [('group_bar', browse_group, browse_implied_group), ...],
|
'group': [('group_bar', [browse_group], browse_implied_group), ...],
|
||||||
'module': [('module_baz', browse_module), ...],
|
'module': [('module_baz', browse_module), ...],
|
||||||
'other': ['other_field', ...],
|
'other': ['other_field', ...],
|
||||||
}
|
}
|
||||||
|
@ -446,15 +447,15 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
|
||||||
ir_module = self.pool['ir.module.module']
|
ir_module = self.pool['ir.module.module']
|
||||||
def ref(xml_id):
|
def ref(xml_id):
|
||||||
mod, xml = xml_id.split('.', 1)
|
mod, xml = xml_id.split('.', 1)
|
||||||
return ir_model_data.get_object(cr, uid, mod, xml, context)
|
return ir_model_data.get_object(cr, uid, mod, xml, context=context)
|
||||||
|
|
||||||
defaults, groups, modules, others = [], [], [], []
|
defaults, groups, modules, others = [], [], [], []
|
||||||
for name, field in self._columns.items():
|
for name, field in self._columns.items():
|
||||||
if name.startswith('default_') and hasattr(field, 'default_model'):
|
if name.startswith('default_') and hasattr(field, 'default_model'):
|
||||||
defaults.append((name, field.default_model, name[8:]))
|
defaults.append((name, field.default_model, name[8:]))
|
||||||
elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
|
elif name.startswith('group_') and isinstance(field, fields.boolean) and hasattr(field, 'implied_group'):
|
||||||
field_group = getattr(field, 'group', 'base.group_user')
|
field_groups = getattr(field, 'group', 'base.group_user').split(',')
|
||||||
groups.append((name, ref(field_group), ref(field.implied_group)))
|
groups.append((name, map(ref, field_groups), ref(field.implied_group)))
|
||||||
elif name.startswith('module_') and isinstance(field, fields.boolean):
|
elif name.startswith('module_') and isinstance(field, fields.boolean):
|
||||||
mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
|
mod_ids = ir_module.search(cr, uid, [('name', '=', name[7:])])
|
||||||
record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
|
record = ir_module.browse(cr, uid, mod_ids[0], context) if mod_ids else None
|
||||||
|
@ -477,8 +478,8 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
|
||||||
res[name] = value
|
res[name] = value
|
||||||
|
|
||||||
# groups: which groups are implied by the group Employee
|
# groups: which groups are implied by the group Employee
|
||||||
for name, group, implied_group in classified['group']:
|
for name, groups, implied_group in classified['group']:
|
||||||
res[name] = implied_group in group.implied_ids
|
res[name] = all(implied_group in group.implied_ids for group in groups)
|
||||||
|
|
||||||
# modules: which modules are installed/to install
|
# modules: which modules are installed/to install
|
||||||
for name, module in classified['module']:
|
for name, module in classified['module']:
|
||||||
|
@ -497,6 +498,7 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
|
||||||
|
|
||||||
ir_values = self.pool['ir.values']
|
ir_values = self.pool['ir.values']
|
||||||
ir_module = self.pool['ir.module.module']
|
ir_module = self.pool['ir.module.module']
|
||||||
|
res_groups = self.pool['res.groups']
|
||||||
|
|
||||||
classified = self._get_classified_fields(cr, uid, context)
|
classified = self._get_classified_fields(cr, uid, context)
|
||||||
|
|
||||||
|
@ -507,12 +509,16 @@ class res_config_settings(osv.osv_memory, res_config_module_installation_mixin):
|
||||||
ir_values.set_default(cr, SUPERUSER_ID, model, field, config[name])
|
ir_values.set_default(cr, SUPERUSER_ID, model, field, config[name])
|
||||||
|
|
||||||
# group fields: modify group / implied groups
|
# group fields: modify group / implied groups
|
||||||
for name, group, implied_group in classified['group']:
|
for name, groups, implied_group in classified['group']:
|
||||||
|
gids = map(int, groups)
|
||||||
if config[name]:
|
if config[name]:
|
||||||
group.write({'implied_ids': [(4, implied_group.id)]})
|
res_groups.write(cr, uid, gids, {'implied_ids': [(4, implied_group.id)]}, context=context)
|
||||||
else:
|
else:
|
||||||
group.write({'implied_ids': [(3, implied_group.id)]})
|
res_groups.write(cr, uid, gids, {'implied_ids': [(3, implied_group.id)]}, context=context)
|
||||||
implied_group.write({'users': [(3, u.id) for u in group.users]})
|
uids = set()
|
||||||
|
for group in groups:
|
||||||
|
uids.update(map(int, group.users))
|
||||||
|
implied_group.write({'users': [(3, u) for u in uids]})
|
||||||
|
|
||||||
# other fields: execute all methods that start with 'set_'
|
# other fields: execute all methods that start with 'set_'
|
||||||
for method in dir(self):
|
for method in dir(self):
|
||||||
|
|
|
@ -40,20 +40,21 @@ class res_currency(osv.osv):
|
||||||
if context is None:
|
if context is None:
|
||||||
context = {}
|
context = {}
|
||||||
res = {}
|
res = {}
|
||||||
if 'date' in context:
|
|
||||||
date = context['date']
|
date = context.get('date') or time.strftime('%Y-%m-%d')
|
||||||
else:
|
|
||||||
date = time.strftime('%Y-%m-%d')
|
|
||||||
date = date or time.strftime('%Y-%m-%d')
|
|
||||||
# Convert False values to None ...
|
# Convert False values to None ...
|
||||||
currency_rate_type = context.get('currency_rate_type_id') or None
|
currency_rate_type = context.get('currency_rate_type_id') or None
|
||||||
# ... and use 'is NULL' instead of '= some-id'.
|
# ... and use 'is NULL' instead of '= some-id'.
|
||||||
operator = '=' if currency_rate_type else 'is'
|
operator = '=' if currency_rate_type else 'is'
|
||||||
for id in ids:
|
for id in ids:
|
||||||
cr.execute("SELECT currency_id, rate FROM res_currency_rate WHERE currency_id = %s AND name <= %s AND currency_rate_type_id " + operator +" %s ORDER BY name desc LIMIT 1" ,(id, date, currency_rate_type))
|
cr.execute('SELECT rate FROM res_currency_rate '
|
||||||
|
'WHERE currency_id = %s '
|
||||||
|
'AND name <= %s '
|
||||||
|
'AND currency_rate_type_id ' + operator + ' %s '
|
||||||
|
'ORDER BY name desc LIMIT 1',
|
||||||
|
(id, date, currency_rate_type))
|
||||||
if cr.rowcount:
|
if cr.rowcount:
|
||||||
id, rate = cr.fetchall()[0]
|
res[id] = cr.fetchone()[0]
|
||||||
res[id] = rate
|
|
||||||
elif not raise_on_no_rate:
|
elif not raise_on_no_rate:
|
||||||
res[id] = 0
|
res[id] = 0
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -162,9 +162,13 @@ class lang(osv.osv):
|
||||||
]
|
]
|
||||||
|
|
||||||
@tools.ormcache(skiparg=3)
|
@tools.ormcache(skiparg=3)
|
||||||
def _lang_data_get(self, cr, uid, lang_id, monetary=False):
|
def _lang_data_get(self, cr, uid, lang, monetary=False):
|
||||||
|
if type(lang) in (str, unicode):
|
||||||
|
lang = self.search(cr, uid, [('code', '=', lang)]) or \
|
||||||
|
self.search(cr, uid, [('code', '=', 'en_US')])
|
||||||
|
lang = lang[0]
|
||||||
conv = localeconv()
|
conv = localeconv()
|
||||||
lang_obj = self.browse(cr, uid, lang_id)
|
lang_obj = self.browse(cr, uid, lang)
|
||||||
thousands_sep = lang_obj.thousands_sep or conv[monetary and 'mon_thousands_sep' or 'thousands_sep']
|
thousands_sep = lang_obj.thousands_sep or conv[monetary and 'mon_thousands_sep' or 'thousands_sep']
|
||||||
decimal_point = lang_obj.decimal_point
|
decimal_point = lang_obj.decimal_point
|
||||||
grouping = lang_obj.grouping
|
grouping = lang_obj.grouping
|
||||||
|
@ -192,32 +196,29 @@ class lang(osv.osv):
|
||||||
trans_obj.unlink(cr, uid, trans_ids, context=context)
|
trans_obj.unlink(cr, uid, trans_ids, context=context)
|
||||||
return super(lang, self).unlink(cr, uid, ids, context=context)
|
return super(lang, self).unlink(cr, uid, ids, context=context)
|
||||||
|
|
||||||
|
#
|
||||||
|
# IDS: can be a list of IDS or a list of XML_IDS
|
||||||
|
#
|
||||||
def format(self, cr, uid, ids, percent, value, grouping=False, monetary=False, context=None):
|
def format(self, cr, uid, ids, percent, value, grouping=False, monetary=False, context=None):
|
||||||
""" Format() will return the language-specific output for float values"""
|
""" Format() will return the language-specific output for float values"""
|
||||||
|
|
||||||
if percent[0] != '%':
|
if percent[0] != '%':
|
||||||
raise ValueError("format() must be given exactly one %char format specifier")
|
raise ValueError("format() must be given exactly one %char format specifier")
|
||||||
|
|
||||||
lang_grouping, thousands_sep, decimal_point = self._lang_data_get(cr, uid, ids[0], monetary)
|
|
||||||
eval_lang_grouping = eval(lang_grouping)
|
|
||||||
|
|
||||||
formatted = percent % value
|
formatted = percent % value
|
||||||
|
|
||||||
# floats and decimal ints need special action!
|
# floats and decimal ints need special action!
|
||||||
if percent[-1] in 'eEfFgG':
|
if grouping:
|
||||||
seps = 0
|
lang_grouping, thousands_sep, decimal_point = \
|
||||||
parts = formatted.split('.')
|
self._lang_data_get(cr, uid, ids[0], monetary)
|
||||||
|
eval_lang_grouping = eval(lang_grouping)
|
||||||
|
|
||||||
if grouping:
|
if percent[-1] in 'eEfFgG':
|
||||||
parts[0], seps = intersperse(parts[0], eval_lang_grouping, thousands_sep)
|
parts = formatted.split('.')
|
||||||
|
parts[0], _ = intersperse(parts[0], eval_lang_grouping, thousands_sep)
|
||||||
|
|
||||||
formatted = decimal_point.join(parts)
|
formatted = decimal_point.join(parts)
|
||||||
while seps:
|
|
||||||
sp = formatted.find(' ')
|
elif percent[-1] in 'diu':
|
||||||
if sp == -1: break
|
|
||||||
formatted = formatted[:sp] + formatted[sp+1:]
|
|
||||||
seps -= 1
|
|
||||||
elif percent[-1] in 'diu':
|
|
||||||
if grouping:
|
|
||||||
formatted = intersperse(formatted, eval_lang_grouping, thousands_sep)[0]
|
formatted = intersperse(formatted, eval_lang_grouping, thousands_sep)[0]
|
||||||
|
|
||||||
return formatted
|
return formatted
|
||||||
|
|
|
@ -23,14 +23,12 @@ import datetime
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
import math
|
import math
|
||||||
import pytz
|
import pytz
|
||||||
import re
|
|
||||||
|
|
||||||
import openerp
|
import openerp
|
||||||
from openerp import SUPERUSER_ID
|
from openerp import SUPERUSER_ID
|
||||||
from openerp import tools
|
from openerp import tools
|
||||||
from openerp.osv import osv, fields
|
from openerp.osv import osv, fields
|
||||||
from openerp.tools.translate import _
|
from openerp.tools.translate import _
|
||||||
from openerp.tools.yaml_import import is_comment
|
|
||||||
|
|
||||||
class format_address(object):
|
class format_address(object):
|
||||||
def fields_view_get_address(self, cr, uid, arch, context={}):
|
def fields_view_get_address(self, cr, uid, arch, context={}):
|
||||||
|
@ -210,6 +208,7 @@ class res_partner(osv.osv, format_address):
|
||||||
def _display_name_compute(self, cr, uid, ids, name, args, context=None):
|
def _display_name_compute(self, cr, uid, ids, name, args, context=None):
|
||||||
context = dict(context or {})
|
context = dict(context or {})
|
||||||
context.pop('show_address', None)
|
context.pop('show_address', None)
|
||||||
|
context.pop('show_address_only', None)
|
||||||
return dict(self.name_get(cr, uid, ids, context=context))
|
return dict(self.name_get(cr, uid, ids, context=context))
|
||||||
|
|
||||||
# indirections to avoid passing a copy of the overridable method when declaring the function field
|
# indirections to avoid passing a copy of the overridable method when declaring the function field
|
||||||
|
@ -217,12 +216,12 @@ class res_partner(osv.osv, format_address):
|
||||||
_display_name = lambda self, *args, **kwargs: self._display_name_compute(*args, **kwargs)
|
_display_name = lambda self, *args, **kwargs: self._display_name_compute(*args, **kwargs)
|
||||||
|
|
||||||
_commercial_partner_store_triggers = {
|
_commercial_partner_store_triggers = {
|
||||||
'res.partner': (lambda self,cr,uid,ids,context=None: self.search(cr, uid, [('id','child_of',ids)]),
|
'res.partner': (lambda self,cr,uid,ids,context=None: self.search(cr, uid, [('id','child_of',ids)], context=dict(active_test=False)),
|
||||||
['parent_id', 'is_company'], 10)
|
['parent_id', 'is_company'], 10)
|
||||||
}
|
}
|
||||||
_display_name_store_triggers = {
|
_display_name_store_triggers = {
|
||||||
'res.partner': (lambda self,cr,uid,ids,context=None: self.search(cr, uid, [('id','child_of',ids)]),
|
'res.partner': (lambda self,cr,uid,ids,context=None: self.search(cr, uid, [('id','child_of',ids)], context=dict(active_test=False)),
|
||||||
['parent_id', 'is_company', 'name'], 10)
|
['parent_id', 'is_company', 'name'], 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
_order = "display_name"
|
_order = "display_name"
|
||||||
|
@ -232,7 +231,7 @@ class res_partner(osv.osv, format_address):
|
||||||
'date': fields.date('Date', select=1),
|
'date': fields.date('Date', select=1),
|
||||||
'title': fields.many2one('res.partner.title', 'Title'),
|
'title': fields.many2one('res.partner.title', 'Title'),
|
||||||
'parent_id': fields.many2one('res.partner', 'Related Company'),
|
'parent_id': fields.many2one('res.partner', 'Related Company'),
|
||||||
'child_ids': fields.one2many('res.partner', 'parent_id', 'Contacts', domain=[('active','=',True)]), # force "active_test" domain to bypass _search() override
|
'child_ids': fields.one2many('res.partner', 'parent_id', 'Contacts', domain=[('active','=',True)]), # force "active_test" domain to bypass _search() override
|
||||||
'ref': fields.char('Reference', size=64, select=1),
|
'ref': fields.char('Reference', size=64, select=1),
|
||||||
'lang': fields.selection(_lang_get, 'Language',
|
'lang': fields.selection(_lang_get, 'Language',
|
||||||
help="If the selected language is loaded in the system, all documents related to this contact will be printed in this language. If not, it will be English."),
|
help="If the selected language is loaded in the system, all documents related to this contact will be printed in this language. If not, it will be English."),
|
||||||
|
@ -560,10 +559,12 @@ class res_partner(osv.osv, format_address):
|
||||||
name = record.name
|
name = record.name
|
||||||
if record.parent_id and not record.is_company:
|
if record.parent_id and not record.is_company:
|
||||||
name = "%s, %s" % (record.parent_id.name, name)
|
name = "%s, %s" % (record.parent_id.name, name)
|
||||||
|
if context.get('show_address_only'):
|
||||||
|
name = self._display_address(cr, uid, record, without_company=True, context=context)
|
||||||
if context.get('show_address'):
|
if context.get('show_address'):
|
||||||
name = name + "\n" + self._display_address(cr, uid, record, without_company=True, context=context)
|
name = name + "\n" + self._display_address(cr, uid, record, without_company=True, context=context)
|
||||||
name = name.replace('\n\n','\n')
|
name = name.replace('\n\n','\n')
|
||||||
name = name.replace('\n\n','\n')
|
name = name.replace('\n\n','\n')
|
||||||
if context.get('show_email') and record.email:
|
if context.get('show_email') and record.email:
|
||||||
name = "%s <%s>" % (name, record.email)
|
name = "%s <%s>" % (name, record.email)
|
||||||
res.append((record.id, name))
|
res.append((record.id, name))
|
||||||
|
@ -602,7 +603,8 @@ class res_partner(osv.osv, format_address):
|
||||||
""" Override search() to always show inactive children when searching via ``child_of`` operator. The ORM will
|
""" Override search() to always show inactive children when searching via ``child_of`` operator. The ORM will
|
||||||
always call search() with a simple domain of the form [('parent_id', 'in', [ids])]. """
|
always call search() with a simple domain of the form [('parent_id', 'in', [ids])]. """
|
||||||
# a special ``domain`` is set on the ``child_ids`` o2m to bypass this logic, as it uses similar domain expressions
|
# a special ``domain`` is set on the ``child_ids`` o2m to bypass this logic, as it uses similar domain expressions
|
||||||
if len(args) == 1 and len(args[0]) == 3 and args[0][:2] == ('parent_id','in'):
|
if len(args) == 1 and len(args[0]) == 3 and args[0][:2] == ('parent_id','in') \
|
||||||
|
and args[0][2] != [False]:
|
||||||
context = dict(context or {}, active_test=False)
|
context = dict(context or {}, active_test=False)
|
||||||
return super(res_partner, self)._search(cr, user, args, offset=offset, limit=limit, order=order, context=context,
|
return super(res_partner, self)._search(cr, user, args, offset=offset, limit=limit, order=order, context=context,
|
||||||
count=count, access_rights_uid=access_rights_uid)
|
count=count, access_rights_uid=access_rights_uid)
|
||||||
|
|
|
@ -75,7 +75,7 @@
|
||||||
<field name="zip">106</field>
|
<field name="zip">106</field>
|
||||||
<field name="country_id" ref="base.tw"/>
|
<field name="country_id" ref="base.tw"/>
|
||||||
<field name="street">31 Hong Kong street</field>
|
<field name="street">31 Hong Kong street</field>
|
||||||
<field name="email">info@asustek.com</field>
|
<field name="email">asusteK@yourcompany.example.com</field>
|
||||||
<field name="phone">(+886) (02) 4162 2023</field>
|
<field name="phone">(+886) (02) 4162 2023</field>
|
||||||
<field name="website">www.asustek.com</field>
|
<field name="website">www.asustek.com</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_1-image.jpg"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_1-image.jpg"/>
|
||||||
|
@ -88,7 +88,7 @@
|
||||||
<field name="zip">1300</field>
|
<field name="zip">1300</field>
|
||||||
<field name="country_id" ref="base.be"/>
|
<field name="country_id" ref="base.be"/>
|
||||||
<field name="street">69 rue de Namur</field>
|
<field name="street">69 rue de Namur</field>
|
||||||
<field name="email">info@agrolait.com</field>
|
<field name="email">agrolait@yourcompany.example.com</field>
|
||||||
<field name="phone">+32 10 588 558</field>
|
<field name="phone">+32 10 588 558</field>
|
||||||
<field name="website">www.agrolait.com</field>
|
<field name="website">www.agrolait.com</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_2-image.jpg"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_2-image.jpg"/>
|
||||||
|
@ -103,7 +103,7 @@
|
||||||
<field model="res.country" name="country_id" search="[('name','=','China')]"/>
|
<field model="res.country" name="country_id" search="[('name','=','China')]"/>
|
||||||
<field name="country_id" ref="base.cn"/>
|
<field name="country_id" ref="base.cn"/>
|
||||||
<field name="street">52 Chop Suey street</field>
|
<field name="street">52 Chop Suey street</field>
|
||||||
<field name="email">info@chinaexport.com</field>
|
<field name="email">chinaexport@yourcompany.example.com</field>
|
||||||
<field name="phone">+86 21 6484 5671</field>
|
<field name="phone">+86 21 6484 5671</field>
|
||||||
<field name="website">www.chinaexport.com/</field>
|
<field name="website">www.chinaexport.com/</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_3-image.png"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_3-image.png"/>
|
||||||
|
@ -120,7 +120,7 @@
|
||||||
<field name="country_id" ref="base.us"/>
|
<field name="country_id" ref="base.us"/>
|
||||||
<field model="res.country.state" name="state_id" search="[('code','ilike','ca')]"/>
|
<field model="res.country.state" name="state_id" search="[('code','ilike','ca')]"/>
|
||||||
<field name="street">3661 Station Street</field>
|
<field name="street">3661 Station Street</field>
|
||||||
<field name="email">info@deltapc.com</field>
|
<field name="email">deltapc@yourcompany.example.com</field>
|
||||||
<field name="phone">+1 510 340 2385</field>
|
<field name="phone">+1 510 340 2385</field>
|
||||||
<field name="website">www.distribpc.com/</field>
|
<field name="website">www.distribpc.com/</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_4-image.png"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_4-image.png"/>
|
||||||
|
@ -135,25 +135,25 @@
|
||||||
<field model="res.country.state" name="state_id" search="[('code','ilike','il')]"/>
|
<field model="res.country.state" name="state_id" search="[('code','ilike','il')]"/>
|
||||||
<field name="zip">60610</field>
|
<field name="zip">60610</field>
|
||||||
<field name="city">Chicago</field>
|
<field name="city">Chicago</field>
|
||||||
<field name="email">epic@tech.info</field>
|
<field name="email">epic@yourcompany.example.com</field>
|
||||||
<field name="phone">+1 312 349 2324</field>
|
<field name="phone">+1 312 349 2324</field>
|
||||||
<field name="website">www.epic-tech.info//</field>
|
<field name="website">www.epic-tech.info//</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_5-image.jpg"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_5-image.jpg"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="res_partner_6" model="res.partner">
|
<record id="res_partner_6" model="res.partner">
|
||||||
<field name="name">Elec Import</field>
|
<field name="name">OpenElec Applications</field>
|
||||||
<field eval="[(6, 0, [ref('res_partner_category_12')])]" name="category_id"/>
|
<field eval="[(6, 0, [ref('res_partner_category_12')])]" name="category_id"/>
|
||||||
<field eval="1" name="supplier"/>
|
<field eval="1" name="supplier"/>
|
||||||
<field eval="0" name="customer"/>
|
<field eval="0" name="customer"/>
|
||||||
<field name="is_company">1</field>
|
<field name="is_company">1</field>
|
||||||
<field name="city">Chicago</field>
|
<field name="zip">90001</field>
|
||||||
<field name="zip">60623</field>
|
<field name="city">Los Angeles</field>
|
||||||
<field name="country_id" ref="base.us"/>
|
<field name="country_id" ref="base.us"/>
|
||||||
<field model="res.country.state" name="state_id" search="[('code','ilike','il')]"/>
|
<field model="res.country.state" name="state_id" search="[('code','ilike','ca')]"/>
|
||||||
<field name="street">23 Rockwell Lane</field>
|
<field name="street">23 Rockwell Lane</field>
|
||||||
<field name="email">info@elecimport.com</field>
|
<field name="email">openelecapplications@yourcompany.example.com</field>
|
||||||
<field name="phone">+1 773 439 3000</field>
|
<field name="phone">+1 312 349 2121</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_6-image.jpg"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_6-image.jpg"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
@ -168,7 +168,7 @@
|
||||||
<field name="zip">B46 3AG</field>
|
<field name="zip">B46 3AG</field>
|
||||||
<field name="country_id" ref="base.uk"/>
|
<field name="country_id" ref="base.uk"/>
|
||||||
<field name="phone">+44 121 690 4596</field>
|
<field name="phone">+44 121 690 4596</field>
|
||||||
<field name="email">email@wealthyandsons.com</field>
|
<field name="email">wealthyandsons@yourcompany.example.com</field>
|
||||||
<field name="website">www.wealthyandsons.com/</field>
|
<field name="website">www.wealthyandsons.com/</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_7-image.jpg"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_7-image.jpg"/>
|
||||||
</record>
|
</record>
|
||||||
|
@ -184,6 +184,7 @@
|
||||||
<field name="zip">80352</field>
|
<field name="zip">80352</field>
|
||||||
<field name="country_id" ref="base.de"/>
|
<field name="country_id" ref="base.de"/>
|
||||||
<field name="phone">+49 8932 450203 </field>
|
<field name="phone">+49 8932 450203 </field>
|
||||||
|
<field name="email">mediapole@yourcompany.example.com</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_8-image.jpg"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_8-image.jpg"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_9" model="res.partner">
|
<record id="res_partner_9" model="res.partner">
|
||||||
|
@ -195,7 +196,7 @@
|
||||||
<field name="street">203, Systems Plaza</field>
|
<field name="street">203, Systems Plaza</field>
|
||||||
<field name="city">Mumbai</field>
|
<field name="city">Mumbai</field>
|
||||||
<field name="country_id" ref="base.in"/>
|
<field name="country_id" ref="base.in"/>
|
||||||
<field name="email">info@bestdesigners.in</field>
|
<field name="email">bestdesigners@yourcompany.example.com</field>
|
||||||
<field name="phone">+91 22 3445 0349</field>
|
<field name="phone">+91 22 3445 0349</field>
|
||||||
<field name="website">www.bestdesigners.com</field>
|
<field name="website">www.bestdesigners.com</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_9-image.jpg"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_9-image.jpg"/>
|
||||||
|
@ -209,7 +210,7 @@
|
||||||
<field name="zip">33169</field>
|
<field name="zip">33169</field>
|
||||||
<field name="country_id" ref="base.us"/>
|
<field name="country_id" ref="base.us"/>
|
||||||
<field model="res.country.state" name="state_id" search="[('code','=','FL')]"/>
|
<field model="res.country.state" name="state_id" search="[('code','=','FL')]"/>
|
||||||
<field name="email">contact@jackson.com</field>
|
<field name="email">jackson@yourcompany.example.com</field>
|
||||||
<field name="phone">+1 786 525 0724</field>
|
<field name="phone">+1 786 525 0724</field>
|
||||||
<field name="street">3203 Lamberts Branch Road</field>
|
<field name="street">3203 Lamberts Branch Road</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_10-image.jpg"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_10-image.jpg"/>
|
||||||
|
@ -228,6 +229,7 @@
|
||||||
<field name="zip">08078</field>
|
<field name="zip">08078</field>
|
||||||
<field name="phone">+34 934 340 230</field>
|
<field name="phone">+34 934 340 230</field>
|
||||||
<field name="website">www.lumitech.com</field>
|
<field name="website">www.lumitech.com</field>
|
||||||
|
<field name="email">luminous@yourcompany.example.com</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_11-image.png"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_11-image.png"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
@ -242,7 +244,7 @@
|
||||||
<field name="phone">+33 4 49 23 44 54</field>
|
<field name="phone">+33 4 49 23 44 54</field>
|
||||||
<field name="country_id" ref="base.fr"/>
|
<field name="country_id" ref="base.fr"/>
|
||||||
<field name="street">93, Press Avenue</field>
|
<field name="street">93, Press Avenue</field>
|
||||||
<field name="email">info@c2c.com</field>
|
<field name="email">camptocamp@yourcompany.example.com</field>
|
||||||
<field name="website">www.camptocamp.com</field>
|
<field name="website">www.camptocamp.com</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_12-image.jpg"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_12-image.jpg"/>
|
||||||
</record>
|
</record>
|
||||||
|
@ -255,7 +257,7 @@
|
||||||
<field name="city">Champs sur Marne</field>
|
<field name="city">Champs sur Marne</field>
|
||||||
<field name="zip">77420</field>
|
<field name="zip">77420</field>
|
||||||
<field name="country_id" ref="base.fr"/>
|
<field name="country_id" ref="base.fr"/>
|
||||||
<field name="email">info@axelor.com</field>
|
<field name="email">axelor@yourcompany.example.com</field>
|
||||||
<field name="phone">+33 1 64 61 04 01</field>
|
<field name="phone">+33 1 64 61 04 01</field>
|
||||||
<field name="street">12 rue Albert Einstein</field>
|
<field name="street">12 rue Albert Einstein</field>
|
||||||
<field name="website">www.axelor.com/</field>
|
<field name="website">www.axelor.com/</field>
|
||||||
|
@ -274,7 +276,7 @@
|
||||||
<field name="country_id" ref="base.us"/>
|
<field name="country_id" ref="base.us"/>
|
||||||
<field model="res.country.state" name="state_id" search="[('code','ilike','mi')]"/>
|
<field model="res.country.state" name="state_id" search="[('code','ilike','mi')]"/>
|
||||||
<field name="street">60, Rosewood Court</field>
|
<field name="street">60, Rosewood Court</field>
|
||||||
<field name="email">info@chamberworks.com</field>
|
<field name="email">chamberworks@yourcompany.example.com</field>
|
||||||
<field name="phone">+1 313 222 3456</field>
|
<field name="phone">+1 313 222 3456</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_14-image.jpg"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_14-image.jpg"/>
|
||||||
</record>
|
</record>
|
||||||
|
@ -290,6 +292,7 @@
|
||||||
<field name="country_id" ref="base.uk"/>
|
<field name="country_id" ref="base.uk"/>
|
||||||
<field name="city">London</field>
|
<field name="city">London</field>
|
||||||
<field name="phone">+44 20 1294 2193</field>
|
<field name="phone">+44 20 1294 2193</field>
|
||||||
|
<field name="email">millennium@yourcompany.example.com</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_15-image.jpg"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_15-image.jpg"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
@ -304,6 +307,7 @@
|
||||||
<field name="phone">+55 11 2402 2045</field>
|
<field name="phone">+55 11 2402 2045</field>
|
||||||
<field name="country_id" ref="base.br"/>
|
<field name="country_id" ref="base.br"/>
|
||||||
<field name="is_company">1</field>
|
<field name="is_company">1</field>
|
||||||
|
<field name="email">sparksystems@yourcompany.example.com</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_16-image.png"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_16-image.png"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
@ -313,7 +317,7 @@
|
||||||
<field eval="1" name="customer"/>
|
<field eval="1" name="customer"/>
|
||||||
<field name="is_company">1</field>
|
<field name="is_company">1</field>
|
||||||
<field name="city">Rosario</field>
|
<field name="city">Rosario</field>
|
||||||
<field name="email">contact@nebula.ar</field>
|
<field name="email">nebula@yourcompany.example.com</field>
|
||||||
<field name="street">34 Westwood Avenue</field>
|
<field name="street">34 Westwood Avenue</field>
|
||||||
<field name="street2">Capital Federal</field>
|
<field name="street2">Capital Federal</field>
|
||||||
<field name="phone">+54 341 324 9459 </field>
|
<field name="phone">+54 341 324 9459 </field>
|
||||||
|
@ -328,7 +332,7 @@
|
||||||
<field eval="[(6, 0, [ref('res_partner_category_5')])]" name="category_id"/>
|
<field eval="[(6, 0, [ref('res_partner_category_5')])]" name="category_id"/>
|
||||||
<field name="city">Boston</field>
|
<field name="city">Boston</field>
|
||||||
<field name="zip">02203</field>
|
<field name="zip">02203</field>
|
||||||
<field name="email">info@thinkbig.com</field>
|
<field name="email">thinkbig@yourcompany.example.com</field>
|
||||||
<field name="phone">+1 857 349 3049</field>
|
<field name="phone">+1 857 349 3049</field>
|
||||||
<field name="country_id" ref="base.us"/>
|
<field name="country_id" ref="base.us"/>
|
||||||
<field name="street">One Lincoln Street</field>
|
<field name="street">One Lincoln Street</field>
|
||||||
|
@ -347,7 +351,7 @@
|
||||||
<field name="country_id" ref="base.us"/>
|
<field name="country_id" ref="base.us"/>
|
||||||
<field model="res.country.state" name="state_id" search="[('code','ilike','ca')]"/>
|
<field model="res.country.state" name="state_id" search="[('code','ilike','ca')]"/>
|
||||||
<field name="street">10200 S. De Anza Blvd</field>
|
<field name="street">10200 S. De Anza Blvd</field>
|
||||||
<field name="email">info@seagate.com</field>
|
<field name="email">seagate@yourcompany.example.com</field>
|
||||||
<field name="phone">+1 800 732 4283</field>
|
<field name="phone">+1 800 732 4283</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_19-image.jpg"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_19-image.jpg"/>
|
||||||
</record>
|
</record>
|
||||||
|
@ -374,6 +378,7 @@
|
||||||
<field name="city">Liverpool</field>
|
<field name="city">Liverpool</field>
|
||||||
<field name="country_id" ref="base.uk"/>
|
<field name="country_id" ref="base.uk"/>
|
||||||
<field name="zip">L25 4RL</field>
|
<field name="zip">L25 4RL</field>
|
||||||
|
<field name="email">globalsolutions@yourcompany.example.com</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_21-image.png"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_21-image.png"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
@ -390,6 +395,7 @@
|
||||||
<field name="phone">+44 20 1294 2193</field>
|
<field name="phone">+44 20 1294 2193</field>
|
||||||
<field name="country_id" ref="base.uk"/>
|
<field name="country_id" ref="base.uk"/>
|
||||||
<field name="website">www.vicking-direct.com</field>
|
<field name="website">www.vicking-direct.com</field>
|
||||||
|
<field name="email">vickingdirect@yourcompany.example.com</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_22-image.jpg"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_22-image.jpg"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
@ -404,12 +410,136 @@
|
||||||
<field name="street2">Carretera Panamericana, Km 1, Urb. Delgado Chalbaud</field>
|
<field name="street2">Carretera Panamericana, Km 1, Urb. Delgado Chalbaud</field>
|
||||||
<field name="city">Caracas</field>
|
<field name="city">Caracas</field>
|
||||||
<field name="zip">1090</field>
|
<field name="zip">1090</field>
|
||||||
<field name="email">info@vauxoo.com</field>
|
<field name="email">vauxoo@yourcompany.example.com</field>
|
||||||
<field name="phone">+58 212 681 0538</field>
|
<field name="phone">+58 212 681 0538</field>
|
||||||
<field name="country_id" ref="base.ve"/>
|
<field name="country_id" ref="base.ve"/>
|
||||||
<field name="website">vauxoo.com</field>
|
<field name="website">vauxoo.com</field>
|
||||||
<field name="image" type="base64" file="base/static/img/res_partner_23-image.jpg"/>
|
<field name="image" type="base64" file="base/static/img/res_partner_23-image.jpg"/>
|
||||||
</record>
|
</record>
|
||||||
|
<record id="res_partner_24" model="res.partner">
|
||||||
|
<field name="name">OpenCorp</field>
|
||||||
|
<field eval="[(6, 0, [ref('base.res_partner_category_7'), ref('base.res_partner_category_9')])]" name="category_id"/>
|
||||||
|
<field name="is_company">1</field>
|
||||||
|
<field name="city">Mortsel</field>
|
||||||
|
<field name="zip">2640</field>
|
||||||
|
<field name="country_id" ref="base.be"/>
|
||||||
|
<field name="street">Avenue Louise 149/24</field>
|
||||||
|
<field name="email">opencorp@yourcompany.example.com</field>
|
||||||
|
<field name="phone">+32 3 450 97 11</field>
|
||||||
|
<field name="website">http://www.opencorp.com</field>
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_25" model="res.partner">
|
||||||
|
<field name="name">Míng</field>
|
||||||
|
<field name="supplier">1</field>
|
||||||
|
<field eval="[(6, 0, [ref('res_partner_category_8'), ref('res_partner_category_14')])]" name="category_id"/>
|
||||||
|
<field name="is_company">1</field>
|
||||||
|
<field name="city">Shanghai</field>
|
||||||
|
<field name="zip">201208</field>
|
||||||
|
<field model="res.country" name="country_id" search="[('name','=','China')]"/>
|
||||||
|
<field name="country_id" ref="base.cn"/>
|
||||||
|
<field name="street">89 Dong Lu Road</field>
|
||||||
|
<field name="email">míng@yourcompany.example.com</field>
|
||||||
|
<field name="phone">+86 215 069 5177</field>
|
||||||
|
<field name="website">http://www.míng.com</field>
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_26" model="res.partner">
|
||||||
|
<field name="name">Federal</field>
|
||||||
|
<field name="supplier">1</field>
|
||||||
|
<field eval="[(6, 0, [ref('res_partner_category_8'), ref('res_partner_category_16')])]" name="category_id"/>
|
||||||
|
<field name="is_company">1</field>
|
||||||
|
<field name="city">Düsseldorf</field>
|
||||||
|
<field name="zip">40227</field>
|
||||||
|
<field name="country_id" ref="base.de"/>
|
||||||
|
<field name="street">Willi-Becker-Allee 10</field>
|
||||||
|
<field name="email">federal@yourcompany.example.com</field>
|
||||||
|
<field name="website">http://www.federal.com</field>
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_address_1" model="res.partner">
|
||||||
|
<field name="name">Tang Tsui</field>
|
||||||
|
<field name="parent_id" eval="ref('res_partner_1')"/>
|
||||||
|
<field name="use_parent_address">1</field>
|
||||||
|
<field name="function">Service Manager</field>
|
||||||
|
<field name="email">tang@asustek.com</field>
|
||||||
|
<field name="supplier">1</field>
|
||||||
|
<field eval="0" name="customer"/>
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_address_2" model="res.partner">
|
||||||
|
<field name="name">Joseph Walters</field>
|
||||||
|
<field name="parent_id" eval="ref('res_partner_1')"/>
|
||||||
|
<field name="use_parent_address">1</field>
|
||||||
|
<field name="function">Store Manager</field>
|
||||||
|
<field name="email">joseph.walters@asustek.com</field>
|
||||||
|
<field name="supplier">1</field>
|
||||||
|
<field eval="0" name="customer"/>
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_address_7" model="res.partner">
|
||||||
|
<field name="name">Richard Ellis</field>
|
||||||
|
<field name="parent_id" eval="ref('res_partner_4')"/>
|
||||||
|
<field name="use_parent_address">1</field>
|
||||||
|
<field name="function">Production Supervisor</field>
|
||||||
|
<field name="email">richard.ellis@deltapc.example.com</field>
|
||||||
|
<field name="supplier">1</field>
|
||||||
|
<field eval="0" name="customer"/>
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_address_8" model="res.partner">
|
||||||
|
<field name="name">Paul Williams</field>
|
||||||
|
<field name="parent_id" eval="ref('res_partner_4')"/>
|
||||||
|
<field name="use_parent_address">1</field>
|
||||||
|
<field name="function">Line Mechanic</field>
|
||||||
|
<field name="email">paul.williams@deltapc.example.com</field>
|
||||||
|
<field name="supplier">1</field>
|
||||||
|
<field eval="0" name="customer"/>
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_address_9" model="res.partner">
|
||||||
|
<field name="name">Brian Williams</field>
|
||||||
|
<field name="parent_id" eval="ref('res_partner_4')"/>
|
||||||
|
<field name="use_parent_address">1</field>
|
||||||
|
<field name="function">Computer Technician</field>
|
||||||
|
<field name="email">brian.williams@deltapc.example.com</field>
|
||||||
|
<field name="supplier">1</field>
|
||||||
|
<field eval="0" name="customer"/>
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_address_12" model="res.partner">
|
||||||
|
<field name="name">James Miller</field>
|
||||||
|
<field name="parent_id" eval="ref('res_partner_6')"/>
|
||||||
|
<field name="use_parent_address">1</field>
|
||||||
|
<field name="function">Electrical Supervisor</field>
|
||||||
|
<field name="supplier">1</field>
|
||||||
|
<field eval="0" name="customer"/>
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_address_31" model="res.partner">
|
||||||
|
<field name="name">Edward Foster</field>
|
||||||
|
<field name="parent_id" eval="ref('res_partner_19')"/>
|
||||||
|
<field name="use_parent_address">1</field>
|
||||||
|
<field name="function">Sales Representative</field>
|
||||||
|
<field name="email">efoster@seagate.com</field>
|
||||||
|
<field name="supplier">1</field>
|
||||||
|
<field eval="0" name="customer"/>
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_address_27" model="res.partner">
|
||||||
|
<field name="name">Arthur Gomez</field>
|
||||||
|
<field name="parent_id" eval="ref('res_partner_16')"/>
|
||||||
|
<field name="use_parent_address">1</field>
|
||||||
|
<field name="function">Software Developer</field>
|
||||||
|
<field name="supplier">1</field>
|
||||||
|
<field eval="0" name="customer"/>
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_address_26" model="res.partner">
|
||||||
|
<field name="name">Julia Rivero</field>
|
||||||
|
<field name="parent_id" eval="ref('res_partner_16')"/>
|
||||||
|
<field name="use_parent_address">1</field>
|
||||||
|
<field name="function">Technical Director</field>
|
||||||
|
<field name="supplier">1</field>
|
||||||
|
<field eval="0" name="customer"/>
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_address_35" model="res.partner">
|
||||||
|
<field name="name">Peter Mitchell</field>
|
||||||
|
<field name="parent_id" eval="ref('res_partner_22')"/>
|
||||||
|
<field name="use_parent_address">1</field>
|
||||||
|
<field name="function">Store Manager</field>
|
||||||
|
<field name="supplier">1</field>
|
||||||
|
<field eval="0" name="customer"/>
|
||||||
|
</record>
|
||||||
</data>
|
</data>
|
||||||
</openerp>
|
</openerp>
|
||||||
|
|
||||||
|
|
|
@ -1,242 +1,185 @@
|
||||||
-
|
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_1}:
|
|
||||||
name: Tang Tsui
|
|
||||||
parent_id: base.res_partner_1
|
|
||||||
use_parent_address: True
|
|
||||||
function: Service Manager
|
|
||||||
email: tang@asustek.com
|
|
||||||
-
|
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_2}:
|
|
||||||
name: Joseph Walters
|
|
||||||
parent_id: base.res_partner_1
|
|
||||||
use_parent_address: True
|
|
||||||
function: Store Manager
|
|
||||||
email: joseph.walters@asustek.com
|
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_3}:
|
!record {model: 'res.partner', id: base.res_partner_address_3}:
|
||||||
name: Thomas Passot
|
name: Thomas Passot
|
||||||
parent_id: base.res_partner_2
|
parent_id: base.res_partner_2
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Functional Consultant
|
function: Functional Consultant
|
||||||
email: p.thomas@agrolait.com
|
email: thomas.passot@agrolait.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_4}:
|
!record {model: 'res.partner', id: base.res_partner_address_4}:
|
||||||
name: Michel Fletcher
|
name: Michel Fletcher
|
||||||
parent_id: base.res_partner_2
|
parent_id: base.res_partner_2
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Analyst
|
function: Analyst
|
||||||
email: m.fletcher@agrolait.com
|
email: michel.fletcher@agrolait.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_5}:
|
!record {model: 'res.partner', id: base.res_partner_address_5}:
|
||||||
name: Chao Wang
|
name: Chao Wang
|
||||||
parent_id: base.res_partner_3
|
parent_id: base.res_partner_3
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Marketing Manager
|
function: Marketing Manager
|
||||||
email: chao_wang@chinaexport.com
|
email: chao.wang@chinaexport.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_6}:
|
!record {model: 'res.partner', id: base.res_partner_address_6}:
|
||||||
name: Zhi Ch'ang
|
name: Zhi Ch'ang
|
||||||
parent_id: base.res_partner_3
|
parent_id: base.res_partner_3
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Supervisor
|
function: Supervisor
|
||||||
email: zhi_chang@chinaexport.com
|
email: zhi.chang@chinaexport.example.com
|
||||||
-
|
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_7}:
|
|
||||||
name: Richard Ellis
|
|
||||||
parent_id: base.res_partner_4
|
|
||||||
use_parent_address: True
|
|
||||||
function: Production Supervisor
|
|
||||||
-
|
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_8}:
|
|
||||||
name: Paul Williams
|
|
||||||
parent_id: base.res_partner_4
|
|
||||||
use_parent_address: True
|
|
||||||
function: Line Mechanic
|
|
||||||
email:
|
|
||||||
-
|
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_9}:
|
|
||||||
name: Brian Williams
|
|
||||||
parent_id: base.res_partner_4
|
|
||||||
use_parent_address: True
|
|
||||||
function: Computer Technician
|
|
||||||
email:
|
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_10}:
|
!record {model: 'res.partner', id: base.res_partner_address_10}:
|
||||||
name: David Simpson
|
name: David Simpson
|
||||||
parent_id: base.res_partner_5
|
parent_id: base.res_partner_5
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Senior Consultant
|
function: Senior Consultant
|
||||||
email: david.s@tech.info
|
email: david.simpson@epic.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_11}:
|
!record {model: 'res.partner', id: base.res_partner_address_11}:
|
||||||
name: John M. Brown
|
name: John M. Brown
|
||||||
parent_id: base.res_partner_5
|
parent_id: base.res_partner_5
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Director
|
function: Director
|
||||||
email: john.b@tech.info
|
email: john.brown@epic.example.com
|
||||||
-
|
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_12}:
|
|
||||||
name: James Miller
|
|
||||||
parent_id: base.res_partner_6
|
|
||||||
use_parent_address: True
|
|
||||||
function: Electrical Supervisor
|
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_13}:
|
!record {model: 'res.partner', id: base.res_partner_address_13}:
|
||||||
name: Charlie Bernard
|
name: Charlie Bernard
|
||||||
parent_id: base.res_partner_7
|
parent_id: base.res_partner_7
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Senior Associate
|
function: Senior Associate
|
||||||
email: charlie.bernard@wealthyandsons.com
|
email: charlie.bernard@wealthyandsons.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_14}:
|
!record {model: 'res.partner', id: base.res_partner_address_14}:
|
||||||
name: Jessica Dupont
|
name: Jessica Dupont
|
||||||
parent_id: base.res_partner_7
|
parent_id: base.res_partner_7
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Analyst
|
function: Analyst
|
||||||
email: jessica.dupont@wealthyandsons.com
|
email: jessica.dupont@wealthyandsons.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_15}:
|
!record {model: 'res.partner', id: base.res_partner_address_15}:
|
||||||
name: Phillipp Miller
|
name: Phillipp Miller
|
||||||
parent_id: base.res_partner_8
|
parent_id: base.res_partner_8
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Creative Director
|
function: Creative Director
|
||||||
|
email: phillipp.miller@mediapole.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_16}:
|
!record {model: 'res.partner', id: base.res_partner_address_16}:
|
||||||
name: Ayaan Agarwal
|
name: Ayaan Agarwal
|
||||||
parent_id: base.res_partner_9
|
parent_id: base.res_partner_9
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Director
|
function: Director
|
||||||
|
email: ayaan.agarwal@bestdesigners.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_17}:
|
!record {model: 'res.partner', id: base.res_partner_address_17}:
|
||||||
name: Daniel Jackson
|
name: Daniel Jackson
|
||||||
parent_id: base.res_partner_10
|
parent_id: base.res_partner_10
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Managing Partner
|
function: Managing Partner
|
||||||
email: daniel@jackson.com
|
email: daniel.jackson@jackson.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_18}:
|
!record {model: 'res.partner', id: base.res_partner_address_18}:
|
||||||
name: William Thomas
|
name: William Thomas
|
||||||
parent_id: base.res_partner_10
|
parent_id: base.res_partner_10
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Senior Consultant
|
function: Senior Consultant
|
||||||
email: william@jackson.com
|
email: william.jackson@jackson.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_19}:
|
!record {model: 'res.partner', id: base.res_partner_address_19}:
|
||||||
name: Sergio Pérez
|
name: Sergio Pérez
|
||||||
parent_id: base.res_partner_11
|
parent_id: base.res_partner_11
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Accountant
|
function: Accountant
|
||||||
email:
|
email: sergio.perez@luminous.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_20}:
|
!record {model: 'res.partner', id: base.res_partner_address_20}:
|
||||||
name: Laura Castro
|
name: Laura Castro
|
||||||
parent_id: base.res_partner_11
|
parent_id: base.res_partner_11
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Goods Supervisor
|
function: Goods Supervisor
|
||||||
email:
|
email: laura.castro@luminous.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_21}:
|
!record {model: 'res.partner', id: base.res_partner_address_21}:
|
||||||
name: Luc Maurer
|
name: Luc Maurer
|
||||||
parent_id: base.res_partner_12
|
parent_id: base.res_partner_12
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Director
|
function: Director
|
||||||
|
email: luc.maurer@camptocamp.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_22}:
|
!record {model: 'res.partner', id: base.res_partner_address_22}:
|
||||||
name: Laith Jubair
|
name: Laith Jubair
|
||||||
parent_id: base.res_partner_13
|
parent_id: base.res_partner_13
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Director
|
function: Director
|
||||||
|
email: laith.jubair@axelor.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_23}:
|
!record {model: 'res.partner', id: base.res_partner_address_23}:
|
||||||
name: Angel Cook
|
name: Angel Cook
|
||||||
parent_id: base.res_partner_14
|
parent_id: base.res_partner_14
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: General Manager
|
function: General Manager
|
||||||
email: angel.cook@chamberworks.com
|
email: angel.cook@chamberworks.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_24}:
|
!record {model: 'res.partner', id: base.res_partner_address_24}:
|
||||||
name: Robert Anderson
|
name: Robert Anderson
|
||||||
parent_id: base.res_partner_14
|
parent_id: base.res_partner_14
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: System Analyst
|
function: System Analyst
|
||||||
email: robert.anderson@chamberworks.com
|
email: robert.anderson@chamberworks.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_25}:
|
!record {model: 'res.partner', id: base.res_partner_address_25}:
|
||||||
name: Jacob Taylor
|
name: Jacob Taylor
|
||||||
parent_id: base.res_partner_15
|
parent_id: base.res_partner_15
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Order Clerk
|
function: Order Clerk
|
||||||
-
|
email: jacob.taylor@millennium.example.com
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_26}:
|
|
||||||
name: Arthur Gomez
|
|
||||||
parent_id: base.res_partner_16
|
|
||||||
use_parent_address: True
|
|
||||||
function: Software Developer
|
|
||||||
email:
|
|
||||||
-
|
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_27}:
|
|
||||||
name: Julia Rivero
|
|
||||||
parent_id: base.res_partner_16
|
|
||||||
use_parent_address: True
|
|
||||||
function: Technical Director
|
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_28}:
|
!record {model: 'res.partner', id: base.res_partner_address_28}:
|
||||||
name: Benjamin Flores
|
name: Benjamin Flores
|
||||||
parent_id: base.res_partner_17
|
parent_id: base.res_partner_17
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Business Executive
|
function: Business Executive
|
||||||
email: ben@nebula.ar
|
email: benjamin.flores@nebula.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_29}:
|
!record {model: 'res.partner', id: base.res_partner_address_29}:
|
||||||
name: George Wilson
|
name: George Wilson
|
||||||
parent_id: base.res_partner_18
|
parent_id: base.res_partner_18
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Chief Information Officer (CIO)
|
function: Chief Information Officer (CIO)
|
||||||
email:
|
email: george.wilson@thinkbig.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_30}:
|
!record {model: 'res.partner', id: base.res_partner_address_30}:
|
||||||
name: Lucas Jones
|
name: Lucas Jones
|
||||||
parent_id: base.res_partner_18
|
parent_id: base.res_partner_18
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Functional Consultant
|
function: Functional Consultant
|
||||||
email: jones@thinkbig.com
|
email: lucas.jones@thinkbig.example.com
|
||||||
-
|
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_31}:
|
|
||||||
name: Edward Foster
|
|
||||||
parent_id: base.res_partner_19
|
|
||||||
use_parent_address: True
|
|
||||||
function: Sales Representative
|
|
||||||
email: efoster@seagate.com
|
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_32}:
|
!record {model: 'res.partner', id: base.res_partner_address_32}:
|
||||||
name: Robin Smith
|
name: Robin Smith
|
||||||
parent_id: base.res_partner_21
|
parent_id: base.res_partner_21
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Sales Manager
|
function: Sales Manager
|
||||||
|
email: robin.smith@globalsolutions.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_33}:
|
!record {model: 'res.partner', id: base.res_partner_address_33}:
|
||||||
name: Morgan Rose
|
name: Morgan Rose
|
||||||
parent_id: base.res_partner_21
|
parent_id: base.res_partner_21
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Financial Manager
|
function: Financial Manager
|
||||||
|
email: morgan.rose@globalsolutions.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_34}:
|
!record {model: 'res.partner', id: base.res_partner_address_34}:
|
||||||
name: Kevin Clarke
|
name: Kevin Clarke
|
||||||
parent_id: base.res_partner_21
|
parent_id: base.res_partner_21
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Knowledge Manager
|
function: Knowledge Manager
|
||||||
-
|
email: kevin.clarke@globalsolutions.example.com
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_35}:
|
|
||||||
name: Peter Mitchell
|
|
||||||
parent_id: base.res_partner_22
|
|
||||||
use_parent_address: True
|
|
||||||
function: Store Manager
|
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_address_36}:
|
!record {model: 'res.partner', id: base.res_partner_address_36}:
|
||||||
name: Nhomar Hernandez
|
name: Nhomar Hernandez
|
||||||
parent_id: base.res_partner_23
|
parent_id: base.res_partner_23
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Chief Executive Officer
|
function: Chief Executive Officer
|
||||||
|
email: nhomar.hernandez@vauxoo.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_main1}:
|
!record {model: 'res.partner', id: base.res_partner_main1}:
|
||||||
name: Mark Davis
|
name: Mark Davis
|
||||||
|
@ -244,7 +187,7 @@
|
||||||
parent_id: base.main_partner
|
parent_id: base.main_partner
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Chief Executive Officer (CEO)
|
function: Chief Executive Officer (CEO)
|
||||||
email: mark@yourcompany.com
|
email: mark.davis@yourcompany.example.com
|
||||||
-
|
-
|
||||||
!record {model: 'res.partner', id: base.res_partner_main2}:
|
!record {model: 'res.partner', id: base.res_partner_main2}:
|
||||||
name: Roger Scott
|
name: Roger Scott
|
||||||
|
@ -252,4 +195,4 @@
|
||||||
parent_id: base.main_partner
|
parent_id: base.main_partner
|
||||||
use_parent_address: True
|
use_parent_address: True
|
||||||
function: Chief Operations Officer (COO)
|
function: Chief Operations Officer (COO)
|
||||||
email: roger@yourcompany.com
|
email: roger.scott@yourcompany.example.com
|
||||||
|
|
|
@ -600,6 +600,7 @@ class users_implied(osv.osv):
|
||||||
if groups:
|
if groups:
|
||||||
# delegate addition of groups to add implied groups
|
# delegate addition of groups to add implied groups
|
||||||
self.write(cr, uid, [user_id], {'groups_id': groups}, context)
|
self.write(cr, uid, [user_id], {'groups_id': groups}, context)
|
||||||
|
self.pool['ir.ui.view'].clear_cache()
|
||||||
return user_id
|
return user_id
|
||||||
|
|
||||||
def write(self, cr, uid, ids, values, context=None):
|
def write(self, cr, uid, ids, values, context=None):
|
||||||
|
@ -612,6 +613,7 @@ class users_implied(osv.osv):
|
||||||
gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
|
gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
|
||||||
vals = {'groups_id': [(4, g.id) for g in gs]}
|
vals = {'groups_id': [(4, g.id) for g in gs]}
|
||||||
super(users_implied, self).write(cr, uid, [user.id], vals, context)
|
super(users_implied, self).write(cr, uid, [user.id], vals, context)
|
||||||
|
self.pool['ir.ui.view'].clear_cache()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
|
@ -681,8 +683,10 @@ class groups_view(osv.osv):
|
||||||
def update_user_groups_view(self, cr, uid, context=None):
|
def update_user_groups_view(self, cr, uid, context=None):
|
||||||
# the view with id 'base.user_groups_view' inherits the user form view,
|
# the view with id 'base.user_groups_view' inherits the user form view,
|
||||||
# and introduces the reified group fields
|
# and introduces the reified group fields
|
||||||
view = self.get_user_groups_view(cr, uid, context)
|
# we have to try-catch this, because at first init the view does not exist
|
||||||
if view:
|
# but we are already creating some basic groups
|
||||||
|
view = self.pool['ir.model.data'].xmlid_to_object(cr, SUPERUSER_ID, 'base.user_groups_view', context=context)
|
||||||
|
if view and view.exists() and view._table_name == 'ir.ui.view':
|
||||||
xml1, xml2 = [], []
|
xml1, xml2 = [], []
|
||||||
xml1.append(E.separator(string=_('Application'), colspan="4"))
|
xml1.append(E.separator(string=_('Application'), colspan="4"))
|
||||||
for app, kind, gs in self.get_groups_by_application(cr, uid, context):
|
for app, kind, gs in self.get_groups_by_application(cr, uid, context):
|
||||||
|
@ -707,14 +711,6 @@ class groups_view(osv.osv):
|
||||||
view.write({'arch': xml_content})
|
view.write({'arch': xml_content})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_user_groups_view(self, cr, uid, context=None):
|
|
||||||
try:
|
|
||||||
view = self.pool['ir.model.data'].get_object(cr, SUPERUSER_ID, 'base', 'user_groups_view', context)
|
|
||||||
assert view and view._table_name == 'ir.ui.view'
|
|
||||||
except Exception:
|
|
||||||
view = False
|
|
||||||
return view
|
|
||||||
|
|
||||||
def get_application_groups(self, cr, uid, domain=None, context=None):
|
def get_application_groups(self, cr, uid, domain=None, context=None):
|
||||||
return self.search(cr, uid, domain or [])
|
return self.search(cr, uid, domain or [])
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,16 @@
|
||||||
<field name="domain_force">['|','|',('company_id.child_ids','child_of',[user.company_id.id]),('company_id','child_of',[user.company_id.id]),('company_id','=',False)]</field>
|
<field name="domain_force">['|','|',('company_id.child_ids','child_of',[user.company_id.id]),('company_id','child_of',[user.company_id.id]),('company_id','=',False)]</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record model="ir.rule" id="res_partner_portal_public_rule">
|
||||||
|
<field name="name">res_partner: portal/public: read access on my commercial partner</field>
|
||||||
|
<field name="model_id" ref="base.model_res_partner"/>
|
||||||
|
<field name="domain_force">[('id', 'child_of', user.commercial_partner_id.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_portal')), (4, ref('base.group_public'))]"/>
|
||||||
|
<field name="perm_create" eval="False"/>
|
||||||
|
<field name="perm_unlink" eval="False"/>
|
||||||
|
<field name="perm_write" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
<record model="ir.rule" id="multi_company_default_rule">
|
<record model="ir.rule" id="multi_company_default_rule">
|
||||||
<field name="name">Multi_company_default company</field>
|
<field name="name">Multi_company_default company</field>
|
||||||
<field name="model_id" ref="model_multi_company_default"/>
|
<field name="model_id" ref="model_multi_company_default"/>
|
||||||
|
|
|
@ -43,8 +43,6 @@
|
||||||
"access_ir_values_group_all","ir_values group_all","model_ir_values",,1,1,1,1
|
"access_ir_values_group_all","ir_values group_all","model_ir_values",,1,1,1,1
|
||||||
"access_res_company_group_erp_manager","res_company group_erp_manager","model_res_company","group_erp_manager",1,1,1,1
|
"access_res_company_group_erp_manager","res_company group_erp_manager","model_res_company","group_erp_manager",1,1,1,1
|
||||||
"access_res_company_group_user","res_company group_user","model_res_company",,1,0,0,0
|
"access_res_company_group_user","res_company group_user","model_res_company",,1,0,0,0
|
||||||
"access_res_font_group_erp_manager","res_font group_erp_manager","model_res_font","group_erp_manager",1,1,1,1
|
|
||||||
"access_res_font_group_all","res_font group_all","model_res_font",,1,0,0,0
|
|
||||||
"access_res_country_group_all","res_country group_user_all","model_res_country",,1,0,0,0
|
"access_res_country_group_all","res_country group_user_all","model_res_country",,1,0,0,0
|
||||||
"access_res_country_state_group_all","res_country_state group_user_all","model_res_country_state",,1,0,0,0
|
"access_res_country_state_group_all","res_country_state group_user_all","model_res_country_state",,1,0,0,0
|
||||||
"access_res_country_group_user","res_country group_user","model_res_country","group_partner_manager",1,1,1,1
|
"access_res_country_group_user","res_country group_user","model_res_country","group_partner_manager",1,1,1,1
|
||||||
|
@ -58,6 +56,8 @@
|
||||||
"access_res_groups_group_user","res_groups group_user","model_res_groups",,1,0,0,0
|
"access_res_groups_group_user","res_groups group_user","model_res_groups",,1,0,0,0
|
||||||
"access_res_lang_group_all","res_lang group_all","model_res_lang",,1,0,0,0
|
"access_res_lang_group_all","res_lang group_all","model_res_lang",,1,0,0,0
|
||||||
"access_res_lang_group_user","res_lang group_user","model_res_lang","group_system",1,1,1,1
|
"access_res_lang_group_user","res_lang group_user","model_res_lang","group_system",1,1,1,1
|
||||||
|
"access_res_partner_public","res_partner group_public","model_res_partner","group_public",1,0,0,0
|
||||||
|
"access_res_partner_portal","res_partner group_portal","model_res_partner","group_portal",1,0,0,0
|
||||||
"access_res_partner_group_partner_manager","res_partner group_partner_manager","model_res_partner","group_partner_manager",1,1,1,1
|
"access_res_partner_group_partner_manager","res_partner group_partner_manager","model_res_partner","group_partner_manager",1,1,1,1
|
||||||
"access_res_partner_group_user","res_partner group_user","model_res_partner","group_user",1,0,0,0
|
"access_res_partner_group_user","res_partner group_user","model_res_partner","group_user",1,0,0,0
|
||||||
"access_res_partner_bank_group_user","res_partner_bank group_user","model_res_partner_bank","group_user",1,0,0,0
|
"access_res_partner_bank_group_user","res_partner_bank group_user","model_res_partner_bank","group_user",1,0,0,0
|
||||||
|
@ -111,4 +111,5 @@
|
||||||
"access_ir_mail_server","ir_mail_server","model_ir_mail_server","group_system",1,1,1,1
|
"access_ir_mail_server","ir_mail_server","model_ir_mail_server","group_system",1,1,1,1
|
||||||
"access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0
|
"access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0
|
||||||
"access_ir_needaction_mixin","ir_needaction_mixin","model_ir_needaction_mixin",,1,1,1,1
|
"access_ir_needaction_mixin","ir_needaction_mixin","model_ir_needaction_mixin",,1,1,1,1
|
||||||
|
"access_res_font_all","res_res_font all","model_res_font",,1,0,0,0
|
||||||
|
"access_res_font_group_user","res_res_font group_user","model_res_font","group_user",1,1,1,1
|
||||||
|
|
|
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
|
@ -1,48 +1,425 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import unittest2
|
|
||||||
|
|
||||||
import lxml.etree
|
from lxml import etree as ET
|
||||||
from lxml.builder import E
|
from lxml.builder import E
|
||||||
|
|
||||||
import openerp.tests.common as common
|
from openerp.tests import common
|
||||||
from openerp.osv.orm import except_orm
|
|
||||||
from openerp.tools import mute_logger
|
Field = E.field
|
||||||
|
|
||||||
|
class TestNodeLocator(common.BaseCase):
|
||||||
|
"""
|
||||||
|
The node locator returns None when it can not find a node, and the first
|
||||||
|
match when it finds something (no jquery-style node sets)
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
super(TestNodeLocator, self).setUp()
|
||||||
|
self.Views = self.registry('ir.ui.view')
|
||||||
|
|
||||||
|
def test_no_match_xpath(self):
|
||||||
|
"""
|
||||||
|
xpath simply uses the provided @expr pattern to find a node
|
||||||
|
"""
|
||||||
|
node = self.Views.locate_node(
|
||||||
|
E.root(E.foo(), E.bar(), E.baz()),
|
||||||
|
E.xpath(expr="//qux"))
|
||||||
|
self.assertIsNone(node)
|
||||||
|
|
||||||
|
def test_match_xpath(self):
|
||||||
|
bar = E.bar()
|
||||||
|
node = self.Views.locate_node(
|
||||||
|
E.root(E.foo(), bar, E.baz()),
|
||||||
|
E.xpath(expr="//bar"))
|
||||||
|
self.assertIs(node, bar)
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_match_field(self):
|
||||||
|
"""
|
||||||
|
A field spec will match by @name against all fields of the view
|
||||||
|
"""
|
||||||
|
node = self.Views.locate_node(
|
||||||
|
E.root(E.foo(), E.bar(), E.baz()),
|
||||||
|
Field(name="qux"))
|
||||||
|
self.assertIsNone(node)
|
||||||
|
|
||||||
|
node = self.Views.locate_node(
|
||||||
|
E.root(Field(name="foo"), Field(name="bar"), Field(name="baz")),
|
||||||
|
Field(name="qux"))
|
||||||
|
self.assertIsNone(node)
|
||||||
|
|
||||||
|
def test_match_field(self):
|
||||||
|
bar = Field(name="bar")
|
||||||
|
node = self.Views.locate_node(
|
||||||
|
E.root(Field(name="foo"), bar, Field(name="baz")),
|
||||||
|
Field(name="bar"))
|
||||||
|
self.assertIs(node, bar)
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_match_other(self):
|
||||||
|
"""
|
||||||
|
Non-xpath non-fields are matched by node name first
|
||||||
|
"""
|
||||||
|
node = self.Views.locate_node(
|
||||||
|
E.root(E.foo(), E.bar(), E.baz()),
|
||||||
|
E.qux())
|
||||||
|
self.assertIsNone(node)
|
||||||
|
|
||||||
|
def test_match_other(self):
|
||||||
|
bar = E.bar()
|
||||||
|
node = self.Views.locate_node(
|
||||||
|
E.root(E.foo(), bar, E.baz()),
|
||||||
|
E.bar())
|
||||||
|
self.assertIs(bar, node)
|
||||||
|
|
||||||
|
def test_attribute_mismatch(self):
|
||||||
|
"""
|
||||||
|
Non-xpath non-field are filtered by matching attributes on spec and
|
||||||
|
matched nodes
|
||||||
|
"""
|
||||||
|
node = self.Views.locate_node(
|
||||||
|
E.root(E.foo(attr='1'), E.bar(attr='2'), E.baz(attr='3')),
|
||||||
|
E.bar(attr='5'))
|
||||||
|
self.assertIsNone(node)
|
||||||
|
|
||||||
|
def test_attribute_filter(self):
|
||||||
|
match = E.bar(attr='2')
|
||||||
|
node = self.Views.locate_node(
|
||||||
|
E.root(E.bar(attr='1'), match, E.root(E.bar(attr='3'))),
|
||||||
|
E.bar(attr='2'))
|
||||||
|
self.assertIs(node, match)
|
||||||
|
|
||||||
|
def test_version_mismatch(self):
|
||||||
|
"""
|
||||||
|
A @version on the spec will be matched against the view's version
|
||||||
|
"""
|
||||||
|
node = self.Views.locate_node(
|
||||||
|
E.root(E.foo(attr='1'), version='4'),
|
||||||
|
E.foo(attr='1', version='3'))
|
||||||
|
self.assertIsNone(node)
|
||||||
|
|
||||||
|
class TestViewInheritance(common.TransactionCase):
|
||||||
|
def arch_for(self, name, view_type='form', parent=None):
|
||||||
|
""" Generates a trivial view of the specified ``view_type``.
|
||||||
|
|
||||||
|
The generated view is empty but ``name`` is set as its root's ``@string``.
|
||||||
|
|
||||||
|
If ``parent`` is not falsy, generates an extension view (instead of
|
||||||
|
a root view) replacing the parent's ``@string`` by ``name``
|
||||||
|
|
||||||
|
:param str name: ``@string`` value for the view root
|
||||||
|
:param str view_type:
|
||||||
|
:param bool parent:
|
||||||
|
:return: generated arch
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
if not parent:
|
||||||
|
element = E(view_type, string=name)
|
||||||
|
else:
|
||||||
|
element = E(view_type,
|
||||||
|
E.attribute(name, name='string'),
|
||||||
|
position='attributes'
|
||||||
|
)
|
||||||
|
return ET.tostring(element)
|
||||||
|
|
||||||
|
def makeView(self, name, parent=None, arch=None):
|
||||||
|
""" Generates a basic ir.ui.view with the provided name, parent and arch.
|
||||||
|
|
||||||
|
If no parent is provided, the view is top-level.
|
||||||
|
|
||||||
|
If no arch is provided, generates one by calling :meth:`~.arch_for`.
|
||||||
|
|
||||||
|
:param str name:
|
||||||
|
:param int parent: id of the parent view, if any
|
||||||
|
:param str arch:
|
||||||
|
:returns: the created view's id.
|
||||||
|
:rtype: int
|
||||||
|
"""
|
||||||
|
view_id = self.View.create(self.cr, self.uid, {
|
||||||
|
'model': self.model,
|
||||||
|
'name': name,
|
||||||
|
'arch': arch or self.arch_for(name, parent=parent),
|
||||||
|
'inherit_id': parent,
|
||||||
|
'priority': 5, # higher than default views
|
||||||
|
})
|
||||||
|
self.ids[name] = view_id
|
||||||
|
return view_id
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestViewInheritance, self).setUp()
|
||||||
|
|
||||||
|
self.model = 'ir.ui.view.custom'
|
||||||
|
self.View = self.registry('ir.ui.view')
|
||||||
|
self._init = self.View.pool._init
|
||||||
|
self.View.pool._init = False
|
||||||
|
self.ids = {}
|
||||||
|
|
||||||
|
a = self.makeView("A")
|
||||||
|
a1 = self.makeView("A1", a)
|
||||||
|
a11 = self.makeView("A11", a1)
|
||||||
|
self.makeView("A111", a11)
|
||||||
|
self.makeView("A12", a1)
|
||||||
|
a2 = self.makeView("A2", a)
|
||||||
|
self.makeView("A21", a2)
|
||||||
|
a22 = self.makeView("A22", a2)
|
||||||
|
self.makeView("A221", a22)
|
||||||
|
|
||||||
|
b = self.makeView('B', arch=self.arch_for("B", 'tree'))
|
||||||
|
self.makeView('B1', b, arch=self.arch_for("B1", 'tree', parent=b))
|
||||||
|
c = self.makeView('C', arch=self.arch_for("C", 'tree'))
|
||||||
|
self.View.write(self.cr, self.uid, c, {'priority': 1})
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.View.pool._init = self._init
|
||||||
|
super(TestViewInheritance, self).tearDown()
|
||||||
|
|
||||||
|
def test_get_inheriting_views_arch(self):
|
||||||
|
self.assertEqual(self.View.get_inheriting_views_arch(
|
||||||
|
self.cr, self.uid, self.ids['A'], self.model), [
|
||||||
|
(self.arch_for('A1', parent=True), self.ids['A1']),
|
||||||
|
(self.arch_for('A2', parent=True), self.ids['A2']),
|
||||||
|
])
|
||||||
|
|
||||||
|
self.assertEqual(self.View.get_inheriting_views_arch(
|
||||||
|
self.cr, self.uid, self.ids['A21'], self.model),
|
||||||
|
[])
|
||||||
|
|
||||||
|
self.assertEqual(self.View.get_inheriting_views_arch(
|
||||||
|
self.cr, self.uid, self.ids['A11'], self.model),
|
||||||
|
[(self.arch_for('A111', parent=True), self.ids['A111'])])
|
||||||
|
|
||||||
|
def test_default_view(self):
|
||||||
|
default = self.View.default_view(
|
||||||
|
self.cr, self.uid, model=self.model, view_type='form')
|
||||||
|
self.assertEqual(default, self.ids['A'])
|
||||||
|
|
||||||
|
default_tree = self.View.default_view(
|
||||||
|
self.cr, self.uid, model=self.model, view_type='tree')
|
||||||
|
self.assertEqual(default_tree, self.ids['C'])
|
||||||
|
|
||||||
|
def test_no_default_view(self):
|
||||||
|
self.assertFalse(
|
||||||
|
self.View.default_view(
|
||||||
|
self.cr, self.uid, model='does.not.exist', view_type='form'))
|
||||||
|
|
||||||
|
self.assertFalse(
|
||||||
|
self.View.default_view(
|
||||||
|
self.cr, self.uid, model=self.model, view_type='graph'))
|
||||||
|
|
||||||
|
class TestApplyInheritanceSpecs(common.TransactionCase):
|
||||||
|
""" Applies a sequence of inheritance specification nodes to a base
|
||||||
|
architecture. IO state parameters (cr, uid, model, context) are used for
|
||||||
|
error reporting
|
||||||
|
|
||||||
|
The base architecture is altered in-place.
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
super(TestApplyInheritanceSpecs, self).setUp()
|
||||||
|
self.View = self.registry('ir.ui.view')
|
||||||
|
self.base_arch = E.form(
|
||||||
|
Field(name="target"),
|
||||||
|
string="Title")
|
||||||
|
|
||||||
|
def test_replace(self):
|
||||||
|
spec = Field(
|
||||||
|
Field(name="replacement"),
|
||||||
|
name="target", position="replace")
|
||||||
|
|
||||||
|
self.View.apply_inheritance_specs(self.cr, self.uid,
|
||||||
|
self.base_arch,
|
||||||
|
spec, None)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
ET.tostring(self.base_arch),
|
||||||
|
ET.tostring(E.form(Field(name="replacement"), string="Title")))
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
spec = Field(name="target", position="replace")
|
||||||
|
|
||||||
|
self.View.apply_inheritance_specs(self.cr, self.uid,
|
||||||
|
self.base_arch,
|
||||||
|
spec, None)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
ET.tostring(self.base_arch),
|
||||||
|
ET.tostring(E.form(string="Title")))
|
||||||
|
|
||||||
|
def test_insert_after(self):
|
||||||
|
spec = Field(
|
||||||
|
Field(name="inserted"),
|
||||||
|
name="target", position="after")
|
||||||
|
|
||||||
|
self.View.apply_inheritance_specs(self.cr, self.uid,
|
||||||
|
self.base_arch,
|
||||||
|
spec, None)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
ET.tostring(self.base_arch),
|
||||||
|
ET.tostring(E.form(
|
||||||
|
Field(name="target"),
|
||||||
|
Field(name="inserted"),
|
||||||
|
string="Title"
|
||||||
|
)))
|
||||||
|
|
||||||
|
def test_insert_before(self):
|
||||||
|
spec = Field(
|
||||||
|
Field(name="inserted"),
|
||||||
|
name="target", position="before")
|
||||||
|
|
||||||
|
self.View.apply_inheritance_specs(self.cr, self.uid,
|
||||||
|
self.base_arch,
|
||||||
|
spec, None)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
ET.tostring(self.base_arch),
|
||||||
|
ET.tostring(E.form(
|
||||||
|
Field(name="inserted"),
|
||||||
|
Field(name="target"),
|
||||||
|
string="Title")))
|
||||||
|
|
||||||
|
def test_insert_inside(self):
|
||||||
|
default = Field(Field(name="inserted"), name="target")
|
||||||
|
spec = Field(Field(name="inserted 2"), name="target", position='inside')
|
||||||
|
|
||||||
|
self.View.apply_inheritance_specs(self.cr, self.uid,
|
||||||
|
self.base_arch,
|
||||||
|
default, None)
|
||||||
|
self.View.apply_inheritance_specs(self.cr, self.uid,
|
||||||
|
self.base_arch,
|
||||||
|
spec, None)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
ET.tostring(self.base_arch),
|
||||||
|
ET.tostring(E.form(
|
||||||
|
Field(
|
||||||
|
Field(name="inserted"),
|
||||||
|
Field(name="inserted 2"),
|
||||||
|
name="target"),
|
||||||
|
string="Title")))
|
||||||
|
|
||||||
|
def test_unpack_data(self):
|
||||||
|
spec = E.data(
|
||||||
|
Field(Field(name="inserted 0"), name="target"),
|
||||||
|
Field(Field(name="inserted 1"), name="target"),
|
||||||
|
Field(Field(name="inserted 2"), name="target"),
|
||||||
|
Field(Field(name="inserted 3"), name="target"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.View.apply_inheritance_specs(self.cr, self.uid,
|
||||||
|
self.base_arch,
|
||||||
|
spec, None)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
ET.tostring(self.base_arch),
|
||||||
|
ET.tostring(E.form(
|
||||||
|
Field(
|
||||||
|
Field(name="inserted 0"),
|
||||||
|
Field(name="inserted 1"),
|
||||||
|
Field(name="inserted 2"),
|
||||||
|
Field(name="inserted 3"),
|
||||||
|
name="target"),
|
||||||
|
string="Title")))
|
||||||
|
|
||||||
|
def test_invalid_position(self):
|
||||||
|
spec = Field(
|
||||||
|
Field(name="whoops"),
|
||||||
|
name="target", position="serious_series")
|
||||||
|
|
||||||
|
with self.assertRaises(AttributeError):
|
||||||
|
self.View.apply_inheritance_specs(self.cr, self.uid,
|
||||||
|
self.base_arch,
|
||||||
|
spec, None)
|
||||||
|
|
||||||
|
def test_incorrect_version(self):
|
||||||
|
# Version ignored on //field elements, so use something else
|
||||||
|
arch = E.form(E.element(foo="42"))
|
||||||
|
spec = E.element(
|
||||||
|
Field(name="placeholder"),
|
||||||
|
foo="42", version="7.0")
|
||||||
|
|
||||||
|
with self.assertRaises(AttributeError):
|
||||||
|
self.View.apply_inheritance_specs(self.cr, self.uid,
|
||||||
|
arch,
|
||||||
|
spec, None)
|
||||||
|
|
||||||
|
def test_target_not_found(self):
|
||||||
|
spec = Field(name="targut")
|
||||||
|
|
||||||
|
with self.assertRaises(AttributeError):
|
||||||
|
self.View.apply_inheritance_specs(self.cr, self.uid,
|
||||||
|
self.base_arch,
|
||||||
|
spec, None)
|
||||||
|
|
||||||
|
class TestApplyInheritedArchs(common.TransactionCase):
|
||||||
|
""" Applies a sequence of modificator archs to a base view
|
||||||
|
"""
|
||||||
|
|
||||||
|
class TestViewCombined(common.TransactionCase):
|
||||||
|
"""
|
||||||
|
Test fallback operations of View.read_combined:
|
||||||
|
* defaults mapping
|
||||||
|
* ?
|
||||||
|
"""
|
||||||
|
|
||||||
|
class TestNoModel(common.TransactionCase):
|
||||||
|
def test_create_view_nomodel(self):
|
||||||
|
View = self.registry('ir.ui.view')
|
||||||
|
view_id = View.create(self.cr, self.uid, {
|
||||||
|
'name': 'dummy',
|
||||||
|
'arch': '<template name="foo"/>',
|
||||||
|
'inherit_id': False,
|
||||||
|
'type': 'qweb',
|
||||||
|
})
|
||||||
|
fields = ['name', 'arch', 'type', 'priority', 'inherit_id', 'model']
|
||||||
|
[view] = View.read(self.cr, self.uid, [view_id], fields)
|
||||||
|
self.assertEqual(view, {
|
||||||
|
'id': view_id,
|
||||||
|
'name': 'dummy',
|
||||||
|
'arch': '<template name="foo"/>',
|
||||||
|
'type': 'qweb',
|
||||||
|
'priority': 16,
|
||||||
|
'inherit_id': False,
|
||||||
|
'model': False,
|
||||||
|
})
|
||||||
|
|
||||||
|
text_para = E.p("", {'class': 'legalese'})
|
||||||
|
arch = E.body(
|
||||||
|
E.div(
|
||||||
|
E.h1("Title"),
|
||||||
|
id="header"),
|
||||||
|
E.p("Welcome!"),
|
||||||
|
E.div(
|
||||||
|
E.hr(),
|
||||||
|
text_para,
|
||||||
|
id="footer"),
|
||||||
|
{'class': "index"},)
|
||||||
|
|
||||||
|
def test_qweb_translation(self):
|
||||||
|
"""
|
||||||
|
Test if translations work correctly without a model
|
||||||
|
"""
|
||||||
|
View = self.registry('ir.ui.view')
|
||||||
|
self.registry('res.lang').load_lang(self.cr, self.uid, 'fr_FR')
|
||||||
|
orig_text = "Copyright copyrighter"
|
||||||
|
translated_text = u"Copyrighter, tous droits réservés"
|
||||||
|
self.text_para.text = orig_text
|
||||||
|
self.registry('ir.translation').create(self.cr, self.uid, {
|
||||||
|
'name': 'website',
|
||||||
|
'type': 'view',
|
||||||
|
'lang': 'fr_FR',
|
||||||
|
'src': orig_text,
|
||||||
|
'value': translated_text,
|
||||||
|
})
|
||||||
|
sarch = View.translate_qweb(self.cr, self.uid, None, self.arch, 'fr_FR')
|
||||||
|
|
||||||
|
self.text_para.text = translated_text
|
||||||
|
self.assertEqual(
|
||||||
|
ET.tostring(sarch, encoding='utf-8'),
|
||||||
|
ET.tostring(self.arch, encoding='utf-8'))
|
||||||
|
|
||||||
|
|
||||||
class test_views(common.TransactionCase):
|
class test_views(common.TransactionCase):
|
||||||
|
|
||||||
@mute_logger('openerp.osv.orm', 'openerp.addons.base.ir.ir_ui_view')
|
|
||||||
def test_whatever(self):
|
|
||||||
Views = self.registry('ir.ui.view')
|
|
||||||
|
|
||||||
self.assertTrue(Views.pool._init)
|
|
||||||
|
|
||||||
error_msg = "The model name does not exist or the view architecture cannot be rendered"
|
|
||||||
# test arch check is call for views without xmlid during registry initialization
|
|
||||||
with self.assertRaisesRegexp(except_orm, error_msg):
|
|
||||||
Views.create(self.cr, self.uid, {
|
|
||||||
'name': 'Test View #1',
|
|
||||||
'model': 'ir.ui.view',
|
|
||||||
'arch': """<?xml version="1.0"?>
|
|
||||||
<tree>
|
|
||||||
<field name="test_1"/>
|
|
||||||
</tree>
|
|
||||||
""",
|
|
||||||
})
|
|
||||||
|
|
||||||
# same for inherited views
|
|
||||||
with self.assertRaisesRegexp(except_orm, error_msg):
|
|
||||||
# Views.pudb = True
|
|
||||||
Views.create(self.cr, self.uid, {
|
|
||||||
'name': 'Test View #2',
|
|
||||||
'model': 'ir.ui.view',
|
|
||||||
'inherit_id': self.browse_ref('base.view_view_tree').id,
|
|
||||||
'arch': """<?xml version="1.0"?>
|
|
||||||
<xpath expr="//field[@name='name']" position="after">
|
|
||||||
<field name="test_2"/>
|
|
||||||
</xpath>
|
|
||||||
""",
|
|
||||||
})
|
|
||||||
|
|
||||||
def test_nonexistent_attribute_removal(self):
|
def test_nonexistent_attribute_removal(self):
|
||||||
Views = self.registry('ir.ui.view')
|
Views = self.registry('ir.ui.view')
|
||||||
Views.create(self.cr, self.uid, {
|
Views.create(self.cr, self.uid, {
|
||||||
|
@ -75,47 +452,32 @@ class test_views(common.TransactionCase):
|
||||||
validate = partial(Views._validate_custom_views, self.cr, self.uid, model)
|
validate = partial(Views._validate_custom_views, self.cr, self.uid, model)
|
||||||
|
|
||||||
# validation of a single view
|
# validation of a single view
|
||||||
vid = self._insert_view(**{
|
vid = self._insert_view(
|
||||||
'name': 'base view',
|
name='base view',
|
||||||
'model': model,
|
model=model,
|
||||||
'priority': 1,
|
priority=1,
|
||||||
'arch': """<?xml version="1.0"?>
|
arch="""<?xml version="1.0"?>
|
||||||
<tree string="view">
|
<tree string="view">
|
||||||
<field name="url"/>
|
<field name="url"/>
|
||||||
</tree>
|
</tree>
|
||||||
""",
|
""",
|
||||||
})
|
)
|
||||||
self.assertTrue(validate()) # single view
|
self.assertTrue(validate()) # single view
|
||||||
|
|
||||||
# validation of a inherited view
|
# validation of a inherited view
|
||||||
self._insert_view(**{
|
self._insert_view(
|
||||||
'name': 'inherited view',
|
name='inherited view',
|
||||||
'model': model,
|
model=model,
|
||||||
'priority': 1,
|
priority=1,
|
||||||
'inherit_id': vid,
|
inherit_id=vid,
|
||||||
'arch': """<?xml version="1.0"?>
|
arch="""<?xml version="1.0"?>
|
||||||
<xpath expr="//field[@name='url']" position="before">
|
<xpath expr="//field[@name='url']" position="before">
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
""",
|
""",
|
||||||
})
|
)
|
||||||
self.assertTrue(validate()) # inherited view
|
self.assertTrue(validate()) # inherited view
|
||||||
|
|
||||||
# validation of a bad inherited view
|
|
||||||
self._insert_view(**{
|
|
||||||
'name': 'bad inherited view',
|
|
||||||
'model': model,
|
|
||||||
'priority': 2,
|
|
||||||
'inherit_id': vid,
|
|
||||||
'arch': """<?xml version="1.0"?>
|
|
||||||
<xpath expr="//field[@name='url']" position="after">
|
|
||||||
<field name="bad"/>
|
|
||||||
</xpath>
|
|
||||||
""",
|
|
||||||
})
|
|
||||||
with mute_logger('openerp.osv.orm', 'openerp.addons.base.ir.ir_ui_view'):
|
|
||||||
self.assertFalse(validate()) # bad inherited view
|
|
||||||
|
|
||||||
def test_view_inheritance(self):
|
def test_view_inheritance(self):
|
||||||
Views = self.registry('ir.ui.view')
|
Views = self.registry('ir.ui.view')
|
||||||
|
|
||||||
|
@ -172,9 +534,9 @@ class test_views(common.TransactionCase):
|
||||||
})
|
})
|
||||||
self.assertEqual(view['type'], 'form')
|
self.assertEqual(view['type'], 'form')
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
lxml.etree.tostring(lxml.etree.fromstring(
|
ET.tostring(ET.fromstring(
|
||||||
view['arch'],
|
view['arch'],
|
||||||
parser=lxml.etree.XMLParser(remove_blank_text=True)
|
parser=ET.XMLParser(remove_blank_text=True)
|
||||||
)),
|
)),
|
||||||
'<form string="Replacement title" version="7.0">'
|
'<form string="Replacement title" version="7.0">'
|
||||||
'<p>Replacement data</p>'
|
'<p>Replacement data</p>'
|
||||||
|
@ -239,9 +601,9 @@ class test_views(common.TransactionCase):
|
||||||
})
|
})
|
||||||
self.assertEqual(view['type'], 'form')
|
self.assertEqual(view['type'], 'form')
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
lxml.etree.tostring(lxml.etree.fromstring(
|
ET.tostring(ET.fromstring(
|
||||||
view['arch'],
|
view['arch'],
|
||||||
parser=lxml.etree.XMLParser(remove_blank_text=True)
|
parser=ET.XMLParser(remove_blank_text=True)
|
||||||
)),
|
)),
|
||||||
'<form string="Replacement title" version="7.0">'
|
'<form string="Replacement title" version="7.0">'
|
||||||
'<p>Replacement data</p>'
|
'<p>Replacement data</p>'
|
||||||
|
@ -250,5 +612,3 @@ class test_views(common.TransactionCase):
|
||||||
'</footer>'
|
'</footer>'
|
||||||
'</form>')
|
'</form>')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest2.main()
|
|
||||||
|
|
142
openerp/http.py
142
openerp/http.py
|
@ -35,6 +35,7 @@ import werkzeug.wsgi
|
||||||
|
|
||||||
import openerp
|
import openerp
|
||||||
from openerp.service import security, model as service_model
|
from openerp.service import security, model as service_model
|
||||||
|
import openerp.tools
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -49,6 +50,28 @@ request = _request_stack()
|
||||||
A global proxy that always redirect to the current request object.
|
A global proxy that always redirect to the current request object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def local_redirect(path, query=None, keep_hash=False, forward_debug=True, code=303):
|
||||||
|
url = path
|
||||||
|
if not query:
|
||||||
|
query = {}
|
||||||
|
if forward_debug and request and request.debug:
|
||||||
|
query['debug'] = None
|
||||||
|
if query:
|
||||||
|
url += '?' + werkzeug.url_encode(query)
|
||||||
|
if keep_hash:
|
||||||
|
return redirect_with_hash(url, code)
|
||||||
|
else:
|
||||||
|
return werkzeug.utils.redirect(url, code)
|
||||||
|
|
||||||
|
def redirect_with_hash(url, code=303):
|
||||||
|
# Most IE and Safari versions decided not to preserve location.hash upon
|
||||||
|
# redirect. And even if IE10 pretends to support it, it still fails
|
||||||
|
# inexplicably in case of multiple redirects (and we do have some).
|
||||||
|
# See extensive test page at http://greenbytes.de/tech/tc/httpredirects/
|
||||||
|
if request.httprequest.user_agent.browser in ('firefox',):
|
||||||
|
return werkzeug.utils.redirect(url, code)
|
||||||
|
return "<html><head><script>window.location = '%s' + location.hash;</script></head></html>" % url
|
||||||
|
|
||||||
class WebRequest(object):
|
class WebRequest(object):
|
||||||
""" Parent class for all OpenERP Web request types, mostly deals with
|
""" Parent class for all OpenERP Web request types, mostly deals with
|
||||||
initialization and setup of the request object (the dispatching itself has
|
initialization and setup of the request object (the dispatching itself has
|
||||||
|
@ -168,7 +191,7 @@ class WebRequest(object):
|
||||||
if not k.startswith("_ignored_"))
|
if not k.startswith("_ignored_"))
|
||||||
|
|
||||||
self.func = func
|
self.func = func
|
||||||
self.func_request_type = func.exposed
|
self.func_request_type = func.routing['type']
|
||||||
self.func_arguments = arguments
|
self.func_arguments = arguments
|
||||||
self.auth_method = auth
|
self.auth_method = auth
|
||||||
|
|
||||||
|
@ -181,7 +204,7 @@ class WebRequest(object):
|
||||||
kwargs.update(self.func_arguments)
|
kwargs.update(self.func_arguments)
|
||||||
|
|
||||||
# Backward for 7.0
|
# Backward for 7.0
|
||||||
if getattr(self.func, '_first_arg_is_req', False):
|
if getattr(self.func.method, '_first_arg_is_req', False):
|
||||||
args = (request,) + args
|
args = (request,) + args
|
||||||
# Correct exception handling and concurency retry
|
# Correct exception handling and concurency retry
|
||||||
@service_model.check
|
@service_model.check
|
||||||
|
@ -207,7 +230,7 @@ class WebRequest(object):
|
||||||
warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
|
warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
|
||||||
yield (self.registry, self.cr)
|
yield (self.registry, self.cr)
|
||||||
|
|
||||||
def route(route, type="http", auth="user", methods=None, cors=None):
|
def route(route=None, **kw):
|
||||||
"""
|
"""
|
||||||
Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
|
Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
|
||||||
of ``Controller``.
|
of ``Controller``.
|
||||||
|
@ -227,17 +250,16 @@ def route(route, type="http", auth="user", methods=None, cors=None):
|
||||||
:param methods: A sequence of http methods this route applies to. If not specified, all methods are allowed.
|
:param methods: A sequence of http methods this route applies to. If not specified, all methods are allowed.
|
||||||
:param cors: The Access-Control-Allow-Origin cors directive value.
|
:param cors: The Access-Control-Allow-Origin cors directive value.
|
||||||
"""
|
"""
|
||||||
assert type in ["http", "json"]
|
routing = kw.copy()
|
||||||
|
assert not 'type' in routing or routing['type'] in ("http", "json")
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
if isinstance(route, list):
|
if route:
|
||||||
f.routes = route
|
if isinstance(route, list):
|
||||||
else:
|
routes = route
|
||||||
f.routes = [route]
|
else:
|
||||||
f.methods = methods
|
routes = [route]
|
||||||
f.exposed = type
|
routing['routes'] = routes
|
||||||
f.cors = cors
|
f.routing = routing
|
||||||
if getattr(f, "auth", None) is None:
|
|
||||||
f.auth = auth
|
|
||||||
return f
|
return f
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
@ -308,7 +330,7 @@ class JsonRequest(WebRequest):
|
||||||
# Read POST content or POST Form Data named "request"
|
# Read POST content or POST Form Data named "request"
|
||||||
self.jsonrequest = simplejson.loads(request)
|
self.jsonrequest = simplejson.loads(request)
|
||||||
self.params = dict(self.jsonrequest.get("params", {}))
|
self.params = dict(self.jsonrequest.get("params", {}))
|
||||||
self.context = self.params.pop('context', self.session.context)
|
self.context = self.params.pop('context', dict(self.session.context))
|
||||||
|
|
||||||
def dispatch(self):
|
def dispatch(self):
|
||||||
""" Calls the method asked for by the JSON-RPC2 or JSONP request
|
""" Calls the method asked for by the JSON-RPC2 or JSONP request
|
||||||
|
@ -390,11 +412,10 @@ def jsonrequest(f):
|
||||||
|
|
||||||
Use the ``route()`` decorator instead.
|
Use the ``route()`` decorator instead.
|
||||||
"""
|
"""
|
||||||
f.combine = True
|
|
||||||
base = f.__name__.lstrip('/')
|
base = f.__name__.lstrip('/')
|
||||||
if f.__name__ == "index":
|
if f.__name__ == "index":
|
||||||
base = ""
|
base = ""
|
||||||
return route([base, base + "/<path:_ignored_path>"], type="json", auth="user")(f)
|
return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
|
||||||
|
|
||||||
class HttpRequest(WebRequest):
|
class HttpRequest(WebRequest):
|
||||||
""" Regular GET/POST request
|
""" Regular GET/POST request
|
||||||
|
@ -403,9 +424,9 @@ class HttpRequest(WebRequest):
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
super(HttpRequest, self).__init__(*args)
|
super(HttpRequest, self).__init__(*args)
|
||||||
params = dict(self.httprequest.args)
|
params = self.httprequest.args.to_dict()
|
||||||
params.update(self.httprequest.form)
|
params.update(self.httprequest.form.to_dict())
|
||||||
params.update(self.httprequest.files)
|
params.update(self.httprequest.files.to_dict())
|
||||||
params.pop('session_id', None)
|
params.pop('session_id', None)
|
||||||
self.params = params
|
self.params = params
|
||||||
|
|
||||||
|
@ -459,11 +480,10 @@ def httprequest(f):
|
||||||
|
|
||||||
Use the ``route()`` decorator instead.
|
Use the ``route()`` decorator instead.
|
||||||
"""
|
"""
|
||||||
f.combine = True
|
|
||||||
base = f.__name__.lstrip('/')
|
base = f.__name__.lstrip('/')
|
||||||
if f.__name__ == "index":
|
if f.__name__ == "index":
|
||||||
base = ""
|
base = ""
|
||||||
return route([base, base + "/<path:_ignored_path>"], type="http", auth="user")(f)
|
return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
|
||||||
|
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
# Controller and route registration
|
# Controller and route registration
|
||||||
|
@ -500,6 +520,13 @@ class ControllerType(type):
|
||||||
class Controller(object):
|
class Controller(object):
|
||||||
__metaclass__ = ControllerType
|
__metaclass__ = ControllerType
|
||||||
|
|
||||||
|
class EndPoint(object):
|
||||||
|
def __init__(self, method, routing):
|
||||||
|
self.method = method
|
||||||
|
self.routing = routing
|
||||||
|
def __call__(self, *args, **kw):
|
||||||
|
return self.method(*args, **kw)
|
||||||
|
|
||||||
def routing_map(modules, nodb_only, converters=None):
|
def routing_map(modules, nodb_only, converters=None):
|
||||||
routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
|
routing_map = werkzeug.routing.Map(strict_slashes=False, converters=converters)
|
||||||
for module in modules:
|
for module in modules:
|
||||||
|
@ -516,13 +543,25 @@ def routing_map(modules, nodb_only, converters=None):
|
||||||
o = cls()
|
o = cls()
|
||||||
members = inspect.getmembers(o)
|
members = inspect.getmembers(o)
|
||||||
for mk, mv in members:
|
for mk, mv in members:
|
||||||
if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and (not nodb_only or nodb_only == (mv.auth == "none")):
|
if inspect.ismethod(mv) and hasattr(mv, 'routing'):
|
||||||
for url in mv.routes:
|
routing = dict(type='http', auth='user', methods=None, routes=None)
|
||||||
if getattr(mv, "combine", False):
|
methods_done = list()
|
||||||
url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
|
for claz in reversed(mv.im_class.mro()):
|
||||||
if url.endswith("/") and len(url) > 1:
|
fn = getattr(claz, mv.func_name, None)
|
||||||
url = url[: -1]
|
if fn and hasattr(fn, 'routing') and fn not in methods_done:
|
||||||
routing_map.add(werkzeug.routing.Rule(url, endpoint=mv, methods=mv.methods))
|
methods_done.append(fn)
|
||||||
|
routing.update(fn.routing)
|
||||||
|
if not nodb_only or nodb_only == (routing['auth'] == "none"):
|
||||||
|
assert routing['routes'], "Method %r has not route defined" % mv
|
||||||
|
endpoint = EndPoint(mv, routing)
|
||||||
|
for url in routing['routes']:
|
||||||
|
if routing.get("combine", False):
|
||||||
|
# deprecated
|
||||||
|
url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
|
||||||
|
if url.endswith("/") and len(url) > 1:
|
||||||
|
url = url[: -1]
|
||||||
|
|
||||||
|
routing_map.add(werkzeug.routing.Rule(url, endpoint=endpoint, methods=routing['methods']))
|
||||||
return routing_map
|
return routing_map
|
||||||
|
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
|
@ -641,9 +680,10 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
|
||||||
raise SessionExpiredException("Session expired")
|
raise SessionExpiredException("Session expired")
|
||||||
security.check(self.db, self.uid, self.password)
|
security.check(self.db, self.uid, self.password)
|
||||||
|
|
||||||
def logout(self):
|
def logout(self, keep_db=False):
|
||||||
for k in self.keys():
|
for k in self.keys():
|
||||||
del self[k]
|
if not (keep_db and k == 'db'):
|
||||||
|
del self[k]
|
||||||
self._default_values()
|
self._default_values()
|
||||||
|
|
||||||
def _default_values(self):
|
def _default_values(self):
|
||||||
|
@ -815,8 +855,10 @@ class LazyResponse(werkzeug.wrappers.Response):
|
||||||
""" Lazy werkzeug response.
|
""" Lazy werkzeug response.
|
||||||
API not yet frozen"""
|
API not yet frozen"""
|
||||||
|
|
||||||
def __init__(self, callback, **kwargs):
|
def __init__(self, callback, status_code=None, **kwargs):
|
||||||
super(LazyResponse, self).__init__(mimetype='text/html')
|
super(LazyResponse, self).__init__(mimetype='text/html')
|
||||||
|
if status_code:
|
||||||
|
self.status_code = status_code
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
self.params = kwargs
|
self.params = kwargs
|
||||||
def process(self):
|
def process(self):
|
||||||
|
@ -928,8 +970,13 @@ class Root(object):
|
||||||
return explicit_session
|
return explicit_session
|
||||||
|
|
||||||
def setup_db(self, httprequest):
|
def setup_db(self, httprequest):
|
||||||
if not httprequest.session.db:
|
db = httprequest.session.db
|
||||||
# allow "admin" routes to works without being logged in when in monodb.
|
# Check if session.db is legit
|
||||||
|
if db and db not in db_filter([db], httprequest=httprequest):
|
||||||
|
httprequest.session.logout()
|
||||||
|
db = None
|
||||||
|
|
||||||
|
if not db:
|
||||||
httprequest.session.db = db_monodb(httprequest)
|
httprequest.session.db = db_monodb(httprequest)
|
||||||
|
|
||||||
def setup_lang(self, httprequest):
|
def setup_lang(self, httprequest):
|
||||||
|
@ -952,7 +999,10 @@ class Root(object):
|
||||||
try:
|
try:
|
||||||
result.process()
|
result.process()
|
||||||
except(Exception), e:
|
except(Exception), e:
|
||||||
result = request.registry['ir.http']._handle_exception(e)
|
if request.db:
|
||||||
|
result = request.registry['ir.http']._handle_exception(e)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
if isinstance(result, basestring):
|
if isinstance(result, basestring):
|
||||||
response = werkzeug.wrappers.Response(result, mimetype='text/html')
|
response = werkzeug.wrappers.Response(result, mimetype='text/html')
|
||||||
|
@ -971,13 +1021,13 @@ class Root(object):
|
||||||
response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
|
response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
|
||||||
|
|
||||||
# Support for Cross-Origin Resource Sharing
|
# Support for Cross-Origin Resource Sharing
|
||||||
if request.func.cors:
|
if request.func and 'cors' in request.func.routing:
|
||||||
response.headers.set('Access-Control-Allow-Origin', request.func.cors)
|
response.headers.set('Access-Control-Allow-Origin', request.func.routing['cors'])
|
||||||
methods = 'GET, POST'
|
methods = 'GET, POST'
|
||||||
if request.func_request_type == 'json':
|
if request.func_request_type == 'json':
|
||||||
methods = 'POST'
|
methods = 'POST'
|
||||||
elif request.func.methods:
|
elif request.func.routing.get('methods'):
|
||||||
methods = ', '.join(request.func.methods)
|
methods = ', '.join(request.func.routing['methods'])
|
||||||
response.headers.set('Access-Control-Allow-Methods', methods)
|
response.headers.set('Access-Control-Allow-Methods', methods)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
@ -988,7 +1038,6 @@ class Root(object):
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
httprequest = werkzeug.wrappers.Request(environ)
|
httprequest = werkzeug.wrappers.Request(environ)
|
||||||
httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
|
|
||||||
httprequest.app = self
|
httprequest.app = self
|
||||||
|
|
||||||
explicit_session = self.setup_session(httprequest)
|
explicit_session = self.setup_session(httprequest)
|
||||||
|
@ -1008,10 +1057,12 @@ class Root(object):
|
||||||
if db:
|
if db:
|
||||||
openerp.modules.registry.RegistryManager.check_registry_signaling(db)
|
openerp.modules.registry.RegistryManager.check_registry_signaling(db)
|
||||||
try:
|
try:
|
||||||
ir_http = request.registry['ir.http']
|
with openerp.tools.mute_logger('openerp.sql_db'):
|
||||||
|
ir_http = request.registry['ir.http']
|
||||||
except psycopg2.OperationalError:
|
except psycopg2.OperationalError:
|
||||||
# psycopg2 error. At this point, that's mean the database does not exists
|
# psycopg2 error. At this point, that means the
|
||||||
# anymore. We unlog the user and failback in nodb mode
|
# database probably does not exists anymore. Log the
|
||||||
|
# user out and fall back to nodb
|
||||||
request.session.logout()
|
request.session.logout()
|
||||||
result = _dispatch_nodb()
|
result = _dispatch_nodb()
|
||||||
else:
|
else:
|
||||||
|
@ -1032,8 +1083,11 @@ class Root(object):
|
||||||
return request.registry['ir.http'].routing_map()
|
return request.registry['ir.http'].routing_map()
|
||||||
|
|
||||||
def db_list(force=False, httprequest=None):
|
def db_list(force=False, httprequest=None):
|
||||||
httprequest = httprequest or request.httprequest
|
|
||||||
dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
|
dbs = openerp.netsvc.dispatch_rpc("db", "list", [force])
|
||||||
|
return db_filter(dbs, httprequest=httprequest)
|
||||||
|
|
||||||
|
def db_filter(dbs, httprequest=None):
|
||||||
|
httprequest = httprequest or request.httprequest
|
||||||
h = httprequest.environ['HTTP_HOST'].split(':')[0]
|
h = httprequest.environ['HTTP_HOST'].split(':')[0]
|
||||||
d = h.split('.')[0]
|
d = h.split('.')[0]
|
||||||
r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
|
r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
|
||||||
|
@ -1052,8 +1106,6 @@ def db_monodb(httprequest=None):
|
||||||
Returns ``None`` if the magic is not magic enough.
|
Returns ``None`` if the magic is not magic enough.
|
||||||
"""
|
"""
|
||||||
httprequest = httprequest or request.httprequest
|
httprequest = httprequest or request.httprequest
|
||||||
db = None
|
|
||||||
redirect = None
|
|
||||||
|
|
||||||
dbs = db_list(True, httprequest)
|
dbs = db_list(True, httprequest)
|
||||||
|
|
||||||
|
|
|
@ -207,6 +207,33 @@
|
||||||
</rng:element>
|
</rng:element>
|
||||||
</rng:define>
|
</rng:define>
|
||||||
|
|
||||||
|
<rng:define name="template">
|
||||||
|
<rng:element name="template">
|
||||||
|
<rng:optional><rng:attribute name="id"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="t-name"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="name"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="forcecreate"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="context"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="priority"/></rng:optional>
|
||||||
|
<rng:choice>
|
||||||
|
<rng:group>
|
||||||
|
<rng:optional><rng:attribute name="inherit_id"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="inherit_option_id"/></rng:optional>
|
||||||
|
<rng:optional><rng:attribute name="groups"/></rng:optional>
|
||||||
|
</rng:group>
|
||||||
|
<rng:optional>
|
||||||
|
<rng:attribute name="page"><rng:value>True</rng:value></rng:attribute>
|
||||||
|
</rng:optional>
|
||||||
|
</rng:choice>
|
||||||
|
<rng:zeroOrMore>
|
||||||
|
<rng:choice>
|
||||||
|
<rng:text/>
|
||||||
|
<rng:ref name="any"/>
|
||||||
|
</rng:choice>
|
||||||
|
</rng:zeroOrMore>
|
||||||
|
</rng:element>
|
||||||
|
</rng:define>
|
||||||
|
|
||||||
<rng:define name="delete">
|
<rng:define name="delete">
|
||||||
<rng:element name="delete">
|
<rng:element name="delete">
|
||||||
<rng:attribute name="model" />
|
<rng:attribute name="model" />
|
||||||
|
@ -285,6 +312,7 @@
|
||||||
<rng:text/>
|
<rng:text/>
|
||||||
<rng:ref name="menuitem" />
|
<rng:ref name="menuitem" />
|
||||||
<rng:ref name="record" />
|
<rng:ref name="record" />
|
||||||
|
<rng:ref name="template" />
|
||||||
<rng:ref name="delete" />
|
<rng:ref name="delete" />
|
||||||
<rng:ref name="wizard" />
|
<rng:ref name="wizard" />
|
||||||
<rng:ref name="act_window" />
|
<rng:ref name="act_window" />
|
||||||
|
|
|
@ -70,6 +70,9 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=
|
||||||
cr.commit()
|
cr.commit()
|
||||||
else:
|
else:
|
||||||
cr.rollback()
|
cr.rollback()
|
||||||
|
# avoid keeping stale xml_id, etc. in cache
|
||||||
|
openerp.modules.registry.RegistryManager.clear_caches(cr.dbname)
|
||||||
|
|
||||||
|
|
||||||
def _get_files_of_kind(kind):
|
def _get_files_of_kind(kind):
|
||||||
if kind == 'demo':
|
if kind == 'demo':
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import datetime as DT
|
import datetime as DT
|
||||||
|
import functools
|
||||||
import logging
|
import logging
|
||||||
import pytz
|
import pytz
|
||||||
import re
|
import re
|
||||||
|
@ -451,6 +452,39 @@ class selection(_column):
|
||||||
_column.__init__(self, string=string, **args)
|
_column.__init__(self, string=string, **args)
|
||||||
self.selection = selection
|
self.selection = selection
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def reify(cls, cr, uid, model, field, context=None):
|
||||||
|
""" Munges the field's ``selection`` attribute as necessary to get
|
||||||
|
something useable out of it: calls it if it's a function, applies
|
||||||
|
translations to labels if it's not.
|
||||||
|
|
||||||
|
A callable ``selection`` is considered translated on its own.
|
||||||
|
|
||||||
|
:param orm.Model model:
|
||||||
|
:param _column field:
|
||||||
|
"""
|
||||||
|
if callable(field.selection):
|
||||||
|
return field.selection(model, cr, uid, context)
|
||||||
|
|
||||||
|
if not (context and 'lang' in context):
|
||||||
|
return field.selection
|
||||||
|
|
||||||
|
# field_to_dict isn't given a field name, only a field object, we
|
||||||
|
# need to get the name back in order to perform the translation lookup
|
||||||
|
field_name = next(
|
||||||
|
name for name, column in model._columns.iteritems()
|
||||||
|
if column == field)
|
||||||
|
|
||||||
|
translation_filter = "%s,%s" % (model._name, field_name)
|
||||||
|
translate = functools.partial(
|
||||||
|
model.pool['ir.translation']._get_source,
|
||||||
|
cr, uid, translation_filter, 'selection', context['lang'])
|
||||||
|
|
||||||
|
return [
|
||||||
|
(value, translate(label))
|
||||||
|
for value, label in field.selection
|
||||||
|
]
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Relationals fields
|
# Relationals fields
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
@ -567,9 +601,12 @@ class one2many(_column):
|
||||||
domain = self._domain(obj) if callable(self._domain) else self._domain
|
domain = self._domain(obj) if callable(self._domain) else self._domain
|
||||||
model = obj.pool[self._obj]
|
model = obj.pool[self._obj]
|
||||||
ids2 = model.search(cr, user, domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
|
ids2 = model.search(cr, user, domain + [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
|
||||||
for r in model._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
|
if len(ids) != 1:
|
||||||
if r[self._fields_id] in res:
|
for r in model._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
|
||||||
res[r[self._fields_id]].append(r['id'])
|
if r[self._fields_id] in res:
|
||||||
|
res[r[self._fields_id]].append(r['id'])
|
||||||
|
else:
|
||||||
|
res[ids[0]] = ids2
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def set(self, cr, obj, id, field, values, user=None, context=None):
|
def set(self, cr, obj, id, field, values, user=None, context=None):
|
||||||
|
@ -1562,11 +1599,7 @@ def field_to_dict(model, cr, user, field, context=None):
|
||||||
res[arg] = getattr(field, arg)
|
res[arg] = getattr(field, arg)
|
||||||
|
|
||||||
if hasattr(field, 'selection'):
|
if hasattr(field, 'selection'):
|
||||||
if isinstance(field.selection, (tuple, list)):
|
res['selection'] = selection.reify(cr, user, model, field, context=context)
|
||||||
res['selection'] = field.selection
|
|
||||||
else:
|
|
||||||
# call the 'dynamic selection' function
|
|
||||||
res['selection'] = field.selection(model, cr, user, context)
|
|
||||||
if res['type'] in ('one2many', 'many2many', 'many2one'):
|
if res['type'] in ('one2many', 'many2many', 'many2one'):
|
||||||
res['relation'] = field._obj
|
res['relation'] = field._obj
|
||||||
res['domain'] = field._domain(model) if callable(field._domain) else field._domain
|
res['domain'] = field._domain(model) if callable(field._domain) else field._domain
|
||||||
|
|
|
@ -516,7 +516,7 @@ class browse_record(object):
|
||||||
return self._id
|
return self._id
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "browse_record(%s, %d)" % (self._table_name, self._id)
|
return "browse_record(%s, %s)" % (self._table_name, self._id)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if not isinstance(other, browse_record):
|
if not isinstance(other, browse_record):
|
||||||
|
@ -1544,9 +1544,16 @@ class BaseModel(object):
|
||||||
error_msgs = []
|
error_msgs = []
|
||||||
for constraint in self._constraints:
|
for constraint in self._constraints:
|
||||||
fun, msg, fields = constraint
|
fun, msg, fields = constraint
|
||||||
# We don't pass around the context here: validation code
|
try:
|
||||||
# must always yield the same results.
|
# We don't pass around the context here: validation code
|
||||||
if not fun(self, cr, uid, ids):
|
# must always yield the same results.
|
||||||
|
valid = fun(self, cr, uid, ids)
|
||||||
|
extra_error = None
|
||||||
|
except Exception, e:
|
||||||
|
_logger.debug('Exception while validating constraint', exc_info=True)
|
||||||
|
valid = False
|
||||||
|
extra_error = tools.ustr(e)
|
||||||
|
if not valid:
|
||||||
# Check presence of __call__ directly instead of using
|
# Check presence of __call__ directly instead of using
|
||||||
# callable() because it will be deprecated as of Python 3.0
|
# callable() because it will be deprecated as of Python 3.0
|
||||||
if hasattr(msg, '__call__'):
|
if hasattr(msg, '__call__'):
|
||||||
|
@ -1558,6 +1565,8 @@ class BaseModel(object):
|
||||||
translated_msg = tmp_msg
|
translated_msg = tmp_msg
|
||||||
else:
|
else:
|
||||||
translated_msg = trans._get_source(cr, uid, self._name, 'constraint', lng, msg)
|
translated_msg = trans._get_source(cr, uid, self._name, 'constraint', lng, msg)
|
||||||
|
if extra_error:
|
||||||
|
translated_msg += "\n\n%s\n%s" % (_('Error details:'), extra_error)
|
||||||
error_msgs.append(
|
error_msgs.append(
|
||||||
_("The field(s) `%s` failed against a constraint: %s") % (', '.join(fields), translated_msg)
|
_("The field(s) `%s` failed against a constraint: %s") % (', '.join(fields), translated_msg)
|
||||||
)
|
)
|
||||||
|
@ -1699,265 +1708,6 @@ class BaseModel(object):
|
||||||
return any([self.pool.get('res.users').has_group(cr, uid, group_ext_id)
|
return any([self.pool.get('res.users').has_group(cr, uid, group_ext_id)
|
||||||
for group_ext_id in groups.split(',')])
|
for group_ext_id in groups.split(',')])
|
||||||
|
|
||||||
def __view_look_dom(self, cr, user, node, view_id, in_tree_view, model_fields, context=None):
|
|
||||||
"""Return the description of the fields in the node.
|
|
||||||
|
|
||||||
In a normal call to this method, node is a complete view architecture
|
|
||||||
but it is actually possible to give some sub-node (this is used so
|
|
||||||
that the method can call itself recursively).
|
|
||||||
|
|
||||||
Originally, the field descriptions are drawn from the node itself.
|
|
||||||
But there is now some code calling fields_get() in order to merge some
|
|
||||||
of those information in the architecture.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if context is None:
|
|
||||||
context = {}
|
|
||||||
result = False
|
|
||||||
fields = {}
|
|
||||||
children = True
|
|
||||||
|
|
||||||
modifiers = {}
|
|
||||||
|
|
||||||
def encode(s):
|
|
||||||
if isinstance(s, unicode):
|
|
||||||
return s.encode('utf8')
|
|
||||||
return s
|
|
||||||
|
|
||||||
def check_group(node):
|
|
||||||
"""Apply group restrictions, may be set at view level or model level::
|
|
||||||
* at view level this means the element should be made invisible to
|
|
||||||
people who are not members
|
|
||||||
* at model level (exclusively for fields, obviously), this means
|
|
||||||
the field should be completely removed from the view, as it is
|
|
||||||
completely unavailable for non-members
|
|
||||||
|
|
||||||
:return: True if field should be included in the result of fields_view_get
|
|
||||||
"""
|
|
||||||
if node.tag == 'field' and node.get('name') in self._all_columns:
|
|
||||||
column = self._all_columns[node.get('name')].column
|
|
||||||
if column.groups and not self.user_has_groups(cr, user,
|
|
||||||
groups=column.groups,
|
|
||||||
context=context):
|
|
||||||
node.getparent().remove(node)
|
|
||||||
fields.pop(node.get('name'), None)
|
|
||||||
# no point processing view-level ``groups`` anymore, return
|
|
||||||
return False
|
|
||||||
if node.get('groups'):
|
|
||||||
can_see = self.user_has_groups(cr, user,
|
|
||||||
groups=node.get('groups'),
|
|
||||||
context=context)
|
|
||||||
if not can_see:
|
|
||||||
node.set('invisible', '1')
|
|
||||||
modifiers['invisible'] = True
|
|
||||||
if 'attrs' in node.attrib:
|
|
||||||
del(node.attrib['attrs']) #avoid making field visible later
|
|
||||||
del(node.attrib['groups'])
|
|
||||||
return True
|
|
||||||
|
|
||||||
if node.tag in ('field', 'node', 'arrow'):
|
|
||||||
if node.get('object'):
|
|
||||||
attrs = {}
|
|
||||||
views = {}
|
|
||||||
xml = "<form>"
|
|
||||||
for f in node:
|
|
||||||
if f.tag == 'field':
|
|
||||||
xml += etree.tostring(f, encoding="utf-8")
|
|
||||||
xml += "</form>"
|
|
||||||
new_xml = etree.fromstring(encode(xml))
|
|
||||||
ctx = context.copy()
|
|
||||||
ctx['base_model_name'] = self._name
|
|
||||||
xarch, xfields = self.pool[node.get('object')].__view_look_dom_arch(cr, user, new_xml, view_id, ctx)
|
|
||||||
views['form'] = {
|
|
||||||
'arch': xarch,
|
|
||||||
'fields': xfields
|
|
||||||
}
|
|
||||||
attrs = {'views': views}
|
|
||||||
fields = xfields
|
|
||||||
if node.get('name'):
|
|
||||||
attrs = {}
|
|
||||||
try:
|
|
||||||
if node.get('name') in self._columns:
|
|
||||||
column = self._columns[node.get('name')]
|
|
||||||
else:
|
|
||||||
column = self._inherit_fields[node.get('name')][2]
|
|
||||||
except Exception:
|
|
||||||
column = False
|
|
||||||
|
|
||||||
if column:
|
|
||||||
relation = self.pool[column._obj] if column._obj else None
|
|
||||||
|
|
||||||
children = False
|
|
||||||
views = {}
|
|
||||||
for f in node:
|
|
||||||
if f.tag in ('form', 'tree', 'graph', 'kanban', 'calendar'):
|
|
||||||
node.remove(f)
|
|
||||||
ctx = context.copy()
|
|
||||||
ctx['base_model_name'] = self._name
|
|
||||||
xarch, xfields = relation.__view_look_dom_arch(cr, user, f, view_id, ctx)
|
|
||||||
views[str(f.tag)] = {
|
|
||||||
'arch': xarch,
|
|
||||||
'fields': xfields
|
|
||||||
}
|
|
||||||
attrs = {'views': views}
|
|
||||||
if node.get('widget') and node.get('widget') == 'selection':
|
|
||||||
# Prepare the cached selection list for the client. This needs to be
|
|
||||||
# done even when the field is invisible to the current user, because
|
|
||||||
# other events could need to change its value to any of the selectable ones
|
|
||||||
# (such as on_change events, refreshes, etc.)
|
|
||||||
|
|
||||||
# If domain and context are strings, we keep them for client-side, otherwise
|
|
||||||
# we evaluate them server-side to consider them when generating the list of
|
|
||||||
# possible values
|
|
||||||
# TODO: find a way to remove this hack, by allow dynamic domains
|
|
||||||
dom = []
|
|
||||||
if column._domain and not isinstance(column._domain, basestring):
|
|
||||||
dom = list(column._domain)
|
|
||||||
dom += eval(node.get('domain', '[]'), {'uid': user, 'time': time})
|
|
||||||
search_context = dict(context)
|
|
||||||
if column._context and not isinstance(column._context, basestring):
|
|
||||||
search_context.update(column._context)
|
|
||||||
attrs['selection'] = relation._name_search(cr, user, '', dom, context=search_context, limit=None, name_get_uid=1)
|
|
||||||
if (node.get('required') and not int(node.get('required'))) or not column.required:
|
|
||||||
attrs['selection'].append((False, ''))
|
|
||||||
fields[node.get('name')] = attrs
|
|
||||||
|
|
||||||
field = model_fields.get(node.get('name'))
|
|
||||||
if field:
|
|
||||||
transfer_field_to_modifiers(field, modifiers)
|
|
||||||
|
|
||||||
|
|
||||||
elif node.tag in ('form', 'tree'):
|
|
||||||
result = self.view_header_get(cr, user, False, node.tag, context)
|
|
||||||
if result:
|
|
||||||
node.set('string', result)
|
|
||||||
in_tree_view = node.tag == 'tree'
|
|
||||||
|
|
||||||
elif node.tag == 'calendar':
|
|
||||||
for additional_field in ('date_start', 'date_delay', 'date_stop', 'color', 'all_day','attendee'):
|
|
||||||
if node.get(additional_field):
|
|
||||||
fields[node.get(additional_field)] = {}
|
|
||||||
|
|
||||||
if not check_group(node):
|
|
||||||
# node must be removed, no need to proceed further with its children
|
|
||||||
return fields
|
|
||||||
|
|
||||||
# The view architeture overrides the python model.
|
|
||||||
# Get the attrs before they are (possibly) deleted by check_group below
|
|
||||||
transfer_node_to_modifiers(node, modifiers, context, in_tree_view)
|
|
||||||
|
|
||||||
# TODO remove attrs couterpart in modifiers when invisible is true ?
|
|
||||||
|
|
||||||
# translate view
|
|
||||||
if 'lang' in context:
|
|
||||||
if node.text and node.text.strip():
|
|
||||||
trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.text.strip())
|
|
||||||
if trans:
|
|
||||||
node.text = node.text.replace(node.text.strip(), trans)
|
|
||||||
if node.tail and node.tail.strip():
|
|
||||||
trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.tail.strip())
|
|
||||||
if trans:
|
|
||||||
node.tail = node.tail.replace(node.tail.strip(), trans)
|
|
||||||
|
|
||||||
if node.get('string') and not result:
|
|
||||||
trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.get('string'))
|
|
||||||
if trans == node.get('string') and ('base_model_name' in context):
|
|
||||||
# If translation is same as source, perhaps we'd have more luck with the alternative model name
|
|
||||||
# (in case we are in a mixed situation, such as an inherited view where parent_view.model != model
|
|
||||||
trans = self.pool.get('ir.translation')._get_source(cr, user, context['base_model_name'], 'view', context['lang'], node.get('string'))
|
|
||||||
if trans:
|
|
||||||
node.set('string', trans)
|
|
||||||
|
|
||||||
for attr_name in ('confirm', 'sum', 'avg', 'help', 'placeholder'):
|
|
||||||
attr_value = node.get(attr_name)
|
|
||||||
if attr_value:
|
|
||||||
trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], attr_value)
|
|
||||||
if trans:
|
|
||||||
node.set(attr_name, trans)
|
|
||||||
|
|
||||||
for f in node:
|
|
||||||
if children or (node.tag == 'field' and f.tag in ('filter','separator')):
|
|
||||||
fields.update(self.__view_look_dom(cr, user, f, view_id, in_tree_view, model_fields, context))
|
|
||||||
|
|
||||||
transfer_modifiers_to_node(modifiers, node)
|
|
||||||
return fields
|
|
||||||
|
|
||||||
def _disable_workflow_buttons(self, cr, user, node):
|
|
||||||
""" Set the buttons in node to readonly if the user can't activate them. """
|
|
||||||
if user == 1:
|
|
||||||
# admin user can always activate workflow buttons
|
|
||||||
return node
|
|
||||||
|
|
||||||
# TODO handle the case of more than one workflow for a model or multiple
|
|
||||||
# transitions with different groups and same signal
|
|
||||||
usersobj = self.pool.get('res.users')
|
|
||||||
buttons = (n for n in node.getiterator('button') if n.get('type') != 'object')
|
|
||||||
for button in buttons:
|
|
||||||
user_groups = usersobj.read(cr, user, [user], ['groups_id'])[0]['groups_id']
|
|
||||||
cr.execute("""SELECT DISTINCT t.group_id
|
|
||||||
FROM wkf
|
|
||||||
INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
|
|
||||||
INNER JOIN wkf_transition t ON (t.act_to = a.id)
|
|
||||||
WHERE wkf.osv = %s
|
|
||||||
AND t.signal = %s
|
|
||||||
AND t.group_id is NOT NULL
|
|
||||||
""", (self._name, button.get('name')))
|
|
||||||
group_ids = [x[0] for x in cr.fetchall() if x[0]]
|
|
||||||
can_click = not group_ids or bool(set(user_groups).intersection(group_ids))
|
|
||||||
button.set('readonly', str(int(not can_click)))
|
|
||||||
return node
|
|
||||||
|
|
||||||
def __view_look_dom_arch(self, cr, user, node, view_id, context=None):
|
|
||||||
""" Return an architecture and a description of all the fields.
|
|
||||||
|
|
||||||
The field description combines the result of fields_get() and
|
|
||||||
__view_look_dom().
|
|
||||||
|
|
||||||
:param node: the architecture as as an etree
|
|
||||||
:return: a tuple (arch, fields) where arch is the given node as a
|
|
||||||
string and fields is the description of all the fields.
|
|
||||||
|
|
||||||
"""
|
|
||||||
fields = {}
|
|
||||||
if node.tag == 'diagram':
|
|
||||||
if node.getchildren()[0].tag == 'node':
|
|
||||||
node_model = self.pool[node.getchildren()[0].get('object')]
|
|
||||||
node_fields = node_model.fields_get(cr, user, None, context)
|
|
||||||
fields.update(node_fields)
|
|
||||||
if not node.get("create") and not node_model.check_access_rights(cr, user, 'create', raise_exception=False):
|
|
||||||
node.set("create", 'false')
|
|
||||||
if node.getchildren()[1].tag == 'arrow':
|
|
||||||
arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context)
|
|
||||||
fields.update(arrow_fields)
|
|
||||||
else:
|
|
||||||
fields = self.fields_get(cr, user, None, context)
|
|
||||||
fields_def = self.__view_look_dom(cr, user, node, view_id, False, fields, context=context)
|
|
||||||
node = self._disable_workflow_buttons(cr, user, node)
|
|
||||||
if node.tag in ('kanban', 'tree', 'form', 'gantt'):
|
|
||||||
for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')):
|
|
||||||
if not node.get(action) and not self.check_access_rights(cr, user, operation, raise_exception=False):
|
|
||||||
node.set(action, 'false')
|
|
||||||
arch = etree.tostring(node, encoding="utf-8").replace('\t', '')
|
|
||||||
for k in fields.keys():
|
|
||||||
if k not in fields_def:
|
|
||||||
del fields[k]
|
|
||||||
for field in fields_def:
|
|
||||||
if field == 'id':
|
|
||||||
# sometime, the view may contain the (invisible) field 'id' needed for a domain (when 2 objects have cross references)
|
|
||||||
fields['id'] = {'readonly': True, 'type': 'integer', 'string': 'ID'}
|
|
||||||
elif field in fields:
|
|
||||||
fields[field].update(fields_def[field])
|
|
||||||
else:
|
|
||||||
cr.execute('select name, model from ir_ui_view where (id=%s or inherit_id=%s) and arch like %s', (view_id, view_id, '%%%s%%' % field))
|
|
||||||
res = cr.fetchall()[:]
|
|
||||||
model = res[0][1]
|
|
||||||
res.insert(0, ("Can't find field '%s' in the following view parts composing the view of object model '%s':" % (field, model), None))
|
|
||||||
msg = "\n * ".join([r[0] for r in res])
|
|
||||||
msg += "\n\nEither you wrongly customized this view, or some modules bringing those views are not compatible with your current data model"
|
|
||||||
_logger.error(msg)
|
|
||||||
raise except_orm('View error', msg)
|
|
||||||
return arch, fields
|
|
||||||
|
|
||||||
def _get_default_form_view(self, cr, user, context=None):
|
def _get_default_form_view(self, cr, user, context=None):
|
||||||
""" Generates a default single-line form view using all fields
|
""" Generates a default single-line form view using all fields
|
||||||
of the current model except the m2m and o2m ones.
|
of the current model except the m2m and o2m ones.
|
||||||
|
@ -2055,18 +1805,12 @@ class BaseModel(object):
|
||||||
|
|
||||||
return view
|
return view
|
||||||
|
|
||||||
#
|
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
|
||||||
# if view_id, view_type is not required
|
|
||||||
#
|
|
||||||
def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
|
|
||||||
"""
|
"""
|
||||||
Get the detailed composition of the requested view like fields, model, view architecture
|
Get the detailed composition of the requested view like fields, model, view architecture
|
||||||
|
|
||||||
:param cr: database cursor
|
|
||||||
:param user: current user id
|
|
||||||
:param view_id: id of the view or None
|
:param view_id: id of the view or None
|
||||||
:param view_type: type of the view to return if view_id is None ('form', tree', ...)
|
:param view_type: type of the view to return if view_id is None ('form', tree', ...)
|
||||||
:param context: context arguments, like lang, time zone
|
|
||||||
:param toolbar: true to include contextual actions
|
:param toolbar: true to include contextual actions
|
||||||
:param submenu: deprecated
|
:param submenu: deprecated
|
||||||
:return: dictionary describing the composition of the requested view (including inherited views and extensions)
|
:return: dictionary describing the composition of the requested view (including inherited views and extensions)
|
||||||
|
@ -2074,156 +1818,22 @@ class BaseModel(object):
|
||||||
* if the inherited view has unknown position to work with other than 'before', 'after', 'inside', 'replace'
|
* if the inherited view has unknown position to work with other than 'before', 'after', 'inside', 'replace'
|
||||||
* if some tag other than 'position' is found in parent view
|
* if some tag other than 'position' is found in parent view
|
||||||
:raise Invalid ArchitectureError: if there is view type other than form, tree, calendar, search etc defined on the structure
|
:raise Invalid ArchitectureError: if there is view type other than form, tree, calendar, search etc defined on the structure
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if context is None:
|
if context is None:
|
||||||
context = {}
|
context = {}
|
||||||
|
View = self.pool['ir.ui.view']
|
||||||
|
|
||||||
def encode(s):
|
result = {
|
||||||
if isinstance(s, unicode):
|
'model': self._name,
|
||||||
return s.encode('utf8')
|
'field_parent': False,
|
||||||
return s
|
}
|
||||||
|
|
||||||
def raise_view_error(error_msg, child_view_id):
|
# try to find a view_id if none provided
|
||||||
view, child_view = self.pool.get('ir.ui.view').browse(cr, user, [view_id, child_view_id], context)
|
if not view_id:
|
||||||
error_msg = error_msg % {'parent_xml_id': view.xml_id}
|
# <view_type>_view_ref in context can be used to overrride the default view
|
||||||
raise AttributeError("View definition error for inherited view '%s' on model '%s': %s"
|
view_ref_key = view_type + '_view_ref'
|
||||||
% (child_view.xml_id, self._name, error_msg))
|
view_ref = context.get(view_ref_key)
|
||||||
|
if view_ref:
|
||||||
def locate(source, spec):
|
|
||||||
""" Locate a node in a source (parent) architecture.
|
|
||||||
|
|
||||||
Given a complete source (parent) architecture (i.e. the field
|
|
||||||
`arch` in a view), and a 'spec' node (a node in an inheriting
|
|
||||||
view that specifies the location in the source view of what
|
|
||||||
should be changed), return (if it exists) the node in the
|
|
||||||
source view matching the specification.
|
|
||||||
|
|
||||||
:param source: a parent architecture to modify
|
|
||||||
:param spec: a modifying node in an inheriting view
|
|
||||||
:return: a node in the source matching the spec
|
|
||||||
|
|
||||||
"""
|
|
||||||
if spec.tag == 'xpath':
|
|
||||||
nodes = source.xpath(spec.get('expr'))
|
|
||||||
return nodes[0] if nodes else None
|
|
||||||
elif spec.tag == 'field':
|
|
||||||
# Only compare the field name: a field can be only once in a given view
|
|
||||||
# at a given level (and for multilevel expressions, we should use xpath
|
|
||||||
# inheritance spec anyway).
|
|
||||||
for node in source.getiterator('field'):
|
|
||||||
if node.get('name') == spec.get('name'):
|
|
||||||
return node
|
|
||||||
return None
|
|
||||||
|
|
||||||
for node in source.getiterator(spec.tag):
|
|
||||||
if isinstance(node, SKIPPED_ELEMENT_TYPES):
|
|
||||||
continue
|
|
||||||
if all(node.get(attr) == spec.get(attr) \
|
|
||||||
for attr in spec.attrib
|
|
||||||
if attr not in ('position','version')):
|
|
||||||
# Version spec should match parent's root element's version
|
|
||||||
if spec.get('version') and spec.get('version') != source.get('version'):
|
|
||||||
return None
|
|
||||||
return node
|
|
||||||
return None
|
|
||||||
|
|
||||||
def apply_inheritance_specs(source, specs_arch, inherit_id=None):
|
|
||||||
""" Apply an inheriting view.
|
|
||||||
|
|
||||||
Apply to a source architecture all the spec nodes (i.e. nodes
|
|
||||||
describing where and what changes to apply to some parent
|
|
||||||
architecture) given by an inheriting view.
|
|
||||||
|
|
||||||
:param source: a parent architecture to modify
|
|
||||||
:param specs_arch: a modifying architecture in an inheriting view
|
|
||||||
:param inherit_id: the database id of the inheriting view
|
|
||||||
:return: a modified source where the specs are applied
|
|
||||||
|
|
||||||
"""
|
|
||||||
specs_tree = etree.fromstring(encode(specs_arch))
|
|
||||||
# Queue of specification nodes (i.e. nodes describing where and
|
|
||||||
# changes to apply to some parent architecture).
|
|
||||||
specs = [specs_tree]
|
|
||||||
|
|
||||||
while len(specs):
|
|
||||||
spec = specs.pop(0)
|
|
||||||
if isinstance(spec, SKIPPED_ELEMENT_TYPES):
|
|
||||||
continue
|
|
||||||
if spec.tag == 'data':
|
|
||||||
specs += [ c for c in specs_tree ]
|
|
||||||
continue
|
|
||||||
node = locate(source, spec)
|
|
||||||
if node is not None:
|
|
||||||
pos = spec.get('position', 'inside')
|
|
||||||
if pos == 'replace':
|
|
||||||
if node.getparent() is None:
|
|
||||||
source = copy.deepcopy(spec[0])
|
|
||||||
else:
|
|
||||||
for child in spec:
|
|
||||||
node.addprevious(child)
|
|
||||||
node.getparent().remove(node)
|
|
||||||
elif pos == 'attributes':
|
|
||||||
for child in spec.getiterator('attribute'):
|
|
||||||
attribute = (child.get('name'), child.text and child.text.encode('utf8') or None)
|
|
||||||
if attribute[1]:
|
|
||||||
node.set(attribute[0], attribute[1])
|
|
||||||
elif attribute[0] in node.attrib:
|
|
||||||
del node.attrib[attribute[0]]
|
|
||||||
else:
|
|
||||||
sib = node.getnext()
|
|
||||||
for child in spec:
|
|
||||||
if pos == 'inside':
|
|
||||||
node.append(child)
|
|
||||||
elif pos == 'after':
|
|
||||||
if sib is None:
|
|
||||||
node.addnext(child)
|
|
||||||
node = child
|
|
||||||
else:
|
|
||||||
sib.addprevious(child)
|
|
||||||
elif pos == 'before':
|
|
||||||
node.addprevious(child)
|
|
||||||
else:
|
|
||||||
raise_view_error("Invalid position value: '%s'" % pos, inherit_id)
|
|
||||||
else:
|
|
||||||
attrs = ''.join([
|
|
||||||
' %s="%s"' % (attr, spec.get(attr))
|
|
||||||
for attr in spec.attrib
|
|
||||||
if attr != 'position'
|
|
||||||
])
|
|
||||||
tag = "<%s%s>" % (spec.tag, attrs)
|
|
||||||
if spec.get('version') and spec.get('version') != source.get('version'):
|
|
||||||
raise_view_error("Mismatching view API version for element '%s': %r vs %r in parent view '%%(parent_xml_id)s'" % \
|
|
||||||
(tag, spec.get('version'), source.get('version')), inherit_id)
|
|
||||||
raise_view_error("Element '%s' not found in parent view '%%(parent_xml_id)s'" % tag, inherit_id)
|
|
||||||
|
|
||||||
return source
|
|
||||||
|
|
||||||
def apply_view_inheritance(cr, user, source, inherit_id):
|
|
||||||
""" Apply all the (directly and indirectly) inheriting views.
|
|
||||||
|
|
||||||
:param source: a parent architecture to modify (with parent
|
|
||||||
modifications already applied)
|
|
||||||
:param inherit_id: the database view_id of the parent view
|
|
||||||
:return: a modified source where all the modifying architecture
|
|
||||||
are applied
|
|
||||||
|
|
||||||
"""
|
|
||||||
sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, user, inherit_id, self._name, context=context)
|
|
||||||
for (view_arch, view_id) in sql_inherit:
|
|
||||||
source = apply_inheritance_specs(source, view_arch, view_id)
|
|
||||||
source = apply_view_inheritance(cr, user, source, view_id)
|
|
||||||
return source
|
|
||||||
|
|
||||||
result = {'type': view_type, 'model': self._name}
|
|
||||||
|
|
||||||
sql_res = False
|
|
||||||
parent_view_model = None
|
|
||||||
view_ref_key = view_type + '_view_ref'
|
|
||||||
view_ref = context.get(view_ref_key)
|
|
||||||
# Search for a root (i.e. without any parent) view.
|
|
||||||
while True:
|
|
||||||
if view_ref and not view_id:
|
|
||||||
if '.' in view_ref:
|
if '.' in view_ref:
|
||||||
module, view_ref = view_ref.split('.', 1)
|
module, view_ref = view_ref.split('.', 1)
|
||||||
cr.execute("SELECT res_id FROM ir_model_data WHERE model='ir.ui.view' AND module=%s AND name=%s", (module, view_ref))
|
cr.execute("SELECT res_id FROM ir_model_data WHERE model='ir.ui.view' AND module=%s AND name=%s", (module, view_ref))
|
||||||
|
@ -2235,82 +1845,53 @@ class BaseModel(object):
|
||||||
'Please use the complete `module.view_id` form instead.', view_ref_key, view_ref,
|
'Please use the complete `module.view_id` form instead.', view_ref_key, view_ref,
|
||||||
self._name)
|
self._name)
|
||||||
|
|
||||||
if view_id:
|
if not view_id:
|
||||||
cr.execute("""SELECT arch,name,field_parent,id,type,inherit_id,model
|
# otherwise try to find the lowest priority matching ir.ui.view
|
||||||
FROM ir_ui_view
|
view_id = View.default_view(cr, uid, self._name, view_type, context=context)
|
||||||
WHERE id=%s""", (view_id,))
|
|
||||||
else:
|
|
||||||
cr.execute("""SELECT arch,name,field_parent,id,type,inherit_id,model
|
|
||||||
FROM ir_ui_view
|
|
||||||
WHERE model=%s AND type=%s AND inherit_id IS NULL
|
|
||||||
ORDER BY priority""", (self._name, view_type))
|
|
||||||
sql_res = cr.dictfetchone()
|
|
||||||
|
|
||||||
if not sql_res:
|
# context for post-processing might be overriden
|
||||||
break
|
ctx = context
|
||||||
|
if view_id:
|
||||||
view_id = sql_res['inherit_id'] or sql_res['id']
|
# read the view with inherited views applied
|
||||||
parent_view_model = sql_res['model']
|
root_view = View.read_combined(cr, uid, view_id, fields=['id', 'name', 'field_parent', 'type', 'model', 'arch'], context=context)
|
||||||
if not sql_res['inherit_id']:
|
result['arch'] = root_view['arch']
|
||||||
break
|
result['name'] = root_view['name']
|
||||||
|
result['type'] = root_view['type']
|
||||||
# if a view was found
|
result['view_id'] = root_view['id']
|
||||||
if sql_res:
|
result['field_parent'] = root_view['field_parent']
|
||||||
source = etree.fromstring(encode(sql_res['arch']))
|
# override context fro postprocessing
|
||||||
result.update(
|
if root_view.get('model') != self._name:
|
||||||
arch=apply_view_inheritance(cr, user, source, sql_res['id']),
|
ctx = dict(context, base_model_name=root_view.get('model'))
|
||||||
type=sql_res['type'],
|
|
||||||
view_id=sql_res['id'],
|
|
||||||
name=sql_res['name'],
|
|
||||||
field_parent=sql_res['field_parent'] or False)
|
|
||||||
else:
|
else:
|
||||||
# otherwise, build some kind of default view
|
# fallback on default views methods if no ir.ui.view could be found
|
||||||
try:
|
try:
|
||||||
view = getattr(self, '_get_default_%s_view' % view_type)(
|
get_func = getattr(self, '_get_default_%s_view' % view_type)
|
||||||
cr, user, context)
|
arch_etree = get_func(cr, uid, context)
|
||||||
|
result['arch'] = etree.tostring(arch_etree, encoding='utf-8')
|
||||||
|
result['type'] = view_type
|
||||||
|
result['name'] = 'default'
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# what happens here, graph case?
|
raise except_orm(_('Invalid Architecture!'), _("No default view of type '%s' could be found !") % view_type)
|
||||||
raise except_orm(_('Invalid Architecture!'), _("There is no view of type '%s' defined for the structure!") % view_type)
|
|
||||||
|
|
||||||
result.update(
|
# Apply post processing, groups and modifiers etc...
|
||||||
arch=view,
|
xarch, xfields = View.postprocess_and_fields(cr, uid, self._name, etree.fromstring(result['arch']), view_id, context=ctx)
|
||||||
name='default',
|
|
||||||
field_parent=False,
|
|
||||||
view_id=0)
|
|
||||||
|
|
||||||
if parent_view_model != self._name:
|
|
||||||
ctx = context.copy()
|
|
||||||
ctx['base_model_name'] = parent_view_model
|
|
||||||
else:
|
|
||||||
ctx = context
|
|
||||||
xarch, xfields = self.__view_look_dom_arch(cr, user, result['arch'], view_id, context=ctx)
|
|
||||||
result['arch'] = xarch
|
result['arch'] = xarch
|
||||||
result['fields'] = xfields
|
result['fields'] = xfields
|
||||||
|
|
||||||
|
# Add related action information if aksed
|
||||||
if toolbar:
|
if toolbar:
|
||||||
|
toclean = ('report_sxw_content', 'report_rml_content', 'report_sxw', 'report_rml', 'report_sxw_content_data', 'report_rml_content_data')
|
||||||
def clean(x):
|
def clean(x):
|
||||||
x = x[2]
|
x = x[2]
|
||||||
for key in ('report_sxw_content', 'report_rml_content',
|
for key in toclean:
|
||||||
'report_sxw', 'report_rml',
|
x.pop(key, None)
|
||||||
'report_sxw_content_data', 'report_rml_content_data'):
|
|
||||||
if key in x:
|
|
||||||
del x[key]
|
|
||||||
return x
|
return x
|
||||||
ir_values_obj = self.pool.get('ir.values')
|
ir_values_obj = self.pool.get('ir.values')
|
||||||
resprint = ir_values_obj.get(cr, user, 'action',
|
resprint = ir_values_obj.get(cr, uid, 'action', 'client_print_multi', [(self._name, False)], False, context)
|
||||||
'client_print_multi', [(self._name, False)], False,
|
resaction = ir_values_obj.get(cr, uid, 'action', 'client_action_multi', [(self._name, False)], False, context)
|
||||||
context)
|
resrelate = ir_values_obj.get(cr, uid, 'action', 'client_action_relate', [(self._name, False)], False, context)
|
||||||
resaction = ir_values_obj.get(cr, user, 'action',
|
resaction = [clean(action) for action in resaction if view_type == 'tree' or not action[2].get('multi')]
|
||||||
'client_action_multi', [(self._name, False)], False,
|
resprint = [clean(print_) for print_ in resprint if view_type == 'tree' or not print_[2].get('multi')]
|
||||||
context)
|
|
||||||
|
|
||||||
resrelate = ir_values_obj.get(cr, user, 'action',
|
|
||||||
'client_action_relate', [(self._name, False)], False,
|
|
||||||
context)
|
|
||||||
resaction = [clean(action) for action in resaction
|
|
||||||
if view_type == 'tree' or not action[2].get('multi')]
|
|
||||||
resprint = [clean(print_) for print_ in resprint
|
|
||||||
if view_type == 'tree' or not print_[2].get('multi')]
|
|
||||||
#When multi="True" set it will display only in More of the list view
|
#When multi="True" set it will display only in More of the list view
|
||||||
resrelate = [clean(action) for action in resrelate
|
resrelate = [clean(action) for action in resrelate
|
||||||
if (action[2].get('multi') and view_type == 'tree') or (not action[2].get('multi') and view_type == 'form')]
|
if (action[2].get('multi') and view_type == 'tree') or (not action[2].get('multi') and view_type == 'form')]
|
||||||
|
@ -2325,11 +1906,11 @@ class BaseModel(object):
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
_view_look_dom_arch = __view_look_dom_arch
|
def _view_look_dom_arch(self, cr, uid, node, view_id, context=None):
|
||||||
|
return self['ir.ui.view'].postprocess_and_fields(
|
||||||
|
cr, uid, self._name, node, view_id, context=context)
|
||||||
|
|
||||||
def search_count(self, cr, user, args, context=None):
|
def search_count(self, cr, user, args, context=None):
|
||||||
if not context:
|
|
||||||
context = {}
|
|
||||||
res = self.search(cr, user, args, context=context, count=True)
|
res = self.search(cr, user, args, context=context, count=True)
|
||||||
if isinstance(res, list):
|
if isinstance(res, list):
|
||||||
return len(res)
|
return len(res)
|
||||||
|
@ -3537,31 +3118,6 @@ class BaseModel(object):
|
||||||
self._columns[field_name].required = True
|
self._columns[field_name].required = True
|
||||||
self._columns[field_name].ondelete = "cascade"
|
self._columns[field_name].ondelete = "cascade"
|
||||||
|
|
||||||
#def __getattr__(self, name):
|
|
||||||
# """
|
|
||||||
# Proxies attribute accesses to the `inherits` parent so we can call methods defined on the inherited parent
|
|
||||||
# (though inherits doesn't use Python inheritance).
|
|
||||||
# Handles translating between local ids and remote ids.
|
|
||||||
# Known issue: doesn't work correctly when using python's own super(), don't involve inherit-based inheritance
|
|
||||||
# when you have inherits.
|
|
||||||
# """
|
|
||||||
# for model, field in self._inherits.iteritems():
|
|
||||||
# proxy = self.pool.get(model)
|
|
||||||
# if hasattr(proxy, name):
|
|
||||||
# attribute = getattr(proxy, name)
|
|
||||||
# if not hasattr(attribute, '__call__'):
|
|
||||||
# return attribute
|
|
||||||
# break
|
|
||||||
# else:
|
|
||||||
# return super(orm, self).__getattr__(name)
|
|
||||||
|
|
||||||
# def _proxy(cr, uid, ids, *args, **kwargs):
|
|
||||||
# objects = self.browse(cr, uid, ids, kwargs.get('context', None))
|
|
||||||
# lst = [obj[field].id for obj in objects if obj[field]]
|
|
||||||
# return getattr(proxy, name)(cr, uid, lst, *args, **kwargs)
|
|
||||||
|
|
||||||
# return _proxy
|
|
||||||
|
|
||||||
|
|
||||||
def fields_get(self, cr, user, allfields=None, context=None, write_access=True):
|
def fields_get(self, cr, user, allfields=None, context=None, write_access=True):
|
||||||
""" Return the definition of each field.
|
""" Return the definition of each field.
|
||||||
|
@ -3610,16 +3166,6 @@ class BaseModel(object):
|
||||||
help_trans = translation_obj._get_source(cr, user, self._name + ',' + f, 'help', context['lang'])
|
help_trans = translation_obj._get_source(cr, user, self._name + ',' + f, 'help', context['lang'])
|
||||||
if help_trans:
|
if help_trans:
|
||||||
res[f]['help'] = help_trans
|
res[f]['help'] = help_trans
|
||||||
if 'selection' in res[f]:
|
|
||||||
if isinstance(field.selection, (tuple, list)):
|
|
||||||
sel = field.selection
|
|
||||||
sel2 = []
|
|
||||||
for key, val in sel:
|
|
||||||
val2 = None
|
|
||||||
if val:
|
|
||||||
val2 = translation_obj._get_source(cr, user, self._name + ',' + f, 'selection', context['lang'], val)
|
|
||||||
sel2.append((key, val2 or val))
|
|
||||||
res[f]['selection'] = sel2
|
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@ -4347,7 +3893,7 @@ class BaseModel(object):
|
||||||
for (parent_pright, parent_id) in parents:
|
for (parent_pright, parent_id) in parents:
|
||||||
if parent_id == id:
|
if parent_id == id:
|
||||||
break
|
break
|
||||||
position = parent_pright + 1
|
position = parent_pright and parent_pright + 1 or 1
|
||||||
|
|
||||||
# It's the first node of the parent
|
# It's the first node of the parent
|
||||||
if not position:
|
if not position:
|
||||||
|
@ -5471,7 +5017,7 @@ class BaseModel(object):
|
||||||
:rtype: List of dictionaries.
|
:rtype: List of dictionaries.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
record_ids = self.search(cr, uid, domain or [], offset, limit or False, order or False, context or {})
|
record_ids = self.search(cr, uid, domain or [], offset=offset, limit=limit, order=order, context=context)
|
||||||
if not record_ids:
|
if not record_ids:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@ -5479,14 +5025,13 @@ class BaseModel(object):
|
||||||
# shortcut read if we only want the ids
|
# shortcut read if we only want the ids
|
||||||
return [{'id': id} for id in record_ids]
|
return [{'id': id} for id in record_ids]
|
||||||
|
|
||||||
result = self.read(cr, uid, record_ids, fields or [], context or {})
|
result = self.read(cr, uid, record_ids, fields, context=context)
|
||||||
|
if len(result) <= 1:
|
||||||
|
return result
|
||||||
|
|
||||||
# reorder read
|
# reorder read
|
||||||
if len(result) >= 1:
|
index = dict((r['id'], r) for r in result)
|
||||||
index = {}
|
return [index[x] for x in record_ids if x in index]
|
||||||
for r in result:
|
|
||||||
index[r['id']] = r
|
|
||||||
result = [index[x] for x in record_ids if x in index]
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _register_hook(self, cr):
|
def _register_hook(self, cr):
|
||||||
""" stuff to do right after the registry is built """
|
""" stuff to do right after the registry is built """
|
||||||
|
@ -5557,12 +5102,12 @@ def itemgetter_tuple(items):
|
||||||
if len(items) == 1:
|
if len(items) == 1:
|
||||||
return lambda gettable: (gettable[items[0]],)
|
return lambda gettable: (gettable[items[0]],)
|
||||||
return operator.itemgetter(*items)
|
return operator.itemgetter(*items)
|
||||||
|
|
||||||
class ImportWarning(Warning):
|
class ImportWarning(Warning):
|
||||||
""" Used to send warnings upwards the stack during the import process
|
""" Used to send warnings upwards the stack during the import process
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def convert_pgerror_23502(model, fields, info, e):
|
def convert_pgerror_23502(model, fields, info, e):
|
||||||
m = re.match(r'^null value in column "(?P<field>\w+)" violates '
|
m = re.match(r'^null value in column "(?P<field>\w+)" violates '
|
||||||
r'not-null constraint\n',
|
r'not-null constraint\n',
|
||||||
|
@ -5578,6 +5123,7 @@ def convert_pgerror_23502(model, fields, info, e):
|
||||||
'message': message,
|
'message': message,
|
||||||
'field': field_name,
|
'field': field_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
def convert_pgerror_23505(model, fields, info, e):
|
def convert_pgerror_23505(model, fields, info, e):
|
||||||
m = re.match(r'^duplicate key (?P<field>\w+) violates unique constraint',
|
m = re.match(r'^duplicate key (?P<field>\w+) violates unique constraint',
|
||||||
str(e))
|
str(e))
|
||||||
|
|
|
@ -7,7 +7,6 @@ This module groups a few sub-modules containing unittest2 test cases.
|
||||||
Tests can be explicitely added to the `fast_suite` or `checks` lists or not.
|
Tests can be explicitely added to the `fast_suite` or `checks` lists or not.
|
||||||
See the :ref:`test-framework` section in the :ref:`features` list.
|
See the :ref:`test-framework` section in the :ref:`features` list.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import test_acl
|
import test_acl
|
||||||
import test_basecase
|
import test_basecase
|
||||||
import test_db_cursor
|
import test_db_cursor
|
||||||
|
@ -20,8 +19,9 @@ import test_misc
|
||||||
import test_orm
|
import test_orm
|
||||||
import test_osv
|
import test_osv
|
||||||
import test_translate
|
import test_translate
|
||||||
import test_uninstall
|
|
||||||
import test_view_validation
|
import test_view_validation
|
||||||
|
import test_qweb
|
||||||
|
import test_func
|
||||||
# This need a change in `oe run-tests` to only run fast_suite + checks by default.
|
# This need a change in `oe run-tests` to only run fast_suite + checks by default.
|
||||||
# import test_xmlrpc
|
# import test_xmlrpc
|
||||||
|
|
||||||
|
@ -42,6 +42,8 @@ checks = [
|
||||||
test_misc,
|
test_misc,
|
||||||
test_osv,
|
test_osv,
|
||||||
test_translate,
|
test_translate,
|
||||||
|
test_qweb,
|
||||||
|
test_func,
|
||||||
]
|
]
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
'name': 'test_convert',
|
||||||
|
'description': "Data for xml conversion tests",
|
||||||
|
'version': '0.0.1',
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
nothing to see here, move along
|
|
@ -0,0 +1,6 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import test_convert
|
||||||
|
|
||||||
|
checks = [
|
||||||
|
test_convert
|
||||||
|
]
|
|
@ -0,0 +1,83 @@
|
||||||
|
import collections
|
||||||
|
import unittest2
|
||||||
|
from lxml import etree as ET
|
||||||
|
from lxml.builder import E
|
||||||
|
|
||||||
|
from openerp.tests import common
|
||||||
|
|
||||||
|
from openerp.tools.convert import _eval_xml
|
||||||
|
|
||||||
|
Field = E.field
|
||||||
|
Value = E.value
|
||||||
|
class TestEvalXML(common.TransactionCase):
|
||||||
|
def eval_xml(self, node, obj=None, idref=None):
|
||||||
|
return _eval_xml(obj, node, pool=None, cr=self.cr, uid=self.uid,
|
||||||
|
idref=idref, context=None)
|
||||||
|
|
||||||
|
def test_char(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self.eval_xml(Field("foo")),
|
||||||
|
"foo")
|
||||||
|
self.assertEqual(
|
||||||
|
self.eval_xml(Field("None")),
|
||||||
|
"None")
|
||||||
|
|
||||||
|
def test_int(self):
|
||||||
|
self.assertIsNone(
|
||||||
|
self.eval_xml(Field("None", type='int')),
|
||||||
|
"what the fuck?")
|
||||||
|
self.assertEqual(
|
||||||
|
self.eval_xml(Field(" 42 ", type="int")),
|
||||||
|
42)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.eval_xml(Field("4.82", type="int"))
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.eval_xml(Field("Whelp", type="int"))
|
||||||
|
|
||||||
|
def test_float(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self.eval_xml(Field("4.78", type="float")),
|
||||||
|
4.78)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.eval_xml(Field("None", type="float"))
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.eval_xml(Field("Foo", type="float"))
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self.eval_xml(Field(type="list")),
|
||||||
|
[])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.eval_xml(Field(
|
||||||
|
Value("foo"),
|
||||||
|
Value("5", type="int"),
|
||||||
|
Value("4.76", type="float"),
|
||||||
|
Value("None", type="int"),
|
||||||
|
type="list"
|
||||||
|
)),
|
||||||
|
["foo", 5, 4.76, None])
|
||||||
|
|
||||||
|
def test_file(self):
|
||||||
|
Obj = collections.namedtuple('Obj', 'module')
|
||||||
|
obj = Obj('test_convert')
|
||||||
|
self.assertEqual(
|
||||||
|
self.eval_xml(Field('test_file.txt', type='file'), obj),
|
||||||
|
'test_convert,test_file.txt')
|
||||||
|
|
||||||
|
with self.assertRaises(IOError):
|
||||||
|
self.eval_xml(Field('test_nofile.txt', type='file'), obj)
|
||||||
|
|
||||||
|
@unittest2.skip("not tested")
|
||||||
|
def test_xml(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@unittest2.skip("not tested")
|
||||||
|
def test_html(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import models
|
|
@ -0,0 +1,15 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'test-field-converter',
|
||||||
|
'version': '0.1',
|
||||||
|
'category': 'Tests',
|
||||||
|
'description': """Tests of field conversions""",
|
||||||
|
'author': 'OpenERP SA',
|
||||||
|
'maintainer': 'OpenERP SA',
|
||||||
|
'website': 'http://www.openerp.com',
|
||||||
|
'depends': ['base'],
|
||||||
|
'data': ['ir.model.access.csv'],
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': False,
|
||||||
|
}
|
||||||
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1,4 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_converter_model,access_converter_model,model_test_converter_test_model,,1,1,1,1
|
||||||
|
access_test_converter_test_model_sub,access_test_converter_test_model_sub,model_test_converter_test_model_sub,,1,1,1,1
|
||||||
|
access_test_converter_monetary,access_test_converter_monetary,model_test_converter_monetary,,1,1,1,1
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from openerp.osv import orm, fields
|
||||||
|
|
||||||
|
class test_model(orm.Model):
|
||||||
|
_name = 'test_converter.test_model'
|
||||||
|
|
||||||
|
_columns = {
|
||||||
|
'char': fields.char(),
|
||||||
|
'integer': fields.integer(),
|
||||||
|
'float': fields.float(),
|
||||||
|
'numeric': fields.float(digits=(16, 2)),
|
||||||
|
'many2one': fields.many2one('test_converter.test_model.sub'),
|
||||||
|
'binary': fields.binary(),
|
||||||
|
'date': fields.date(),
|
||||||
|
'datetime': fields.datetime(),
|
||||||
|
'selection': fields.selection([
|
||||||
|
(1, "réponse A"),
|
||||||
|
(2, "réponse B"),
|
||||||
|
(3, "réponse C"),
|
||||||
|
(4, "réponse D"),
|
||||||
|
]),
|
||||||
|
'selection_str': fields.selection([
|
||||||
|
('A', "Qu'il n'est pas arrivé à Toronto"),
|
||||||
|
('B', "Qu'il était supposé arriver à Toronto"),
|
||||||
|
('C', "Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?"),
|
||||||
|
('D', "La réponse D"),
|
||||||
|
], string="Lorsqu'un pancake prend l'avion à destination de Toronto et "
|
||||||
|
"qu'il fait une escale technique à St Claude, on dit:"),
|
||||||
|
'html': fields.html(),
|
||||||
|
'text': fields.text(),
|
||||||
|
}
|
||||||
|
|
||||||
|
class test_model_sub(orm.Model):
|
||||||
|
_name = 'test_converter.test_model.sub'
|
||||||
|
|
||||||
|
_columns = {
|
||||||
|
'name': fields.char()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class test_model_monetary(orm.Model):
|
||||||
|
_name = 'test_converter.monetary'
|
||||||
|
|
||||||
|
_columns = {
|
||||||
|
'value': fields.float(digits=(16, 55)),
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import test_html
|
||||||
|
|
||||||
|
fast_suite = [
|
||||||
|
]
|
||||||
|
|
||||||
|
checks = [
|
||||||
|
test_html
|
||||||
|
]
|
||||||
|
|
||||||
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1,387 @@
|
||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import xml.dom.minidom
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from werkzeug.utils import escape as e
|
||||||
|
|
||||||
|
from openerp.tests import common
|
||||||
|
from openerp.addons.base.ir import ir_qweb
|
||||||
|
|
||||||
|
directory = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
impl = xml.dom.minidom.getDOMImplementation()
|
||||||
|
doc = impl.createDocument(None, None, None)
|
||||||
|
|
||||||
|
class TestExport(common.TransactionCase):
|
||||||
|
_model = None
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestExport, self).setUp()
|
||||||
|
self.Model = self.registry(self._model)
|
||||||
|
self.columns = self.Model._all_columns
|
||||||
|
|
||||||
|
def get_column(self, name):
|
||||||
|
return self.Model._all_columns[name].column
|
||||||
|
|
||||||
|
def get_converter(self, name, type=None):
|
||||||
|
column = self.get_column(name)
|
||||||
|
|
||||||
|
for postfix in type, column._type, '':
|
||||||
|
fs = ['ir', 'qweb', 'field']
|
||||||
|
if postfix is None: continue
|
||||||
|
if postfix: fs.append(postfix)
|
||||||
|
|
||||||
|
try:
|
||||||
|
model = self.registry('.'.join(fs))
|
||||||
|
break
|
||||||
|
except KeyError: pass
|
||||||
|
|
||||||
|
return lambda value, options=None, context=None: e(model.value_to_html(
|
||||||
|
self.cr, self.uid, value, column, options=options, context=context))
|
||||||
|
|
||||||
|
class TestBasicExport(TestExport):
|
||||||
|
_model = 'test_converter.test_model'
|
||||||
|
|
||||||
|
class TestCharExport(TestBasicExport):
|
||||||
|
def test_char(self):
|
||||||
|
converter = self.get_converter('char')
|
||||||
|
|
||||||
|
value = converter('foo')
|
||||||
|
self.assertEqual(value, 'foo')
|
||||||
|
|
||||||
|
value = converter("foo<bar>")
|
||||||
|
self.assertEqual(value, "foo<bar>")
|
||||||
|
|
||||||
|
class TestIntegerExport(TestBasicExport):
|
||||||
|
def test_integer(self):
|
||||||
|
converter = self.get_converter('integer')
|
||||||
|
|
||||||
|
value = converter(42)
|
||||||
|
self.assertEqual(value, "42")
|
||||||
|
|
||||||
|
class TestFloatExport(TestBasicExport):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestFloatExport, self).setUp()
|
||||||
|
self.registry('res.lang').write(self.cr, self.uid, [1], {
|
||||||
|
'grouping': '[3,0]'
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_float(self):
|
||||||
|
converter = self.get_converter('float')
|
||||||
|
|
||||||
|
value = converter(42.0)
|
||||||
|
self.assertEqual(value, "42.0")
|
||||||
|
|
||||||
|
value = converter(42.0100)
|
||||||
|
self.assertEqual(value, "42.01")
|
||||||
|
|
||||||
|
value = converter(42.01234)
|
||||||
|
self.assertEqual(value, "42.01234")
|
||||||
|
|
||||||
|
value = converter(1234567.89)
|
||||||
|
self.assertEqual(value, '1,234,567.89')
|
||||||
|
|
||||||
|
def test_numeric(self):
|
||||||
|
converter = self.get_converter('numeric')
|
||||||
|
|
||||||
|
value = converter(42.0)
|
||||||
|
self.assertEqual(value, '42.00')
|
||||||
|
|
||||||
|
value = converter(42.01234)
|
||||||
|
self.assertEqual(value, '42.01')
|
||||||
|
|
||||||
|
class TestCurrencyExport(TestExport):
|
||||||
|
_model = 'test_converter.monetary'
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestCurrencyExport, self).setUp()
|
||||||
|
self.Currency = self.registry('res.currency')
|
||||||
|
self.base = self.create(self.Currency, name="Source", symbol=u'source')
|
||||||
|
|
||||||
|
def create(self, model, context=None, **values):
|
||||||
|
return model.browse(
|
||||||
|
self.cr, self.uid,
|
||||||
|
model.create(self.cr, self.uid, values, context=context),
|
||||||
|
context=context)
|
||||||
|
|
||||||
|
def convert(self, obj, dest):
|
||||||
|
converter = self.registry('ir.qweb.field.monetary')
|
||||||
|
options = {
|
||||||
|
'widget': 'monetary',
|
||||||
|
'display_currency': 'c2'
|
||||||
|
}
|
||||||
|
converted = converter.to_html(
|
||||||
|
self.cr, self.uid, 'value', obj, options,
|
||||||
|
doc.createElement('span'),
|
||||||
|
{'field': 'obj.value', 'field-options': json.dumps(options)},
|
||||||
|
'', ir_qweb.QWebContext(self.cr, self.uid, {'obj': obj, 'c2': dest, }))
|
||||||
|
return converted
|
||||||
|
|
||||||
|
def test_currency_post(self):
|
||||||
|
currency = self.create(self.Currency, name="Test", symbol=u"test")
|
||||||
|
obj = self.create(self.Model, value=0.12)
|
||||||
|
|
||||||
|
converted = self.convert(obj, dest=currency)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
converted,
|
||||||
|
'<span data-oe-model="{obj._model._name}" data-oe-id="{obj.id}" '
|
||||||
|
'data-oe-field="value" data-oe-type="monetary" '
|
||||||
|
'data-oe-expression="obj.value">'
|
||||||
|
'<span class="oe_currency_value">0.12</span>'
|
||||||
|
' {symbol}</span>'.format(
|
||||||
|
obj=obj,
|
||||||
|
symbol=currency.symbol.encode('utf-8')
|
||||||
|
),)
|
||||||
|
|
||||||
|
def test_currency_pre(self):
|
||||||
|
currency = self.create(
|
||||||
|
self.Currency, name="Test", symbol=u"test", position='before')
|
||||||
|
obj = self.create(self.Model, value=0.12)
|
||||||
|
|
||||||
|
converted = self.convert(obj, dest=currency)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
converted,
|
||||||
|
'<span data-oe-model="{obj._model._name}" data-oe-id="{obj.id}" '
|
||||||
|
'data-oe-field="value" data-oe-type="monetary" '
|
||||||
|
'data-oe-expression="obj.value">'
|
||||||
|
'{symbol} '
|
||||||
|
'<span class="oe_currency_value">0.12</span>'
|
||||||
|
'</span>'.format(
|
||||||
|
obj=obj,
|
||||||
|
symbol=currency.symbol.encode('utf-8')
|
||||||
|
),)
|
||||||
|
|
||||||
|
def test_currency_precision(self):
|
||||||
|
""" Precision should be the currency's, not the float field's
|
||||||
|
"""
|
||||||
|
currency = self.create(self.Currency, name="Test", symbol=u"test",)
|
||||||
|
obj = self.create(self.Model, value=0.1234567)
|
||||||
|
|
||||||
|
converted = self.convert(obj, dest=currency)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
converted,
|
||||||
|
'<span data-oe-model="{obj._model._name}" data-oe-id="{obj.id}" '
|
||||||
|
'data-oe-field="value" data-oe-type="monetary" '
|
||||||
|
'data-oe-expression="obj.value">'
|
||||||
|
'<span class="oe_currency_value">0.12</span>'
|
||||||
|
' {symbol}</span>'.format(
|
||||||
|
obj=obj,
|
||||||
|
symbol=currency.symbol.encode('utf-8')
|
||||||
|
),)
|
||||||
|
|
||||||
|
class TestTextExport(TestBasicExport):
|
||||||
|
def test_text(self):
|
||||||
|
converter = self.get_converter('text')
|
||||||
|
|
||||||
|
value = converter("This is my text-kai")
|
||||||
|
self.assertEqual(value, "This is my text-kai")
|
||||||
|
|
||||||
|
value = converter("""
|
||||||
|
. The current line (address) in the buffer.
|
||||||
|
$ The last line in the buffer.
|
||||||
|
n The nth, line in the buffer where n is a number in the range [0,$].
|
||||||
|
$ The last line in the buffer.
|
||||||
|
- The previous line. This is equivalent to -1 and may be repeated with cumulative effect.
|
||||||
|
-n The nth previous line, where n is a non-negative number.
|
||||||
|
+ The next line. This is equivalent to +1 and may be repeated with cumulative effect.
|
||||||
|
""")
|
||||||
|
self.assertEqual(value, """<br>
|
||||||
|
. The current line (address) in the buffer.<br>
|
||||||
|
$ The last line in the buffer.<br>
|
||||||
|
n The nth, line in the buffer where n is a number in the range [0,$].<br>
|
||||||
|
$ The last line in the buffer.<br>
|
||||||
|
- The previous line. This is equivalent to -1 and may be repeated with cumulative effect.<br>
|
||||||
|
-n The nth previous line, where n is a non-negative number.<br>
|
||||||
|
+ The next line. This is equivalent to +1 and may be repeated with cumulative effect.<br>
|
||||||
|
""")
|
||||||
|
|
||||||
|
value = converter("""
|
||||||
|
fgdkls;hjas;lj <b>fdslkj</b> d;lasjfa lkdja <a href=http://spam.com>lfks</a>
|
||||||
|
fldkjsfhs <i style="color: red"><a href="http://spamspam.com">fldskjh</a></i>
|
||||||
|
""")
|
||||||
|
self.assertEqual(value, """<br>
|
||||||
|
fgdkls;hjas;lj <b>fdslkj</b> d;lasjfa lkdja <a href=http://spam.com>lfks</a><br>
|
||||||
|
fldkjsfhs <i style="color: red"><a href="http://spamspam.com">fldskjh</a></i><br>
|
||||||
|
""")
|
||||||
|
|
||||||
|
class TestMany2OneExport(TestBasicExport):
|
||||||
|
def test_many2one(self):
|
||||||
|
Sub = self.registry('test_converter.test_model.sub')
|
||||||
|
|
||||||
|
|
||||||
|
id0 = self.Model.create(self.cr, self.uid, {
|
||||||
|
'many2one': Sub.create(self.cr, self.uid, {'name': "Foo"})
|
||||||
|
})
|
||||||
|
id1 = self.Model.create(self.cr, self.uid, {
|
||||||
|
'many2one': Sub.create(self.cr, self.uid, {'name': "Fo<b>o</b>"})
|
||||||
|
})
|
||||||
|
|
||||||
|
def converter(record):
|
||||||
|
column = self.get_column('many2one')
|
||||||
|
model = self.registry('ir.qweb.field.many2one')
|
||||||
|
|
||||||
|
return e(model.record_to_html(
|
||||||
|
self.cr, self.uid, 'many2one', record, column))
|
||||||
|
|
||||||
|
value = converter(self.Model.browse(self.cr, self.uid, id0))
|
||||||
|
self.assertEqual(value, "Foo")
|
||||||
|
|
||||||
|
value = converter(self.Model.browse(self.cr, self.uid, id1))
|
||||||
|
self.assertEqual(value, "Fo<b>o</b>")
|
||||||
|
|
||||||
|
class TestBinaryExport(TestBasicExport):
|
||||||
|
def test_image(self):
|
||||||
|
column = self.get_column('binary')
|
||||||
|
converter = self.registry('ir.qweb.field.image')
|
||||||
|
|
||||||
|
with open(os.path.join(directory, 'test_vectors', 'image'), 'rb') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
encoded_content = content.encode('base64')
|
||||||
|
value = e(converter.value_to_html(
|
||||||
|
self.cr, self.uid, encoded_content, column))
|
||||||
|
self.assertEqual(
|
||||||
|
value, '<img src="data:image/jpeg;base64,%s">' % (
|
||||||
|
encoded_content
|
||||||
|
))
|
||||||
|
|
||||||
|
with open(os.path.join(directory, 'test_vectors', 'pdf'), 'rb') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
e(converter.value_to_html(
|
||||||
|
self.cr, self.uid, 'binary', content.encode('base64'), column))
|
||||||
|
|
||||||
|
with open(os.path.join(directory, 'test_vectors', 'pptx'), 'rb') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
e(converter.value_to_html(
|
||||||
|
self.cr, self.uid, 'binary', content.encode('base64'), column))
|
||||||
|
|
||||||
|
class TestSelectionExport(TestBasicExport):
|
||||||
|
def test_selection(self):
|
||||||
|
[record] = self.Model.browse(self.cr, self.uid, [self.Model.create(self.cr, self.uid, {
|
||||||
|
'selection': 2,
|
||||||
|
'selection_str': 'C',
|
||||||
|
})])
|
||||||
|
|
||||||
|
column_name = 'selection'
|
||||||
|
column = self.get_column(column_name)
|
||||||
|
converter = self.registry('ir.qweb.field.selection')
|
||||||
|
|
||||||
|
value = converter.record_to_html(
|
||||||
|
self.cr, self.uid, column_name, record, column)
|
||||||
|
self.assertEqual(value, "réponse B")
|
||||||
|
|
||||||
|
column_name = 'selection_str'
|
||||||
|
column = self.get_column(column_name)
|
||||||
|
value = converter.record_to_html(
|
||||||
|
self.cr, self.uid, column_name, record, column)
|
||||||
|
self.assertEqual(value, "Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?")
|
||||||
|
|
||||||
|
class TestHTMLExport(TestBasicExport):
|
||||||
|
def test_html(self):
|
||||||
|
converter = self.get_converter('html')
|
||||||
|
|
||||||
|
input = '<span>span</span>'
|
||||||
|
value = converter(input)
|
||||||
|
self.assertEqual(value, input)
|
||||||
|
|
||||||
|
class TestDatetimeExport(TestBasicExport):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestDatetimeExport, self).setUp()
|
||||||
|
# set user tz to known value
|
||||||
|
Users = self.registry('res.users')
|
||||||
|
Users.write(self.cr, self.uid, self.uid, {
|
||||||
|
'tz': 'Pacific/Niue'
|
||||||
|
}, context=None)
|
||||||
|
|
||||||
|
def test_date(self):
|
||||||
|
converter = self.get_converter('date')
|
||||||
|
|
||||||
|
value = converter('2011-05-03')
|
||||||
|
|
||||||
|
# default lang/format is US
|
||||||
|
self.assertEqual(value, '05/03/2011')
|
||||||
|
|
||||||
|
def test_datetime(self):
|
||||||
|
converter = self.get_converter('datetime')
|
||||||
|
|
||||||
|
value = converter('2011-05-03 11:12:13')
|
||||||
|
|
||||||
|
# default lang/format is US
|
||||||
|
self.assertEqual(value, '05/03/2011 00:12:13')
|
||||||
|
|
||||||
|
def test_custom_format(self):
|
||||||
|
converter = self.get_converter('datetime')
|
||||||
|
converter2 = self.get_converter('date')
|
||||||
|
opts = {'format': 'MMMM d'}
|
||||||
|
|
||||||
|
value = converter('2011-03-02 11:12:13', options=opts)
|
||||||
|
value2 = converter2('2001-03-02', options=opts)
|
||||||
|
self.assertEqual(
|
||||||
|
value,
|
||||||
|
'March 2'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
value2,
|
||||||
|
'March 2'
|
||||||
|
)
|
||||||
|
|
||||||
|
class TestDurationExport(TestBasicExport):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestDurationExport, self).setUp()
|
||||||
|
# needs to have lang installed otherwise falls back on en_US
|
||||||
|
self.registry('res.lang').load_lang(self.cr, self.uid, 'fr_FR')
|
||||||
|
|
||||||
|
def test_negative(self):
|
||||||
|
converter = self.get_converter('float', 'duration')
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
converter(-4)
|
||||||
|
|
||||||
|
def test_missing_unit(self):
|
||||||
|
converter = self.get_converter('float', 'duration')
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
converter(4)
|
||||||
|
|
||||||
|
def test_basic(self):
|
||||||
|
converter = self.get_converter('float', 'duration')
|
||||||
|
|
||||||
|
result = converter(4, {'unit': 'hour'}, {'lang': 'fr_FR'})
|
||||||
|
self.assertEqual(result, u'4 heures')
|
||||||
|
|
||||||
|
result = converter(50, {'unit': 'second'}, {'lang': 'fr_FR'})
|
||||||
|
self.assertEqual(result, u'50 secondes')
|
||||||
|
|
||||||
|
def test_multiple(self):
|
||||||
|
converter = self.get_converter('float', 'duration')
|
||||||
|
|
||||||
|
result = converter(1.5, {'unit': 'hour'}, {'lang': 'fr_FR'})
|
||||||
|
self.assertEqual(result, u"1 heure 30 minutes")
|
||||||
|
|
||||||
|
result = converter(72, {'unit': 'second'}, {'lang': 'fr_FR'})
|
||||||
|
self.assertEqual(result, u"1 minute 12 secondes")
|
||||||
|
|
||||||
|
class TestRelativeDatetime(TestBasicExport):
|
||||||
|
# not sure how a test based on "current time" should be tested. Even less
|
||||||
|
# so as it would mostly be a test of babel...
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestRelativeDatetime, self).setUp()
|
||||||
|
# needs to have lang installed otherwise falls back on en_US
|
||||||
|
self.registry('res.lang').load_lang(self.cr, self.uid, 'fr_FR')
|
||||||
|
|
||||||
|
def test_basic(self):
|
||||||
|
converter = self.get_converter('datetime', 'relative')
|
||||||
|
t = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
|
||||||
|
|
||||||
|
result = converter(t, context={'lang': 'fr_FR'})
|
||||||
|
self.assertEqual(result, u"il y a 1 heure")
|
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
Binary file not shown.
|
@ -17,7 +17,7 @@ class TestACL(common.TransactionCase):
|
||||||
self.res_currency = self.registry('res.currency')
|
self.res_currency = self.registry('res.currency')
|
||||||
self.res_partner = self.registry('res.partner')
|
self.res_partner = self.registry('res.partner')
|
||||||
self.res_users = self.registry('res.users')
|
self.res_users = self.registry('res.users')
|
||||||
self.demo_uid = 3
|
_, self.demo_uid = self.registry('ir.model.data').get_object_reference(self.cr, self.uid, 'base', 'user_demo')
|
||||||
self.tech_group = self.registry('ir.model.data').get_object(self.cr, self.uid,
|
self.tech_group = self.registry('ir.model.data').get_object(self.cr, self.uid,
|
||||||
*(GROUP_TECHNICAL_FEATURES.split('.')))
|
*(GROUP_TECHNICAL_FEATURES.split('.')))
|
||||||
|
|
||||||
|
@ -104,7 +104,6 @@ class TestACL(common.TransactionCase):
|
||||||
finally:
|
finally:
|
||||||
self.res_partner._columns['email'].groups = False
|
self.res_partner._columns['email'].groups = False
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest2.main()
|
unittest2.main()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import functools
|
||||||
|
import unittest2
|
||||||
|
|
||||||
|
from ..tools.func import compose
|
||||||
|
|
||||||
|
class TestCompose(unittest2.TestCase):
|
||||||
|
def test_basic(self):
|
||||||
|
str_add = compose(str, lambda a, b: a + b)
|
||||||
|
self.assertEqual(
|
||||||
|
str_add(1, 2),
|
||||||
|
"3")
|
||||||
|
|
||||||
|
def test_decorator(self):
|
||||||
|
""" ensure compose() can be partially applied as a decorator
|
||||||
|
"""
|
||||||
|
@functools.partial(compose, unicode)
|
||||||
|
def mul(a, b):
|
||||||
|
return a * b
|
||||||
|
|
||||||
|
self.assertEqual(mul(5, 42), u"210")
|
||||||
|
|
|
@ -4,22 +4,6 @@ import functools
|
||||||
from openerp import exceptions
|
from openerp import exceptions
|
||||||
from . import common
|
from . import common
|
||||||
|
|
||||||
class Fixtures(object):
|
|
||||||
def __init__(self, *args):
|
|
||||||
self.fixtures = args
|
|
||||||
|
|
||||||
def __call__(self, fn):
|
|
||||||
@functools.wraps(fn)
|
|
||||||
def wrapper(case):
|
|
||||||
for model, vars in self.fixtures:
|
|
||||||
case.registry(model).create(
|
|
||||||
case.cr, common.ADMIN_USER_ID, vars, {})
|
|
||||||
|
|
||||||
fn(case)
|
|
||||||
return wrapper
|
|
||||||
def fixtures(*args):
|
|
||||||
return Fixtures(*args)
|
|
||||||
|
|
||||||
def noid(d):
|
def noid(d):
|
||||||
""" Removes `id` key from a dict so we don't have to keep these things
|
""" Removes `id` key from a dict so we don't have to keep these things
|
||||||
around when trying to match
|
around when trying to match
|
||||||
|
@ -27,17 +11,26 @@ def noid(d):
|
||||||
if 'id' in d: del d['id']
|
if 'id' in d: del d['id']
|
||||||
return d
|
return d
|
||||||
|
|
||||||
class TestGetFilters(common.TransactionCase):
|
class FiltersCase(common.TransactionCase):
|
||||||
USER_ID = 3
|
def build(self, model, *args):
|
||||||
USER = (3, u'Demo User')
|
Model = self.registry(model)
|
||||||
|
for vars in args:
|
||||||
|
Model.create(self.cr, common.ADMIN_USER_ID, vars, {})
|
||||||
|
|
||||||
|
class TestGetFilters(FiltersCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestGetFilters, self).setUp()
|
||||||
|
self.USER = self.registry('res.users').name_search(self.cr, self.uid, 'demo')[0]
|
||||||
|
self.USER_ID = self.USER[0]
|
||||||
|
|
||||||
@fixtures(
|
|
||||||
('ir.filters', dict(name='a', user_id=USER_ID, model_id='ir.filters')),
|
|
||||||
('ir.filters', dict(name='b', user_id=USER_ID, model_id='ir.filters')),
|
|
||||||
('ir.filters', dict(name='c', user_id=USER_ID, model_id='ir.filters')),
|
|
||||||
('ir.filters', dict(name='d', user_id=USER_ID, model_id='ir.filters')),
|
|
||||||
)
|
|
||||||
def test_own_filters(self):
|
def test_own_filters(self):
|
||||||
|
self.build(
|
||||||
|
'ir.filters',
|
||||||
|
dict(name='a', user_id=self.USER_ID, model_id='ir.filters'),
|
||||||
|
dict(name='b', user_id=self.USER_ID, model_id='ir.filters'),
|
||||||
|
dict(name='c', user_id=self.USER_ID, model_id='ir.filters'),
|
||||||
|
dict(name='d', user_id=self.USER_ID, model_id='ir.filters'))
|
||||||
|
|
||||||
filters = self.registry('ir.filters').get_filters(
|
filters = self.registry('ir.filters').get_filters(
|
||||||
self.cr, self.USER_ID, 'ir.filters')
|
self.cr, self.USER_ID, 'ir.filters')
|
||||||
|
|
||||||
|
@ -48,13 +41,15 @@ class TestGetFilters(common.TransactionCase):
|
||||||
dict(name='d', is_default=False, user_id=self.USER, domain='[]', context='{}'),
|
dict(name='d', is_default=False, user_id=self.USER, domain='[]', context='{}'),
|
||||||
])
|
])
|
||||||
|
|
||||||
@fixtures(
|
|
||||||
('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')),
|
|
||||||
('ir.filters', dict(name='b', user_id=False, model_id='ir.filters')),
|
|
||||||
('ir.filters', dict(name='c', user_id=False, model_id='ir.filters')),
|
|
||||||
('ir.filters', dict(name='d', user_id=False, model_id='ir.filters')),
|
|
||||||
)
|
|
||||||
def test_global_filters(self):
|
def test_global_filters(self):
|
||||||
|
self.build(
|
||||||
|
'ir.filters',
|
||||||
|
dict(name='a', user_id=False, model_id='ir.filters'),
|
||||||
|
dict(name='b', user_id=False, model_id='ir.filters'),
|
||||||
|
dict(name='c', user_id=False, model_id='ir.filters'),
|
||||||
|
dict(name='d', user_id=False, model_id='ir.filters'),
|
||||||
|
)
|
||||||
|
|
||||||
filters = self.registry('ir.filters').get_filters(
|
filters = self.registry('ir.filters').get_filters(
|
||||||
self.cr, self.USER_ID, 'ir.filters')
|
self.cr, self.USER_ID, 'ir.filters')
|
||||||
|
|
||||||
|
@ -65,13 +60,14 @@ class TestGetFilters(common.TransactionCase):
|
||||||
dict(name='d', is_default=False, user_id=False, domain='[]', context='{}'),
|
dict(name='d', is_default=False, user_id=False, domain='[]', context='{}'),
|
||||||
])
|
])
|
||||||
|
|
||||||
@fixtures(
|
|
||||||
('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')),
|
|
||||||
('ir.filters', dict(name='b', user_id=common.ADMIN_USER_ID, model_id='ir.filters')),
|
|
||||||
('ir.filters', dict(name='c', user_id=USER_ID, model_id='ir.filters')),
|
|
||||||
('ir.filters', dict(name='d', user_id=common.ADMIN_USER_ID, model_id='ir.filters')),
|
|
||||||
)
|
|
||||||
def test_no_third_party_filters(self):
|
def test_no_third_party_filters(self):
|
||||||
|
self.build(
|
||||||
|
'ir.filters',
|
||||||
|
dict(name='a', user_id=False, model_id='ir.filters'),
|
||||||
|
dict(name='b', user_id=common.ADMIN_USER_ID, model_id='ir.filters'),
|
||||||
|
dict(name='c', user_id=self.USER_ID, model_id='ir.filters'),
|
||||||
|
dict(name='d', user_id=common.ADMIN_USER_ID, model_id='ir.filters') )
|
||||||
|
|
||||||
filters = self.registry('ir.filters').get_filters(
|
filters = self.registry('ir.filters').get_filters(
|
||||||
self.cr, self.USER_ID, 'ir.filters')
|
self.cr, self.USER_ID, 'ir.filters')
|
||||||
|
|
||||||
|
@ -80,9 +76,11 @@ class TestGetFilters(common.TransactionCase):
|
||||||
dict(name='c', is_default=False, user_id=self.USER, domain='[]', context='{}'),
|
dict(name='c', is_default=False, user_id=self.USER, domain='[]', context='{}'),
|
||||||
])
|
])
|
||||||
|
|
||||||
class TestOwnDefaults(common.TransactionCase):
|
class TestOwnDefaults(FiltersCase):
|
||||||
USER_ID = 3
|
def setUp(self):
|
||||||
USER = (3, u'Demo User')
|
super(TestOwnDefaults, self).setUp()
|
||||||
|
self.USER = self.registry('res.users').name_search(self.cr, self.uid, 'demo')[0]
|
||||||
|
self.USER_ID = self.USER[0]
|
||||||
|
|
||||||
def test_new_no_filter(self):
|
def test_new_no_filter(self):
|
||||||
"""
|
"""
|
||||||
|
@ -103,15 +101,17 @@ class TestOwnDefaults(common.TransactionCase):
|
||||||
domain='[]', context='{}')
|
domain='[]', context='{}')
|
||||||
])
|
])
|
||||||
|
|
||||||
@fixtures(
|
|
||||||
('ir.filters', dict(name='a', user_id=USER_ID, model_id='ir.filters')),
|
|
||||||
('ir.filters', dict(name='b', user_id=USER_ID, model_id='ir.filters')),
|
|
||||||
)
|
|
||||||
def test_new_filter_not_default(self):
|
def test_new_filter_not_default(self):
|
||||||
"""
|
"""
|
||||||
When creating a @is_default filter with existing non-default filters,
|
When creating a @is_default filter with existing non-default filters,
|
||||||
the new filter gets the flag
|
the new filter gets the flag
|
||||||
"""
|
"""
|
||||||
|
self.build(
|
||||||
|
'ir.filters',
|
||||||
|
dict(name='a', user_id=self.USER_ID, model_id='ir.filters'),
|
||||||
|
dict(name='b', user_id=self.USER_ID, model_id='ir.filters'),
|
||||||
|
)
|
||||||
|
|
||||||
Filters = self.registry('ir.filters')
|
Filters = self.registry('ir.filters')
|
||||||
Filters.create_or_replace(self.cr, self.USER_ID, {
|
Filters.create_or_replace(self.cr, self.USER_ID, {
|
||||||
'name': 'c',
|
'name': 'c',
|
||||||
|
@ -127,15 +127,17 @@ class TestOwnDefaults(common.TransactionCase):
|
||||||
dict(name='c', user_id=self.USER, is_default=True, domain='[]', context='{}'),
|
dict(name='c', user_id=self.USER, is_default=True, domain='[]', context='{}'),
|
||||||
])
|
])
|
||||||
|
|
||||||
@fixtures(
|
|
||||||
('ir.filters', dict(name='a', user_id=USER_ID, model_id='ir.filters')),
|
|
||||||
('ir.filters', dict(name='b', is_default=True, user_id=USER_ID, model_id='ir.filters')),
|
|
||||||
)
|
|
||||||
def test_new_filter_existing_default(self):
|
def test_new_filter_existing_default(self):
|
||||||
"""
|
"""
|
||||||
When creating a @is_default filter where an existing filter is already
|
When creating a @is_default filter where an existing filter is already
|
||||||
@is_default, the flag should be *moved* from the old to the new filter
|
@is_default, the flag should be *moved* from the old to the new filter
|
||||||
"""
|
"""
|
||||||
|
self.build(
|
||||||
|
'ir.filters',
|
||||||
|
dict(name='a', user_id=self.USER_ID, model_id='ir.filters'),
|
||||||
|
dict(name='b', is_default=True, user_id=self.USER_ID, model_id='ir.filters'),
|
||||||
|
)
|
||||||
|
|
||||||
Filters = self.registry('ir.filters')
|
Filters = self.registry('ir.filters')
|
||||||
Filters.create_or_replace(self.cr, self.USER_ID, {
|
Filters.create_or_replace(self.cr, self.USER_ID, {
|
||||||
'name': 'c',
|
'name': 'c',
|
||||||
|
@ -151,15 +153,17 @@ class TestOwnDefaults(common.TransactionCase):
|
||||||
dict(name='c', user_id=self.USER, is_default=True, domain='[]', context='{}'),
|
dict(name='c', user_id=self.USER, is_default=True, domain='[]', context='{}'),
|
||||||
])
|
])
|
||||||
|
|
||||||
@fixtures(
|
|
||||||
('ir.filters', dict(name='a', user_id=USER_ID, model_id='ir.filters')),
|
|
||||||
('ir.filters', dict(name='b', is_default=True, user_id=USER_ID, model_id='ir.filters')),
|
|
||||||
)
|
|
||||||
def test_update_filter_set_default(self):
|
def test_update_filter_set_default(self):
|
||||||
"""
|
"""
|
||||||
When updating an existing filter to @is_default, if an other filter
|
When updating an existing filter to @is_default, if an other filter
|
||||||
already has the flag the flag should be moved
|
already has the flag the flag should be moved
|
||||||
"""
|
"""
|
||||||
|
self.build(
|
||||||
|
'ir.filters',
|
||||||
|
dict(name='a', user_id=self.USER_ID, model_id='ir.filters'),
|
||||||
|
dict(name='b', is_default=True, user_id=self.USER_ID, model_id='ir.filters'),
|
||||||
|
)
|
||||||
|
|
||||||
Filters = self.registry('ir.filters')
|
Filters = self.registry('ir.filters')
|
||||||
Filters.create_or_replace(self.cr, self.USER_ID, {
|
Filters.create_or_replace(self.cr, self.USER_ID, {
|
||||||
'name': 'a',
|
'name': 'a',
|
||||||
|
@ -174,18 +178,23 @@ class TestOwnDefaults(common.TransactionCase):
|
||||||
dict(name='b', user_id=self.USER, is_default=False, domain='[]', context='{}'),
|
dict(name='b', user_id=self.USER, is_default=False, domain='[]', context='{}'),
|
||||||
])
|
])
|
||||||
|
|
||||||
class TestGlobalDefaults(common.TransactionCase):
|
class TestGlobalDefaults(FiltersCase):
|
||||||
USER_ID = 3
|
def setUp(self):
|
||||||
|
super(TestGlobalDefaults, self).setUp()
|
||||||
|
self.USER = self.registry('res.users').name_search(self.cr, self.uid, 'demo')[0]
|
||||||
|
self.USER_ID = self.USER[0]
|
||||||
|
|
||||||
@fixtures(
|
|
||||||
('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')),
|
|
||||||
('ir.filters', dict(name='b', user_id=False, model_id='ir.filters')),
|
|
||||||
)
|
|
||||||
def test_new_filter_not_default(self):
|
def test_new_filter_not_default(self):
|
||||||
"""
|
"""
|
||||||
When creating a @is_default filter with existing non-default filters,
|
When creating a @is_default filter with existing non-default filters,
|
||||||
the new filter gets the flag
|
the new filter gets the flag
|
||||||
"""
|
"""
|
||||||
|
self.build(
|
||||||
|
'ir.filters',
|
||||||
|
dict(name='a', user_id=False, model_id='ir.filters'),
|
||||||
|
dict(name='b', user_id=False, model_id='ir.filters'),
|
||||||
|
)
|
||||||
|
|
||||||
Filters = self.registry('ir.filters')
|
Filters = self.registry('ir.filters')
|
||||||
Filters.create_or_replace(self.cr, self.USER_ID, {
|
Filters.create_or_replace(self.cr, self.USER_ID, {
|
||||||
'name': 'c',
|
'name': 'c',
|
||||||
|
@ -201,15 +210,17 @@ class TestGlobalDefaults(common.TransactionCase):
|
||||||
dict(name='c', user_id=False, is_default=True, domain='[]', context='{}'),
|
dict(name='c', user_id=False, is_default=True, domain='[]', context='{}'),
|
||||||
])
|
])
|
||||||
|
|
||||||
@fixtures(
|
|
||||||
('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')),
|
|
||||||
('ir.filters', dict(name='b', is_default=True, user_id=False, model_id='ir.filters')),
|
|
||||||
)
|
|
||||||
def test_new_filter_existing_default(self):
|
def test_new_filter_existing_default(self):
|
||||||
"""
|
"""
|
||||||
When creating a @is_default filter where an existing filter is already
|
When creating a @is_default filter where an existing filter is already
|
||||||
@is_default, an error should be generated
|
@is_default, an error should be generated
|
||||||
"""
|
"""
|
||||||
|
self.build(
|
||||||
|
'ir.filters',
|
||||||
|
dict(name='a', user_id=False, model_id='ir.filters'),
|
||||||
|
dict(name='b', is_default=True, user_id=False, model_id='ir.filters'),
|
||||||
|
)
|
||||||
|
|
||||||
Filters = self.registry('ir.filters')
|
Filters = self.registry('ir.filters')
|
||||||
with self.assertRaises(exceptions.Warning):
|
with self.assertRaises(exceptions.Warning):
|
||||||
Filters.create_or_replace(self.cr, self.USER_ID, {
|
Filters.create_or_replace(self.cr, self.USER_ID, {
|
||||||
|
@ -219,15 +230,17 @@ class TestGlobalDefaults(common.TransactionCase):
|
||||||
'is_default': True,
|
'is_default': True,
|
||||||
})
|
})
|
||||||
|
|
||||||
@fixtures(
|
|
||||||
('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')),
|
|
||||||
('ir.filters', dict(name='b', is_default=True, user_id=False, model_id='ir.filters')),
|
|
||||||
)
|
|
||||||
def test_update_filter_set_default(self):
|
def test_update_filter_set_default(self):
|
||||||
"""
|
"""
|
||||||
When updating an existing filter to @is_default, if an other filter
|
When updating an existing filter to @is_default, if an other filter
|
||||||
already has the flag an error should be generated
|
already has the flag an error should be generated
|
||||||
"""
|
"""
|
||||||
|
self.build(
|
||||||
|
'ir.filters',
|
||||||
|
dict(name='a', user_id=False, model_id='ir.filters'),
|
||||||
|
dict(name='b', is_default=True, user_id=False, model_id='ir.filters'),
|
||||||
|
)
|
||||||
|
|
||||||
Filters = self.registry('ir.filters')
|
Filters = self.registry('ir.filters')
|
||||||
|
|
||||||
with self.assertRaises(exceptions.Warning):
|
with self.assertRaises(exceptions.Warning):
|
||||||
|
@ -238,14 +251,16 @@ class TestGlobalDefaults(common.TransactionCase):
|
||||||
'is_default': True,
|
'is_default': True,
|
||||||
})
|
})
|
||||||
|
|
||||||
@fixtures(
|
|
||||||
('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')),
|
|
||||||
('ir.filters', dict(name='b', is_default=True, user_id=False, model_id='ir.filters')),
|
|
||||||
)
|
|
||||||
def test_update_default_filter(self):
|
def test_update_default_filter(self):
|
||||||
"""
|
"""
|
||||||
Replacing the current default global filter should not generate any error
|
Replacing the current default global filter should not generate any error
|
||||||
"""
|
"""
|
||||||
|
self.build(
|
||||||
|
'ir.filters',
|
||||||
|
dict(name='a', user_id=False, model_id='ir.filters'),
|
||||||
|
dict(name='b', is_default=True, user_id=False, model_id='ir.filters'),
|
||||||
|
)
|
||||||
|
|
||||||
Filters = self.registry('ir.filters')
|
Filters = self.registry('ir.filters')
|
||||||
context_value = "{'some_key': True}"
|
context_value = "{'some_key': True}"
|
||||||
Filters.create_or_replace(self.cr, self.USER_ID, {
|
Filters.create_or_replace(self.cr, self.USER_ID, {
|
||||||
|
|
|
@ -22,18 +22,21 @@
|
||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
import unittest2
|
import unittest2
|
||||||
from . import test_mail_examples
|
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from openerp.tests import test_mail_examples
|
||||||
from openerp.tools import html_sanitize, html_email_clean, append_content_to_html, plaintext2html, email_split
|
from openerp.tools import html_sanitize, html_email_clean, append_content_to_html, plaintext2html, email_split
|
||||||
|
|
||||||
|
|
||||||
class TestSanitizer(unittest2.TestCase):
|
class TestSanitizer(unittest2.TestCase):
|
||||||
""" Test the html sanitizer that filters html to remove unwanted attributes """
|
""" Test the html sanitizer that filters html to remove unwanted attributes """
|
||||||
|
|
||||||
def test_basic_sanitizer(self):
|
def test_basic_sanitizer(self):
|
||||||
cases = [
|
cases = [
|
||||||
("yop", "<p>yop</p>"), # simple
|
("yop", "<p>yop</p>"), # simple
|
||||||
("lala<p>yop</p>xxx", "<div><p>lala</p><p>yop</p>xxx</div>"), # trailing text
|
("lala<p>yop</p>xxx", "<p>lala</p><p>yop</p>xxx"), # trailing text
|
||||||
("Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci",
|
("Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci",
|
||||||
u"<p>Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci</p>"), # unicode
|
u"<p>Merci à l'intérêt pour notre produit.nous vous contacterons bientôt. Merci</p>"), # unicode
|
||||||
]
|
]
|
||||||
|
@ -276,6 +279,40 @@ class TestCleaner(unittest2.TestCase):
|
||||||
for ext in test_mail_examples.THUNDERBIRD_1_OUT:
|
for ext in test_mail_examples.THUNDERBIRD_1_OUT:
|
||||||
self.assertNotIn(ext, new_html, 'html_email_cleaner did not erase signature / quoted content')
|
self.assertNotIn(ext, new_html, 'html_email_cleaner did not erase signature / quoted content')
|
||||||
|
|
||||||
|
def test_70_read_more_and_shorten(self):
|
||||||
|
expand_options = {
|
||||||
|
'oe_expand_container_class': 'span_class',
|
||||||
|
'oe_expand_container_content': 'Herbert Einstein',
|
||||||
|
'oe_expand_separator_node': 'br_lapin',
|
||||||
|
'oe_expand_a_class': 'a_class',
|
||||||
|
'oe_expand_a_content': 'read mee',
|
||||||
|
}
|
||||||
|
new_html = html_email_clean(test_mail_examples.OERP_WEBSITE_HTML_1, remove=True, shorten=True, max_length=100, expand_options=expand_options)
|
||||||
|
for ext in test_mail_examples.OERP_WEBSITE_HTML_1_IN:
|
||||||
|
self.assertIn(ext, new_html, 'html_email_cleaner wrongly removed not quoted content')
|
||||||
|
for ext in test_mail_examples.OERP_WEBSITE_HTML_1_OUT:
|
||||||
|
self.assertNotIn(ext, new_html, 'html_email_cleaner did not erase overlimit content')
|
||||||
|
for ext in ['<span class="span_class">Herbert Einstein<br_lapin></br_lapin><a href="#" class="a_class">read mee</a></span>']:
|
||||||
|
self.assertIn(ext, new_html, 'html_email_cleaner wrongly take into account specific expand options')
|
||||||
|
|
||||||
|
new_html = html_email_clean(test_mail_examples.OERP_WEBSITE_HTML_2, remove=True, shorten=True, max_length=200, expand_options=expand_options, protect_sections=False)
|
||||||
|
for ext in test_mail_examples.OERP_WEBSITE_HTML_2_IN:
|
||||||
|
self.assertIn(ext, new_html, 'html_email_cleaner wrongly removed not quoted content')
|
||||||
|
for ext in test_mail_examples.OERP_WEBSITE_HTML_2_OUT:
|
||||||
|
self.assertNotIn(ext, new_html, 'html_email_cleaner did not erase overlimit content')
|
||||||
|
for ext in ['<span class="span_class">Herbert Einstein<br_lapin></br_lapin><a href="#" class="a_class">read mee</a></span>']:
|
||||||
|
self.assertIn(ext, new_html, 'html_email_cleaner wrongly take into account specific expand options')
|
||||||
|
|
||||||
|
new_html = html_email_clean(test_mail_examples.OERP_WEBSITE_HTML_2, remove=True, shorten=True, max_length=200, expand_options=expand_options, protect_sections=True)
|
||||||
|
for ext in test_mail_examples.OERP_WEBSITE_HTML_2_IN:
|
||||||
|
self.assertIn(ext, new_html, 'html_email_cleaner wrongly removed not quoted content')
|
||||||
|
for ext in test_mail_examples.OERP_WEBSITE_HTML_2_OUT:
|
||||||
|
self.assertNotIn(ext, new_html, 'html_email_cleaner did not erase overlimit content')
|
||||||
|
for ext in [
|
||||||
|
'<span class="span_class">Herbert Einstein<br_lapin></br_lapin><a href="#" class="a_class">read mee</a></span>',
|
||||||
|
'tasks using the gantt chart and control deadlines']:
|
||||||
|
self.assertIn(ext, new_html, 'html_email_cleaner wrongly take into account specific expand options')
|
||||||
|
|
||||||
def test_70_read_more(self):
|
def test_70_read_more(self):
|
||||||
new_html = html_email_clean(test_mail_examples.BUG1, remove=True, shorten=True, max_length=100)
|
new_html = html_email_clean(test_mail_examples.BUG1, remove=True, shorten=True, max_length=100)
|
||||||
for ext in test_mail_examples.BUG_1_IN:
|
for ext in test_mail_examples.BUG_1_IN:
|
||||||
|
|
|
@ -60,6 +60,154 @@ EDI_LIKE_HTML_SOURCE = """<div style="font-family: 'Lucica Grande', Ubuntu, Aria
|
||||||
</div>
|
</div>
|
||||||
</div></body></html>"""
|
</div></body></html>"""
|
||||||
|
|
||||||
|
OERP_WEBSITE_HTML_1 = """
|
||||||
|
<div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 text-center mt16 mb16" data-snippet-id="colmd">
|
||||||
|
<h2>OpenERP HR Features</h2>
|
||||||
|
<h3 class="text-muted">Manage your company most important asset: People</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4" data-snippet-id="colmd">
|
||||||
|
<img class="img-rounded img-responsive" src="/website/static/src/img/china_thumb.jpg">
|
||||||
|
<h4 class="mt16">Streamline Recruitments</h4>
|
||||||
|
<p>Post job offers and keep track of each application received. Follow applicants in your recruitment process with the smart kanban view.</p>
|
||||||
|
<p>Save time by automating some communications with email templates. Resumes are indexed automatically, allowing you to easily find for specific profiles.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4" data-snippet-id="colmd">
|
||||||
|
<img class="img-rounded img-responsive" src="/website/static/src/img/desert_thumb.jpg">
|
||||||
|
<h4 class="mt16">Enterprise Social Network</h4>
|
||||||
|
<p>Break down information silos. Share knowledge and best practices amongst all employees. Follow specific people or documents and join groups of interests to share expertise and documents.</p>
|
||||||
|
<p>Interact with your collegues in real time with live chat.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4" data-snippet-id="colmd">
|
||||||
|
<img class="img-rounded img-responsive" src="/website/static/src/img/deers_thumb.jpg">
|
||||||
|
<h4 class="mt16">Leaves Management</h4>
|
||||||
|
<p>Keep track of the vacation days accrued by each employee. Employees enter their requests (paid holidays, sick leave, etc), for managers to approve and validate. It's all done in just a few clicks. The agenda of each employee is updated accordingly.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>"""
|
||||||
|
|
||||||
|
OERP_WEBSITE_HTML_1_IN = [
|
||||||
|
'Manage your company most important asset: People',
|
||||||
|
'img class="img-rounded img-responsive" src="/website/static/src/img/china_thumb.jpg"',
|
||||||
|
]
|
||||||
|
OERP_WEBSITE_HTML_1_OUT = [
|
||||||
|
'Break down information silos.',
|
||||||
|
'Keep track of the vacation days accrued by each employee',
|
||||||
|
'img class="img-rounded img-responsive" src="/website/static/src/img/deers_thumb.jpg',
|
||||||
|
]
|
||||||
|
|
||||||
|
OERP_WEBSITE_HTML_2 = """
|
||||||
|
<div class="mt16 cke_widget_editable cke_widget_element oe_editable oe_dirty" data-oe-model="blog.post" data-oe-id="6" data-oe-field="content" data-oe-type="html" data-oe-translate="0" data-oe-expression="blog_post.content" data-cke-widget-data="{}" data-cke-widget-keep-attr="0" data-widget="oeref" contenteditable="true" data-cke-widget-editable="text">
|
||||||
|
<section class="mt16 mb16" data-snippet-id="text-block">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 text-center mt16 mb32" data-snippet-id="colmd">
|
||||||
|
<h2>
|
||||||
|
OpenERP Project Management
|
||||||
|
</h2>
|
||||||
|
<h3 class="text-muted">Infinitely flexible. Incredibly easy to use.</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 mb16 mt16" data-snippet-id="colmd">
|
||||||
|
<p>
|
||||||
|
OpenERP's <b>collaborative and realtime</b> project
|
||||||
|
management helps your team get work done. Keep
|
||||||
|
track of everything, from the big picture to the
|
||||||
|
minute details, from the customer contract to the
|
||||||
|
billing.
|
||||||
|
</p><p>
|
||||||
|
Organize projects around <b>your own processes</b>. Work
|
||||||
|
on tasks and issues using the kanban view, schedule
|
||||||
|
tasks using the gantt chart and control deadlines
|
||||||
|
in the calendar view. Every project may have it's
|
||||||
|
own stages allowing teams to optimize their job.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="" data-snippet-id="image-text">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mt16 mb16" data-snippet-id="colmd">
|
||||||
|
<img class="img-responsive shadow" src="/website/static/src/img/image_text.jpg">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mt32" data-snippet-id="colmd">
|
||||||
|
<h3>Manage Your Shops</h3>
|
||||||
|
<p>
|
||||||
|
OpenERP's Point of Sale introduces a super clean
|
||||||
|
interface with no installation required that runs
|
||||||
|
online and offline on modern hardwares.
|
||||||
|
</p><p>
|
||||||
|
It's full integration with the company inventory
|
||||||
|
and accounting, gives you real time statistics and
|
||||||
|
consolidations amongst all shops without the hassle
|
||||||
|
of integrating several applications.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="" data-snippet-id="text-image">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mt32" data-snippet-id="colmd">
|
||||||
|
<h3>Enterprise Social Network</h3>
|
||||||
|
<p>
|
||||||
|
Make every employee feel more connected and engaged
|
||||||
|
with twitter-like features for your own company. Follow
|
||||||
|
people, share best practices, 'like' top ideas, etc.
|
||||||
|
</p><p>
|
||||||
|
Connect with experts, follow what interests you, share
|
||||||
|
documents and promote best practices with OpenERP
|
||||||
|
Social application. Get work done with effective
|
||||||
|
collaboration across departments, geographies
|
||||||
|
and business applications.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mt16 mb16" data-snippet-id="colmd">
|
||||||
|
<img class="img-responsive shadow" src="/website/static/src/img/text_image.png">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section><section class="" data-snippet-id="portfolio">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 text-center mt16 mb32" data-snippet-id="colmd">
|
||||||
|
<h2>Our Porfolio</h2>
|
||||||
|
<h4 class="text-muted">More than 500 successful projects</h4>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4" data-snippet-id="colmd">
|
||||||
|
<img class="img-thumbnail img-responsive" src="/website/static/src/img/deers.jpg">
|
||||||
|
<img class="img-thumbnail img-responsive" src="/website/static/src/img/desert.jpg">
|
||||||
|
<img class="img-thumbnail img-responsive" src="/website/static/src/img/china.jpg">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4" data-snippet-id="colmd">
|
||||||
|
<img class="img-thumbnail img-responsive" src="/website/static/src/img/desert.jpg">
|
||||||
|
<img class="img-thumbnail img-responsive" src="/website/static/src/img/china.jpg">
|
||||||
|
<img class="img-thumbnail img-responsive" src="/website/static/src/img/deers.jpg">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4" data-snippet-id="colmd">
|
||||||
|
<img class="img-thumbnail img-responsive" src="/website/static/src/img/landscape.jpg">
|
||||||
|
<img class="img-thumbnail img-responsive" src="/website/static/src/img/china.jpg">
|
||||||
|
<img class="img-thumbnail img-responsive" src="/website/static/src/img/desert.jpg">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
OERP_WEBSITE_HTML_2_IN = [
|
||||||
|
'management helps your team get work done',
|
||||||
|
]
|
||||||
|
OERP_WEBSITE_HTML_2_OUT = [
|
||||||
|
'Make every employee feel more connected',
|
||||||
|
'img class="img-responsive shadow" src="/website/static/src/img/text_image.png',
|
||||||
|
]
|
||||||
|
|
||||||
TEXT_1 = """I contact you about our meeting tomorrow. Here is the schedule I propose:
|
TEXT_1 = """I contact you about our meeting tomorrow. Here is the schedule I propose:
|
||||||
9 AM: brainstorming about our new amazing business app
|
9 AM: brainstorming about our new amazing business app
|
||||||
9.45 AM: summary
|
9.45 AM: summary
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
# This test can be run stand-alone with something like:
|
# This test can be run stand-alone with something like:
|
||||||
# > PYTHONPATH=. python2 openerp/tests/test_misc.py
|
# > PYTHONPATH=. python2 openerp/tests/test_misc.py
|
||||||
|
import datetime
|
||||||
|
import locale
|
||||||
import unittest2
|
import unittest2
|
||||||
|
|
||||||
|
import babel
|
||||||
|
import babel.dates
|
||||||
|
|
||||||
from ..tools import misc
|
from ..tools import misc
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import cgi
|
||||||
|
from xml.dom import minidom as dom
|
||||||
|
|
||||||
|
import common
|
||||||
|
from openerp.addons.base.ir import ir_qweb
|
||||||
|
|
||||||
|
impl = dom.getDOMImplementation()
|
||||||
|
document = impl.createDocument(None, None, None)
|
||||||
|
|
||||||
|
class TestQWebTField(common.TransactionCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestQWebTField, self).setUp()
|
||||||
|
self.engine = self.registry('ir.qweb')
|
||||||
|
|
||||||
|
def context(self, values):
|
||||||
|
return ir_qweb.QWebContext(self.cr, self.uid, values)
|
||||||
|
|
||||||
|
def test_trivial(self):
|
||||||
|
field = document.createElement('span')
|
||||||
|
field.setAttribute('t-field', u'company.name')
|
||||||
|
|
||||||
|
Companies = self.registry('res.company')
|
||||||
|
company_id = Companies.create(self.cr, self.uid, {
|
||||||
|
'name': "My Test Company"
|
||||||
|
})
|
||||||
|
result = self.engine.render_node(field, self.context({
|
||||||
|
'company': Companies.browse(self.cr, self.uid, company_id),
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
'<span data-oe-model="res.company" data-oe-id="%d" '
|
||||||
|
'data-oe-field="name" data-oe-type="char" '
|
||||||
|
'data-oe-expression="company.name">%s</span>' % (
|
||||||
|
company_id,
|
||||||
|
"My Test Company",))
|
||||||
|
|
||||||
|
def test_i18n(self):
|
||||||
|
field = document.createElement('span')
|
||||||
|
field.setAttribute('t-field', u'company.name')
|
||||||
|
|
||||||
|
Companies = self.registry('res.company')
|
||||||
|
s = u"Testing «ταБЬℓσ»: 1<2 & 4+1>3, now 20% off!"
|
||||||
|
company_id = Companies.create(self.cr, self.uid, {
|
||||||
|
'name': s,
|
||||||
|
})
|
||||||
|
result = self.engine.render_node(field, self.context({
|
||||||
|
'company': Companies.browse(self.cr, self.uid, company_id),
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
'<span data-oe-model="res.company" data-oe-id="%d" '
|
||||||
|
'data-oe-field="name" data-oe-type="char" '
|
||||||
|
'data-oe-expression="company.name">%s</span>' % (
|
||||||
|
company_id,
|
||||||
|
cgi.escape(s.encode('utf-8')),))
|
||||||
|
|
||||||
|
def test_reject_crummy_tags(self):
|
||||||
|
field = document.createElement('td')
|
||||||
|
field.setAttribute('t-field', u'company.name')
|
||||||
|
|
||||||
|
with self.assertRaisesRegexp(
|
||||||
|
AssertionError,
|
||||||
|
r'^RTE widgets do not work correctly'):
|
||||||
|
self.engine.render_node(field, self.context({
|
||||||
|
'company': None
|
||||||
|
}))
|
||||||
|
|
||||||
|
def test_reject_t_tag(self):
|
||||||
|
field = document.createElement('t')
|
||||||
|
field.setAttribute('t-field', u'company.name')
|
||||||
|
|
||||||
|
with self.assertRaisesRegexp(
|
||||||
|
AssertionError,
|
||||||
|
r'^t-field can not be used on a t element'):
|
||||||
|
self.engine.render_node(field, self.context({
|
||||||
|
'company': None
|
||||||
|
}))
|
|
@ -1,89 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# This assumes an existing but uninitialized database.
|
|
||||||
import unittest2
|
|
||||||
|
|
||||||
import openerp
|
|
||||||
from openerp import SUPERUSER_ID
|
|
||||||
import common
|
|
||||||
|
|
||||||
DB = common.DB
|
|
||||||
ADMIN_USER_ID = common.ADMIN_USER_ID
|
|
||||||
|
|
||||||
def registry(model):
|
|
||||||
return openerp.modules.registry.RegistryManager.get(DB)[model]
|
|
||||||
|
|
||||||
def cursor():
|
|
||||||
return openerp.modules.registry.RegistryManager.get(DB).db.cursor()
|
|
||||||
|
|
||||||
def model_exists(model_name):
|
|
||||||
registry = openerp.modules.registry.RegistryManager.get(DB)
|
|
||||||
return model_name in registry
|
|
||||||
|
|
||||||
def reload_registry():
|
|
||||||
openerp.modules.registry.RegistryManager.new(
|
|
||||||
DB, update_module=True)
|
|
||||||
|
|
||||||
def search_registry(model_name, domain):
|
|
||||||
cr = cursor()
|
|
||||||
model = registry(model_name)
|
|
||||||
record_ids = model.search(cr, SUPERUSER_ID, domain, {})
|
|
||||||
cr.close()
|
|
||||||
return record_ids
|
|
||||||
|
|
||||||
def install_module(module_name):
|
|
||||||
ir_module_module = registry('ir.module.module')
|
|
||||||
cr = cursor()
|
|
||||||
module_ids = ir_module_module.search(cr, SUPERUSER_ID,
|
|
||||||
[('name', '=', module_name)], {})
|
|
||||||
assert len(module_ids) == 1
|
|
||||||
ir_module_module.button_install(cr, SUPERUSER_ID, module_ids, {})
|
|
||||||
cr.commit()
|
|
||||||
cr.close()
|
|
||||||
reload_registry()
|
|
||||||
|
|
||||||
def uninstall_module(module_name):
|
|
||||||
ir_module_module = registry('ir.module.module')
|
|
||||||
cr = cursor()
|
|
||||||
module_ids = ir_module_module.search(cr, SUPERUSER_ID,
|
|
||||||
[('name', '=', module_name)], {})
|
|
||||||
assert len(module_ids) == 1
|
|
||||||
ir_module_module.button_uninstall(cr, SUPERUSER_ID, module_ids, {})
|
|
||||||
cr.commit()
|
|
||||||
cr.close()
|
|
||||||
reload_registry()
|
|
||||||
|
|
||||||
class test_uninstall(unittest2.TestCase):
|
|
||||||
"""
|
|
||||||
Test the install/uninstall of a test module. The module is available in
|
|
||||||
`openerp.tests` which should be present in the addons-path.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_01_install(self):
|
|
||||||
""" Check a few things showing the module is installed. """
|
|
||||||
install_module('test_uninstall')
|
|
||||||
assert model_exists('test_uninstall.model')
|
|
||||||
|
|
||||||
assert search_registry('ir.model.data',
|
|
||||||
[('module', '=', 'test_uninstall')])
|
|
||||||
|
|
||||||
assert search_registry('ir.model.fields',
|
|
||||||
[('model', '=', 'test_uninstall.model')])
|
|
||||||
|
|
||||||
def test_02_uninstall(self):
|
|
||||||
""" Check a few things showing the module is uninstalled. """
|
|
||||||
uninstall_module('test_uninstall')
|
|
||||||
assert not model_exists('test_uninstall.model')
|
|
||||||
|
|
||||||
assert not search_registry('ir.model.data',
|
|
||||||
[('module', '=', 'test_uninstall')])
|
|
||||||
|
|
||||||
assert not search_registry('ir.model.fields',
|
|
||||||
[('model', '=', 'test_uninstall.model')])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest2.main()
|
|
||||||
|
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
|
@ -4,7 +4,10 @@ from lxml import etree
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
import unittest2
|
import unittest2
|
||||||
|
|
||||||
from openerp.tools.view_validation import *
|
from openerp.tools.view_validation import (valid_page_in_book, valid_att_in_form, valid_type_in_colspan,
|
||||||
|
valid_type_in_col, valid_att_in_field, valid_att_in_label,
|
||||||
|
valid_field_in_graph, valid_field_in_tree
|
||||||
|
)
|
||||||
|
|
||||||
invalid_form = etree.parse(StringIO('''\
|
invalid_form = etree.parse(StringIO('''\
|
||||||
<form>
|
<form>
|
||||||
|
@ -79,7 +82,7 @@ invalid_tree = etree.parse(StringIO('''\
|
||||||
</tree>
|
</tree>
|
||||||
''')).getroot()
|
''')).getroot()
|
||||||
|
|
||||||
valid_tree= etree.parse(StringIO('''\
|
valid_tree = etree.parse(StringIO('''\
|
||||||
<tree string="">
|
<tree string="">
|
||||||
<field name=""></field>
|
<field name=""></field>
|
||||||
<field name=""></field>
|
<field name=""></field>
|
||||||
|
@ -97,15 +100,14 @@ class test_view_validation(unittest2.TestCase):
|
||||||
assert valid_page_in_book(valid_form)
|
assert valid_page_in_book(valid_form)
|
||||||
|
|
||||||
def test_all_field_validation(self):
|
def test_all_field_validation(self):
|
||||||
assert not valid_att_in_field(invalid_form)
|
assert not valid_att_in_field(invalid_form)
|
||||||
assert valid_att_in_field(valid_form)
|
assert valid_att_in_field(valid_form)
|
||||||
|
|
||||||
def test_all_label_validation(self):
|
def test_all_label_validation(self):
|
||||||
assert not valid_att_in_label(invalid_form)
|
assert not valid_att_in_label(invalid_form)
|
||||||
assert valid_att_in_label(valid_form)
|
assert valid_att_in_label(valid_form)
|
||||||
|
|
||||||
def test_form_string_validation(self):
|
def test_form_string_validation(self):
|
||||||
assert not valid_att_in_form(invalid_form)
|
|
||||||
assert valid_att_in_form(valid_form)
|
assert valid_att_in_form(valid_form)
|
||||||
|
|
||||||
def test_graph_validation(self):
|
def test_graph_validation(self):
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import lru
|
import lru
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class ormcache(object):
|
class ormcache(object):
|
||||||
""" LRU cache decorator for orm methods,
|
""" LRU cache decorator for orm methods,
|
||||||
|
@ -14,8 +17,8 @@ class ormcache(object):
|
||||||
|
|
||||||
def __call__(self,m):
|
def __call__(self,m):
|
||||||
self.method = m
|
self.method = m
|
||||||
def lookup(self2, cr, *args):
|
def lookup(self2, cr, *args, **argv):
|
||||||
r = self.lookup(self2, cr, *args)
|
r = self.lookup(self2, cr, *args, **argv)
|
||||||
return r
|
return r
|
||||||
lookup.clear_cache = self.clear
|
lookup.clear_cache = self.clear
|
||||||
return lookup
|
return lookup
|
||||||
|
@ -34,7 +37,7 @@ class ormcache(object):
|
||||||
d = ormcache[self.method] = lru.LRU(self.size)
|
d = ormcache[self.method] = lru.LRU(self.size)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def lookup(self, self2, cr, *args):
|
def lookup(self, self2, cr, *args, **argv):
|
||||||
d = self.lru(self2)
|
d = self.lru(self2)
|
||||||
key = args[self.skiparg-2:]
|
key = args[self.skiparg-2:]
|
||||||
try:
|
try:
|
||||||
|
@ -54,22 +57,45 @@ class ormcache(object):
|
||||||
"""
|
"""
|
||||||
d = self.lru(self2)
|
d = self.lru(self2)
|
||||||
if args:
|
if args:
|
||||||
try:
|
logger.warn("ormcache.clear arguments are deprecated and ignored "
|
||||||
key = args[self.skiparg-2:]
|
"(while clearing caches on (%s).%s)",
|
||||||
del d[key]
|
self2._name, self.method.__name__)
|
||||||
self2.pool._any_cache_cleared = True
|
d.clear()
|
||||||
except KeyError:
|
self2.pool._any_cache_cleared = True
|
||||||
pass
|
|
||||||
else:
|
class ormcache_context(ormcache):
|
||||||
d.clear()
|
def __init__(self, skiparg=2, size=8192, accepted_keys=()):
|
||||||
self2.pool._any_cache_cleared = True
|
super(ormcache_context,self).__init__(skiparg,size)
|
||||||
|
self.accepted_keys = accepted_keys
|
||||||
|
|
||||||
|
def lookup(self, self2, cr, *args, **argv):
|
||||||
|
d = self.lru(self2)
|
||||||
|
|
||||||
|
context = argv.get('context', {})
|
||||||
|
ckey = filter(lambda x: x[0] in self.accepted_keys, context.items())
|
||||||
|
ckey.sort()
|
||||||
|
|
||||||
|
d = self.lru(self2)
|
||||||
|
key = args[self.skiparg-2:]+tuple(ckey)
|
||||||
|
try:
|
||||||
|
r = d[key]
|
||||||
|
self.stat_hit += 1
|
||||||
|
return r
|
||||||
|
except KeyError:
|
||||||
|
self.stat_miss += 1
|
||||||
|
value = d[key] = self.method(self2, cr, *args, **argv)
|
||||||
|
return value
|
||||||
|
except TypeError:
|
||||||
|
self.stat_err += 1
|
||||||
|
return self.method(self2, cr, *args, **argv)
|
||||||
|
|
||||||
|
|
||||||
class ormcache_multi(ormcache):
|
class ormcache_multi(ormcache):
|
||||||
def __init__(self, skiparg=2, size=8192, multi=3):
|
def __init__(self, skiparg=2, size=8192, multi=3):
|
||||||
super(ormcache_multi,self).__init__(skiparg,size)
|
super(ormcache_multi,self).__init__(skiparg,size)
|
||||||
self.multi = multi - 2
|
self.multi = multi - 2
|
||||||
|
|
||||||
def lookup(self, self2, cr, *args):
|
def lookup(self, self2, cr, *args, **argv):
|
||||||
d = self.lru(self2)
|
d = self.lru(self2)
|
||||||
args = list(args)
|
args = list(args)
|
||||||
multi = self.multi
|
multi = self.multi
|
||||||
|
@ -130,6 +156,7 @@ if __name__ == '__main__':
|
||||||
print r
|
print r
|
||||||
for i in a._ormcache:
|
for i in a._ormcache:
|
||||||
print a._ormcache[i].d
|
print a._ormcache[i].d
|
||||||
|
a.m.clear_cache()
|
||||||
a.n.clear_cache(a,1,1)
|
a.n.clear_cache(a,1,1)
|
||||||
r=a.n("cr",1,[1,2])
|
r=a.n("cr",1,[1,2])
|
||||||
print r
|
print r
|
||||||
|
|
|
@ -50,7 +50,7 @@ except:
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from lxml import etree
|
from lxml import etree, builder
|
||||||
import misc
|
import misc
|
||||||
from config import config
|
from config import config
|
||||||
from translate import _
|
from translate import _
|
||||||
|
@ -854,6 +854,48 @@ form: module.record_id""" % (xml_id,)
|
||||||
cr.commit()
|
cr.commit()
|
||||||
return rec_model, id
|
return rec_model, id
|
||||||
|
|
||||||
|
def _tag_template(self, cr, el, data_node=None):
|
||||||
|
# This helper transforms a <template> element into a <record> and forwards it
|
||||||
|
tpl_id = el.get('id', el.get('t-name', '')).encode('ascii')
|
||||||
|
module = self.module
|
||||||
|
if '.' in tpl_id:
|
||||||
|
module, tpl_id = tpl_id.split('.', 1)
|
||||||
|
# set the full template name for qweb <module>.<id>
|
||||||
|
if not (el.get('inherit_id') or el.get('inherit_option_id')):
|
||||||
|
el.set('t-name', '%s.%s' % (module, tpl_id))
|
||||||
|
el.tag = 't'
|
||||||
|
else:
|
||||||
|
el.tag = 'data'
|
||||||
|
el.attrib.pop('id', None)
|
||||||
|
|
||||||
|
record_attrs = {
|
||||||
|
'id': tpl_id,
|
||||||
|
'model': 'ir.ui.view',
|
||||||
|
}
|
||||||
|
for att in ['forcecreate', 'context']:
|
||||||
|
if att in el.keys():
|
||||||
|
record_attrs[att] = el.attrib.pop(att)
|
||||||
|
|
||||||
|
Field = builder.E.field
|
||||||
|
name = el.get('name', tpl_id)
|
||||||
|
|
||||||
|
record = etree.Element('record', attrib=record_attrs)
|
||||||
|
record.append(Field(name, name='name'))
|
||||||
|
record.append(Field("qweb", name='type'))
|
||||||
|
record.append(Field(el.get('priority', "16"), name='priority'))
|
||||||
|
record.append(Field(el, name="arch", type="xml"))
|
||||||
|
for field_name in ('inherit_id','inherit_option_id'):
|
||||||
|
value = el.attrib.pop(field_name, None)
|
||||||
|
if value: record.append(Field(name=field_name, ref=value))
|
||||||
|
groups = el.attrib.pop('groups', None)
|
||||||
|
if groups:
|
||||||
|
grp_lst = map(lambda x: "ref('%s')" % x, groups.split(','))
|
||||||
|
record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]"))
|
||||||
|
if el.attrib.pop('page', None) == 'True':
|
||||||
|
record.append(Field(name="page", eval="True"))
|
||||||
|
|
||||||
|
return self._tag_record(cr, record, data_node)
|
||||||
|
|
||||||
def id_get(self, cr, id_str):
|
def id_get(self, cr, id_str):
|
||||||
if id_str in self.idref:
|
if id_str in self.idref:
|
||||||
return self.idref[id_str]
|
return self.idref[id_str]
|
||||||
|
@ -898,6 +940,7 @@ form: module.record_id""" % (xml_id,)
|
||||||
self._tags = {
|
self._tags = {
|
||||||
'menuitem': self._tag_menuitem,
|
'menuitem': self._tag_menuitem,
|
||||||
'record': self._tag_record,
|
'record': self._tag_record,
|
||||||
|
'template': self._tag_template,
|
||||||
'assert': self._tag_assert,
|
'assert': self._tag_assert,
|
||||||
'report': self._tag_report,
|
'report': self._tag_report,
|
||||||
'wizard': self._tag_wizard,
|
'wizard': self._tag_wizard,
|
||||||
|
|
|
@ -57,4 +57,19 @@ def frame_codeinfo(fframe, back=0):
|
||||||
except Exception:
|
except Exception:
|
||||||
return "<unknown>", ''
|
return "<unknown>", ''
|
||||||
|
|
||||||
|
def compose(a, b):
|
||||||
|
""" Composes the callables ``a`` and ``b``. ``compose(a, b)(*args)`` is
|
||||||
|
equivalent to ``a(b(*args))``.
|
||||||
|
|
||||||
|
Can be used as a decorator by partially applying ``a``::
|
||||||
|
|
||||||
|
@partial(compose, a)
|
||||||
|
def b():
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
@wraps(b)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return a(b(*args, **kwargs))
|
||||||
|
return wrapper
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
# OpenERP, Open Source Business Applications
|
# OpenERP, Open Source Business Applications
|
||||||
# Copyright (C) 2012-2013 OpenERP S.A. (<http://openerp.com>).
|
# Copyright (C) 2012-TODAY OpenERP S.A. (<http://openerp.com>).
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU Affero General Public License as
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
@ -45,10 +45,15 @@ tags_to_kill = ["script", "head", "meta", "title", "link", "style", "frame", "if
|
||||||
tags_to_remove = ['html', 'body', 'font']
|
tags_to_remove = ['html', 'body', 'font']
|
||||||
|
|
||||||
# allow new semantic HTML5 tags
|
# allow new semantic HTML5 tags
|
||||||
allowed_tags = clean.defs.tags | frozenset('article section header footer hgroup nav aside figure'.split())
|
allowed_tags = clean.defs.tags | frozenset('article section header footer hgroup nav aside figure main'.split())
|
||||||
safe_attrs = clean.defs.safe_attrs | frozenset(['style'])
|
safe_attrs = clean.defs.safe_attrs | frozenset(
|
||||||
|
['style',
|
||||||
|
'data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-type', 'data-oe-expression', 'data-oe-translate', 'data-oe-nodeid',
|
||||||
|
'data-snippet-id', 'data-publish', 'data-id', 'data-res_id', 'data-member_id', 'data-view-id'
|
||||||
|
])
|
||||||
|
|
||||||
def html_sanitize(src, silent=True):
|
|
||||||
|
def html_sanitize(src, silent=True, strict=False):
|
||||||
if not src:
|
if not src:
|
||||||
return src
|
return src
|
||||||
src = ustr(src, errors='replace')
|
src = ustr(src, errors='replace')
|
||||||
|
@ -75,22 +80,31 @@ def html_sanitize(src, silent=True):
|
||||||
else:
|
else:
|
||||||
kwargs['remove_tags'] = tags_to_kill + tags_to_remove
|
kwargs['remove_tags'] = tags_to_kill + tags_to_remove
|
||||||
|
|
||||||
if etree.LXML_VERSION >= (3, 1, 0):
|
if strict:
|
||||||
kwargs.update({
|
if etree.LXML_VERSION >= (3, 1, 0):
|
||||||
'safe_attrs_only': True,
|
# lxml < 3.1.0 does not allow to specify safe_attrs. We keep all attributes in order to keep "style"
|
||||||
'safe_attrs': safe_attrs,
|
kwargs.update({
|
||||||
})
|
'safe_attrs_only': True,
|
||||||
|
'safe_attrs': safe_attrs,
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
# lxml < 3.1.0 does not allow to specify safe_attrs. We keep all attributes in order to keep "style"
|
kwargs['safe_attrs_only'] = False # keep oe-data attributes + style
|
||||||
kwargs['safe_attrs_only'] = False
|
kwargs['frames'] = False, # do not remove frames (embbed video in CMS blogs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# some corner cases make the parser crash (such as <SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT> in test_mail)
|
# some corner cases make the parser crash (such as <SCRIPT/XSS SRC=\"http://ha.ckers.org/xss.js\"></SCRIPT> in test_mail)
|
||||||
cleaner = clean.Cleaner(**kwargs)
|
cleaner = clean.Cleaner(**kwargs)
|
||||||
cleaned = cleaner.clean_html(src)
|
cleaned = cleaner.clean_html(src)
|
||||||
|
# MAKO compatibility: $, { and } inside quotes are escaped, preventing correct mako execution
|
||||||
|
cleaned = cleaned.replace('%24', '$')
|
||||||
|
cleaned = cleaned.replace('%7B', '{')
|
||||||
|
cleaned = cleaned.replace('%7D', '}')
|
||||||
|
cleaned = cleaned.replace('%20', ' ')
|
||||||
|
cleaned = cleaned.replace('%5B', '[')
|
||||||
|
cleaned = cleaned.replace('%5D', ']')
|
||||||
except etree.ParserError, e:
|
except etree.ParserError, e:
|
||||||
if 'empty' in str(e):
|
if 'empty' in str(e):
|
||||||
return ""
|
return ""
|
||||||
if not silent:
|
if not silent:
|
||||||
raise
|
raise
|
||||||
logger.warning('ParserError obtained when sanitizing %r', src, exc_info=True)
|
logger.warning('ParserError obtained when sanitizing %r', src, exc_info=True)
|
||||||
|
@ -100,6 +114,11 @@ def html_sanitize(src, silent=True):
|
||||||
raise
|
raise
|
||||||
logger.warning('unknown error obtained when sanitizing %r', src, exc_info=True)
|
logger.warning('unknown error obtained when sanitizing %r', src, exc_info=True)
|
||||||
cleaned = '<p>Unknown error when sanitizing</p>'
|
cleaned = '<p>Unknown error when sanitizing</p>'
|
||||||
|
|
||||||
|
# this is ugly, but lxml/etree tostring want to put everything in a 'div' that breaks the editor -> remove that
|
||||||
|
if cleaned.startswith('<div>') and cleaned.endswith('</div>'):
|
||||||
|
cleaned = cleaned[5:-6]
|
||||||
|
|
||||||
return cleaned
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
|
@ -107,7 +126,8 @@ def html_sanitize(src, silent=True):
|
||||||
# HTML Cleaner
|
# HTML Cleaner
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
|
|
||||||
def html_email_clean(html, remove=False, shorten=False, max_length=300):
|
def html_email_clean(html, remove=False, shorten=False, max_length=300, expand_options=None,
|
||||||
|
protect_sections=False):
|
||||||
""" html_email_clean: clean the html by doing the following steps:
|
""" html_email_clean: clean the html by doing the following steps:
|
||||||
|
|
||||||
- try to strip email quotes, by removing blockquotes or having some client-
|
- try to strip email quotes, by removing blockquotes or having some client-
|
||||||
|
@ -132,6 +152,32 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300):
|
||||||
be flagged as to remove
|
be flagged as to remove
|
||||||
:param int max_length: if shortening, maximum number of characters before
|
:param int max_length: if shortening, maximum number of characters before
|
||||||
shortening
|
shortening
|
||||||
|
:param dict expand_options: options for the read more link when shortening
|
||||||
|
the content.The used keys are the following:
|
||||||
|
|
||||||
|
- oe_expand_container_tag: class applied to the
|
||||||
|
container of the whole read more link
|
||||||
|
- oe_expand_container_class: class applied to the
|
||||||
|
link container (default: oe_mail_expand)
|
||||||
|
- oe_expand_container_content: content of the
|
||||||
|
container (default: ...)
|
||||||
|
- oe_expand_separator_node: optional separator, like
|
||||||
|
adding ... <br /><br /> <a ...>read more</a> (default: void)
|
||||||
|
- oe_expand_a_href: href of the read more link itself
|
||||||
|
(default: #)
|
||||||
|
- oe_expand_a_class: class applied to the <a> containing
|
||||||
|
the link itself (default: oe_mail_expand)
|
||||||
|
- oe_expand_a_content: content of the <a> (default: read more)
|
||||||
|
|
||||||
|
The formatted read more link is the following:
|
||||||
|
<cont_tag class="oe_expand_container_class">
|
||||||
|
oe_expand_container_content
|
||||||
|
if expand_options.get('oe_expand_separator_node'):
|
||||||
|
<oe_expand_separator_node/>
|
||||||
|
<a href="oe_expand_a_href" class="oe_expand_a_class">
|
||||||
|
oe_expand_a_content
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
"""
|
"""
|
||||||
def _replace_matching_regex(regex, source, replace=''):
|
def _replace_matching_regex(regex, source, replace=''):
|
||||||
""" Replace all matching expressions in source by replace """
|
""" Replace all matching expressions in source by replace """
|
||||||
|
@ -212,8 +258,29 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300):
|
||||||
node.text = innertext
|
node.text = innertext
|
||||||
|
|
||||||
# create <span> ... <a href="#">read more</a></span> node
|
# create <span> ... <a href="#">read more</a></span> node
|
||||||
read_more_node = _create_node('span', ' ... ', None, {'class': 'oe_mail_expand'})
|
read_more_node = _create_node(
|
||||||
read_more_link_node = _create_node('a', 'read more', None, {'href': '#', 'class': 'oe_mail_expand'})
|
expand_options.get('oe_expand_container_tag', 'span'),
|
||||||
|
expand_options.get('oe_expand_container_content', ' ... '),
|
||||||
|
None,
|
||||||
|
{'class': expand_options.get('oe_expand_container_class', 'oe_mail_expand')}
|
||||||
|
)
|
||||||
|
if expand_options.get('oe_expand_separator_node'):
|
||||||
|
read_more_separator_node = _create_node(
|
||||||
|
expand_options.get('oe_expand_separator_node'),
|
||||||
|
'',
|
||||||
|
None,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
read_more_node.append(read_more_separator_node)
|
||||||
|
read_more_link_node = _create_node(
|
||||||
|
'a',
|
||||||
|
expand_options.get('oe_expand_a_content', 'read more'),
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
'href': expand_options.get('oe_expand_a_href', '#'),
|
||||||
|
'class': expand_options.get('oe_expand_a_class', 'oe_mail_expand'),
|
||||||
|
}
|
||||||
|
)
|
||||||
read_more_node.append(read_more_link_node)
|
read_more_node.append(read_more_link_node)
|
||||||
# create outertext node
|
# create outertext node
|
||||||
overtext_node = _create_node('span', outertext)
|
overtext_node = _create_node('span', outertext)
|
||||||
|
@ -223,6 +290,9 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300):
|
||||||
node.append(read_more_node)
|
node.append(read_more_node)
|
||||||
node.append(overtext_node)
|
node.append(overtext_node)
|
||||||
|
|
||||||
|
if expand_options is None:
|
||||||
|
expand_options = {}
|
||||||
|
|
||||||
if not html or not isinstance(html, basestring):
|
if not html or not isinstance(html, basestring):
|
||||||
return html
|
return html
|
||||||
html = ustr(html)
|
html = ustr(html)
|
||||||
|
@ -265,6 +335,8 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300):
|
||||||
# signature_begin = False # try dynamic signature recognition
|
# signature_begin = False # try dynamic signature recognition
|
||||||
quote_begin = False
|
quote_begin = False
|
||||||
overlength = False
|
overlength = False
|
||||||
|
overlength_section_id = None
|
||||||
|
overlength_section_count = 0
|
||||||
cur_char_nbr = 0
|
cur_char_nbr = 0
|
||||||
for node in root.iter():
|
for node in root.iter():
|
||||||
# do not take into account multiple spaces that are displayed as max 1 space in html
|
# do not take into account multiple spaces that are displayed as max 1 space in html
|
||||||
|
@ -276,14 +348,22 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300):
|
||||||
if 'SkyDrivePlaceholder' in node.get('class', '') or 'SkyDrivePlaceholder' in node.get('id', ''):
|
if 'SkyDrivePlaceholder' in node.get('class', '') or 'SkyDrivePlaceholder' in node.get('id', ''):
|
||||||
root.set('hotmail', '1')
|
root.set('hotmail', '1')
|
||||||
|
|
||||||
|
# protect sections by tagging section limits and blocks contained inside sections, using an increasing id to re-find them later
|
||||||
|
if node.tag == 'section':
|
||||||
|
overlength_section_count += 1
|
||||||
|
node.set('section_closure', str(overlength_section_count))
|
||||||
|
if node.getparent() is not None and (node.getparent().get('section_closure') or node.getparent().get('section_inner')):
|
||||||
|
node.set('section_inner', str(overlength_section_count))
|
||||||
|
|
||||||
# state of the parsing: flag quotes and tails to remove
|
# state of the parsing: flag quotes and tails to remove
|
||||||
if quote_begin:
|
if quote_begin:
|
||||||
node.set('in_quote', '1')
|
node.set('in_quote', '1')
|
||||||
node.set('tail_remove', '1')
|
node.set('tail_remove', '1')
|
||||||
# state of the parsing: flag when being in over-length content
|
# state of the parsing: flag when being in over-length content, depending on section content if defined (only when having protect_sections)
|
||||||
if overlength:
|
if overlength:
|
||||||
node.set('in_overlength', '1')
|
if not overlength_section_id or int(node.get('section_inner', overlength_section_count + 1)) > overlength_section_count:
|
||||||
node.set('tail_remove', '1')
|
node.set('in_overlength', '1')
|
||||||
|
node.set('tail_remove', '1')
|
||||||
|
|
||||||
# find quote in msoffice / hotmail / blockquote / text quote and signatures
|
# find quote in msoffice / hotmail / blockquote / text quote and signatures
|
||||||
if root.get('msoffice') and node.tag == 'div' and 'border-top:solid' in node.get('style', ''):
|
if root.get('msoffice') and node.tag == 'div' and 'border-top:solid' in node.get('style', ''):
|
||||||
|
@ -298,13 +378,25 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300):
|
||||||
node.set('in_quote', '1')
|
node.set('in_quote', '1')
|
||||||
|
|
||||||
# shorten:
|
# shorten:
|
||||||
# 1/ truncate the text at the next available space
|
# if protect section:
|
||||||
# 2/ create a 'read more' node, next to current node
|
# 1/ find the first parent not being inside a section
|
||||||
# 3/ add the truncated text in a new node, next to 'read more' node
|
# 2/ add the read more link
|
||||||
|
# else:
|
||||||
|
# 1/ truncate the text at the next available space
|
||||||
|
# 2/ create a 'read more' node, next to current node
|
||||||
|
# 3/ add the truncated text in a new node, next to 'read more' node
|
||||||
|
node_text = (node.text or '').strip().strip('\n').strip()
|
||||||
if shorten and not overlength and cur_char_nbr + len(node_text) > max_length:
|
if shorten and not overlength and cur_char_nbr + len(node_text) > max_length:
|
||||||
node_to_truncate = node
|
node_to_truncate = node
|
||||||
while node_to_truncate.get('in_quote') and node_to_truncate.getparent() is not None:
|
while node_to_truncate.getparent() is not None:
|
||||||
node_to_truncate = node_to_truncate.getparent()
|
if node_to_truncate.get('in_quote'):
|
||||||
|
node_to_truncate = node_to_truncate.getparent()
|
||||||
|
elif protect_sections and (node_to_truncate.getparent().get('section_inner') or node_to_truncate.getparent().get('section_closure')):
|
||||||
|
node_to_truncate = node_to_truncate.getparent()
|
||||||
|
overlength_section_id = node_to_truncate.get('section_closure')
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
overlength = True
|
overlength = True
|
||||||
node_to_truncate.set('truncate', '1')
|
node_to_truncate.set('truncate', '1')
|
||||||
if node_to_truncate == node:
|
if node_to_truncate == node:
|
||||||
|
@ -340,7 +432,7 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300):
|
||||||
if remove:
|
if remove:
|
||||||
node.getparent().remove(node)
|
node.getparent().remove(node)
|
||||||
else:
|
else:
|
||||||
if not 'oe_mail_expand' in node.get('class', ''): # trick: read more link should be displayed even if it's in overlength
|
if not expand_options.get('oe_expand_a_class', 'oe_mail_expand') in node.get('class', ''): # trick: read more link should be displayed even if it's in overlength
|
||||||
node_class = node.get('class', '') + ' oe_mail_cleaned'
|
node_class = node.get('class', '') + ' oe_mail_cleaned'
|
||||||
node.set('class', node_class)
|
node.set('class', node_class)
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import zipfile
|
import zipfile
|
||||||
from collections import defaultdict
|
from collections import defaultdict, Mapping
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from itertools import islice, izip, groupby
|
from itertools import islice, izip, groupby
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
@ -822,6 +822,76 @@ DATETIME_FORMATS_MAP = {
|
||||||
'%Z': '',
|
'%Z': '',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
POSIX_TO_LDML = {
|
||||||
|
'a': 'E',
|
||||||
|
'A': 'EEEE',
|
||||||
|
'b': 'MMM',
|
||||||
|
'B': 'MMMM',
|
||||||
|
#'c': '',
|
||||||
|
'd': 'dd',
|
||||||
|
'H': 'HH',
|
||||||
|
'I': 'hh',
|
||||||
|
'j': 'DDD',
|
||||||
|
'm': 'MM',
|
||||||
|
'M': 'mm',
|
||||||
|
'p': 'a',
|
||||||
|
'S': 'ss',
|
||||||
|
'U': 'w',
|
||||||
|
'w': 'e',
|
||||||
|
'W': 'w',
|
||||||
|
'y': 'yy',
|
||||||
|
'Y': 'yyyy',
|
||||||
|
# see comments above, and babel's format_datetime assumes an UTC timezone
|
||||||
|
# for naive datetime objects
|
||||||
|
#'z': 'Z',
|
||||||
|
#'Z': 'z',
|
||||||
|
}
|
||||||
|
|
||||||
|
def posix_to_ldml(fmt, locale):
|
||||||
|
""" Converts a posix/strftime pattern into an LDML date format pattern.
|
||||||
|
|
||||||
|
:param fmt: non-extended C89/C90 strftime pattern
|
||||||
|
:param locale: babel locale used for locale-specific conversions (e.g. %x and %X)
|
||||||
|
:return: unicode
|
||||||
|
"""
|
||||||
|
buf = []
|
||||||
|
pc = False
|
||||||
|
quoted = []
|
||||||
|
|
||||||
|
for c in fmt:
|
||||||
|
# LDML date format patterns uses letters, so letters must be quoted
|
||||||
|
if not pc and c.isalpha():
|
||||||
|
quoted.append(c if c != "'" else "''")
|
||||||
|
continue
|
||||||
|
if quoted:
|
||||||
|
buf.append("'")
|
||||||
|
buf.append(''.join(quoted))
|
||||||
|
buf.append("'")
|
||||||
|
quoted = []
|
||||||
|
|
||||||
|
if pc:
|
||||||
|
if c == '%': # escaped percent
|
||||||
|
buf.append('%')
|
||||||
|
elif c == 'x': # date format, short seems to match
|
||||||
|
buf.append(locale.date_formats['short'].pattern)
|
||||||
|
elif c == 'X': # time format, seems to include seconds. short does not
|
||||||
|
buf.append(locale.time_formats['medium'].pattern)
|
||||||
|
else: # look up format char in static mapping
|
||||||
|
buf.append(POSIX_TO_LDML[c])
|
||||||
|
pc = False
|
||||||
|
elif c == '%':
|
||||||
|
pc = True
|
||||||
|
else:
|
||||||
|
buf.append(c)
|
||||||
|
|
||||||
|
# flush anything remaining in quoted buffer
|
||||||
|
if quoted:
|
||||||
|
buf.append("'")
|
||||||
|
buf.append(''.join(quoted))
|
||||||
|
buf.append("'")
|
||||||
|
|
||||||
|
return ''.join(buf)
|
||||||
|
|
||||||
def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
|
def server_to_local_timestamp(src_tstamp_str, src_format, dst_format, dst_tz_name,
|
||||||
tz_offset=True, ignore_unparsable_time=True):
|
tz_offset=True, ignore_unparsable_time=True):
|
||||||
"""
|
"""
|
||||||
|
@ -1005,6 +1075,8 @@ class mute_logger(object):
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
for logger in self.loggers:
|
for logger in self.loggers:
|
||||||
|
assert isinstance(logger, basestring),\
|
||||||
|
"A logger name must be a string, got %s" % type(logger)
|
||||||
logging.getLogger(logger).addFilter(self)
|
logging.getLogger(logger).addFilter(self)
|
||||||
|
|
||||||
def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
|
def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
|
||||||
|
@ -1070,6 +1142,34 @@ def stripped_sys_argv(*strip_args):
|
||||||
return [x for i, x in enumerate(args) if not strip(args, i)]
|
return [x for i, x in enumerate(args) if not strip(args, i)]
|
||||||
|
|
||||||
|
|
||||||
|
class ConstantMapping(Mapping):
|
||||||
|
"""
|
||||||
|
An immutable mapping returning the provided value for every single key.
|
||||||
|
|
||||||
|
Useful for default value to methods
|
||||||
|
"""
|
||||||
|
__slots__ = ['_value']
|
||||||
|
def __init__(self, val):
|
||||||
|
self._value = val
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
"""
|
||||||
|
defaultdict updates its length for each individually requested key, is
|
||||||
|
that really useful?
|
||||||
|
"""
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""
|
||||||
|
same as len, defaultdict udpates its iterable keyset with each key
|
||||||
|
requested, is there a point for this?
|
||||||
|
"""
|
||||||
|
return iter([])
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
|
||||||
def dumpstacks(sig, frame):
|
def dumpstacks(sig, frame):
|
||||||
""" Signal handler: dump a stack trace for each existing thread."""
|
""" Signal handler: dump a stack trace for each existing thread."""
|
||||||
code = []
|
code = []
|
||||||
|
@ -1107,4 +1207,5 @@ def dumpstacks(sig, frame):
|
||||||
_logger.info("\n".join(code))
|
_logger.info("\n".join(code))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
|
@ -174,7 +174,7 @@ def _import(name, globals=None, locals=None, fromlist=None, level=-1):
|
||||||
return __import__(name, globals, locals, level)
|
return __import__(name, globals, locals, level)
|
||||||
raise ImportError(name)
|
raise ImportError(name)
|
||||||
|
|
||||||
def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False):
|
def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=False, locals_builtins=False):
|
||||||
"""safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
|
"""safe_eval(expression[, globals[, locals[, mode[, nocopy]]]]) -> result
|
||||||
|
|
||||||
System-restricted Python expression evaluation
|
System-restricted Python expression evaluation
|
||||||
|
@ -218,29 +218,37 @@ def safe_eval(expr, globals_dict=None, locals_dict=None, mode="eval", nocopy=Fal
|
||||||
locals_dict = dict(locals_dict)
|
locals_dict = dict(locals_dict)
|
||||||
|
|
||||||
globals_dict.update(
|
globals_dict.update(
|
||||||
__builtins__ = {
|
__builtins__={
|
||||||
'__import__': _import,
|
'__import__': _import,
|
||||||
'True': True,
|
'True': True,
|
||||||
'False': False,
|
'False': False,
|
||||||
'None': None,
|
'None': None,
|
||||||
'str': str,
|
'str': str,
|
||||||
'globals': locals,
|
'globals': locals,
|
||||||
'locals': locals,
|
'locals': locals,
|
||||||
'bool': bool,
|
'bool': bool,
|
||||||
'dict': dict,
|
'dict': dict,
|
||||||
'list': list,
|
'list': list,
|
||||||
'tuple': tuple,
|
'tuple': tuple,
|
||||||
'map': map,
|
'map': map,
|
||||||
'abs': abs,
|
'abs': abs,
|
||||||
'min': min,
|
'min': min,
|
||||||
'max': max,
|
'max': max,
|
||||||
'reduce': reduce,
|
'reduce': reduce,
|
||||||
'filter': filter,
|
'filter': filter,
|
||||||
'round': round,
|
'round': round,
|
||||||
'len': len,
|
'len': len,
|
||||||
'set' : set
|
'set': set,
|
||||||
}
|
'repr': repr,
|
||||||
|
'int': int,
|
||||||
|
'float': float,
|
||||||
|
'range': range,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
if locals_builtins:
|
||||||
|
if locals_dict is None:
|
||||||
|
locals_dict = {}
|
||||||
|
locals_dict.update(globals_dict.get('__builtins__'))
|
||||||
c = test_expr(expr, _SAFE_OPCODES, mode=mode)
|
c = test_expr(expr, _SAFE_OPCODES, mode=mode)
|
||||||
try:
|
try:
|
||||||
return eval(c, globals_dict, locals_dict)
|
return eval(c, globals_dict, locals_dict)
|
||||||
|
|
|
@ -41,8 +41,7 @@ def valid_att_in_label(arch):
|
||||||
|
|
||||||
|
|
||||||
def valid_att_in_form(arch):
|
def valid_att_in_form(arch):
|
||||||
"""A `string` attribute must be on a `form` node."""
|
return True
|
||||||
return not arch.xpath('//form[not (@string)]')
|
|
||||||
|
|
||||||
|
|
||||||
def valid_type_in_colspan(arch):
|
def valid_type_in_colspan(arch):
|
||||||
|
@ -67,8 +66,8 @@ def valid_type_in_col(arch):
|
||||||
|
|
||||||
def valid_view(arch):
|
def valid_view(arch):
|
||||||
if arch.tag == 'form':
|
if arch.tag == 'form':
|
||||||
for pred in [valid_page_in_book, valid_att_in_form, valid_type_in_colspan,\
|
for pred in [valid_page_in_book, valid_att_in_form, valid_type_in_colspan,
|
||||||
valid_type_in_col, valid_att_in_field, valid_att_in_label]:
|
valid_type_in_col, valid_att_in_field, valid_att_in_label]:
|
||||||
if not pred(arch):
|
if not pred(arch):
|
||||||
_logger.error('Invalid XML: %s', pred.__doc__)
|
_logger.error('Invalid XML: %s', pred.__doc__)
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -328,7 +328,7 @@ class YamlInterpreter(object):
|
||||||
if config.get('import_partial'):
|
if config.get('import_partial'):
|
||||||
self.cr.commit()
|
self.cr.commit()
|
||||||
|
|
||||||
def _create_record(self, model, fields, view_info=False, parent={}, default=True):
|
def _create_record(self, model, fields, view_info=None, parent={}, default=True):
|
||||||
"""This function processes the !record tag in yalm files. It simulates the record creation through an xml
|
"""This function processes the !record tag in yalm files. It simulates the record creation through an xml
|
||||||
view (either specified on the !record tag or the default one for this object), including the calls to
|
view (either specified on the !record tag or the default one for this object), including the calls to
|
||||||
on_change() functions, and sending only values for fields that aren't set as readonly.
|
on_change() functions, and sending only values for fields that aren't set as readonly.
|
||||||
|
@ -447,7 +447,13 @@ class YamlInterpreter(object):
|
||||||
args = map(lambda x: eval(x, ctx), match.group(2).split(','))
|
args = map(lambda x: eval(x, ctx), match.group(2).split(','))
|
||||||
result = getattr(model, match.group(1))(self.cr, SUPERUSER_ID, [], *args)
|
result = getattr(model, match.group(1))(self.cr, SUPERUSER_ID, [], *args)
|
||||||
for key, val in (result or {}).get('value', {}).items():
|
for key, val in (result or {}).get('value', {}).items():
|
||||||
assert key in fg, "The returning field '%s' from your on_change call '%s' does not exist either on the object '%s', either in the view '%s' used for the creation" % (key, match.group(1), model._name, view_info['name'])
|
assert key in fg, (
|
||||||
|
"The field %r returned from the onchange call %r "
|
||||||
|
"does not exist in the source view %r (of object "
|
||||||
|
"%r). This field will be ignored (and thus not "
|
||||||
|
"populated) when clients saves the new record" % (
|
||||||
|
key, match.group(1), view_info.get('name', '?'), model._name
|
||||||
|
))
|
||||||
if key not in fields:
|
if key not in fields:
|
||||||
# do not shadow values explicitly set in yaml.
|
# do not shadow values explicitly set in yaml.
|
||||||
record_dict[key] = process_val(key, val)
|
record_dict[key] = process_val(key, val)
|
||||||
|
|
|
@ -106,7 +106,7 @@ class WorkflowInstance(object):
|
||||||
for cur_instance_id, cur_model_name, cur_record_id in cr.fetchall():
|
for cur_instance_id, cur_model_name, cur_record_id in cr.fetchall():
|
||||||
cur_record = Record(cur_model_name, cur_record_id)
|
cur_record = Record(cur_model_name, cur_record_id)
|
||||||
for act_name in act_names:
|
for act_name in act_names:
|
||||||
WorkflowInstance(self.session, cur_record, {'id':cur_instance_id}).validate('subflow.{}'.format(act_name[0]))
|
WorkflowInstance(self.session, cur_record, {'id':cur_instance_id}).validate('subflow.%s' % act_name[0])
|
||||||
|
|
||||||
return ok
|
return ok
|
||||||
|
|
||||||
|
|
|
@ -57,11 +57,11 @@ def required_or_default(name, h):
|
||||||
a mandatory argument.
|
a mandatory argument.
|
||||||
"""
|
"""
|
||||||
if os.environ.get('OPENERP_' + name.upper()):
|
if os.environ.get('OPENERP_' + name.upper()):
|
||||||
d = {'default': os.environ['OPENERP_' + name.upper()]}
|
d = {'default': os.environ['OPENERP_' + name.upper()]}
|
||||||
else:
|
else:
|
||||||
d = {'required': True}
|
d = {'required': True}
|
||||||
d['help'] = h + '. The environment variable OPENERP_' + \
|
d['help'] = h + '. The environment variable OPENERP_' + \
|
||||||
name.upper() + ' can be used instead.'
|
name.upper() + ' can be used instead.'
|
||||||
return d
|
return d
|
||||||
|
|
||||||
class Command(object):
|
class Command(object):
|
||||||
|
@ -77,7 +77,7 @@ class Command(object):
|
||||||
self.parser = parser = subparsers.add_parser(self.command_name,
|
self.parser = parser = subparsers.add_parser(self.command_name,
|
||||||
description=self.__class__.__doc__)
|
description=self.__class__.__doc__)
|
||||||
else:
|
else:
|
||||||
self.parser = parser = argparse.ArgumentParser(
|
self.parser = parser = argparse.ArgumentParser(
|
||||||
description=self.__class__.__doc__)
|
description=self.__class__.__doc__)
|
||||||
|
|
||||||
parser.add_argument('-d', '--database', metavar='DATABASE',
|
parser.add_argument('-d', '--database', metavar='DATABASE',
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
Execute the unittest2 tests available in OpenERP addons.
|
Execute the unittest2 tests available in OpenERP addons.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
|
import argparse
|
||||||
|
|
||||||
import common
|
import common
|
||||||
|
|
||||||
|
@ -56,31 +56,33 @@ def get_test_modules(module, submodule, explode):
|
||||||
print ' ', x
|
print ' ', x
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
fast_suite = getattr(m, 'fast_suite', [])
|
||||||
|
checks = getattr(m, 'checks', [])
|
||||||
if submodule is None:
|
if submodule is None:
|
||||||
# Use auto-discovered sub-modules.
|
# Use auto-discovered sub-modules.
|
||||||
ms = submodules
|
ms = submodules
|
||||||
elif submodule == '__fast_suite__':
|
elif submodule == '__fast_suite__':
|
||||||
# Obtain the explicit test sub-modules list.
|
|
||||||
ms = getattr(sys.modules[module], 'fast_suite', None)
|
|
||||||
# `suite` was used before the 6.1 release instead of `fast_suite`.
|
# `suite` was used before the 6.1 release instead of `fast_suite`.
|
||||||
ms = ms if ms else getattr(sys.modules[module], 'suite', None)
|
ms = fast_suite if hasattr(m, 'fast_suite') else getattr(m, 'suite', None)
|
||||||
if ms is None:
|
if not ms:
|
||||||
if explode:
|
if explode:
|
||||||
print 'The module `%s` has no defined test suite.' % (module,)
|
print 'The module `%s` has no defined test suite.' % (module,)
|
||||||
show_submodules_and_exit()
|
show_submodules_and_exit()
|
||||||
else:
|
else:
|
||||||
ms = []
|
ms = []
|
||||||
elif submodule == '__sanity_checks__':
|
elif submodule == '__sanity_checks__':
|
||||||
ms = getattr(sys.modules[module], 'checks', None)
|
ms = checks
|
||||||
if ms is None:
|
if not ms:
|
||||||
if explode:
|
if explode:
|
||||||
print 'The module `%s` has no defined sanity checks.' % (module,)
|
print 'The module `%s` has no defined sanity checks.' % (module,)
|
||||||
show_submodules_and_exit()
|
show_submodules_and_exit()
|
||||||
else:
|
else:
|
||||||
ms = []
|
ms = []
|
||||||
|
elif submodule == '__slow_suite__':
|
||||||
|
ms = list(set(submodules).difference(fast_suite, checks))
|
||||||
else:
|
else:
|
||||||
# Pick the command-line-specified test sub-module.
|
# Pick the command-line-specified test sub-module.
|
||||||
m = getattr(sys.modules[module], submodule, None)
|
m = getattr(m, submodule, None)
|
||||||
ms = [m]
|
ms = [m]
|
||||||
|
|
||||||
if m is None:
|
if m is None:
|
||||||
|
@ -104,15 +106,15 @@ def run(args):
|
||||||
config['xmlrpc_port'] = int(args.port)
|
config['xmlrpc_port'] = int(args.port)
|
||||||
config['admin_passwd'] = 'admin'
|
config['admin_passwd'] = 'admin'
|
||||||
config['db_password'] = 'a2aevl8w' # TODO from .openerpserverrc
|
config['db_password'] = 'a2aevl8w' # TODO from .openerpserverrc
|
||||||
config['addons_path'] = args.addons.replace(':',',')
|
|
||||||
if args.addons:
|
if args.addons:
|
||||||
args.addons = args.addons.split(':')
|
args.addons = args.addons.replace(':',',').split(',')
|
||||||
else:
|
else:
|
||||||
args.addons = []
|
args.addons = []
|
||||||
if args.sanity_checks and args.fast_suite:
|
|
||||||
print 'Only at most one of `--sanity-checks` and `--fast-suite` ' \
|
# ensure no duplication in addons paths
|
||||||
'can be specified.'
|
args.addons = list(set(args.addons))
|
||||||
sys.exit(1)
|
config['addons_path'] = ','.join(args.addons)
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
openerp.netsvc.init_alternative_logger()
|
openerp.netsvc.init_alternative_logger()
|
||||||
|
@ -121,45 +123,26 @@ def run(args):
|
||||||
# Install the import hook, to import openerp.addons.<module>.
|
# Install the import hook, to import openerp.addons.<module>.
|
||||||
openerp.modules.module.initialize_sys_path()
|
openerp.modules.module.initialize_sys_path()
|
||||||
|
|
||||||
# Extract module, submodule from the command-line args.
|
module = args.module
|
||||||
if args.module is None:
|
submodule = args.submodule
|
||||||
module, submodule = None, None
|
|
||||||
else:
|
|
||||||
splitted = args.module.split('.')
|
|
||||||
if len(splitted) == 1:
|
|
||||||
module, submodule = splitted[0], None
|
|
||||||
elif len(splitted) == 2:
|
|
||||||
module, submodule = splitted
|
|
||||||
else:
|
|
||||||
print 'The `module` argument must have the form ' \
|
|
||||||
'`module[.submodule]`.'
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Import the necessary modules and get the corresponding suite.
|
# Import the necessary modules and get the corresponding suite.
|
||||||
if module is None:
|
if module is None:
|
||||||
# TODO
|
# TODO
|
||||||
modules = common.get_addons_from_paths(args.addons, []) # TODO openerp.addons.base is not included ?
|
modules = common.get_addons_from_paths(args.addons, []) # TODO openerp.addons.base is not included ?
|
||||||
test_modules = []
|
test_modules = []
|
||||||
for module in ['openerp'] + modules:
|
for module in ['openerp'] + modules:
|
||||||
if args.fast_suite:
|
test_modules.extend(
|
||||||
submodule = '__fast_suite__'
|
get_test_modules(module, submodule, explode=False))
|
||||||
if args.sanity_checks:
|
|
||||||
submodule = '__sanity_checks__'
|
|
||||||
test_modules.extend(get_test_modules(module,
|
|
||||||
submodule, explode=False))
|
|
||||||
else:
|
else:
|
||||||
if submodule and args.fast_suite:
|
test_modules = get_test_modules(module, submodule, explode=True)
|
||||||
print "Submodule name `%s` given, ignoring `--fast-suite`." % (submodule,)
|
|
||||||
if submodule and args.sanity_checks:
|
print 'Test modules:'
|
||||||
print "Submodule name `%s` given, ignoring `--sanity-checks`." % (submodule,)
|
for test_module in test_modules:
|
||||||
if not submodule and args.fast_suite:
|
print ' ', test_module.__name__
|
||||||
submodule = '__fast_suite__'
|
print
|
||||||
if not submodule and args.sanity_checks:
|
sys.stdout.flush()
|
||||||
submodule = '__sanity_checks__'
|
|
||||||
test_modules = get_test_modules(module,
|
|
||||||
submodule, explode=True)
|
|
||||||
|
|
||||||
# Run the test suite.
|
|
||||||
if not args.dry_run:
|
if not args.dry_run:
|
||||||
suite = unittest2.TestSuite()
|
suite = unittest2.TestSuite()
|
||||||
for test_module in test_modules:
|
for test_module in test_modules:
|
||||||
|
@ -167,10 +150,6 @@ def run(args):
|
||||||
r = unittest2.TextTestRunner(verbosity=2).run(suite)
|
r = unittest2.TextTestRunner(verbosity=2).run(suite)
|
||||||
if r.errors or r.failures:
|
if r.errors or r.failures:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
|
||||||
print 'Test modules:'
|
|
||||||
for test_module in test_modules:
|
|
||||||
print ' ', test_module.__name__
|
|
||||||
|
|
||||||
def add_parser(subparsers):
|
def add_parser(subparsers):
|
||||||
parser = subparsers.add_parser('run-tests',
|
parser = subparsers.add_parser('run-tests',
|
||||||
|
@ -181,21 +160,55 @@ def add_parser(subparsers):
|
||||||
parser.add_argument('-p', '--port', metavar='PORT',
|
parser.add_argument('-p', '--port', metavar='PORT',
|
||||||
help='the port used for WML-RPC tests')
|
help='the port used for WML-RPC tests')
|
||||||
common.add_addons_argument(parser)
|
common.add_addons_argument(parser)
|
||||||
parser.add_argument('-m', '--module', metavar='MODULE',
|
|
||||||
default=None,
|
parser.add_argument(
|
||||||
help='the module to test in `module[.submodule]` notation. '
|
'-m', '--module', metavar='MODULE', action=ModuleAction, default=None,
|
||||||
'Use `openerp` for the core OpenERP tests. '
|
help="the module to test in `module[.submodule]` notation. "
|
||||||
'Leave empty to run every declared tests. '
|
"Use `openerp` for the core OpenERP tests. "
|
||||||
'Give a module but no submodule to run all the module\'s declared '
|
"Leave empty to run every declared tests. "
|
||||||
'tests. If both the module and the submodule are given, '
|
"Give a module but no submodule to run all the module's declared "
|
||||||
'the sub-module can be run even if it is not declared in the module.')
|
"tests. If both the module and the submodule are given, "
|
||||||
parser.add_argument('--fast-suite', action='store_true',
|
"the sub-module can be run even if it is not declared in the module.")
|
||||||
help='run only the tests explicitely declared in the fast suite (this '
|
group = parser.add_mutually_exclusive_group()
|
||||||
|
group.add_argument(
|
||||||
|
'--fast-suite',
|
||||||
|
dest='submodule', action=GuardAction, nargs=0, const='__fast_suite__',
|
||||||
|
help='run only the tests explicitly declared in the fast suite (this '
|
||||||
'makes sense only with the bare `module` notation or no module at '
|
'makes sense only with the bare `module` notation or no module at '
|
||||||
'all).')
|
'all).')
|
||||||
parser.add_argument('--sanity-checks', action='store_true',
|
group.add_argument(
|
||||||
|
'--sanity-checks',
|
||||||
|
dest='submodule', action=GuardAction, nargs=0, const='__sanity_checks__',
|
||||||
help='run only the sanity check tests')
|
help='run only the sanity check tests')
|
||||||
|
group.add_argument(
|
||||||
|
'--slow-suite',
|
||||||
|
dest='submodule', action=GuardAction, nargs=0, const='__slow_suite__',
|
||||||
|
help="Only run slow tests (tests which are neither in the fast nor in"
|
||||||
|
" the sanity suite)")
|
||||||
parser.add_argument('--dry-run', action='store_true',
|
parser.add_argument('--dry-run', action='store_true',
|
||||||
help='do not run the tests')
|
help='do not run the tests')
|
||||||
|
|
||||||
parser.set_defaults(run=run)
|
parser.set_defaults(run=run, submodule=None)
|
||||||
|
|
||||||
|
class ModuleAction(argparse.Action):
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
split = values.split('.')
|
||||||
|
if len(split) == 1:
|
||||||
|
module, submodule = values, None
|
||||||
|
elif len(split) == 2:
|
||||||
|
module, submodule = split
|
||||||
|
else:
|
||||||
|
raise argparse.ArgumentError(
|
||||||
|
option_string,
|
||||||
|
"must have the form 'module[.submodule]', got '%s'" % values)
|
||||||
|
|
||||||
|
setattr(namespace, self.dest, module)
|
||||||
|
if submodule is not None:
|
||||||
|
setattr(namespace, 'submodule', submodule)
|
||||||
|
|
||||||
|
class GuardAction(argparse.Action):
|
||||||
|
def __call__(self, parser, namespace, values, option_string=None):
|
||||||
|
if getattr(namespace, self.dest, None):
|
||||||
|
print "%s value provided, ignoring '%s'" % (self.dest, option_string)
|
||||||
|
return
|
||||||
|
setattr(namespace, self.dest, self.const)
|
||||||
|
|
Loading…
Reference in New Issue