[MERGE] merged trunk.

bzr revid: vmt@openerp.com-20120104131010-kbw2ej2rrdvyif96
This commit is contained in:
Vo Minh Thu 2012-01-04 14:10:10 +01:00
commit 389257c009
123 changed files with 437845 additions and 87061 deletions

64
README
View File

@ -1,55 +1,29 @@
About OpenERP
pydot - Python interface to Graphviz's Dot language
Ero Carrera (c) 2004-2007
ero@dkbza.org
This code is distributed under the MIT license.
Requirements:
-------------
OpenERP is an OpenSrouce/Free software Enterprise Resource Planning and
Customer Relationship Management software. More info at:
pyparsing: pydot requires the pyparsing module in order to be
able to load DOT files.
http://www.openerp.com
GraphViz: is needed in order to render the graphs into any of
the plethora of output formats supported.
Installation on Debian Ubuntu
-----------------------------
Installation:
-------------
Add the the apt repository in your source.list and type:
Should suffice with doing:
$ apt-get install openerp
python setup.py install
Installation on RedHat, Fedora, CentOS
--------------------------------------
Needless to say, no installation is needed just to use the module. A mere:
Install the required dependencies:
import pydot
$ yum install python
$ easy_install pip
$ pip install .....
Install the openerp rpm
$ rpm -i openerp-VERSION.rpm
Installation on Windows
-----------------------
Installation on MacOSX
-----------------------
Setuping you first database
---------------------------
Point your browser to http://localhost:8069/ and click "Database", the default
master password is "admin".
Detailed System Requirements
----------------------------
You need the following software installed:
python, postgresql-client, python-dateutil, python-gdata, python-ldap,
python-libxslt1, python-lxml, python-mako, python-openid, python-psycopg2,
python-pybabel, python-pychart, python-pydot, python-pyparsing,
python-reportlab, python-simplejson, python-tz, python-vobject, python-webdav,
python-werkzeug, python-yaml, python-zsi, graphviz, ghostscript, postgresql,
python-imaging, python-matplotlib
For Luxembourg localization, you also need:
* pdftk (http://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/)
should do it, provided that the directory containing the modules is on Python
module search path.

BIN
install/openerp-intro.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

BIN
install/openerp-slogan.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

View File

@ -34,6 +34,7 @@
'base_data.xml',
'security/base_security.xml',
'base_menu.xml',
'base_module_meta.xml',
'res/res_security.xml',
'res/res_config.xml',
'data/res.country.state.csv'
@ -99,5 +100,6 @@
'installable': True,
'active': True,
'certificate': '0076807797149',
"css": [ 'static/src/css/modules.css' ],
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -49,6 +49,8 @@ CREATE TABLE ir_model_fields (
primary key(id)
);
ALTER TABLE ir_model_fields ADD column serialization_field_id int references ir_model_fields on delete cascade;
-------------------------------------------------------------------------
-- Actions
@ -287,6 +289,7 @@ CREATE TABLE ir_module_module (
name character varying(128) NOT NULL,
author character varying(128),
url character varying(128),
icon character varying(64),
state character varying(16),
latest_version character varying(64),
shortdesc character varying(256),
@ -294,6 +297,7 @@ CREATE TABLE ir_module_module (
category_id integer REFERENCES ir_module_category ON DELETE SET NULL,
certificate character varying(64),
description text,
application boolean default False,
demo boolean default False,
web boolean DEFAULT FALSE,
license character varying(32),
@ -320,6 +324,12 @@ CREATE TABLE res_company (
primary key(id)
);
CREATE TABLE res_lang (
id serial PRIMARY KEY,
name VARCHAR(64) NOT NULL UNIQUE,
code VARCHAR(16) NOT NULL UNIQUE
);
CREATE TABLE ir_model_data (
id serial NOT NULL,
create_uid integer,

View File

@ -85,6 +85,7 @@
<record id="au" model="res.country">
<field name="name">Australia</field>
<field name="code">au</field>
<field name="address_format" eval="'%(street)s\n%(street2)s %(state_code)s %(zip)s\n%(country_name)s'" />
</record>
<record id="aw" model="res.country">
<field name="name">Aruba</field>
@ -109,6 +110,7 @@
<record id="be" model="res.country">
<field name="name">Belgium</field>
<field name="code">be</field>
<field name="address_format" eval="'%(street)s\n%(zip)s, %(city)s\n%(country_name)s'" />
</record>
<record id="bf" model="res.country">
<field name="name">Burkina Faso</field>
@ -145,6 +147,7 @@
<record id="br" model="res.country">
<field name="name">Brazil</field>
<field name="code">br</field>
<field name="address_format" eval="'%(street)s\n%(street2)s\n%(city)s,%(state_code)s %(zip)s%(country_name)s'" />
</record>
<record id="bs" model="res.country">
<field name="name">Bahamas</field>
@ -173,6 +176,7 @@
<record id="ca" model="res.country">
<field name="name">Canada</field>
<field name="code">ca</field>
<field name="address_format" eval="'%(street)s\n%(street2)s\n%(city)s, %(state_code)s %(zip)s\n%(country_name)s'" />
</record>
<record id="cc" model="res.country">
<field name="name">Cocos (Keeling) Islands</field>
@ -254,6 +258,7 @@
<record id="de" model="res.country">
<field name="name">Germany</field>
<field name="code">de</field>
<field name="address_format" eval="'%(street)s\n%(street2)s\n%(zip)s %(city)s\n%(country_name)s'" />
</record>
<record id="dj" model="res.country">
<field name="name">Djibouti</field>
@ -298,6 +303,7 @@
<record id="es" model="res.country">
<field name="name">Spain</field>
<field name="code">es</field>
<field name="address_format" eval="'%(street)s\n%(zip)s %(city)s,%(state_name)s\n%(country_name)s'" />
</record>
<record id="et" model="res.country">
<field name="name">Ethiopia</field>
@ -326,6 +332,7 @@
<record id="fr" model="res.country">
<field name="name">France</field>
<field name="code">fr</field>
<field name="address_format" eval="'%(street)s\n%(zip)s %(city)s\n%(country_name)s'" />
</record>
<record id="ga" model="res.country">
<field name="name">Gabon</field>
@ -434,6 +441,7 @@
<record id="in" model="res.country">
<field name="name">India</field>
<field name="code">in</field>
<field name="address_format" eval="'%(street)s\n%(street2)s\n%(city)s, %(zip)s\n%(state_name)s%(country_name)s'" />
</record>
<record id="io" model="res.country">
<field name="name">British Indian Ocean Territory</field>
@ -917,6 +925,7 @@
</record>
<record id="uk" model="res.country">
<field name="name">United Kingdom</field>
<field name="address_format" eval="'%(street)s\n%(street2)s\n%(city)s\n%(country_name)s\n%(zip)s'" />
<field name="code">gb</field>
</record>
<record id="um" model="res.country">
@ -926,6 +935,7 @@
<record id="us" model="res.country">
<field name="name">United States</field>
<field name="code">us</field>
<field name="address_format" eval="'%(street)s\n%(street2)s\n%(city)s, %(state_code)s %(zip)s\n%(country_name)s'" />
</record>
<record id="uy" model="res.country">
<field name="name">Uruguay</field>
@ -1033,7 +1043,6 @@
<record id="main_company" model="res.company">
<field name="name">Your Company</field>
<field name="partner_id" ref="main_partner"/>
<field name="rml_header1">Company business slogan</field>
<field name="rml_footer1">Web: www.companyname.com - Tel: +1-212-555-12345</field>
<field name="rml_footer2">IBAN: XX12 3456 7890 1234 5678 - SWIFT: SWIFTCODE - VAT: Company vat number</field>
<field name="currency_id" ref="base.EUR"/>

View File

@ -22,8 +22,7 @@
<menuitem id="menu_users" name="Users" parent="base.menu_administration" sequence="4"/>
<menuitem id="menu_security" name="Security" parent="base.menu_administration" sequence="5"
groups="base.group_extended"/>
<menuitem id="menu_management" name="Modules" parent="base.menu_administration" sequence="10"
groups="base.group_extended"/>
<menuitem id="menu_management" name="Modules" parent="base.menu_administration" sequence="0"/>
<menuitem id="reporting_menu"
parent="base.menu_custom" name="Reporting" sequence="30"
/>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="base.module_account_accountant" model="ir.module.module">
<field name="sequence">10</field>
</record>
<record id="base.module_account_asset" model="ir.module.module">
<field name="sequence">32</field>
</record>
<record id="base.module_account_voucher" model="ir.module.module">
<field name="sequence">4</field>
</record>
<record id="base.module_crm" model="ir.module.module">
<field name="sequence">2</field>
</record>
<record id="base.module_hr" model="ir.module.module">
<field name="sequence">12</field>
</record>
<record id="base.module_hr_expense" model="ir.module.module">
<field name="sequence">30</field>
</record>
<record id="base.module_hr_holidays" model="ir.module.module">
<field name="sequence">28</field>
</record>
<record id="base.module_hr_payroll" model="ir.module.module">
<field name="sequence">38</field>
</record>
<record id="base.module_hr_recruitment" model="ir.module.module">
<field name="sequence">24</field>
</record>
<record id="base.module_hr_timesheet_sheet" model="ir.module.module">
<field name="sequence">16</field>
</record>
<record id="base.module_mrp" model="ir.module.module">
<field name="sequence">18</field>
</record>
<record id="base.module_point_of_sale" model="ir.module.module">
<field name="sequence">6</field>
</record>
<record id="base.module_project" model="ir.module.module">
<field name="sequence">8</field>
</record>
<record id="base.module_project_gtd" model="ir.module.module">
<field name="sequence">20</field>
</record>
<record id="base.module_project_issue" model="ir.module.module">
<field name="sequence">22</field>
</record>
<record id="base.module_purchase" model="ir.module.module">
<field name="sequence">19</field>
</record>
<record id="base.module_sale" model="ir.module.module">
<field name="sequence">14</field>
</record>
<record id="base.module_stock" model="ir.module.module">
<field name="sequence">16</field>
</record>
</data>
</openerp>

View File

@ -19,6 +19,7 @@
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Groups">
<field name="category_id" select="1"/>
<field name="name" select="1"/>
<notebook colspan="4">
<page string="Users">
@ -154,6 +155,17 @@
</form>
</field>
</record>
<record id="user_groups_view" model="ir.ui.view">
<field name="name">res.users.groups</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="view_users_form"/>
<field name="arch" type="xml">
<!-- dummy, will be modified by groups -->
<field name="groups_id" position="after"/>
</field>
</record>
<record id="view_users_tree" model="ir.ui.view">
<field name="name">res.users.tree</field>
<field name="model">res.users</field>
@ -245,6 +257,9 @@
</group>
</page>
<page string="Header/Footer" groups="base.group_extended">
<group colspan="2" col="4">
<field name="paper_format" on_change="onchange_paper_format(paper_format)"/>
</group>
<field colspan="4" name="rml_header" nolabel="1"/>
</page>
<page string="Internal Header/Footer" groups="base.group_extended">

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1018,6 +1018,8 @@
<field name="selection" attrs="{'required': [('ttype','in',['selection','reference'])], 'readonly': [('ttype','not in',['selection','reference'])]}"/>
<field name="size" attrs="{'required': [('ttype','in',['char','reference'])], 'readonly': [('ttype','not in',['char','reference'])]}"/>
<field name="domain" attrs="{'readonly': [('relation','=','')]}"/>
<field name="model_id" invisible="1"/>
<field name="serialization_field_id" attrs="{'readonly': [('state','=','base')]}" domain = "[('ttype','=','serialized'), ('model_id', '=', model_id)]"/>
</group>
<group colspan="2" col="2">
<field name="required"/>
@ -1130,6 +1132,8 @@
<field name="selection" attrs="{'required': [('ttype','in',['selection','reference'])], 'readonly': [('ttype','not in',['selection','reference'])]}"/>
<field name="size" attrs="{'required': [('ttype','in',['char','reference'])], 'readonly': [('ttype','not in',['char','reference'])]}"/>
<field name="domain" attrs="{'readonly': [('relation','=','')]}"/>
<field name="model_id" invisible="1"/>
<field name="serialization_field_id" attrs="{'readonly': [('state','=','base')]}" domain = "[('ttype','=','serialized'), ('model_id', '=', model_id)]"/>
</group>
<group colspan="2" col="2">
@ -1924,7 +1928,8 @@
<field name="view_type">form</field>
<field name="help">The configuration wizards are used to help you configure a new instance of OpenERP. They are launched during the installation of new modules, but you can choose to restart some wizards manually from this menu.</field>
</record>
<menuitem id="next_id_11" name="Configuration Wizards" parent="base.menu_config" sequence="2"/>
<menuitem id="next_id_11" name="Configuration Wizards" parent="base.menu_config" sequence="2"
groups="base.group_extended"/>
<menuitem action="act_ir_actions_todo_form" id="menu_ir_actions_todo_form"
parent="next_id_11" sequence="20"/>

View File

@ -321,6 +321,13 @@ class act_window(osv.osv):
act_window()
VIEW_TYPES = [
('tree', 'Tree'),
('form', 'Form'),
('graph', 'Graph'),
('calendar', 'Calendar'),
('gantt', 'Gantt'),
('kanban', 'Kanban')]
class act_window_view(osv.osv):
_name = 'ir.actions.act_window.view'
_table = 'ir_act_window_view'
@ -329,12 +336,7 @@ class act_window_view(osv.osv):
_columns = {
'sequence': fields.integer('Sequence'),
'view_id': fields.many2one('ir.ui.view', 'View'),
'view_mode': fields.selection((
('tree', 'Tree'),
('form', 'Form'),
('graph', 'Graph'),
('calendar', 'Calendar'),
('gantt', 'Gantt')), string='View Type', required=True),
'view_mode': fields.selection(VIEW_TYPES, string='View Type', required=True),
'act_window_id': fields.many2one('ir.actions.act_window', 'Action', ondelete='cascade'),
'multi': fields.boolean('On Multiple Doc.',
help="If set to true, the action will not be displayed on the right toolbar of a form view."),

View File

@ -19,6 +19,7 @@
#
##############################################################################
import calendar
import time
import logging
import threading
@ -174,7 +175,7 @@ class ir_cron(osv.osv):
if numbercall:
# Reschedule our own main cron thread if necessary.
# This is really needed if this job runs longer than its rescheduling period.
nextcall = time.mktime(nextcall.timetuple())
nextcall = calendar.timegm(nextcall.timetuple())
openerp.cron.schedule_wakeup(nextcall, cr.dbname)
finally:
cr.commit()
@ -251,7 +252,7 @@ class ir_cron(osv.osv):
next_call = cr.dictfetchone()['min_next_call']
if next_call:
next_call = time.mktime(time.strptime(next_call, DEFAULT_SERVER_DATETIME_FORMAT))
next_call = calendar.timegm(time.strptime(next_call, DEFAULT_SERVER_DATETIME_FORMAT))
else:
# no matching cron job found in database, re-schedule arbitrarily in 1 day,
# this delay will likely be modified when running jobs complete their tasks

View File

@ -28,6 +28,7 @@ from email import Encoders
import logging
import re
import smtplib
import threading
from osv import osv
from osv import fields
@ -381,6 +382,11 @@ class ir_mail_server(osv.osv):
smtp_to_list = filter(None, tools.flatten(map(extract_rfc2822_addresses,[email_to, email_cc, email_bcc])))
assert smtp_to_list, "At least one valid recipient address should be specified for outgoing emails (To/Cc/Bcc)"
# Do not actually send emails in testing mode!
if getattr(threading.currentThread(), 'testing', False):
_logger.log(logging.TEST, "skip sending email in test mode")
return message['Message-Id']
# Get SMTP Server Details from Mail Server
mail_server = None
if mail_server_id:

View File

@ -21,6 +21,7 @@
import logging
import re
import time
import types
from osv import fields,osv
import netsvc
@ -32,13 +33,12 @@ from tools.translate import _
import pooler
def _get_fields_type(self, cr, uid, context=None):
cr.execute('select distinct ttype,ttype from ir_model_fields')
field_types = cr.fetchall()
field_types_copy = field_types
for types in field_types_copy:
if not hasattr(fields,types[0]):
field_types.remove(types)
return field_types
return sorted([(k,k) for k,v in fields.__dict__.iteritems()
if type(v) == types.TypeType
if issubclass(v, fields._column)
if v != fields._column
if not v._deprecated
if not issubclass(v, fields.function)])
def _in_modules(self, cr, uid, ids, field_name, arg, context=None):
#pseudo-method used by fields.function in ir.model/ir.model.fields
@ -207,6 +207,11 @@ class ir_model_fields(osv.osv):
'view_load': fields.boolean('View Auto-Load'),
'selectable': fields.boolean('Selectable'),
'modules': fields.function(_in_modules, method=True, type='char', size=128, string='In modules', help='List of modules in which the field is defined'),
'serialization_field_id': fields.many2one('ir.model.fields', 'Serialization Field', domain = "[('ttype','=','serialized')]",
ondelete='cascade', help="If set, this field will be stored in the sparse "
"structure of the serialization field, instead "
"of having its own database column. This cannot be "
"changed after creation."),
}
_rec_name='field_description'
_defaults = {
@ -299,6 +304,14 @@ class ir_model_fields(osv.osv):
if context and context.get('manual',False):
vals['state'] = 'manual'
#For the moment renaming a sparse field or changing the storing system is not allowed. This may be done later
if 'serialization_field_id' in vals or 'name' in vals:
for field in self.browse(cr, user, ids, context=context):
if 'serialization_field_id' in vals and field.serialization_field_id.id != vals['serialization_field_id']:
raise except_orm(_('Error!'), _('Changing the storing system for the field "%s" is not allowed.'%field.name))
if field.serialization_field_id and (field.name != vals['name']):
raise except_orm(_('Error!'), _('Renaming the sparse field "%s" is not allowed'%field.name))
column_rename = None # if set, *one* column can be renamed here
obj = None
models_patch = {} # structs of (obj, [(field, prop, change_to),..])

View File

@ -189,7 +189,14 @@ class ir_sequence(openerp.osv.osv.osv):
def _next(self, cr, uid, seq_ids, context=None):
if not seq_ids:
return False
seq = self.read(cr, uid, seq_ids[:1], ['implementation','number_next','prefix','suffix','padding'])[0]
if context is None:
context = {}
force_company = context.get('force_company')
if not force_company:
force_company = self.pool.get('res.users').browse(cr, uid, uid).company_id.id
sequences = self.read(cr, uid, seq_ids, ['company_id','implementation','number_next','prefix','suffix','padding'])
preferred_sequences = [s for s in sequences if s['company_id'] and s['company_id'][0] == force_company ]
seq = preferred_sequences[0] if preferred_sequences else sequences[0]
if seq['implementation'] == 'standard':
cr.execute("SELECT nextval('ir_sequence_%03d')" % seq['id'])
seq['number_next'] = cr.fetchone()
@ -204,14 +211,24 @@ class ir_sequence(openerp.osv.osv.osv):
def next_by_id(self, cr, uid, sequence_id, context=None):
""" Draw an interpolated string using the specified sequence."""
self.check_read(cr, uid)
company_ids = self.pool.get('res.company').search(cr, uid, [], context=context) + [False]
company_ids = self.pool.get('res.company').search(cr, uid, [], order='company_id', context=context) + [False]
ids = self.search(cr, uid, ['&',('id','=', sequence_id),('company_id','in',company_ids)])
return self._next(cr, uid, ids, context)
def next_by_code(self, cr, uid, sequence_code, context=None):
""" Draw an interpolated string using the specified sequence."""
""" Draw an interpolated string using a sequence with the requested code.
If several sequences with the correct code are available to the user
(multi-company cases), the one from the user's current company will
be used.
:param dict context: context dictionary may contain a
``force_company`` key with the ID of the company to
use instead of the user's current company for the
sequence selection. A matching sequence for that
specific company will get higher priority.
"""
self.check_read(cr, uid)
company_ids = self.pool.get('res.company').search(cr, uid, [], context=context) + [False]
company_ids = self.pool.get('res.company').search(cr, uid, [], order='company_id', context=context) + [False]
ids = self.search(cr, uid, ['&',('code','=', sequence_code),('company_id','in',company_ids)])
return self._next(cr, uid, ids, context)

View File

@ -21,6 +21,7 @@
from osv import fields, osv
import tools
import logging
TRANSLATION_TYPE = [
('field', 'Field'),
@ -39,6 +40,115 @@ TRANSLATION_TYPE = [
('sql_constraint', 'SQL Constraint')
]
class ir_translation_import_cursor(object):
"""Temporary cursor for optimizing mass insert into ir.translation
Open it (attached to a sql cursor), feed it with translation data and
finish() it in order to insert multiple translations in a batch.
"""
_table_name = 'tmp_ir_translation_import'
def __init__(self, cr, uid, parent, context):
""" Initializer
Store some values, and also create a temporary SQL table to accept
the data.
@param parent an instance of ir.translation ORM model
"""
self._cr = cr
self._uid = uid
self._context = context
self._overwrite = context.get('overwrite', False)
self._debug = False
self._parent_table = parent._table
# Note that Postgres will NOT inherit the constraints or indexes
# of ir_translation, so this copy will be much faster.
cr.execute('''CREATE TEMP TABLE %s(
imd_model VARCHAR(64),
imd_module VARCHAR(64),
imd_name VARCHAR(128)
) INHERITS (%s) ''' % (self._table_name, self._parent_table))
def push(self, ddict):
"""Feed a translation, as a dictionary, into the cursor
"""
self._cr.execute("INSERT INTO " + self._table_name \
+ """(name, lang, res_id, src, type,
imd_model, imd_module, imd_name, value)
VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s)""",
(ddict['name'], ddict['lang'], ddict.get('res_id'), ddict['src'], ddict['type'],
ddict.get('imd_model'), ddict.get('imd_module'), ddict.get('imd_name'),
ddict['value']))
def finish(self):
""" Transfer the data from the temp table to ir.translation
"""
logger = logging.getLogger('orm')
cr = self._cr
if self._debug:
cr.execute("SELECT count(*) FROM %s" % self._table_name)
c = cr.fetchone()[0]
logger.debug("ir.translation.cursor: We have %d entries to process", c)
# Step 1: resolve ir.model.data references to res_ids
cr.execute("""UPDATE %s AS ti
SET res_id = imd.res_id
FROM ir_model_data AS imd
WHERE ti.res_id IS NULL
AND ti.imd_module IS NOT NULL AND ti.imd_name IS NOT NULL
AND ti.imd_module = imd.module AND ti.imd_name = imd.name
AND ti.imd_model = imd.model; """ % self._table_name)
if self._debug:
cr.execute("SELECT imd_module, imd_model, imd_name FROM %s " \
"WHERE res_id IS NULL AND imd_module IS NOT NULL" % self._table_name)
for row in cr.fetchall():
logger.debug("ir.translation.cursor: missing res_id for %s. %s/%s ", *row)
cr.execute("DELETE FROM %s WHERE res_id IS NULL AND imd_module IS NOT NULL" % \
self._table_name)
# Records w/o res_id must _not_ be inserted into our db, because they are
# referencing non-existent data.
find_expr = "irt.lang = ti.lang AND irt.type = ti.type " \
" AND irt.name = ti.name AND irt.src = ti.src " \
" AND (ti.type != 'model' OR ti.res_id = irt.res_id) "
# Step 2: update existing (matching) translations
if self._overwrite:
cr.execute("""UPDATE ONLY %s AS irt
SET value = ti.value
FROM %s AS ti
WHERE %s AND ti.value IS NOT NULL AND ti.value != ''
""" % (self._parent_table, self._table_name, find_expr))
# Step 3: insert new translations
cr.execute("""INSERT INTO %s(name, lang, res_id, src, type, value)
SELECT name, lang, res_id, src, type, value
FROM %s AS ti
WHERE NOT EXISTS(SELECT 1 FROM ONLY %s AS irt WHERE %s);
""" % (self._parent_table, self._table_name, self._parent_table, find_expr))
if self._debug:
cr.execute('SELECT COUNT(*) FROM ONLY %s' % (self._parent_table))
c1 = cr.fetchone()[0]
cr.execute('SELECT COUNT(*) FROM ONLY %s AS irt, %s AS ti WHERE %s' % \
(self._parent_table, self._table_name, find_expr))
c = cr.fetchone()[0]
logger.debug("ir.translation.cursor: %d entries now in ir.translation, %d common entries with tmp", c1, c)
# Step 4: cleanup
cr.execute("DROP TABLE %s" % self._table_name)
return True
class ir_translation(osv.osv):
_name = "ir.translation"
_log_access = False
@ -56,11 +166,10 @@ class ir_translation(osv.osv):
'type': fields.selection(TRANSLATION_TYPE, string='Type', size=16, select=True),
'src': fields.text('Source'),
'value': fields.text('Translation Value'),
# These two columns map to ir_model_data.module and ir_model_data.name.
# They are used to resolve the res_id above after loading is done.
'module': fields.char('Module', size=64, help='Maps to the ir_model_data for which this translation is provided.'),
'xml_id': fields.char('External ID', size=128, help='Maps to the ir_model_data for which this translation is provided.'),
}
_sql_constraints = [ ('lang_fkey_res_lang', 'FOREIGN KEY(lang) REFERENCES res_lang(code)',
'Language code of translation item must be among known languages' ), ]
def _auto_init(self, cr, context=None):
super(ir_translation, self)._auto_init(cr, context)
@ -87,6 +196,11 @@ class ir_translation(osv.osv):
cr.execute('CREATE INDEX ir_translation_ltn ON ir_translation (name, lang, type)')
cr.commit()
def _check_selection_field_value(self, cr, uid, field, value, context=None):
if field == 'lang':
return
return super(ir_translation, self)._check_selection_field_value(cr, uid, field, value, context=context)
@tools.ormcache_multi(skiparg=3, multi=6)
def _get_ids(self, cr, uid, name, tt, lang, ids):
translations = dict.fromkeys(ids, False)
@ -203,6 +317,11 @@ class ir_translation(osv.osv):
result = super(ir_translation, self).unlink(cursor, user, ids, context=context)
return result
def _get_import_cursor(self, cr, uid, context=None):
""" Return a cursor-like object for fast inserting translations
"""
return ir_translation_import_cursor(cr, uid, self, context=context)
ir_translation()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -208,7 +208,6 @@ class ir_ui_menu(osv.osv):
'name': 'Menuitem',
'model': self._name,
'value': value,
'object': True,
'key': 'action',
'key2': 'tree_but_open',
'res_id': menu_id,

View File

@ -40,7 +40,6 @@ from tools.translate import _
from osv import fields, osv, orm
ACTION_DICT = {
'view_type': 'form',
'view_mode': 'form',
@ -71,12 +70,12 @@ class module_category(osv.osv):
return result
_columns = {
'name': fields.char("Name", size=128, required=True, select=True),
'name': fields.char("Name", size=128, required=True, translate=True, select=True),
'parent_id': fields.many2one('ir.module.category', 'Parent Application', select=True),
'child_ids': fields.one2many('ir.module.category', 'parent_id', 'Child Applications'),
'module_nr': fields.function(_module_nbr, method=True, string='Number of Modules', type='integer'),
'module_ids' : fields.one2many('ir.module.module', 'category_id', 'Modules'),
'description' : fields.text("Description"),
'description' : fields.text("Description", translate=True),
'sequence' : fields.integer('Sequence'),
'visible' : fields.boolean('Visible'),
}
@ -190,6 +189,7 @@ class module(osv.osv):
'published_version': fields.char('Published Version', size=64, readonly=True),
'url': fields.char('URL', size=128, readonly=True),
'sequence': fields.integer('Sequence'),
'dependencies_id': fields.one2many('ir.module.module.dependency',
'module_id', 'Dependencies', readonly=True),
'state': fields.selection([
@ -214,7 +214,8 @@ class module(osv.osv):
'reports_by_module': fields.function(_get_views, method=True, string='Reports', type='text', multi="meta", store=True),
'views_by_module': fields.function(_get_views, method=True, string='Views', type='text', multi="meta", store=True),
'certificate' : fields.char('Quality Certificate', size=64, readonly=True),
'web': fields.boolean('Has a web component', readonly=True),
'application': fields.boolean('Application', readonly=True),
'icon': fields.char('Icon URL', size=128),
'complexity': fields.selection([('easy','Easy'), ('normal','Normal'), ('expert','Expert')],
string='Complexity', readonly=True,
help='Level of difficulty of module. Easy: intuitive and easy to use for everyone. Normal: easy to use for business experts. Expert: requires technical skills.'),
@ -222,12 +223,12 @@ class module(osv.osv):
_defaults = {
'state': 'uninstalled',
'sequence': 100,
'demo': False,
'license': 'AGPL-3',
'web': False,
'complexity': 'normal',
}
_order = 'name'
_order = 'sequence,name'
def _name_uniq_msg(self, cr, uid, ids, context=None):
return _('The name of the module must be unique !')
@ -251,10 +252,10 @@ class module(osv.osv):
_('You try to remove a module that is installed or will be installed'))
mod_names.append(mod['name'])
#Removing the entry from ir_model_data
ids_meta = self.pool.get('ir.model.data').search(cr, uid, [('name', '=', 'module_meta_information'), ('module', 'in', mod_names)])
#ids_meta = self.pool.get('ir.model.data').search(cr, uid, [('name', '=', 'module_meta_information'), ('module', 'in', mod_names)])
if ids_meta:
self.pool.get('ir.model.data').unlink(cr, uid, ids_meta, context)
#if ids_meta:
# self.pool.get('ir.model.data').unlink(cr, uid, ids_meta, context)
return super(module, self).unlink(cr, uid, ids, context=context)
@ -317,9 +318,47 @@ class module(osv.osv):
return demo
def button_install(self, cr, uid, ids, context=None):
model_obj = self.pool.get('ir.model.data')
self.state_update(cr, uid, ids, 'to install', ['uninstalled'], context)
categ = model_obj.get_object(cr, uid, 'base', 'module_category_hidden_links', context=context)
todo = []
for mod in categ.module_ids:
if mod.state=='uninstalled':
ok = True
for dep in mod.dependencies_id:
ok = ok and (dep.state in ('to install','installed'))
if ok:
todo.append(mod.id)
if todo:
self.button_install(cr, uid, todo, context=context)
return dict(ACTION_DICT, name=_('Install'))
def button_immediate_install(self, cr, uid, ids, context=None):
""" Installs the selected module(s) immediately and fully,
returns the next res.config action to execute
:param ids: identifiers of the modules to install
:returns: next res.config item to execute
:rtype: dict[str, object]
"""
self.button_install(cr, uid, ids, context=context)
cr.commit()
db, pool = pooler.restart_pool(cr.dbname, update_module=True)
config = pool.get('res.config').next(cr, uid, [], context=context) or {}
if config.get('type') not in ('ir.actions.reload', 'ir.actions.act_window_close'):
return config
menu_ids = self.root_menus(cr,uid,ids,context)
if menu_ids:
action = {
'type': 'ir.ui.menu',
'menu_id': menu_ids[0],
'reload' : True,
}
return action
return False
def button_install_cancel(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {'state': 'uninstalled', 'demo':False})
@ -397,7 +436,6 @@ class module(osv.osv):
'website': terp.get('website', ''),
'license': terp.get('license', 'AGPL-3'),
'certificate': terp.get('certificate') or False,
'web': terp.get('web') or False,
'complexity': terp.get('complexity', ''),
}
@ -554,7 +592,6 @@ class module(osv.osv):
tools.trans_load(cr, f, lang, verbose=False, context=context2)
elif iso_lang != 'en':
logger.warning('module %s: no translation for language %s', mod.name, iso_lang)
tools.trans_update_res_ids(cr)
def check(self, cr, uid, ids, context=None):
logger = logging.getLogger('init')
@ -570,113 +607,28 @@ class module(osv.osv):
logger.critical('module %s: invalid quality certificate: %s', mod.name, mod.certificate)
raise osv.except_osv(_('Error'), _('Module %s: Invalid Quality Certificate') % (mod.name,))
def list_web(self, cr, uid, context=None):
""" list_web(cr, uid, context) -> [(module_name, module_version)]
Lists all the currently installed modules with a web component.
def root_menus(self, cr, uid, ids, context=None):
""" Return root menu ids the menus created by the modules whose ids are
provided.
Returns a list of a tuple of addon names and addon versions.
:param list[int] ids: modules to get menus from
"""
return [
(module['name'], module['installed_version'])
for module in self.browse(cr, uid,
self.search(cr, uid,
[('web', '=', True),
('state', 'in', ['installed','to upgrade','to remove'])],
context=context),
context=context)]
def _web_dependencies(self, cr, uid, module, context=None):
for dependency in module.dependencies_id:
(parent,) = self.browse(cr, uid, self.search(cr, uid,
[('name', '=', dependency.name)], context=context),
context=context)
if parent.web:
yield parent.name
else:
self._web_dependencies(
cr, uid, parent, context=context)
values = self.read(cr, uid, ids, ['name'], context=context)
module_names = [i['name'] for i in values]
def _translations_subdir(self, module):
""" Returns the path to the subdirectory holding translations for the
module files, or None if it can't find one
ids = self.pool.get('ir.model.data').search(cr, uid, [ ('model', '=', 'ir.ui.menu'), ('module', 'in', module_names) ], context=context)
values = self.pool.get('ir.model.data').read(cr, uid, ids, ['res_id'], context=context)
all_menu_ids = [i['res_id'] for i in values]
:param module: a module object
:type module: browse(ir.module.module)
"""
subdir = addons.get_module_resource(module.name, 'po')
if subdir: return subdir
# old naming convention
subdir = addons.get_module_resource(module.name, 'i18n')
if subdir: return subdir
return None
def _add_translations(self, module, web_data):
""" Adds translation data to a zipped web module
:param module: a module descriptor
:type module: browse(ir.module.module)
:param web_data: zipped data of a web module
:type web_data: bytes
"""
# cStringIO.StringIO is either read or write, not r/w
web_zip = StringIO.StringIO(web_data)
web_archive = zipfile.ZipFile(web_zip, 'a')
# get the contents of the i18n or po folder and move them to the
# po/messages subdirectory of the web module.
# The POT file will be incorrectly named, but that should not
# matter since the web client is not going to use it, only the PO
# files.
translations_file = cStringIO.StringIO(
addons.zip_directory(self._translations_subdir(module), False))
translations_archive = zipfile.ZipFile(translations_file)
for path in translations_archive.namelist():
web_path = os.path.join(
'web', 'po', 'messages', os.path.basename(path))
web_archive.writestr(
web_path,
translations_archive.read(path))
translations_archive.close()
translations_file.close()
web_archive.close()
try:
return web_zip.getvalue()
finally:
web_zip.close()
def get_web(self, cr, uid, names, context=None):
""" get_web(cr, uid, [module_name], context) -> [{name, depends, content}]
Returns the web content of all the named addons.
The toplevel directory of the zipped content is called 'web',
its final naming has to be managed by the client
"""
modules = self.browse(cr, uid,
self.search(cr, uid, [('name', 'in', names)], context=context),
context=context)
if not modules: return []
self.__logger.info('Sending web content of modules %s '
'to web client', names)
modules_data = []
for module in modules:
web_data = addons.zip_directory(
addons.get_module_resource(module.name, 'web'), False)
if self._translations_subdir(module):
web_data = self._add_translations(module, web_data)
modules_data.append({
'name': module.name,
'version': module.installed_version,
'depends': list(self._web_dependencies(
cr, uid, module, context=context)),
'content': base64.encodestring(web_data)
})
return modules_data
module()
root_menu_ids = []
for menu in self.pool.get('ir.ui.menu').browse(cr, uid, all_menu_ids, context=context):
while menu.parent_id:
menu = menu.parent_id
if not menu.id in root_menu_ids:
root_menu_ids.append((menu.sequence,menu.id))
root_menu_ids.sort()
root_menu_ids = [i[1] for i in root_menu_ids]
return root_menu_ids
class module_dependency(osv.osv):
_name = "ir.module.module.dependency"
@ -706,6 +658,5 @@ class module_dependency(osv.osv):
('unknown', 'Unknown'),
], string='State', readonly=True, select=True),
}
module_dependency()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -7,9 +7,9 @@
<field name="visible" eval="0" />
</record>
<record model="ir.module.category" id="module_category_hidden_link">
<record model="ir.module.category" id="module_category_hidden_links">
<field name="parent_id" ref="module_category_hidden" />
<field name="name">Link</field>
<field name="name">Links</field>
<field name="sequence">0</field>
<field name="visible" eval="0" />
</record>
@ -114,13 +114,44 @@
<field name="sequence">15</field>
</record>
<!--
<record id="ir_ui_view_sc_modules0" model="ir.ui.view_sc">
<field name="name">Modules</field>
<field name="resource">ir.ui.menu</field>
<field name="user_id" ref="base.user_root"/>
<field name="res_id" ref="base.menu_module_tree"/>
<record model="ir.module.category" id="module_category_administration">
<field name="name">Administration</field>
<field name="sequence">100</field>
</record>
-->
<record model="ir.module.category" id="module_category_usability">
<field name="name">Usability</field>
<field name="sequence">101</field>
</record>
<!-- add applications to base groups -->
<record model="res.groups" id="group_erp_manager">
<field name="category_id" ref="module_category_administration"/>
</record>
<record model="res.groups" id="group_system">
<field name="category_id" ref="module_category_administration"/>
</record>
<record model="res.groups" id="group_user">
<field name="category_id" ref="module_category_human_resources"/>
</record>
<record model="res.groups" id="group_multi_company">
<field name="category_id" ref="module_category_usability"/>
</record>
<record model="res.groups" id="group_extended">
<field name="category_id" ref="module_category_usability"/>
</record>
<record model="res.groups" id="group_no_one">
<field name="category_id" ref="module_category_usability"/>
</record>
<record model="res.groups" id="group_sale_salesman">
<field name="category_id" ref="module_category_sales_management"/>
</record>
<record model="res.groups" id="group_sale_manager">
<field name="category_id" ref="module_category_sales_management"/>
</record>
</data>
</openerp>

View File

@ -11,8 +11,10 @@
<field name="field_parent">child_ids</field>
<field name="arch" type="xml">
<form string="Module Category">
<field colspan="4" name="name"/>
<field colspan="4" name="parent_id"/>
<field name="name"/>
<field name="parent_id"/>
<field name="sequence"/>
<field name="description" colspan="4"/>
</form>
</field>
</record>
@ -39,35 +41,68 @@
<field name="arch" type="xml">
<search string="Search modules">
<group col='10' colspan='4'>
<filter name="app" icon="terp-check" string="Apps" domain="[('application', '=', 1)]"/>
<filter name="extra" icon="terp-check" string="Extra" domain="[('application', '=', 0)]"/>
<separator orientation="vertical"/>
<filter icon="terp-check" string="Installed" domain="[('state', 'in', ['installed', 'to upgrade', 'to remove'])]"/>
<filter icon="terp-dialog-close" string="Not Installed" domain="[('state', 'in', ['uninstalled', 'uninstallable'])]"/>
<filter icon="terp-gtk-jump-to-ltr" string="To be upgraded" domain="[('state','in', ['to upgrade', 'to remove', 'to install'])]"/>
<filter icon="terp-dialog-close" string="Not Installed" domain="[('state', 'in', ['uninstalled', 'uninstallable', 'to install'])]"/>
<separator orientation="vertical"/>
<filter icon="terp-camera_test" string="Certified" domain="[('certificate','&lt;&gt;', False)]"/>
<separator orientation="vertical"/>
<field name="name"/>
<field name="complexity"/>
<field name="description"/>
<field name="dependencies_id"/>
<field name="state"/>
</group>
<newline/>
<group expand="0" string="Group By..." colspan="11" col="11" groups="base.group_extended">
<filter string="Author" icon="terp-personal" domain="[]" context="{'group_by':'author'}"/>
<separator orientation="vertical"/>
<filter string="Category" icon="terp-stock_symbol-selection" domain="[]" context="{'group_by':'category_id'}"/>
<filter string="State" icon="terp-stock_effects-object-colorize" domain="[]" context="{'group_by':'state'}"/>
<field name="name"
filter_domain="['|', ('name','ilike',self), ('shortdesc','ilike',self)]"
string="Name"/>
<field name="description" string="Keywords"/>
<field name="category_id">
<filter name="no_hidden" help="Hide technical modules"
icon="STOCK_REMOVE" groups="base.group_no_one"
domain="['!', ('category_id.parent_id','child_of','Hidden')]"/>
</field>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="module_view_kanban">
<field name="name">Modules Kanban</field>
<field name="model">ir.module.module</field>
<field name="type">kanban</field>
<field name="arch" type="xml">
<kanban>
<field name="icon"/>
<field name="name"/>
<field name="state"/>
<field name="complexity"/>
<templates>
<t t-name="kanban-box">
<div class="oe_module_vignette">
<t t-set="installed" t-value="record.state.raw_value == 'installed'"/>
<a type="edit">
<img t-attf-src="#{record.icon.value}" class="oe_module_icon"/>
</a>
<div class="oe_module_desc">
<h4><a type="edit"><field name="shortdesc"/></a></h4>
<p>
<field name="category_id"/><br/>
<field name="name"/><br/>
<span t-if="record.complexity.raw_value == 'Expert'" class="oe_label oe_warning">Complex</span>
</p>
<button type="object" name="button_immediate_install" states="uninstalled" class="oe_button">Install</button>
<button t-if="installed" class="oe_button" disabled="disabled">Installed</button>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_module_open_categ" model="ir.actions.act_window">
<field name="name">Modules</field>
<field name="res_model">ir.module.module</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">tree,form,kanban</field>
<field name="domain">[('category_id','=',active_id)]</field>
</record>
<record id="ir_action_module_category" model="ir.values">
@ -86,12 +121,16 @@
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Module">
<field name="name" select="1"/>
<field name="certificate" />
<field colspan="4" name="shortdesc" select="2"/>
<field name="category_id"/>
<field name="complexity"/>
<field name="demo" readonly="1"/>
<group colspan="4" col="6">
<field name="name"/>
<field name="shortdesc"/>
<field name="certificate" />
<field name="category_id"/>
<field name="complexity"/>
<field name="demo"/>
<field name="icon"/>
<field name="application"/>
</group>
<notebook colspan="4">
<page string="Module">
<group colspan="4" col="4">
@ -163,12 +202,12 @@
<field name="name">Modules</field>
<field name="res_model">ir.module.module</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain"/>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{'search_default_app':1, 'search_default_no_hidden': 1}</field>
<field name="search_view_id" ref="view_module_filter"/>
<field name="help">You can install new modules in order to activate new features, menu, reports or data in your OpenERP instance. To install some modules, click on the button "Install" from the form view and then click on "Start Upgrade".</field>
</record>
<menuitem action="open_module_tree" id="menu_module_tree" parent="base.menu_management"/>
<menuitem action="open_module_tree" id="menu_module_tree" parent="base.menu_management" sequence="1"/>
</data>
</openerp>

View File

@ -60,6 +60,7 @@ After importing a new module you can install it by clicking on the button "Insta
action="action_view_base_module_import"
id="menu_view_base_module_import"
parent="menu_management"
groups="base.group_extended"
sequence="1"/>
</data>

View File

@ -55,12 +55,13 @@
</record>
<menuitem
name="Update Modules List"
action="action_view_base_module_update"
id="menu_view_base_module_update"
parent="menu_management"
sequence="2"
icon="STOCK_CONVERT"/>
name="Update Modules List"
action="action_view_base_module_update"
id="menu_view_base_module_update"
groups="base.group_extended"
parent="menu_management"
sequence="2"
icon="STOCK_CONVERT"/>
</data>
</openerp>

View File

@ -31,11 +31,12 @@
</record>
<menuitem
name="Apply Scheduled Upgrades"
action="action_view_base_module_upgrade"
id="menu_view_base_module_upgrade"
parent="menu_management"
sequence="3"/>
name="Apply Scheduled Upgrades"
action="action_view_base_module_upgrade"
groups="base.group_extended"
id="menu_view_base_module_upgrade"
parent="menu_management"
sequence="3"/>
<record id="view_base_module_upgrade_install" model="ir.ui.view">
<field name="name">Module Upgrade Install</field>

View File

@ -86,7 +86,7 @@ class publisher_warranty_contract(osv.osv):
db_create_date = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.create_date')
user = self.pool.get("res.users").browse(cr, uid, uid)
user_name = user.name
email = user.email
email = user.user_email
msg = {'contract_name': valid_contract.name,
'tb': tb,

View File

@ -139,7 +139,7 @@ class res_partner_bank(osv.osv):
'state': fields.selection(_bank_type_get, 'Bank Account Type', required=True,
change_default=True),
'sequence': fields.integer('Sequence'),
'footer': fields.boolean("Display on Reports")
'footer': fields.boolean("Display on Reports", help="Display this bank account on the footer of printed documents like invoices and sales orders.")
}
_defaults = {
'owner_name': lambda obj, cursor, user, context: obj._default_value(

View File

@ -116,11 +116,11 @@ class res_company(osv.osv):
_columns = {
'name': fields.related('partner_id', 'name', string='Company Name', size=64, required=True, store=True, type='char'),
'name': fields.related('partner_id', 'name', string='Company Name', size=128, required=True, store=True, type='char'),
'parent_id': fields.many2one('res.company', 'Parent Company', select=True),
'child_ids': fields.one2many('res.company', 'parent_id', 'Child Companies'),
'partner_id': fields.many2one('res.partner', 'Partner', required=True),
'rml_header1': fields.char('Report Header / Company Slogan', size=200),
'rml_header1': fields.char('Report Header / Company Slogan', size=200, help="Appears by default on the top right corner of your printed documents."),
'rml_footer1': fields.char('General Information Footer', size=200),
'rml_footer2': fields.function(_get_bank_data, type="char", string='Bank Accounts Footer', size=250, help="This field is computed automatically based on bank accounts defined, having the display on footer checkbox set."),
'rml_header': fields.text('RML Header', required=True),
@ -144,6 +144,7 @@ class res_company(osv.osv):
'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),
'company_registry': fields.char('Company Registry', size=64),
'paper_format': fields.selection([('a4', 'A4'), ('us_letter', 'US Letter')], "Paper Format", required=True),
}
_sql_constraints = [
('name_uniq', 'unique (name)', 'The company name must be unique !')
@ -277,29 +278,31 @@ class res_company(osv.osv):
finally:
header_file.close()
except:
return """
return self._header_a4
_header_main = """
<header>
<pageTemplate>
<frame id="first" x1="1.3cm" y1="2.5cm" height="23.0cm" width="19cm"/>
<frame id="first" x1="1.3cm" y1="2.5cm" height="%s" width="19.0cm"/>
<pageGraphics>
<!-- You Logo - Change X,Y,Width and Height -->
<image x="1.3cm" y="27.6cm" height="40.0" >[[ company.logo or removeParentNode('image') ]]</image>
<image x="1.3cm" y="%s" height="40.0" >[[ company.logo or removeParentNode('image') ]]</image>
<setFont name="DejaVu Sans" size="8"/>
<fill color="black"/>
<stroke color="black"/>
<lines>1.3cm 27.7cm 20cm 27.7cm</lines>
<lines>1.3cm %s 20cm %s</lines>
<drawRightString x="20cm" y="27.8cm">[[ company.rml_header1 ]]</drawRightString>
<drawRightString x="20cm" y="%s">[[ company.rml_header1 ]]</drawRightString>
<drawString x="1.3cm" y="27.2cm">[[ company.partner_id.name ]]</drawString>
<drawString x="1.3cm" y="26.8cm">[[ company.partner_id.address and company.partner_id.address[0].street or '' ]]</drawString>
<drawString x="1.3cm" y="26.4cm">[[ company.partner_id.address and company.partner_id.address[0].zip or '' ]] [[ company.partner_id.address and company.partner_id.address[0].city or '' ]] - [[ company.partner_id.address and company.partner_id.address[0].country_id and company.partner_id.address[0].country_id.name or '']]</drawString>
<drawString x="1.3cm" y="26.0cm">Phone:</drawString>
<drawRightString x="7cm" y="26.0cm">[[ company.partner_id.address and company.partner_id.address[0].phone or '' ]]</drawRightString>
<drawString x="1.3cm" y="25.6cm">Mail:</drawString>
<drawRightString x="7cm" y="25.6cm">[[ company.partner_id.address and company.partner_id.address[0].email or '' ]]</drawRightString>
<lines>1.3cm 25.5cm 7cm 25.5cm</lines>
<drawString x="1.3cm" y="%s">[[ company.partner_id.name ]]</drawString>
<drawString x="1.3cm" y="%s">[[ company.partner_id.address and company.partner_id.address[0].street or '' ]]</drawString>
<drawString x="1.3cm" y="%s">[[ company.partner_id.address and company.partner_id.address[0].zip or '' ]] [[ company.partner_id.address and company.partner_id.address[0].city or '' ]] - [[ company.partner_id.address and company.partner_id.address[0].country_id and company.partner_id.address[0].country_id.name or '']]</drawString>
<drawString x="1.3cm" y="%s">Phone:</drawString>
<drawRightString x="7cm" y="%s">[[ company.partner_id.address and company.partner_id.address[0].phone or '' ]]</drawRightString>
<drawString x="1.3cm" y="%s">Mail:</drawString>
<drawRightString x="7cm" y="%s">[[ company.partner_id.address and company.partner_id.address[0].email or '' ]]</drawRightString>
<lines>1.3cm %s 7cm %s</lines>
<!--page bottom-->
@ -311,8 +314,18 @@ class res_company(osv.osv):
</pageGraphics>
</pageTemplate>
</header>"""
_header_a4 = _header_main % ('23.0cm', '27.6cm', '27.7cm', '27.7cm', '27.8cm', '27.2cm', '26.8cm', '26.4cm', '26.0cm', '26.0cm', '25.6cm', '25.6cm', '25.5cm', '25.5cm')
_header_letter = _header_main % ('21.3cm', '25.9cm', '26.0cm', '26.0cm', '26.1cm', '25.5cm', '25.1cm', '24.7cm', '24.3cm', '24.3cm', '23.9cm', '23.9cm', '23.8cm', '23.8cm')
def onchange_paper_format(self, cr, uid, ids, paper_format, context=None):
if paper_format == 'us_letter':
return {'value': {'rml_header': self._header_letter}}
return {'value': {'rml_header': self._header_a4}}
_defaults = {
'currency_id': _get_euro,
'paper_format': 'a4',
'rml_header':_get_header,
'rml_header2': _header2,
'rml_header3': _header3,

View File

@ -31,6 +31,13 @@ class Country(osv.osv):
'code': fields.char('Country Code', size=2,
help='The ISO country code in two chars.\n'
'You can use this field for quick search.', required=True),
'address_format': fields.text('Address Format', help="""You can state here the usual format to use for the \
addresses belonging to this country.\n\nYou can use the python-style string patern with all the field of the address \
(for example, use '%(street)s' to display the field 'street') plus
\n%(state_name)s: the name of the state
\n%(state_code)s: the code of the state
\n%(country_name)s: the name of the country
\n%(country_code)s: the code of the country"""),
}
_sql_constraints = [
('name_uniq', 'unique (name)',
@ -38,6 +45,9 @@ class Country(osv.osv):
('code_uniq', 'unique (code)',
'The code of the country must be unique !')
]
_defaults = {
'address_format': "%(street)s\n%(street2)s\n%(city)s,%(state_code)s %(zip)s\n%(country_name)s",
}
def name_search(self, cr, user, name='', args=None, operator='ilike',
context=None, limit=100):

View File

@ -26,6 +26,7 @@
<form string="Country">
<field name="name" select="1"/>
<field name="code" select="1"/>
<field name="address_format" colspan="4" groups="base.group_extended"/>
</form>
</field>
</record>

View File

@ -24,7 +24,7 @@ import netsvc
from osv import fields, osv
import tools
from tools.misc import currency
from tools import float_round, float_is_zero, float_compare
from tools.translate import _
CURRENCY_DISPLAY_PATTERN = re.compile(r'(\w+)\s*(?:\((.*)\))?')
@ -127,15 +127,49 @@ class res_currency(osv.osv):
return [(x['id'], tools.ustr(x['name']) + (x['symbol'] and (' (' + tools.ustr(x['symbol']) + ')') or '')) for x in reads]
def round(self, cr, uid, currency, amount):
if currency.rounding == 0:
return 0.0
else:
# /!\ First member below must be rounded to full unit!
# Do not pass a rounding digits value to round()
return round(amount / currency.rounding) * currency.rounding
"""Return ``amount`` rounded according to ``currency``'s
rounding rules.
:param browse_record currency: currency for which we are rounding
:param float amount: the amount to round
:return: rounded float
"""
return float_round(amount, precision_rounding=currency.rounding)
def compare_amounts(self, cr, uid, currency, amount1, amount2):
"""Compare ``amount1`` and ``amount2`` after rounding them according to the
given currency's precision..
An amount is considered lower/greater than another amount if their rounded
value is different. This is not the same as having a non-zero difference!
For example 1.432 and 1.431 are equal at 2 digits precision,
so this method would return 0.
However 0.006 and 0.002 are considered different (returns 1) because
they respectively round to 0.01 and 0.0, even though
0.006-0.002 = 0.004 which would be considered zero at 2 digits precision.
:param browse_record currency: currency for which we are rounding
:param float amount1: first amount to compare
:param float amount2: second amount to compare
:return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than,
equal to, or greater than ``amount2``, according to
``currency``'s rounding.
"""
return float_compare(amount1, amount2, precision_rounding=currency.rounding)
def is_zero(self, cr, uid, currency, amount):
return abs(self.round(cr, uid, currency, amount)) < currency.rounding
"""Returns true if ``amount`` is small enough to be treated as
zero according to ``currency``'s rounding rules.
Warning: ``is_zero(amount1-amount2)`` is not always equivalent to
``compare_amounts(amount1,amount2) == 0``, as the former will round after
computing the difference, while the latter will round before, giving
different results for e.g. 0.006 and 0.002 at 2 digits precision.
:param browse_record currency: currency for which we are rounding
:param float amount: amount to compare with currency's zero
"""
return float_is_zero(amount, precision_rounding=currency.rounding)
def _get_conversion_rate(self, cr, uid, from_currency, to_currency, context=None):
if context is None:

View File

@ -36,9 +36,20 @@ class res_payterm(osv.osv):
res_payterm()
class res_partner_category(osv.osv):
def name_get(self, cr, uid, ids, context=None):
if not len(ids):
return []
"""Return the categories' display name, including their direct
parent by default.
:param dict context: the ``partner_category_display`` key can be
used to select the short version of the
category name (without the direct parent),
when set to ``'short'``. The default is
the long version."""
if context is None:
context = {}
if context.get('partner_category_display') == 'short':
return super(res_partner_category, self).name_get(cr, uid, ids, context=context)
reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
res = []
for record in reads:
@ -141,7 +152,6 @@ class res_partner(osv.osv):
'company_id': fields.many2one('res.company', 'Company', select=1),
'color': fields.integer('Color Index'),
}
def _default_category(self, cr, uid, context=None):
if context is None:
context = {}
@ -235,7 +245,7 @@ class res_partner(osv.osv):
address_obj = self.pool.get('res.partner.address')
address_ids = address_obj.search(cr, uid, [('partner_id', 'in', ids)])
address_rec = address_obj.read(cr, uid, address_ids, ['type'])
res = list(tuple(addr.values()) for addr in address_rec)
res = list((addr['type'],addr['id']) for addr in address_rec)
adr = dict(res)
# get the id of the (first) default address if there is one,
# otherwise get the id of the first address in the list
@ -289,7 +299,7 @@ class res_partner_address(osv.osv):
_columns = {
'partner_id': fields.many2one('res.partner', 'Partner Name', ondelete='set null', select=True, help="Keep empty for a private address, not related to partner."),
'type': fields.selection( [ ('default','Default'),('invoice','Invoice'), ('delivery','Delivery'), ('contact','Contact'), ('other','Other') ],'Address Type', help="Used to select automatically the right address according to the context in sales and purchases documents."),
'function': fields.char('Function', size=64),
'function': fields.char('Function', size=128),
'title': fields.many2one('res.partner.title','Title'),
'name': fields.char('Contact Name', size=64, select=1),
'street': fields.char('Street', size=128),
@ -314,7 +324,6 @@ class res_partner_address(osv.osv):
'active': lambda *a: 1,
'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'res.partner.address', context=c),
}
def name_get(self, cr, user, ids, context=None):
if context is None:
context = {}
@ -356,6 +365,32 @@ class res_partner_address(osv.osv):
def get_city(self, cr, uid, id):
return self.browse(cr, uid, id).city
def _display_address(self, cr, uid, address, context=None):
'''
The purpose of this function is to build and return an address formatted accordingly to the
standards of the country where it belongs.
:param address: browse record of the res.partner.address to format
:returns: the address formatted in a display that fit its country habits (or the default ones
if not country is specified)
:rtype: string
'''
# get the address format
address_format = address.country_id and address.country_id.address_format or \
'%(street)s\n%(street2)s\n%(city)s,%(state_code)s %(zip)s'
# get the information that will be injected into the display format
args = {
'state_code': address.state_id and address.state_id.code or '',
'state_name': address.state_id and address.state_id.name or '',
'country_code': address.country_id and address.country_id.code or '',
'country_name': address.country_id and address.country_id.name or '',
}
address_field = ['title', 'street', 'street2', 'zip', 'city']
for field in address_field :
args[field] = getattr(address, field) or ''
return address_format % args
res_partner_address()
class res_partner_category(osv.osv):

View File

@ -99,7 +99,6 @@
<record id="res_partner_agrolait" model="res.partner">
<field name="name">Agrolait</field>
<field eval="[(6, 0, [ref('base.res_partner_category_0')])]" name="category_id"/>
<field name="user_id" ref="base.user_root"/>
<field name="address" eval="[]"/>
<field name="website">www.agrolait.com</field>
</record>
@ -107,7 +106,6 @@
<field name="name">Camptocamp</field>
<field eval="[(6, 0, [ref('res_partner_category_10'), ref('res_partner_category_5')])]" name="category_id"/>
<field name="supplier">1</field>
<field name="user_id" ref="base.user_root"/>
<field name="address" eval="[]"/>
<field name="website">www.camptocamp.com</field>
</record>
@ -116,39 +114,32 @@
<field name="name">Syleam</field>
<field eval="[(6, 0, [ref('res_partner_category_5')])]" name="category_id"/>
<field name="address" eval="[]"/>
<field name="user_id" ref="user_demo"/>
</record>
<record id="res_partner_thymbra" model="res.partner">
<field name="name">Thymbra</field>
<field name="user_id" ref="base.user_root"/>
<field name="name">SmartBusiness</field>
<field eval="[(6, 0, [ref('res_partner_category_4')])]" name="category_id"/>
<field name="website">www.thymbra.com/</field>
</record>
<record id="res_partner_desertic_hispafuentes" model="res.partner">
<field name="name">Axelor</field>
<field eval="[(6, 0, [ref('res_partner_category_4')])]" name="category_id"/>
<field name="supplier">1</field>
<field name="address" eval="[]"/>
<field name="user_id" ref="user_demo"/>
<field name="website">www.axelor.com/</field>
</record>
<record id="res_partner_tinyatwork" model="res.partner">
<field name="name">Tiny AT Work</field>
<field name="user_id" ref="base.user_root"/>
<field eval="[(6, 0, [ref('res_partner_category_5'), ref('res_partner_category_10')])]" name="category_id"/>
<field name="website">www.tinyatwork.com/</field>
</record>
<record id="res_partner_2" model="res.partner">
<field name="name">Bank Wealthy and sons</field>
<field name="address" eval="[]"/>
<field name="user_id" ref="base.user_root"/>
<field name="website">www.wealthyandsons.com/</field>
</record>
<record id="res_partner_3" model="res.partner">
<field name="name">China Export</field>
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
<field name="address" eval="[]"/>
<field name="user_id" ref="base.user_root"/>
<field name="website">www.chinaexport.com/</field>
</record>
<record id="res_partner_4" model="res.partner">
@ -163,12 +154,10 @@
<field name="name">Ecole de Commerce de Liege</field>
<field eval="[(6, 0, [ref('res_partner_category_1')])]" name="category_id"/>
<field name="address" eval="[]"/>
<field name="user_id" ref="user_demo"/>
<field name="website">www.eci-liege.info//</field>
</record>
<record id="res_partner_6" model="res.partner">
<field name="name">Elec Import</field>
<field name="user_id" ref="user_demo"/>
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
<field name="supplier">1</field>
<field eval="0" name="customer"/>
@ -177,7 +166,6 @@
<record id="res_partner_maxtor" model="res.partner">
<field name="name">Maxtor</field>
<field eval="32000.00" name="credit_limit"/>
<field name="user_id" ref="user_demo"/>
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
<field name="supplier">1</field>
<field eval="0" name="customer"/>
@ -186,7 +174,6 @@
<record id="res_partner_seagate" model="res.partner">
<field name="name">Seagate</field>
<field eval="5000.00" name="credit_limit"/>
<field name="user_id" ref="user_demo"/>
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
<field name="supplier">1</field>
<field name="address" eval="[]"/>
@ -194,7 +181,6 @@
<record id="res_partner_8" model="res.partner">
<field name="website">http://mediapole.net</field>
<field name="name">Mediapole SPRL</field>
<field name="user_id" ref="user_demo"/>
<field eval="[(6, 0, [ref('res_partner_category_1')])]" name="category_id"/>
<field name="address" eval="[]"/>
</record>
@ -203,7 +189,6 @@
<field name="name">BalmerInc S.A.</field>
<field eval="12000.00" name="credit_limit"/>
<field name="ref">or</field>
<field name="user_id" ref="base.user_root"/>
<field eval="[(6, 0, [ref('res_partner_category_1')])]" name="category_id"/>
<field name="address" eval="[]"/>
</record>
@ -212,12 +197,10 @@
<field name="ean13">3020170000003</field>
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
<field name="address" eval="[]"/>
<field name="user_id" ref="user_demo"/>
</record>
<record id="res_partner_11" model="res.partner">
<field name="name">Leclerc</field>
<field eval="1200.00" name="credit_limit"/>
<field name="user_id" ref="user_demo"/>
<field eval="[(6, 0, [ref('res_partner_category_0')])]" name="category_id"/>
<field name="address" eval="[]"/>
</record>
@ -228,14 +211,12 @@
<field name="parent_id" ref="res_partner_10"/>
<field eval="[(6, 0, [ref('res_partner_category_11')])]" name="category_id"/>
<field name="address" eval="[]"/>
<field name="user_id" ref="user_demo"/>
</record>
<record id="res_partner_15" model="res.partner">
<field name="name">Magazin BML 1</field>
<field name="ean13">3020178570171</field>
<field name="parent_id" ref="res_partner_14"/>
<field eval="1500.00" name="credit_limit"/>
<field name="user_id" ref="user_demo"/>
<field eval="[(6, 0, [ref('res_partner_category_11')])]" name="category_id"/>
<field name="address" eval="[]"/>
</record>
@ -243,7 +224,6 @@
<field name="name">Université de Liège</field>
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
<field name="address" eval="[]"/>
<field name="user_id" ref="user_demo"/>
<field name="website">http://www.ulg.ac.be/</field>
</record>
@ -253,7 +233,6 @@
<record id="res_partner_duboissprl0" model="res.partner">
<field eval="'Sprl Dubois would like to sell our bookshelves but they have no storage location, so it would be exclusively on order'" name="comment"/>
<field model="res.users" name="user_id" search="[('name', '=', u'Thomas Lebrun')]"/>
<field name="name">Dubois sprl</field>
<field name="address" eval="[]"/>
<field name="website">http://www.dubois.be/</field>
@ -262,13 +241,11 @@
<record id="res_partner_ericdubois0" model="res.partner">
<field name="name">Eric Dubois</field>
<field name="address" eval="[]"/>
<field name="user_id" ref="user_demo"/>
</record>
<record id="res_partner_fabiendupont0" model="res.partner">
<field name="name">Fabien Dupont</field>
<field name="address" eval="[]"/>
<field name="user_id" ref="base.user_root"/>
</record>
<record id="res_partner_lucievonck0" model="res.partner">
@ -279,7 +256,6 @@
<record id="res_partner_notsotinysarl0" model="res.partner">
<field name="name">NotSoTiny SARL</field>
<field name="address" eval="[]"/>
<field name="user_id" ref="base.user_root"/>
<field name="website">notsotiny.be</field>
</record>
@ -287,7 +263,6 @@
<field name="name">The Shelve House</field>
<field eval="[(6,0,[ref('res_partner_category_retailers0')])]" name="category_id"/>
<field name="address" eval="[]"/>
<field name="user_id" ref="base.user_root"/>
</record>
<record id="res_partner_vickingdirect0" model="res.partner">
@ -312,7 +287,6 @@
<field name="name">ZeroOne Inc</field>
<field eval="[(6,0,[ref('res_partner_category_consumers0')])]" name="category_id"/>
<field name="address" eval="[]"/>
<field name="user_id" ref="base.user_root"/>
<field name="website">http://www.zerooneinc.com/</field>
</record>
@ -574,9 +548,9 @@
</record>
<record id="res_partner_address_thymbra" model="res.partner.address">
<field name="city">Buenos Aires</field>
<field name="name">Thymbra</field>
<field name="name">Jack Daniels</field>
<field name="zip">1659</field>
<field name="email">contact@thymbra.ar</field>
<field name="email">contact@smartbusiness.ar</field>
<field name="street">Palermo, Capital Federal </field>
<field name="street2">C1414CMS Capital Federal </field>
<field name="phone">(5411) 4773-9666 </field>

View File

@ -82,7 +82,7 @@
<field name="company_id" groups="base.group_multi_company" widget="selection" colspan="2"/>
<newline/>
<field name="name"/>
<field domain="[('domain', '=', 'contact')]" name="title" widget="selection"/>
<field domain="[('domain', '=', 'contact')]" name="title"/>
<field name="function"/>
</group>
<group colspan="2" col="2">
@ -214,7 +214,7 @@
<field name="arch" type="xml">
<form string="Contacts">
<field name="name" select="1"/>
<field domain="[('domain', '=', 'contact')]" name="title" widget="selection"/>
<field domain="[('domain', '=', 'contact')]" name="title"/>
<field name="street"/>
<field name="street2"/>
<field name="type"/>
@ -331,7 +331,7 @@
<group colspan="5" col="6">
<field name="name" select="1"/>
<field name="ref" groups="base.group_extended"/>
<field domain="[('domain', '=', 'partner')]" name="title" size="0" groups="base.group_extended" widget="selection"/>
<field domain="[('domain', '=', 'partner')]" name="title" size="0" groups="base.group_extended"/>
<field name="lang"/>
</group>
<group colspan="1" col="2">
@ -345,8 +345,8 @@
<field colspan="4" mode="form,tree" name="address" nolabel="1" select="1" height="260">
<form string="Partner Contacts">
<group colspan="4" col="6">
<field name="name"/>
<field domain="[('domain', '=', 'contact')]" name="title" size="0" widget="selection"/>
<field name="name" string="Contact Name"/>
<field domain="[('domain', '=', 'contact')]" name="title" size="0"/>
<field name="function"/>
</group>
<newline/>

View File

@ -22,11 +22,8 @@
import logging
from functools import partial
from xml.sax.saxutils import quoteattr
import simplejson
import pytz
from lxml import etree
import netsvc
import pooler
@ -40,21 +37,44 @@ import openerp.exceptions
class groups(osv.osv):
_name = "res.groups"
_order = 'name'
_description = "Access Groups"
_rec_name = 'full_name'
def _get_full_name(self, cr, uid, ids, field, arg, context=None):
res = {}
for g in self.browse(cr, uid, ids, context):
if g.category_id:
res[g.id] = '%s / %s' % (g.category_id.name, g.name)
else:
res[g.id] = g.name
return res
_columns = {
'name': fields.char('Group Name', size=64, required=True, translate=True),
'name': fields.char('Name', size=64, required=True, translate=True),
'users': fields.many2many('res.users', 'res_groups_users_rel', 'gid', 'uid', 'Users'),
'model_access': fields.one2many('ir.model.access', 'group_id', 'Access Controls'),
'rule_groups': fields.many2many('ir.rule', 'rule_group_rel',
'group_id', 'rule_group_id', 'Rules', domain=[('global', '=', False)]),
'menu_access': fields.many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', 'Access Menu'),
'comment' : fields.text('Comment',size=250),
'comment' : fields.text('Comment', size=250, translate=True),
'category_id': fields.many2one('ir.module.category', 'Application', select=True),
'full_name': fields.function(_get_full_name, type='char', string='Group Name'),
}
_sql_constraints = [
('name_uniq', 'unique (name)', 'The name of the group must be unique !')
('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique !')
]
def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
# add explicit ordering if search is sorted on full_name
if order and order.startswith('full_name'):
ids = super(groups, self).search(cr, uid, args, context=context)
gs = self.browse(cr, uid, ids, context)
gs.sort(key=lambda g: g.full_name, reverse=order.endswith('DESC'))
gs = gs[offset:offset+limit] if limit else gs[offset:]
return map(int, gs)
return super(groups, self).search(cr, uid, args, offset, limit, order, context, count)
def copy(self, cr, uid, id, default=None, context=None):
group_name = self.read(cr, uid, [id], ['name'])[0]['name']
default.update({'name': _('%s (copy)')%group_name})
@ -240,7 +260,7 @@ class users(osv.osv):
selection=[('simple','Simplified'),('extended','Extended')],
string='Interface', help="OpenERP offers a simplified and an extended user interface. If you use OpenERP for the first time we strongly advise you to select the simplified interface, which has less features but is easier to use. You can switch to the other interface from the User/Preferences menu at any time."),
'menu_tips': fields.boolean('Menu Tips', help="Check out this box if you want to always display tips on each menu action"),
'date': fields.datetime('Last Connection', readonly=True),
'date': fields.datetime('Latest Connection', readonly=True),
}
def on_change_company_id(self, cr, uid, ids, company_id):
@ -337,7 +357,7 @@ class users(osv.osv):
'company_id': _get_company,
'company_ids': _get_companies,
'groups_id': _get_group,
'menu_tips':True
'menu_tips': False
}
# User can write to a few of her own fields (but not her groups for example)
@ -522,30 +542,68 @@ class users(osv.osv):
users()
#
# Extension of res.groups and res.users with a relation for "implied" or
# "inherited" groups. Once a user belongs to a group, it automatically belongs
# to the implied groups (transitively).
#
class cset(object):
""" A cset (constrained set) is a set of elements that may be constrained to
be a subset of other csets. Elements added to a cset are automatically
added to its supersets. Cycles in the subset constraints are supported.
"""
def __init__(self, xs):
self.supersets = set()
self.elements = set(xs)
def subsetof(self, other):
if other is not self:
self.supersets.add(other)
other.update(self.elements)
def update(self, xs):
xs = set(xs) - self.elements
if xs: # xs will eventually be empty in case of a cycle
self.elements.update(xs)
for s in self.supersets:
s.update(xs)
def __iter__(self):
return iter(self.elements)
def concat(ls):
""" return the concatenation of a list of iterables """
res = []
for l in ls: res.extend(l)
return res
class groups_implied(osv.osv):
_inherit = 'res.groups'
def _get_trans_implied(self, cr, uid, ids, field, arg, context=None):
"computes the transitive closure of relation implied_ids"
memo = {} # use a memo for performance and cycle avoidance
def computed_set(g):
if g not in memo:
memo[g] = cset(g.implied_ids)
for h in g.implied_ids:
computed_set(h).subsetof(memo[g])
return memo[g]
res = {}
for g in self.browse(cr, 1, ids, context):
res[g.id] = map(int, computed_set(g))
return res
_columns = {
'implied_ids': fields.many2many('res.groups', 'res_groups_implied_rel', 'gid', 'hid',
string='Inherits', help='Users of this group automatically inherit those groups'),
'trans_implied_ids': fields.function(_get_trans_implied,
type='many2many', relation='res.groups', string='Transitively inherits'),
}
def get_closure(self, cr, uid, ids, context=None):
"return the closure of ids, i.e., all groups recursively implied by ids"
closure = set()
todo = self.browse(cr, 1, ids)
while todo:
g = todo.pop()
if g.id not in closure:
closure.add(g.id)
todo.extend(g.implied_ids)
return list(closure)
def create(self, cr, uid, values, context=None):
users = values.pop('users', None)
gid = super(groups_implied, self).create(cr, uid, values, context)
@ -557,27 +615,13 @@ class groups_implied(osv.osv):
def write(self, cr, uid, ids, values, context=None):
res = super(groups_implied, self).write(cr, uid, ids, values, context)
if values.get('users') or values.get('implied_ids'):
# add implied groups (to all users of each group)
# add all implied groups (to all users of each group)
for g in self.browse(cr, uid, ids):
gids = self.get_closure(cr, uid, [g.id], context)
users = [(4, u.id) for u in g.users]
super(groups_implied, self).write(cr, uid, gids, {'users': users}, context)
gids = map(int, g.trans_implied_ids)
vals = {'users': [(4, u.id) for u in g.users]}
super(groups_implied, self).write(cr, uid, gids, vals, context)
return res
def get_maximal(self, cr, uid, ids, context=None):
"return the maximal element among the group ids"
max_set, max_closure = set(), set()
for gid in ids:
if gid not in max_closure:
closure = set(self.get_closure(cr, uid, [gid], context))
max_set -= closure # remove implied groups from max_set
max_set.add(gid) # gid is maximal
max_closure |= closure # update closure of max_set
if len(max_set) > 1:
log = logging.getLogger('res.groups')
log.warning('Groups %s are maximal among %s, only one expected.', max_set, ids)
return bool(max_set) and max_set.pop()
groups_implied()
class users_implied(osv.osv):
@ -597,13 +641,10 @@ class users_implied(osv.osv):
res = super(users_implied, self).write(cr, uid, ids, values, context)
if values.get('groups_id'):
# add implied groups for all users
groups_obj = self.pool.get('res.groups')
for u in self.browse(cr, uid, ids):
old_gids = map(int, u.groups_id)
new_gids = groups_obj.get_closure(cr, uid, old_gids, context)
if len(old_gids) != len(new_gids):
values = {'groups_id': [(6, 0, new_gids)]}
super(users_implied, self).write(cr, uid, [u.id], values, context)
for user in self.browse(cr, uid, ids):
gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
vals = {'groups_id': [(4, g.id) for g in gs]}
super(users_implied, self).write(cr, uid, [user.id], vals, context)
return res
users_implied()
@ -613,56 +654,16 @@ users_implied()
#
# Extension of res.groups and res.users for the special groups view in the users
# form. This extension presents groups with selection and boolean widgets:
# - Groups named as "App/Name" (corresponding to root menu "App") are presented
# per application, with one boolean and selection field each. The selection
# field defines a role "Name" for the given application.
# - Groups named as "Stuff/Name" are presented as boolean fields and grouped
# under sections "Stuff".
# - The remaining groups are presented as boolean fields and grouped in a
# - Groups are shown by application, with boolean and/or selection fields.
# Selection fields typically defines a role "Name" for the given application.
# - Uncategorized groups are presented as boolean fields and grouped in a
# section "Others".
#
class groups_view(osv.osv):
_inherit = 'res.groups'
def get_classified(self, cr, uid, context=None):
""" classify all groups by prefix; return a pair (apps, others) where
- both are lists like [("App", [("Name", browse_group), ...]), ...];
- apps is sorted in menu order;
- others are sorted in alphabetic order;
- groups not like App/Name are at the end of others, under _('Others')
"""
# sort groups by implication, with implied groups first
groups = self.browse(cr, uid, self.search(cr, uid, []), context)
groups.sort(key=lambda g: set(self.get_closure(cr, uid, [g.id], context)))
# classify groups depending on their names
classified = {}
for g in groups:
# split() returns 1 or 2 elements, so names[-2] is prefix or None
names = [None] + [s.strip() for s in g.name.split('/', 1)]
classified.setdefault(names[-2], []).append((names[-1], g))
# determine the apps (that correspond to root menus, in order)
menu_obj = self.pool.get('ir.ui.menu')
menu_ids = menu_obj.search(cr, uid, [('parent_id','=',False)], context={'ir.ui.menu.full_list': True})
apps = []
for m in menu_obj.browse(cr, uid, menu_ids, context):
if m.name in classified:
# application groups are already sorted by implication
apps.append((m.name, classified.pop(m.name)))
# other groups
others = sorted(classified.items(), key=lambda pair: pair[0])
if others and others[0][0] is None:
others.append((_('Others'), others.pop(0)[1]))
for sec, groups in others:
groups.sort(key=lambda pair: pair[0])
return (apps, others)
groups_view()
# The user form view is modified by an inherited view (base.user_groups_view);
# the inherited view replaces the field 'groups_id' by a set of reified group
# fields (boolean or selection fields). The arch of that view is regenerated
# each time groups are changed.
#
# Naming conventions for reified groups fields:
# - boolean field 'in_group_ID' is True iff
# ID is in 'groups_id'
@ -678,155 +679,211 @@ def name_selection_groups(ids): return 'sel_groups_' + '_'.join(map(str, ids))
def is_boolean_group(name): return name.startswith('in_group_')
def is_boolean_groups(name): return name.startswith('in_groups_')
def is_selection_groups(name): return name.startswith('sel_groups_')
def is_field_group(name):
def is_reified_group(name):
return is_boolean_group(name) or is_boolean_groups(name) or is_selection_groups(name)
def get_boolean_group(name): return int(name[9:])
def get_boolean_groups(name): return map(int, name[10:].split('_'))
def get_selection_groups(name): return map(int, name[11:].split('_'))
def encode(s): return s.encode('utf8') if isinstance(s, unicode) else s
def partition(f, xs):
"return a pair equivalent to (filter(f, xs), filter(lambda x: not f(x), xs))"
yes, nos = [], []
for x in xs:
if f(x):
yes.append(x)
else:
nos.append(x)
(yes if f(x) else nos).append(x)
return yes, nos
class groups_view(osv.osv):
_inherit = 'res.groups'
def create(self, cr, uid, values, context=None):
res = super(groups_view, self).create(cr, uid, values, context)
self.update_user_groups_view(cr, uid, context)
return res
def write(self, cr, uid, ids, values, context=None):
res = super(groups_view, self).write(cr, uid, ids, values, context)
self.update_user_groups_view(cr, uid, context)
return res
def unlink(self, cr, uid, ids, context=None):
res = super(groups_view, self).unlink(cr, uid, ids, context)
self.update_user_groups_view(cr, uid, context)
return res
def update_user_groups_view(self, cr, uid, context=None):
# the view with id 'base.user_groups_view' inherits the user form view,
# and introduces the reified group fields
view = self.get_user_groups_view(cr, uid, context)
if view:
xml = u"""<?xml version="1.0" encoding="utf-8"?>
<!-- GENERATED AUTOMATICALLY BY GROUPS -->
<field name="groups_id" position="replace">
%s
%s
</field>
"""
xml1, xml2 = [], []
xml1.append('<separator string="%s" colspan="4"/>' % _('Applications'))
for app, kind, gs in self.get_groups_by_application(cr, uid, context):
if kind == 'selection':
# application name with a selection field
field_name = name_selection_groups(map(int, gs))
xml1.append('<field name="%s"/>' % field_name)
xml1.append('<newline/>')
else:
# application separator with boolean fields
app_name = app and app.name or _('Other')
xml2.append('<separator string="%s" colspan="4"/>' % app_name)
for g in gs:
field_name = name_boolean_group(g.id)
xml2.append('<field name="%s"/>' % field_name)
view.write({'arch': xml % ('\n'.join(xml1), '\n'.join(xml2))})
return True
def get_user_groups_view(self, cr, uid, context=None):
try:
view = self.pool.get('ir.model.data').get_object(cr, 1, '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):
return self.search(cr, uid, domain or [])
def get_groups_by_application(self, cr, uid, context=None):
""" return all groups classified by application (module category), as a list of pairs:
[(app, kind, [group, ...]), ...],
where app and group are browse records, and kind is either 'boolean' or 'selection'.
Applications are given in sequence order. If kind is 'selection', the groups are
given in reverse implication order.
"""
def linearized(gs):
gs = set(gs)
# determine sequence order: a group should appear after its implied groups
order = dict.fromkeys(gs, 0)
for g in gs:
for h in gs.intersection(g.trans_implied_ids):
order[h] -= 1
# check whether order is total, i.e., sequence orders are distinct
if len(set(order.itervalues())) == len(gs):
return sorted(gs, key=lambda g: order[g])
return None
# classify all groups by application
gids = self.get_application_groups(cr, uid, context=context)
by_app, others = {}, []
for g in self.browse(cr, uid, gids, context):
if g.category_id:
by_app.setdefault(g.category_id, []).append(g)
else:
others.append(g)
# build the result
res = []
apps = sorted(by_app.iterkeys(), key=lambda a: a.sequence or 0)
for app in apps:
gs = linearized(by_app[app])
if gs:
res.append((app, 'selection', gs))
else:
res.append((app, 'boolean', by_app[app]))
if others:
res.append((False, 'boolean', others))
return res
groups_view()
class users_view(osv.osv):
_inherit = 'res.users'
def _process_values_groups(self, cr, uid, values, context=None):
""" transform all reified group fields into a 'groups_id', adding
also the implied groups """
add, rem = [], []
for k in values.keys():
if is_boolean_group(k):
if values.pop(k):
add.append(get_boolean_group(k))
else:
rem.append(get_boolean_group(k))
elif is_boolean_groups(k):
if not values.pop(k):
rem.extend(get_boolean_groups(k))
elif is_selection_groups(k):
gid = values.pop(k)
if gid:
rem.extend(get_selection_groups(k))
add.append(gid)
if add or rem:
# remove groups in 'rem' and add groups in 'add'
gdiff = [(3, id) for id in rem] + [(4, id) for id in add]
values.setdefault('groups_id', []).extend(gdiff)
return True
def create(self, cr, uid, values, context=None):
self._process_values_groups(cr, uid, values, context)
self._set_reified_groups(values)
return super(users_view, self).create(cr, uid, values, context)
def write(self, cr, uid, ids, values, context=None):
self._process_values_groups(cr, uid, values, context)
self._set_reified_groups(values)
return super(users_view, self).write(cr, uid, ids, values, context)
def _set_reified_groups(self, values):
""" reflect reified group fields in values['groups_id'] """
if 'groups_id' in values:
# groups are already given, ignore group fields
for f in filter(is_reified_group, values.iterkeys()):
del values[f]
return
add, remove = [], []
for f in values.keys():
if is_boolean_group(f):
target = add if values.pop(f) else remove
target.append(get_boolean_group(f))
elif is_boolean_groups(f):
if not values.pop(f):
remove.extend(get_boolean_groups(f))
elif is_selection_groups(f):
remove.extend(get_selection_groups(f))
selected = values.pop(f)
if selected:
add.append(selected)
# update values *only* if groups are being modified, otherwise
# we introduce spurious changes that might break the super.write() call.
if add or remove:
# remove groups in 'remove' and add groups in 'add'
values['groups_id'] = [(3, id) for id in remove] + [(4, id) for id in add]
def default_get(self, cr, uid, fields, context=None):
group_fields, fields = partition(is_reified_group, fields)
fields1 = (fields + ['groups_id']) if group_fields else fields
values = super(users_view, self).default_get(cr, uid, fields1, context)
self._get_reified_groups(group_fields, values)
return values
def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
if not fields:
group_fields, fields = [], self.fields_get(cr, uid, context=context).keys()
else:
group_fields, fields = partition(is_field_group, fields)
if group_fields:
group_obj = self.pool.get('res.groups')
fields.append('groups_id')
# read the normal fields (and 'groups_id')
res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
records = res if isinstance(res, list) else [res]
for record in records:
# get the field 'groups_id' and insert the group_fields
groups = set(record['groups_id'])
for f in group_fields:
if is_boolean_group(f):
record[f] = get_boolean_group(f) in groups
elif is_boolean_groups(f):
record[f] = not groups.isdisjoint(get_boolean_groups(f))
elif is_selection_groups(f):
selected = groups.intersection(get_selection_groups(f))
record[f] = group_obj.get_maximal(cr, uid, selected, context=context)
return res
return super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
def fields_get(self, cr, user, allfields=None, context=None, write_access=True):
res = super(users_view, self).fields_get(cr, user, allfields, context, write_access)
apps, others = self.pool.get('res.groups').get_classified(cr, user, context)
for app, groups in apps:
ids = [g.id for name, g in groups]
app_name = name_boolean_groups(ids)
sel_name = name_selection_groups(ids)
selection = [(g.id, name) for name, g in groups]
res[app_name] = {'type': 'boolean', 'string': app}
tips = [name + ': ' + (g.comment or '') for name, g in groups]
if tips:
res[app_name].update(help='\n'.join(tips))
res[sel_name] = {'type': 'selection', 'string': 'Group', 'selection': selection}
for sec, groups in others:
for gname, g in groups:
name = name_boolean_group(g.id)
res[name] = {'type': 'boolean', 'string': gname}
if g.comment:
res[name].update(help=g.comment)
fields = self.fields_get(cr, uid, context=context).keys()
group_fields, fields = partition(is_reified_group, fields)
fields.append('groups_id')
res = super(users_view, self).read(cr, uid, ids, fields, context=context, load=load)
for values in (res if isinstance(res, list) else [res]):
self._get_reified_groups(group_fields, values)
return res
def fields_view_get(self, cr, uid, view_id=None, view_type='form',
context=None, toolbar=False, submenu=False):
# in form views, transform 'groups_id' into reified group fields
res = super(users_view, self).fields_view_get(cr, uid, view_id, view_type,
context, toolbar, submenu)
if view_type == 'form':
root = etree.fromstring(encode(res['arch']))
nodes = root.xpath("//field[@name='groups_id']")
if nodes:
# replace node by the reified group fields
fields = res['fields']
elems = []
apps, others = self.pool.get('res.groups').get_classified(cr, uid, context)
# create section Applications
elems.append('<separator colspan="6" string="%s"/>' % _('Applications'))
for app, groups in apps:
ids = [g.id for name, g in groups]
app_name = name_boolean_groups(ids)
sel_name = name_selection_groups(ids)
selection = [(g.id, name) for name, g in groups]
fields[app_name] = {'type': 'boolean', 'string': app}
tips = [name + ': ' + (g.comment or '') for name, g in groups]
if tips:
fields[app_name].update(help='\n'.join(tips))
fields[sel_name] = {'type': 'selection', 'string': 'Group', 'selection': selection}
attrs = {'invisible': [('%s' % app_name, '=', False)]}
elems.append("""
<field name="%(app)s"/>
<field name="%(sel)s" nolabel="1" colspan="2"
attrs=%(attrs)s modifiers=%(json_attrs)s/>
<newline/>
""" % {'app': app_name, 'sel': sel_name,
'attrs': quoteattr(str(attrs)),
'json_attrs': quoteattr(simplejson.dumps(attrs))})
# create other sections
for sec, groups in others:
elems.append('<separator colspan="6" string="%s"/>' % sec)
for gname, g in groups:
name = name_boolean_group(g.id)
fields[name] = {'type': 'boolean', 'string': gname}
if g.comment:
fields[name].update(help=g.comment)
elems.append('<field name="%s"/>' % name)
elems.append('<newline/>')
# replace xml node by new arch
new_node = etree.fromstring('<group col="6">' + ''.join(elems) + '</group>')
for node in nodes:
node.getparent().replace(node, new_node)
res['arch'] = etree.tostring(root)
def _get_reified_groups(self, fields, values):
""" compute the given reified group fields from values['groups_id'] """
gids = set(values.get('groups_id') or [])
for f in fields:
if is_boolean_group(f):
values[f] = get_boolean_group(f) in gids
elif is_boolean_groups(f):
values[f] = not gids.isdisjoint(get_boolean_groups(f))
elif is_selection_groups(f):
selected = [gid for gid in get_selection_groups(f) if gid in gids]
values[f] = selected and selected[-1] or False
def fields_get(self, cr, uid, allfields=None, context=None, write_access=True):
res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access)
# add reified groups fields
for app, kind, gs in self.pool.get('res.groups').get_groups_by_application(cr, uid, context):
if kind == 'selection':
# selection group field
tips = ['%s: %s' % (g.name, g.comment or '') for g in gs]
res[name_selection_groups(map(int, gs))] = {
'type': 'selection',
'string': app and app.name or _('Other'),
'selection': [(False, '')] + [(g.id, g.name) for g in gs],
'help': '\n'.join(tips),
}
else:
# boolean group fields
for g in gs:
res[name_boolean_group(g.id)] = {
'type': 'boolean',
'string': g.name,
'help': g.comment,
}
return res
users_view()

View File

@ -63,6 +63,9 @@
<!-- In case the action is an act_window,
overrides its own @views. -->
<rng:optional><rng:attribute name="view_mode"/></rng:optional>
<!-- Add a 'Create' button in order to create a new resource of the action's model
values : [true|false|<ID of specific action view>]. -->
<rng:optional><rng:attribute name="creatable"/></rng:optional>
</rng:element>
</rng:zeroOrMore>
</rng:element>
@ -534,6 +537,10 @@
<rng:optional><rng:attribute name="filters"/></rng:optional>
<rng:optional><rng:attribute name="statusbar_visible"/></rng:optional>
<rng:optional><rng:attribute name="statusbar_colors"/></rng:optional>
<!-- Widget *static* options defined as an arbitrary JSON dict, with
widget-dependent parameters. To be ignored if widget/client does
not support them. -->
<rng:optional><rng:attribute name="options"/></rng:optional>
<rng:zeroOrMore>
<rng:choice>
<rng:ref name="diagram"/>

View File

@ -4,36 +4,37 @@
<!--
Users Groups
[Note] Field 'category_id' is set later in base/module/module_data.xml
-->
<record model="res.groups" id="group_erp_manager">
<field name="name">Administration / Access Rights</field>
<field name="name">Access Rights</field>
</record>
<record model="res.groups" id="group_system">
<field name="name">Administration / Configuration</field>
<field name="name">Configuration</field>
<field name="implied_ids" eval="[(4, ref('group_erp_manager'))]"/>
</record>
<record model="res.groups" id="group_user">
<field name="name">Human Resources / Employee</field>
<field name="name">Employee</field>
</record>
<record model="res.groups" context="{'noadmin':True}" id="group_multi_company">
<field name="name">Useability / Multi Companies</field>
<field name="name">Multi Companies</field>
</record>
<record model="res.groups" context="{'noadmin':True}" id="group_extended">
<field name="name">Useability / Extended View</field>
<field name="name">Extended View</field>
</record>
<record model="res.groups" id="group_no_one" context="{'noadmin':True}">
<field name="name">Useability / Technical Features</field>
<field name="name">Technical Features</field>
</record>
<record id="group_sale_salesman" context="{'noadmin':True}" model="res.groups">
<field name="name">Sales / User</field>
<field name="name">User</field>
</record>
<record id="group_sale_manager" context="{'noadmin':True}" model="res.groups">
<field name="name">Sales / Manager</field>
<field name="name">Manager</field>
<field name="implied_ids" eval="[(4, ref('group_sale_salesman'))]"/>
</record>

View File

@ -0,0 +1,43 @@
.oe_module_vignette {
padding: 6px;
min-height: 100px;
}
.oe_module_icon, .oe_module_desc {
display: inline-block;
vertical-align: top;
}
.oe_module_icon {
width: 80px;
height: 80px;
padding: 0 4px;
}
.oe_module_desc {
width: 220px;
font-size: 13px;
padding: 2px 5px;
color: #4c4c4c;
}
.oe_module_desc h4 {
margin: 0;
font-size: 13px;
}
.oe_module_desc h4 a {
color: #4c4c4c;
}
.oe_module_desc h4 a:hover {
text-decoration: underline;
}
.oe_module_desc p {
margin: 3px 0 5px;
}
.oe_module_desc .oe_button {
min-width: 70px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -144,3 +144,163 @@
!python {model: res.partner.category}: |
self.pool._init = True
-
"Float precision tests: verify that float rounding methods are working correctly via res.currency"
-
!python {model: res.currency}: |
from tools import float_repr
from math import log10
currency = self.browse(cr, uid, ref('base.EUR'))
def try_round(amount, expected, self=self, cr=cr, currency=currency, float_repr=float_repr,
log10=log10):
digits = max(0,-int(log10(currency.rounding)))
result = float_repr(self.round(cr, 1, currency, amount), precision_digits=digits)
assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected)
try_round(2.674,'2.67')
try_round(2.675,'2.68') # in Python 2.7.2, round(2.675,2) gives 2.67
try_round(-2.675,'-2.68') # in Python 2.7.2, round(2.675,2) gives 2.67
try_round(0.001,'0.00')
try_round(-0.001,'-0.00')
try_round(0.0049,'0.00') # 0.0049 is closer to 0 than to 0.01, so should round down
try_round(0.005,'0.01') # the rule is to round half away from zero
try_round(-0.005,'-0.01') # the rule is to round half away from zero
def try_zero(amount, expected, self=self, cr=cr, currency=currency):
assert self.is_zero(cr, 1, currency, amount) == expected, "Rounding error: %s should be zero!" % amount
try_zero(0.01, False)
try_zero(-0.01, False)
try_zero(0.001, True)
try_zero(-0.001, True)
try_zero(0.0046, True)
try_zero(-0.0046, True)
try_zero(2.68-2.675, False) # 2.68 - 2.675 = 0.005 -> rounds to 0.01
try_zero(2.68-2.676, True) # 2.68 - 2.675 = 0.004 -> rounds to 0.0
try_zero(2.676-2.68, True) # 2.675 - 2.68 = -0.004 -> rounds to -0.0
try_zero(2.675-2.68, False) # 2.675 - 2.68 = -0.005 -> rounds to -0.01
def try_compare(amount1, amount2, expected, self=self, cr=cr, currency=currency):
assert self.compare_amounts(cr, 1, currency, amount1, amount2) == expected, \
"Rounding error, compare_amounts(%s,%s) should be %s" % (amount1, amount2, expected)
try_compare(0.001, 0.001, 0)
try_compare(-0.001, -0.001, 0)
try_compare(0.001, 0.002, 0)
try_compare(-0.001, -0.002, 0)
try_compare(2.675, 2.68, 0)
try_compare(2.676, 2.68, 0)
try_compare(-2.676, -2.68, 0)
try_compare(2.674, 2.68, -1)
try_compare(-2.674, -2.68, 1)
try_compare(3, 2.68, 1)
try_compare(-3, -2.68, -1)
try_compare(0.01, 0, 1)
try_compare(-0.01, 0, -1)
-
"Float precision tests: verify that float rounding methods are working correctly via tools"
-
!python {model: res.currency}: |
from tools import float_compare, float_is_zero, float_round, float_repr
def try_round(amount, expected, precision_digits=3, float_round=float_round, float_repr=float_repr):
result = float_repr(float_round(amount, precision_digits=precision_digits),
precision_digits=precision_digits)
assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected)
try_round(2.6745, '2.675')
try_round(-2.6745, '-2.675')
try_round(2.6744, '2.674')
try_round(-2.6744, '-2.674')
try_round(0.0004, '0.000')
try_round(-0.0004, '-0.000')
try_round(357.4555, '357.456')
try_round(-357.4555, '-357.456')
try_round(457.4554, '457.455')
try_round(-457.4554, '-457.455')
# Extended float range test, inspired by Cloves Almeida's test on bug #882036.
fractions = [.0, .015, .01499, .675, .67499, .4555, .4555, .45555]
expecteds = ['.00', '.02', '.01', '.68', '.67', '.46', '.456', '.4556']
precisions = [2, 2, 2, 2, 2, 2, 3, 4]
# Note: max precision for double floats is 53 bits of precision or
# 17 significant decimal digits
for magnitude in range(7):
for i in xrange(len(fractions)):
frac, exp, prec = fractions[i], expecteds[i], precisions[i]
for sign in [-1,1]:
for x in xrange(0,10000,97):
n = x * 10**magnitude
f = sign * (n + frac)
f_exp = ('-' if f != 0 and sign == -1 else '') + str(n) + exp
try_round(f, f_exp, precision_digits=prec)
def try_zero(amount, expected, float_is_zero=float_is_zero):
assert float_is_zero(amount, precision_digits=3) == expected, "Rounding error: %s should be zero!" % amount
try_zero(0.0002, True)
try_zero(-0.0002, True)
try_zero(0.00034, True)
try_zero(0.0005, False)
try_zero(-0.0005, False)
try_zero(0.0008, False)
try_zero(-0.0008, False)
def try_compare(amount1, amount2, expected, float_compare=float_compare):
assert float_compare(amount1, amount2, precision_digits=3) == expected, \
"Rounding error, compare_amounts(%s,%s) should be %s" % (amount1, amount2, expected)
try_compare(0.0003, 0.0004, 0)
try_compare(-0.0003, -0.0004, 0)
try_compare(0.0002, 0.0005, -1)
try_compare(-0.0002, -0.0005, 1)
try_compare(0.0009, 0.0004, 1)
try_compare(-0.0009, -0.0004, -1)
try_compare(557.4555, 557.4556, 0)
try_compare(-557.4555, -557.4556, 0)
try_compare(657.4444, 657.445, -1)
try_compare(-657.4444, -657.445, 1)
# Rounding to unusual rounding units (e.g. coin values)
def try_round(amount, expected, precision_rounding=None, float_round=float_round, float_repr=float_repr):
result = float_repr(float_round(amount, precision_rounding=precision_rounding),
precision_digits=2)
assert result == expected, 'Rounding error: got %s, expected %s' % (result, expected)
try_round(-457.4554, '-457.45', precision_rounding=0.05)
try_round(457.444, '457.50', precision_rounding=0.5)
try_round(457.3, '455.00', precision_rounding=5)
try_round(457.5, '460.00', precision_rounding=5)
try_round(457.1, '456.00', precision_rounding=3)
-
"Float precision tests: check that proper rounding is performed for float persistence"
-
!python {model: res.currency}: |
currency = self.browse(cr, uid, ref('base.EUR'))
res_currency_rate = self.pool.get('res.currency.rate')
from tools import float_compare, float_is_zero, float_round, float_repr
def try_roundtrip(value, expected, self=self, cr=cr, currency=currency,
res_currency_rate=res_currency_rate):
rate_id = res_currency_rate.create(cr, 1, {'name':'2000-01-01',
'rate': value,
'currency_id': currency.id})
rate = res_currency_rate.read(cr, 1, rate_id, ['rate'])['rate']
assert rate == expected, 'Roundtrip error: got %s back from db, expected %s' % (rate, expected)
# res.currency.rate uses 6 digits of precision by default
try_roundtrip(2.6748955, 2.674896)
try_roundtrip(-2.6748955, -2.674896)
try_roundtrip(10000.999999, 10000.999999)
try_roundtrip(-10000.999999, -10000.999999)
-
"Float precision tests: verify that invalid parameters are forbidden"
-
!python {model: res.currency}: |
from tools import float_compare, float_is_zero, float_round
try:
float_is_zero(0.01, precision_digits=3, precision_rounding=0.01)
except AssertionError:
pass
try:
float_compare(0.01, 0.02, precision_digits=3, precision_rounding=0.01)
except AssertionError:
pass
try:
float_round(0.01, precision_digits=3, precision_rounding=0.01)
except AssertionError:
pass

View File

@ -75,18 +75,18 @@ def initialize(cr):
cr.execute('INSERT INTO ir_module_module \
(author, website, name, shortdesc, description, \
category_id, state, certificate, web, license, complexity) \
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', (
category_id, state, certificate, web, license, complexity, application, icon) \
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id', (
info['author'],
info['website'], i, info['name'],
info['description'], category_id, state, info['certificate'],
info['web'],
info['license'],
info['complexity']))
info['complexity'], info['application'], info['icon']))
id = cr.fetchone()[0]
cr.execute('INSERT INTO ir_model_data \
(name,model,module, res_id, noupdate) VALUES (%s,%s,%s,%s,%s)', (
'module_meta_information', 'ir.module.module', i, id, True))
'module_'+i, 'ir.module.module', 'base', id, True))
dependencies = info['depends']
for d in dependencies:
cr.execute('INSERT INTO ir_module_module_dependency \

View File

@ -147,7 +147,7 @@ class Graph(dict):
level = 0
done = set(self.keys())
while done:
level_modules = [(name, module) for name, module in self.items() if module.depth==level]
level_modules = sorted((name, module) for name, module in self.items() if module.depth==level)
for name, module in level_modules:
done.remove(name)
yield module

Some files were not shown because too many files have changed in this diff Show More