[MERGE] merged trunk.
bzr revid: vmt@openerp.com-20120104131010-kbw2ej2rrdvyif96
This commit is contained in:
commit
389257c009
64
README
64
README
|
@ -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.
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 201 KiB |
Binary file not shown.
After Width: | Height: | Size: 139 KiB |
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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>
|
|
@ -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
|
@ -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"/>
|
||||
|
|
|
@ -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."),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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),..])
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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','<>', 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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue