[MERGE] sync w/ latest trunk

bzr revid: odo@openerp.com-20110929100200-cq90mv3d3tymluyw
This commit is contained in:
Olivier Dony 2011-09-29 12:02:00 +02:00
commit acecf5e71e
130 changed files with 23696 additions and 5583 deletions

View File

@ -1,18 +1,12 @@
include rpminstall_sh.txt # TODO do we need this file ?
include README
include LICENSE
include MANIFEST.in
include setup.nsi
include setup.cfg
#include openerp/server.cert
#include openerp/server.pkey
#include openerp/gpl.txt
include man/openerp-server.1
include man/openerp_serverrc.5
recursive-include pixmaps *bmp *ico *png
include setup_rpm.sh
recursive-include win32 *.py *.bat
recursive-include openerp *css *csv *html *png *po *pot
recursive-include openerp *rml *rng *sql *sxw *xml *xsl *yml
recursive-include openerp *css *csv *html *png *po *pot *rml *rng *sql *sxw *xml *xsl *yml
graft install
graft debian
graft doc
global-exclude *pyc *~ # Exclude possible garbage from previous graft.

155
README
View File

@ -1,17 +1,138 @@
About OpenERP
---------------
OpenERP is a free Enterprise Resource Planning and Customer Relationship
Management software. It is mainly developed to meet changing needs.
The main functional features are: CRM & SRM, analytic and financial accounting,
double-entry stock management, sales and purchases management, tasks automation,
help desk, marketing campaign, ... and vertical modules for very specific
businesses.
Technical features include a distributed server, flexible workflows, an object
database, dynamic GUIs, customizable reports, NET-RPC and XML-RPC interfaces, ...
For more information, please visit:
http://www.openerp.com
About OpenERP
-------------
OpenERP is a free Enterprise Resource Planning and Customer Relationship
Management software. It is mainly developed to meet changing needs.
The main functional features are: CRM & SRM, analytic and financial accounting,
double-entry stock management, sales and purchases management, tasks automation,
help desk, marketing campaign, ... and vertical modules for very specific
businesses.
Technical features include a distributed server, flexible workflows, an object
database, dynamic GUIs, customizable reports, NET-RPC and XML-RPC interfaces, ...
For more information, please visit:
http://www.openerp.com
OpenERP Quick Installation Guide
---------------------------------
This file contains a quick guide to configure and install the OpenERP server.
Required dependencies:
---------------------
You need the following software installed:
* Python 2.5 or 2.6
* Postgresql 8.2 or above
* Psycopg2 python module
* Reportlab pdf generation library for python
* lxml python module
* pytz python module
* PyYaml python module (install with: easy_install PyYaml)
Some dependencies are only required for specific purposes:
for rendering workflows graphs, you need:
* graphviz
* pyparsing
For Luxembourg localization, you also need:
* pdftk (http://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/)
for generating reports using non .jpg images, you need:
* Python Imaging Library for python
For Debian-based distributions, the required packages can be installed with the
following command:
#> apt-get install -y postgresql graphviz python-psycopg2 python-lxml python-tz python-imaging
For Fedora
if they are not installed, install:
python and postgresql
uses yum or you can recover required packages on fedora web site in "core" or "extra" repository :
postgresql-python
python-lxml
python-imaging
python-psycopg2
python-reportlab
graphviz
You can find pyparsing at http://pyparsing.sourceforge.net/
1. Check that all the required dependencies are installed.
2. Launch the program "python ./bin/openerp-server.py -r db_user -w db_password --db_host 127.0.0.1".
See the man page for more information about options.
3. Connect to the server using the GUI client. And follow the instructions to create a new database.
Installation Steps
------------------
1. Check that all the required dependencies are installed.
2. Create a postgresql database.
The default database name is "terp". If you want to use another name, you
will need to provide it when launching the server (by using the commandline
option --database).
To create a postgresql database named "terp" using the following command:
$ createdb --encoding=UNICODE terp
If it is the first time you use postgresql you might need to create a new user
to the postgres system using the following commands (where myusername is your
unix user name):
$ su -
# su - postgres
$ createuser openerp
Shall the new user be allowed to create databases? (y/n) y
Shall the new user be allowed to create more new users? (y/n) y
CREATE USER
$ logout
# logout
3. Launch service daemon by "service openerp-server start".
The first time it is run, the server will initialise the database with all the default values.
4. Connect to the server using the GUI client.
There are two accounts by default:
* login: admin, password:admin
* login: demo, password:demo
Some instructions to use setup.py for a user-install.
This file should/will be moved on a proper documentation place later.
- Possibly clean any left-over of the previous build.
> rm -rf dist openerp_server.egg-info
- Possibly copy the addons in the server if we want them to be packaged
together:
> rsync -av --delete \
--exclude .bzr/ \
--exclude .bzrignore \
--exclude /__init__.py \
--exclude /base \
--exclude /base_quality_interrogation.py \
<path-to-addons> openerp/addons
- Create the user-local directory where we want the package to be installed:
> mkdir -p /home/openerp/openerp-tmp/lib/python2.6/site-packages/
- Use --prefix to specify where the package is installed and include that
place in PYTHONPATH:
> PYTHONPATH=/home/openerp/openerp-tmp/lib/python2.6/site-packages/ \
python setup.py install --prefix=/home/openerp/openerp-tmp
- Run the main script, again specifying the PYTHONPATH:
> PYTHONPATH=/home/openerp/openerp-tmp/lib/python2.6/site-packages/ \
/home/openerp/openerp-tmp/bin/openerp-server

17
gunicorn.conf.py Normal file
View File

@ -0,0 +1,17 @@
import openerp
# Standard OpenERP XML-RPC port.
bind = '127.0.0.1:8069'
pidfile = '.gunicorn.pid'
# This is the big TODO: safely use more than a single worker.
workers = 1
# Some application-wide initialization is needed.
on_starting = openerp.wsgi.on_starting
when_ready = openerp.wsgi.when_ready
timeout = 240 # openerp request-response cycle can be quite long
# Setting openerp.conf.xxx will be better than setting
# openerp.tools.config['xxx']
conf = openerp.tools.config
conf['addons_path'] = '/home/openerp/repos/addons/trunk-xmlrpc'
conf['static_http_document_root'] = '/tmp'
#conf['log_level'] = 10 # 10 is DEBUG

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -27,7 +27,7 @@ OpenERP is an ERP+CRM program for small and medium businesses.
The whole source code is distributed under the terms of the
GNU Public Licence.
(c) 2003-TODAY, Fabien Pinckaers - OpenERP s.a.
(c) 2003-TODAY, Fabien Pinckaers - OpenERP SA
"""
import logging
@ -88,19 +88,27 @@ def setup_pid_file():
def preload_registry(dbname):
""" Preload a registry, and start the cron."""
db, pool = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False)
pool.get('ir.cron').restart(db.dbname)
try:
db, registry = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False)
# jobs will start to be processed later, when openerp.cron.start_master_thread() is called by openerp.service.start_services()
registry.schedule_cron_jobs()
except Exception:
logging.exception('Failed to initialize database `%s`.', dbname)
def run_test_file(dbname, test_file):
""" Preload a registry, possibly run a test file, and start the cron."""
db, pool = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False)
try:
db, registry = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False)
cr = db.cursor()
logger = logging.getLogger('server')
logger.info('loading test file %s', test_file)
openerp.tools.convert_yaml_import(cr, 'base', file(test_file), {}, 'test', True)
cr.rollback()
cr.close()
except Exception:
logging.exception('Failed to initialize database `%s` and run test file `%s`.', dbname, test_file)
cr = db.cursor()
logger = logging.getLogger('server')
logger.info('loading test file %s', test_file)
openerp.tools.convert_yaml_import(cr, 'base', file(test_file), {}, 'test', True)
cr.rollback()
cr.close()
def export_translation():
config = openerp.tools.config
@ -136,27 +144,6 @@ def import_translation():
cr.commit()
cr.close()
def start_services():
http_server = openerp.service.http_server
netrpc_server = openerp.service.netrpc_server
# Instantiate local services (this is a legacy design).
openerp.osv.osv.start_object_proxy()
# Export (for RPC) services.
openerp.service.web_services.start_web_services()
# Initialize the HTTP stack.
http_server.init_servers()
http_server.init_xmlrpc()
http_server.init_static_http()
netrpc_server.init_servers()
# Start the main cron thread.
openerp.netsvc.start_agent()
# Start the top-level servers threads (normally HTTP, HTTPS, and NETRPC).
openerp.netsvc.Server.startAll()
# Variable keeping track of the number of calls to the signal handler defined
# below. This variable is monitored by ``quit_on_signals()``.
quit_signals_received = 0
@ -208,30 +195,16 @@ def quit_on_signals():
while quit_signals_received == 0:
time.sleep(60)
openerp.netsvc.Agent.quit()
openerp.netsvc.Server.quitAll()
config = openerp.tools.config
if config['pidfile']:
os.unlink(config['pidfile'])
logger = logging.getLogger('server')
logger.info("Initiating shutdown")
logger.info("Hit CTRL-C again or send a second signal to force the shutdown.")
logging.shutdown()
# manually join() all threads before calling sys.exit() to allow a second signal
# to trigger _force_quit() in case some non-daemon threads won't exit cleanly.
# threading.Thread.join() should not mask signals (at least in python 2.5)
for thread in threading.enumerate():
if thread != threading.currentThread() and not thread.isDaemon():
while thread.isAlive():
# need a busyloop here as thread.join() masks signals
# and would present the forced shutdown
thread.join(0.05)
time.sleep(0.05)
openerp.service.stop_services()
sys.exit(0)
if __name__ == "__main__":
os.environ["TZ"] = "UTC"
check_root_user()
openerp.tools.config.parse_config(sys.argv[1:])
check_postgres_user()
@ -257,7 +230,7 @@ if __name__ == "__main__":
if not config["stop_after_init"]:
# Some module register themselves when they are loaded so we need the
# services to be running before loading any registry.
start_services()
openerp.service.start_services()
if config['db_name']:
for dbname in config['db_name'].split(','):
@ -266,6 +239,16 @@ if __name__ == "__main__":
if config["stop_after_init"]:
sys.exit(0)
for m in openerp.conf.server_wide_modules:
try:
__import__(m)
# Call any post_load hook.
info = openerp.modules.module.load_information_from_description_file(m)
if info['post_load']:
getattr(sys.modules[m], info['post_load'])()
except Exception:
logging.exception('Failed to load server-wide module `%s`', m)
setup_pid_file()
logger = logging.getLogger('server')
logger.info('OpenERP server is running, waiting for connections...')

View File

@ -22,6 +22,8 @@
""" OpenERP core library.
"""
# The hard-coded super-user id (a.k.a. administrator, or root user).
SUPERUSER_ID = 1
import addons
import conf
@ -41,6 +43,7 @@ import tiny_socket
import tools
import wizard
import workflow
import wsgi
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -22,10 +22,15 @@
""" Addons module.
This module only serves to contain OpenERP addons. For the code to
manage those addons, see openerp.modules. This module conveniently
reexports some symbols from openerp.modules. Importing them from here
is deprecated.
This module serves to contain all OpenERP addons, across all configured addons
paths. For the code to manage those addons, see openerp.modules.
Addons are made available here (i.e. under openerp.addons) after
openerp.tools.config.parse_config() is called (so that the addons paths
are known).
This module also conveniently reexports some symbols from openerp.modules.
Importing them from here is deprecated.
"""

View File

@ -24,6 +24,7 @@ import module
import res
import publisher_warranty
import report
import test
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -93,7 +93,10 @@
'test/test_ir_rule.yml', # <-- These tests modify/add/delete ir_rules.
'test/test_ir_values.yml',
'test/test_edi_documents.yml',
# Commented because this takes some time.
# This must be (un)commented with the corresponding import statement
# in test/__init__.py.
# 'test/test_ir_cron.yml', # <-- These tests perform a roolback.
],
'installable': True,
'active': True,

View File

@ -1006,7 +1006,7 @@
</record>
<record id="main_partner" model="res.partner">
<field name="name">OpenERP S.A.</field>
<field name="name">Company Name</field>
<!-- Address and Company ID will be set later -->
<field name="address" eval="[]"/>
<field name="company_id" eval="None"/>
@ -1014,13 +1014,13 @@
</record>
<record id="main_address" model="res.partner.address">
<field name="partner_id" ref="main_partner"/>
<field name="name">Fabien Pinckaers</field>
<field name="street">Chaussee de Namur 40</field>
<field name="zip">1367</field>
<field name="city">Gerompont</field>
<field name="phone">(+32).81.81.37.00</field>
<field name="name">Company contact name</field>
<field name="street">Company street, number</field>
<field name="zip">Company zip</field>
<field name="city">Company city</field>
<field name="phone">+1-212-555-12345</field>
<field name="type">default</field>
<field model="res.country" name="country_id" ref="be"/>
<field model="res.country" name="country_id" ref="us"/>
<!-- Company ID will be set later -->
<field name="company_id" eval="None"/>
</record>
@ -1042,19 +1042,14 @@
<!-- Basic Company -->
<record id="main_company" model="res.company">
<field name="name">OpenERP S.A.</field>
<field name="name">Company Name</field>
<field name="partner_id" ref="main_partner"/>
<field name="rml_header1">Free Business Solutions</field>
<field name="rml_footer1">Web: http://www.openerp.com - Tel: (+32).81.81.37.00 - Bank: CPH 126-2013269-07</field>
<field name="rml_footer2">IBAN: BE74 1262 0132 6907 - SWIFT: CPHBBE75 - VAT: BE0477.472.701</field>
<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"/>
</record>
<assert id="main_company" model="res.company">
<test expr="currency_id.name == 'eur'.upper()"/>
<test expr="name">OpenERP S.A.</test>
</assert>
<record model="res.users" id="base.user_root">
<field name="signature">Administrator</field>
<field name="company_id" ref="main_company"/>
@ -1090,16 +1085,17 @@
<field eval="time.strftime('%Y-01-01')" name="name"/>
</record>
<record id="VEB" model="res.currency">
<field name="name">VEB</field>
<field name="symbol">Bs</field>
<field name="rounding">2.95</field>
<!-- VEF was previously VEB -->
<record id="VEF" model="res.currency">
<field name="name">VEF</field>
<field name="symbol">Bs.F</field>
<field name="rounding">0.0001</field>
<field name="accuracy">4</field>
<field name="company_id" ref="main_company"/>
</record>
<record id="rateVEB" model="res.currency.rate">
<field name="rate">2768.45</field>
<field name="currency_id" ref="VEB"/>
<record id="rateVEF" model="res.currency.rate">
<field name="rate">5.864</field>
<field name="currency_id" ref="VEF"/>
<field eval="time.strftime('%Y-01-01')" name="name"/>
</record>
@ -1603,6 +1599,7 @@
<field name="rounding">0.01</field>
<field name="accuracy">4</field>
<field name="symbol">¢</field>
<field name="company_id" ref="main_company"/>
</record>
<record id="rateCRC" model="res.currency.rate">
<field name="rate">691.3153</field>

View File

@ -75,29 +75,29 @@
-->
<record id="view_users_form_simple_modif" model="ir.ui.view">
<field name="name">res.users.form.modif</field>
<field name="name">res.users.preferences.form</field>
<field name="model">res.users</field>
<field name="type">form</field>
<field eval="18" name="priority"/>
<field name="arch" type="xml">
<form string="Users">
<field name="name"/>
<field name="name" readonly="1"/>
<newline/>
<group colspan="2" col="2">
<separator string="Preferences" colspan="2"/>
<field name="view"/>
<field name="context_lang"/>
<field name="context_tz"/>
<field name="menu_tips"/>
<field name="view" readonly="0"/>
<field name="context_lang" readonly="0"/>
<field name="context_tz" readonly="0"/>
<field name="menu_tips" readonly="0"/>
</group>
<group name="default_filters" colspan="2" col="2">
<separator string="Default Filters" colspan="2"/>
<field name="company_id" widget="selection"
<field name="company_id" widget="selection" readonly="0"
groups="base.group_multi_company" on_change="on_change_company_id(company_id)"/>
</group>
<separator string="Email Preferences" colspan="4"/>
<field colspan="4" name="user_email" widget="email"/>
<field colspan="4" name="signature"/>
<field colspan="4" name="user_email" widget="email" readonly="0"/>
<field colspan="4" name="signature" readonly="0"/>
</form>
</field>
</record>
@ -147,7 +147,7 @@
<page string="Access Rights">
<field nolabel="1" name="groups_id"/>
</page>
<page string="Companies" groups="base.group_multi_company">
<page string="Allowed Companies" groups="base.group_multi_company">
<field colspan="4" nolabel="1" name="company_ids" select="1"/>
</page>
</notebook>

View File

@ -7,14 +7,14 @@ msgstr ""
"Project-Id-Version: openobject-server\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2011-01-11 11:14+0000\n"
"PO-Revision-Date: 2011-09-13 11:47+0000\n"
"PO-Revision-Date: 2011-09-16 16:25+0000\n"
"Last-Translator: Jiří Hajda <robie@centrum.cz>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2011-09-14 04:40+0000\n"
"X-Generator: Launchpad (build 13921)\n"
"X-Launchpad-Export-Date: 2011-09-17 04:54+0000\n"
"X-Generator: Launchpad (build 13955)\n"
"X-Poedit-Language: Czech\n"
#. module: base
@ -3812,7 +3812,7 @@ msgstr ""
#. module: base
#: view:publisher_warranty.contract.wizard:0
msgid "Please enter the serial key provided in your contract document:"
msgstr "Prosíme zadejte sériové číslo poskytnuté ve dokumentu vaší smlouvy:"
msgstr "Prosíme zadejte sériové číslo poskytnuté v dokumentu vaší smlouvy:"
#. module: base
#: view:workflow.activity:0

File diff suppressed because it is too large Load Diff

View File

@ -8,14 +8,14 @@ msgstr ""
"Project-Id-Version: openobject-server\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2011-01-11 11:14+0000\n"
"PO-Revision-Date: 2011-07-28 15:35+0000\n"
"PO-Revision-Date: 2011-09-22 14:47+0000\n"
"Last-Translator: John Bradshaw <Unknown>\n"
"Language-Team: English (United Kingdom) <en_GB@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2011-09-01 04:44+0000\n"
"X-Generator: Launchpad (build 13827)\n"
"X-Launchpad-Export-Date: 2011-09-23 04:38+0000\n"
"X-Generator: Launchpad (build 14012)\n"
#. module: base
#: view:ir.filters:0
@ -1972,7 +1972,7 @@ msgstr "Iteration Actions"
#. module: base
#: help:multi_company.default,company_id:0
msgid "Company where the user is connected"
msgstr ""
msgstr "Company where the user is connected"
#. module: base
#: field:publisher_warranty.contract,date_stop:0
@ -2583,7 +2583,7 @@ msgstr "Search Actions"
#: model:ir.actions.act_window,name:base.action_view_partner_wizard_ean_check
#: view:partner.wizard.ean.check:0
msgid "Ean check"
msgstr ""
msgstr "Ean check"
#. module: base
#: field:res.partner,vat:0
@ -2623,7 +2623,7 @@ msgstr "GPL-2 or later version"
#. module: base
#: model:res.partner.title,shortcut:base.res_partner_title_sir
msgid "M."
msgstr ""
msgstr "M."
#. module: base
#: code:addons/base/module/module.py:429
@ -3001,7 +3001,7 @@ msgstr "License"
#. module: base
#: field:ir.attachment,url:0
msgid "Url"
msgstr ""
msgstr "Url"
#. module: base
#: selection:ir.actions.todo,restart:0
@ -3153,7 +3153,7 @@ msgstr "Workflows"
#. module: base
#: field:ir.translation,xml_id:0
msgid "XML Id"
msgstr ""
msgstr "XML Id"
#. module: base
#: model:ir.actions.act_window,name:base.action_config_user_form
@ -3224,7 +3224,7 @@ msgstr "Abkhazian / аҧсуа"
#. module: base
#: view:base.module.configuration:0
msgid "System Configuration Done"
msgstr ""
msgstr "System Configuration Done"
#. module: base
#: code:addons/orm.py:929
@ -3271,7 +3271,7 @@ msgstr "That contract is already registered in the system."
#. module: base
#: help:ir.sequence,suffix:0
msgid "Suffix value of the record for the sequence"
msgstr ""
msgstr "Suffix value of the record for the sequence"
#. module: base
#: selection:base.language.install,lang:0
@ -3343,7 +3343,7 @@ msgstr "Installed"
#. module: base
#: selection:base.language.install,lang:0
msgid "Ukrainian / українська"
msgstr ""
msgstr "Ukrainian / українська"
#. module: base
#: model:ir.actions.act_window,name:base.action_translation
@ -3389,7 +3389,7 @@ msgstr "Next Number"
#. module: base
#: help:workflow.transition,condition:0
msgid "Expression to be satisfied if we want the transition done."
msgstr ""
msgstr "Expression to be satisfied if we want the transition done."
#. module: base
#: selection:base.language.install,lang:0
@ -3518,6 +3518,12 @@ msgid ""
"Would your payment have been carried out after this mail was sent, please "
"consider the present one as void."
msgstr ""
"Please note that the following payments are now due. If your payment "
" has been sent, kindly forward your payment details. If "
"payment will be delayed, please contact us to "
"discuss. \n"
"If payment was performed after this mail was sent, please consider the "
"present one as void."
#. module: base
#: model:res.country,name:base.mx
@ -3673,6 +3679,8 @@ msgid ""
"If set to true, the action will not be displayed on the right toolbar of a "
"form view"
msgstr ""
"If set to true, the action will not be displayed on the right toolbar of a "
"form view"
#. module: base
#: model:res.country,name:base.ms
@ -3717,7 +3725,7 @@ msgstr "English (UK)"
#. module: base
#: selection:base.language.install,lang:0
msgid "Japanese / 日本語"
msgstr ""
msgstr "Japanese / 日本語"
#. module: base
#: help:workflow.transition,act_from:0
@ -3725,6 +3733,8 @@ msgid ""
"Source activity. When this activity is over, the condition is tested to "
"determine if we can start the ACT_TO activity."
msgstr ""
"Source activity. When this activity is over, the condition is tested to "
"determine if we can start the ACT_TO activity."
#. module: base
#: model:res.partner.category,name:base.res_partner_category_3
@ -3885,7 +3895,7 @@ msgstr "Init Date"
#. module: base
#: selection:base.language.install,lang:0
msgid "Gujarati / ગુજરાતી"
msgstr ""
msgstr "Gujarati / ગુજરાતી"
#. module: base
#: code:addons/base/module/module.py:257
@ -3953,6 +3963,9 @@ msgid ""
"form, signal tests the name of the pressed button. If signal is NULL, no "
"button is necessary to validate this transition."
msgstr ""
"When the operation of transition comes from a button press in the client "
"form, signal tests the name of the pressed button. If signal is NULL, no "
"button is necessary to validate this transition."
#. module: base
#: help:multi_company.default,object_id:0
@ -3972,7 +3985,7 @@ msgstr "Menu Name"
#. module: base
#: view:ir.module.module:0
msgid "Author Website"
msgstr ""
msgstr "Author Website"
#. module: base
#: view:ir.attachment:0
@ -4012,6 +4025,8 @@ msgid ""
"Whether values for this field can be translated (enables the translation "
"mechanism for that field)"
msgstr ""
"Whether values for this field can be translated (enables the translation "
"mechanism for that field)"
#. module: base
#: view:res.lang:0
@ -4072,13 +4087,13 @@ msgstr "Price Accuracy"
#. module: base
#: selection:base.language.install,lang:0
msgid "Latvian / latviešu valoda"
msgstr ""
msgstr "Latvian / latviešu valoda"
#. module: base
#: view:res.config:0
#: view:res.config.installer:0
msgid "vsep"
msgstr ""
msgstr "vsep"
#. module: base
#: selection:base.language.install,lang:0
@ -4099,7 +4114,7 @@ msgstr "Workitem"
#. module: base
#: view:ir.actions.todo:0
msgid "Set as Todo"
msgstr ""
msgstr "Set as Todo"
#. module: base
#: field:ir.actions.act_window.view,act_window_id:0
@ -4177,7 +4192,7 @@ msgstr "Menus"
#. module: base
#: selection:base.language.install,lang:0
msgid "Serbian (Latin) / srpski"
msgstr ""
msgstr "Serbian (Latin) / srpski"
#. module: base
#: model:res.country,name:base.il
@ -4353,7 +4368,7 @@ msgstr ""
#. module: base
#: view:base.language.import:0
msgid "- module,type,name,res_id,src,value"
msgstr ""
msgstr "- module,type,name,res_id,src,value"
#. module: base
#: selection:base.language.install,lang:0
@ -4372,7 +4387,7 @@ msgstr ""
#. module: base
#: help:ir.model.fields,relation:0
msgid "For relationship fields, the technical name of the target model"
msgstr ""
msgstr "For relationship fields, the technical name of the target model"
#. module: base
#: selection:base.language.install,lang:0
@ -4387,7 +4402,7 @@ msgstr "Inherited View"
#. module: base
#: view:ir.translation:0
msgid "Source Term"
msgstr ""
msgstr "Source Term"
#. module: base
#: model:ir.ui.menu,name:base.menu_main_pm
@ -4397,7 +4412,7 @@ msgstr "Project"
#. module: base
#: field:ir.ui.menu,web_icon_hover_data:0
msgid "Web Icon Image (hover)"
msgstr ""
msgstr "Web Icon Image (hover)"
#. module: base
#: view:base.module.import:0
@ -4417,7 +4432,7 @@ msgstr "Create User"
#. module: base
#: view:partner.clear.ids:0
msgid "Want to Clear Ids ? "
msgstr ""
msgstr "Want to Clear Ids ? "
#. module: base
#: field:publisher_warranty.contract,name:0
@ -4469,17 +4484,17 @@ msgstr "Fed. State"
#. module: base
#: field:ir.actions.server,copy_object:0
msgid "Copy Of"
msgstr ""
msgstr "Copy Of"
#. module: base
#: field:ir.model,osv_memory:0
msgid "In-memory model"
msgstr ""
msgstr "In-memory model"
#. module: base
#: view:partner.clear.ids:0
msgid "Clear Ids"
msgstr ""
msgstr "Clear Ids"
#. module: base
#: model:res.country,name:base.io
@ -4501,7 +4516,7 @@ msgstr "Field Mapping"
#. module: base
#: view:publisher_warranty.contract:0
msgid "Refresh Validation Dates"
msgstr ""
msgstr "Refresh Validation Dates"
#. module: base
#: view:ir.model:0
@ -4572,7 +4587,7 @@ msgstr "_Ok"
#. module: base
#: help:ir.filters,user_id:0
msgid "False means for every user"
msgstr ""
msgstr "False means for every user"
#. module: base
#: code:addons/base/module/module.py:198
@ -4621,6 +4636,7 @@ msgstr "Contacts"
msgid ""
"Unable to delete this document because it is used as a default property"
msgstr ""
"Unable to delete this document because it is used as a default property"
#. module: base
#: view:res.widget.wizard:0
@ -4674,7 +4690,7 @@ msgstr ""
#: code:addons/orm.py:1350
#, python-format
msgid "Insufficient fields for Calendar View!"
msgstr ""
msgstr "Insufficient fields for Calendar View!"
#. module: base
#: selection:ir.property,type:0
@ -4687,6 +4703,8 @@ msgid ""
"The path to the main report file (depending on Report Type) or NULL if the "
"content is in another data field"
msgstr ""
"The path to the main report file (depending on Report Type) or NULL if the "
"content is in another data field"
#. module: base
#: help:res.config.users,company_id:0
@ -4748,7 +4766,7 @@ msgstr "Close"
#. module: base
#: selection:base.language.install,lang:0
msgid "Spanish (MX) / Español (MX)"
msgstr ""
msgstr "Spanish (MX) / Español (MX)"
#. module: base
#: view:res.log:0
@ -4783,7 +4801,7 @@ msgstr "Publisher Warranty Contracts"
#. module: base
#: help:res.log,name:0
msgid "The logging message."
msgstr ""
msgstr "The logging message."
#. module: base
#: field:base.language.export,format:0
@ -5018,7 +5036,7 @@ msgstr ""
#. module: base
#: help:ir.cron,interval_number:0
msgid "Repeat every x."
msgstr ""
msgstr "Repeat every x."
#. module: base
#: wizard_view:server.action.create,step_1:0
@ -5078,6 +5096,8 @@ msgid ""
"If specified, this action will be opened at logon for this user, in addition "
"to the standard menu."
msgstr ""
"If specified, this action will be opened at logon for this user, in addition "
"to the standard menu."
#. module: base
#: view:ir.values:0
@ -5088,7 +5108,7 @@ msgstr "Client Actions"
#: code:addons/orm.py:1806
#, python-format
msgid "The exists method is not implemented on this object !"
msgstr ""
msgstr "The exists method is not implemented on this object !"
#. module: base
#: code:addons/base/module/module.py:336
@ -5113,7 +5133,7 @@ msgstr "Connect Events to Actions"
#. module: base
#: model:ir.model,name:base.model_base_update_translations
msgid "base.update.translations"
msgstr ""
msgstr "base.update.translations"
#. module: base
#: field:ir.module.category,parent_id:0
@ -5124,7 +5144,7 @@ msgstr "Parent Category"
#. module: base
#: selection:ir.property,type:0
msgid "Integer Big"
msgstr ""
msgstr "Integer Big"
#. module: base
#: selection:res.partner.address,type:0
@ -5158,7 +5178,7 @@ msgstr "Communication"
#. module: base
#: view:ir.actions.report.xml:0
msgid "RML Report"
msgstr ""
msgstr "RML Report"
#. module: base
#: model:ir.model,name:base.model_ir_server_object_lines
@ -5206,7 +5226,7 @@ msgstr "Nigeria"
#: code:addons/base/ir/ir_model.py:250
#, python-format
msgid "For selection fields, the Selection Options must be given!"
msgstr ""
msgstr "For selection fields, the Selection Options must be given!"
#. module: base
#: model:ir.actions.act_window,name:base.action_partner_sms_send
@ -5254,6 +5274,13 @@ msgid ""
"installed the CRM, with the history tab, you can track all the interactions "
"with a partner such as opportunities, emails, or sales orders issued."
msgstr ""
"Customers (also called Partners in other areas of the system) helps you "
"manage your address book of companies whether they are prospects, customers "
"and/or suppliers. The partner form allows you to track and record all the "
"necessary information to interact with your partners from the company "
"address to their contacts as well as pricelists, and much more. If you "
"installed the CRM, with the history tab, you can track all interactions with "
"a partner such as opportunities, emails, or sales orders issued."
#. module: base
#: model:res.country,name:base.ph
@ -5278,7 +5305,7 @@ msgstr "Content"
#. module: base
#: help:ir.rule,global:0
msgid "If no group is specified the rule is global and applied to everyone"
msgstr ""
msgstr "If no group is specified the rule is global and applied to everyone"
#. module: base
#: model:res.country,name:base.td
@ -5355,6 +5382,9 @@ msgid ""
"groups. If this field is empty, OpenERP will compute visibility based on the "
"related object's read access."
msgstr ""
"If you have groups, the visibility of this menu will be based on these "
"groups. If this field is empty, OpenERP will compute visibility based on the "
"related object's read access."
#. module: base
#: model:ir.actions.act_window,name:base.action_ui_view_custom
@ -5496,7 +5526,7 @@ msgstr "Spanish (EC) / Español (EC)"
#. module: base
#: help:ir.ui.view,xml_id:0
msgid "ID of the view defined in xml file"
msgstr ""
msgstr "ID of the view defined in xml file"
#. module: base
#: model:ir.model,name:base.model_base_module_import
@ -5512,7 +5542,7 @@ msgstr "American Samoa"
#. module: base
#: help:ir.actions.act_window,res_model:0
msgid "Model name of the object to open in the view window"
msgstr ""
msgstr "Model name of the object to open in the view window"
#. module: base
#: field:res.log,secondary:0
@ -5692,11 +5722,15 @@ msgid ""
"Warning: if \"email_from\" and \"smtp_server\" aren't configured, it won't "
"be possible to email new users."
msgstr ""
"If an email is provided, the user will be sent a message welcoming them.\n"
"\n"
"Warning: if \"email_from\" and \"smtp_server\" aren't configured, it won't "
"be possible to email new users."
#. module: base
#: selection:base.language.install,lang:0
msgid "Flemish (BE) / Vlaams (BE)"
msgstr ""
msgstr "Flemish (BE) / Vlaams (BE)"
#. module: base
#: field:ir.cron,interval_number:0
@ -5746,7 +5780,7 @@ msgstr "ir.actions.todo"
#: code:addons/base/res/res_config.py:94
#, python-format
msgid "Couldn't find previous ir.actions.todo"
msgstr ""
msgstr "Couldn't find previous ir.actions.todo"
#. module: base
#: view:ir.actions.act_window:0
@ -5761,7 +5795,7 @@ msgstr "Custom Shortcuts"
#. module: base
#: selection:base.language.install,lang:0
msgid "Vietnamese / Tiếng Việt"
msgstr ""
msgstr "Vietnamese / Tiếng Việt"
#. module: base
#: model:res.country,name:base.dz
@ -5776,7 +5810,7 @@ msgstr "Belgium"
#. module: base
#: model:ir.model,name:base.model_osv_memory_autovacuum
msgid "osv_memory.autovacuum"
msgstr ""
msgstr "osv_memory.autovacuum"
#. module: base
#: field:base.language.export,lang:0
@ -5809,30 +5843,30 @@ msgstr "Companies"
#. module: base
#: view:res.lang:0
msgid "%H - Hour (24-hour clock) [00,23]."
msgstr ""
msgstr "%H - Hour (24-hour clock) [00,23]."
#. module: base
#: model:ir.model,name:base.model_res_widget
msgid "res.widget"
msgstr ""
msgstr "res.widget"
#. module: base
#: code:addons/base/ir/ir_model.py:258
#, python-format
msgid "Model %s does not exist!"
msgstr ""
msgstr "Model %s does not exist!"
#. module: base
#: code:addons/base/res/res_lang.py:159
#, python-format
msgid "You cannot delete the language which is User's Preferred Language !"
msgstr ""
msgstr "You cannot delete the language which is User's Preferred Language !"
#. module: base
#: code:addons/fields.py:103
#, python-format
msgid "Not implemented get_memory method !"
msgstr ""
msgstr "Not implemented get_memory method !"
#. module: base
#: view:ir.actions.server:0
@ -5879,7 +5913,7 @@ msgstr "Neutral Zone"
#. module: base
#: selection:base.language.install,lang:0
msgid "Hindi / हिंदी"
msgstr ""
msgstr "Hindi / हिंदी"
#. module: base
#: view:ir.model:0
@ -5926,7 +5960,7 @@ msgstr "Window Actions"
#. module: base
#: view:res.lang:0
msgid "%I - Hour (12-hour clock) [01,12]."
msgstr ""
msgstr "%I - Hour (12-hour clock) [01,12]."
#. module: base
#: selection:publisher_warranty.contract.wizard,state:0
@ -5964,12 +5998,14 @@ msgid ""
"View type: set to 'tree' for a hierarchical tree view, or 'form' for other "
"views"
msgstr ""
"View type: set to 'tree' for a hierarchical tree view, or 'form' for other "
"views"
#. module: base
#: code:addons/base/res/res_config.py:421
#, python-format
msgid "Click 'Continue' to configure the next addon..."
msgstr ""
msgstr "Click 'Continue' to configure the next addon..."
#. module: base
#: field:ir.actions.server,record_id:0
@ -6010,7 +6046,7 @@ msgstr ""
#: code:addons/base/ir/ir_actions.py:629
#, python-format
msgid "Please specify server option --email-from !"
msgstr ""
msgstr "Please specify server option --email-from !"
#. module: base
#: field:base.language.import,name:0
@ -6070,6 +6106,7 @@ msgid ""
"It gives the status if the tip has to be displayed or not when a user "
"executes an action"
msgstr ""
"It shows if the tip is to be displayed or not when a user executes an action"
#. module: base
#: view:ir.model:0
@ -6126,7 +6163,7 @@ msgstr "Code"
#. module: base
#: model:ir.model,name:base.model_res_config_installer
msgid "res.config.installer"
msgstr ""
msgstr "res.config.installer"
#. module: base
#: model:res.country,name:base.mc
@ -6170,7 +6207,7 @@ msgstr "Sequence Codes"
#. module: base
#: selection:base.language.install,lang:0
msgid "Spanish (CO) / Español (CO)"
msgstr ""
msgstr "Spanish (CO) / Español (CO)"
#. module: base
#: view:base.module.configuration:0
@ -6178,6 +6215,8 @@ msgid ""
"All pending configuration wizards have been executed. You may restart "
"individual wizards via the list of configuration wizards."
msgstr ""
"All pending configuration wizards have been executed. You may restart "
"individual wizards via the list of configuration wizards."
#. module: base
#: wizard_button:server.action.create,step_1,create:0
@ -6187,7 +6226,7 @@ msgstr "Create"
#. module: base
#: view:ir.sequence:0
msgid "Current Year with Century: %(year)s"
msgstr ""
msgstr "Current Year with Century: %(year)s"
#. module: base
#: field:ir.exports,export_fields:0
@ -6202,13 +6241,13 @@ msgstr "France"
#. module: base
#: model:ir.model,name:base.model_res_log
msgid "res.log"
msgstr ""
msgstr "res.log"
#. module: base
#: help:ir.translation,module:0
#: help:ir.translation,xml_id:0
msgid "Maps to the ir_model_data for which this translation is provided."
msgstr ""
msgstr "Maps to the ir_model_data for which this translation is provided."
#. module: base
#: view:workflow.activity:0
@ -6302,7 +6341,7 @@ msgstr "Todo"
#. module: base
#: field:ir.attachment,datas:0
msgid "File Content"
msgstr ""
msgstr "File Content"
#. module: base
#: model:res.country,name:base.pa
@ -6319,12 +6358,13 @@ msgstr "Ltd"
msgid ""
"The group that a user must have to be authorized to validate this transition."
msgstr ""
"The group that a user must have to be authorized to validate this transition."
#. module: base
#: constraint:res.config.users:0
#: constraint:res.users:0
msgid "The chosen company is not in the allowed companies for this user"
msgstr ""
msgstr "The chosen company is not in the allowed companies for this user"
#. module: base
#: model:res.country,name:base.gi
@ -6346,6 +6386,7 @@ msgstr "Pitcairn Island"
msgid ""
"We suggest to reload the menu tab to see the new menus (Ctrl+T then Ctrl+R)."
msgstr ""
"We suggest reloading the menu tab to see the new menus (Ctrl+T then Ctrl+R)."
#. module: base
#: model:ir.actions.act_window,name:base.action_rule
@ -6398,7 +6439,7 @@ msgstr "Search View"
#. module: base
#: sql_constraint:res.lang:0
msgid "The code of the language must be unique !"
msgstr ""
msgstr "The code of the language must be unique !"
#. module: base
#: model:ir.actions.act_window,name:base.action_attachment
@ -6441,7 +6482,7 @@ msgstr "Write Access"
#. module: base
#: view:res.lang:0
msgid "%m - Month number [01,12]."
msgstr ""
msgstr "%m - Month number [01,12]."
#. module: base
#: field:res.bank,city:0
@ -6499,7 +6540,7 @@ msgstr "English (US)"
#: view:ir.model.data:0
#: model:ir.ui.menu,name:base.ir_model_data_menu
msgid "Object Identifiers"
msgstr ""
msgstr "Object Identifiers"
#. module: base
#: model:ir.actions.act_window,help:base.action_partner_title_partner
@ -6507,11 +6548,13 @@ msgid ""
"Manage the partner titles you want to have available in your system. The "
"partner titles is the legal status of the company: Private Limited, SA, etc."
msgstr ""
"Manage the partner titles you want to have available in your system. The "
"partner title is the legal status of the company: Private Limited, SA, etc."
#. module: base
#: view:base.language.export:0
msgid "To browse official translations, you can start with these links:"
msgstr ""
msgstr "To browse official translations, you can start with these links:"
#. module: base
#: code:addons/base/ir/ir_model.py:484
@ -6520,6 +6563,8 @@ msgid ""
"You can not read this document (%s) ! Be sure your user belongs to one of "
"these groups: %s."
msgstr ""
"You can not read this document (%s) ! Be sure your user belongs to one of "
"these groups: %s."
#. module: base
#: view:res.bank:0
@ -6538,7 +6583,7 @@ msgstr "Installed version"
#. module: base
#: selection:base.language.install,lang:0
msgid "Mongolian / монгол"
msgstr ""
msgstr "Mongolian / монгол"
#. module: base
#: model:res.country,name:base.mr
@ -6553,7 +6598,7 @@ msgstr "ir.translation"
#. module: base
#: view:base.module.update:0
msgid "Module update result"
msgstr ""
msgstr "Module update result"
#. module: base
#: view:workflow.activity:0
@ -6575,7 +6620,7 @@ msgstr "Parent Company"
#. module: base
#: selection:base.language.install,lang:0
msgid "Spanish (CR) / Español (CR)"
msgstr ""
msgstr "Spanish (CR) / Español (CR)"
#. module: base
#: field:res.currency.rate,rate:0
@ -6615,6 +6660,9 @@ msgid ""
"for the currency: %s \n"
"at the date: %s"
msgstr ""
"No rate found \n"
"for the currency: %s \n"
"at the date: %s"
#. module: base
#: model:ir.actions.act_window,help:base.action_ui_view_custom
@ -6622,6 +6670,8 @@ msgid ""
"Customized views are used when users reorganize the content of their "
"dashboard views (via web client)"
msgstr ""
"Customised views are used when users reorganise the content of their "
"dashboard views (via web client)"
#. module: base
#: field:ir.model,name:0
@ -6660,7 +6710,7 @@ msgstr "Icon"
#. module: base
#: help:ir.model.fields,model_id:0
msgid "The model this field belongs to"
msgstr ""
msgstr "The model this field belongs to"
#. module: base
#: model:res.country,name:base.mq
@ -6670,7 +6720,7 @@ msgstr "Martinique (French)"
#. module: base
#: view:ir.sequence.type:0
msgid "Sequences Type"
msgstr ""
msgstr "Sequences Type"
#. module: base
#: model:ir.actions.act_window,name:base.res_request-act
@ -6694,7 +6744,7 @@ msgstr "Or"
#: model:ir.actions.act_window,name:base.res_log_act_window
#: model:ir.ui.menu,name:base.menu_res_log_act_window
msgid "Client Logs"
msgstr ""
msgstr "Client Logs"
#. module: base
#: model:res.country,name:base.al
@ -6713,6 +6763,8 @@ msgid ""
"You cannot delete the language which is Active !\n"
"Please de-activate the language first."
msgstr ""
"You cannot delete a language which is active !\n"
"Please de-activate the language first."
#. module: base
#: view:base.language.install:0
@ -6721,6 +6773,8 @@ msgid ""
"Please be patient, this operation may take a few minutes (depending on the "
"number of modules currently installed)..."
msgstr ""
"Please be patient, this operation may take a few minutes (depending on the "
"number of modules currently installed)..."
#. module: base
#: field:ir.ui.menu,child_id:0
@ -6739,18 +6793,18 @@ msgstr "Problem in configuration `Record Id` in Server Action!"
#: code:addons/orm.py:2316
#, python-format
msgid "ValidateError"
msgstr ""
msgstr "ValidateError"
#. module: base
#: view:base.module.import:0
#: view:base.module.update:0
msgid "Open Modules"
msgstr ""
msgstr "Open Modules"
#. module: base
#: model:ir.actions.act_window,help:base.action_res_bank_form
msgid "Manage bank records you want to be used in the system."
msgstr ""
msgstr "Manage bank records you want to be used in the system."
#. module: base
#: view:base.module.import:0
@ -6768,6 +6822,8 @@ msgid ""
"The path to the main report file (depending on Report Type) or NULL if the "
"content is in another field"
msgstr ""
"The path to the main report file (depending on Report Type) or NULL if the "
"content is in another field"
#. module: base
#: model:res.country,name:base.la
@ -6794,6 +6850,8 @@ msgid ""
"The sum of the data (2nd field) is null.\n"
"We can't draw a pie chart !"
msgstr ""
"The sum of the data (2nd field) is null.\n"
"We can't draw a pie chart !"
#. module: base
#: model:ir.ui.menu,name:base.menu_lunch_reporting
@ -6815,7 +6873,7 @@ msgstr "Togo"
#. module: base
#: selection:ir.module.module,license:0
msgid "Other Proprietary"
msgstr ""
msgstr "Other Proprietary"
#. module: base
#: selection:workflow.activity,kind:0
@ -6826,7 +6884,7 @@ msgstr "Stop All"
#: code:addons/orm.py:412
#, python-format
msgid "The read_group method is not implemented on this object !"
msgstr ""
msgstr "The read_group method is not implemented on this object !"
#. module: base
#: view:ir.model.data:0
@ -6846,7 +6904,7 @@ msgstr "Cascade"
#. module: base
#: field:workflow.transition,group_id:0
msgid "Group Required"
msgstr ""
msgstr "Group Required"
#. module: base
#: view:ir.actions.configuration.wizard:0
@ -6869,17 +6927,19 @@ msgid ""
"Enable this if you want to execute missed occurences as soon as the server "
"restarts."
msgstr ""
"Enable this if you want to execute missed occurences as soon as the server "
"restarts."
#. module: base
#: view:base.module.upgrade:0
msgid "Start update"
msgstr ""
msgstr "Start update"
#. module: base
#: code:addons/base/publisher_warranty/publisher_warranty.py:144
#, python-format
msgid "Contract validation error"
msgstr ""
msgstr "Contract validation error"
#. module: base
#: field:res.country.state,name:0
@ -6906,7 +6966,7 @@ msgstr "ir.actions.report.xml"
#. module: base
#: model:res.partner.title,shortcut:base.res_partner_title_miss
msgid "Mss"
msgstr ""
msgstr "Mss"
#. module: base
#: model:ir.model,name:base.model_ir_ui_view
@ -6916,7 +6976,7 @@ msgstr "ir.ui.view"
#. module: base
#: constraint:res.partner:0
msgid "Error ! You can not create recursive associated members."
msgstr ""
msgstr "Error ! You can not create recursive associated members."
#. module: base
#: help:res.lang,code:0
@ -6931,7 +6991,7 @@ msgstr "OpenERP Partners"
#. module: base
#: model:ir.ui.menu,name:base.menu_hr_manager
msgid "HR Manager Dashboard"
msgstr ""
msgstr "HR Manager Dashboard"
#. module: base
#: code:addons/base/module/module.py:253
@ -6939,11 +6999,12 @@ msgstr ""
msgid ""
"Unable to install module \"%s\" because an external dependency is not met: %s"
msgstr ""
"Unable to install module \"%s\" because an external dependency is not met: %s"
#. module: base
#: view:ir.module.module:0
msgid "Search modules"
msgstr ""
msgstr "Search modules"
#. module: base
#: model:res.country,name:base.by
@ -6968,6 +7029,10 @@ msgid ""
"not connect to the system. You can assign them groups in order to give them "
"specific access to the applications they need to use in the system."
msgstr ""
"Create and manage users that will connect to the system. Users can be "
"deactivated should there be a period of time during which they will/should "
"not connect to the system. You can assign them groups to give them specific "
"access to the applications they need to use."
#. module: base
#: selection:res.request,priority:0
@ -6983,13 +7048,13 @@ msgstr "Street2"
#. module: base
#: model:ir.actions.act_window,name:base.action_view_base_module_update
msgid "Module Update"
msgstr ""
msgstr "Module Update"
#. module: base
#: code:addons/base/module/wizard/base_module_upgrade.py:95
#, python-format
msgid "Following modules are not installed or unknown: %s"
msgstr ""
msgstr "Following modules are not installed or unknown: %s"
#. module: base
#: view:ir.cron:0
@ -7018,7 +7083,7 @@ msgstr "Open Window"
#. module: base
#: field:ir.actions.act_window,auto_search:0
msgid "Auto Search"
msgstr ""
msgstr "Auto Search"
#. module: base
#: field:ir.actions.act_window,filter:0
@ -7064,25 +7129,25 @@ msgstr "Load"
#: help:res.config.users,name:0
#: help:res.users,name:0
msgid "The new user's real name, used for searching and most listings"
msgstr ""
msgstr "The new user's real name, used for searching and most listings"
#. module: base
#: code:addons/osv.py:154
#: code:addons/osv.py:156
#, python-format
msgid "Integrity Error"
msgstr ""
msgstr "Integrity Error"
#. module: base
#: model:ir.model,name:base.model_ir_wizard_screen
msgid "ir.wizard.screen"
msgstr ""
msgstr "ir.wizard.screen"
#. module: base
#: code:addons/base/ir/ir_model.py:223
#, python-format
msgid "Size of the field can never be less than 1 !"
msgstr ""
msgstr "Size of the field can never be less than 1 !"
#. module: base
#: model:res.country,name:base.so
@ -7092,7 +7157,7 @@ msgstr "Somalia"
#. module: base
#: selection:publisher_warranty.contract,state:0
msgid "Terminated"
msgstr ""
msgstr "Terminated"
#. module: base
#: model:res.partner.category,name:base.res_partner_category_13
@ -7102,7 +7167,7 @@ msgstr "Important customers"
#. module: base
#: view:res.lang:0
msgid "Update Terms"
msgstr ""
msgstr "Update Terms"
#. module: base
#: field:partner.sms.send,mobile_to:0
@ -7121,7 +7186,7 @@ msgstr "Arguments"
#: code:addons/orm.py:716
#, python-format
msgid "Database ID doesn't exist: %s : %s"
msgstr ""
msgstr "Database ID doesn't exist: %s : %s"
#. module: base
#: selection:ir.module.module,license:0
@ -7137,7 +7202,7 @@ msgstr "GPL Version 3"
#: code:addons/orm.py:836
#, python-format
msgid "key '%s' not found in selection field '%s'"
msgstr ""
msgstr "key '%s' not found in selection field '%s'"
#. module: base
#: view:partner.wizard.ean.check:0
@ -7148,7 +7213,7 @@ msgstr "Correct EAN13"
#: code:addons/orm.py:2317
#, python-format
msgid "The value \"%s\" for the field \"%s\" is not in the selection"
msgstr ""
msgstr "The value \"%s\" for the field \"%s\" is not in the selection"
#. module: base
#: field:res.partner,customer:0

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,14 +7,14 @@ msgstr ""
"Project-Id-Version: OpenERP Server 5.0.0\n"
"Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2011-01-11 11:14+0000\n"
"PO-Revision-Date: 2011-01-28 02:14+0000\n"
"PO-Revision-Date: 2011-09-27 16:28+0000\n"
"Last-Translator: Walter Cheuk <wwycheuk@gmail.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2011-09-01 04:45+0000\n"
"X-Generator: Launchpad (build 13827)\n"
"X-Launchpad-Export-Date: 2011-09-28 05:19+0000\n"
"X-Generator: Launchpad (build 14049)\n"
#. module: base
#: view:ir.filters:0
@ -46,7 +46,7 @@ msgstr "日期時間"
msgid ""
"The second argument of the many2many field %s must be a SQL table !You used "
"%s, which is not a valid SQL table name."
msgstr "many2many 欄位 %s 之第二個引數須為 SQL 表格!您用了並非有效 SQL 表格名稱之 %s。"
msgstr "「多對多(many2many)」欄位 %s 之第二個引數須為 SQL 表格!您用了並非有效 SQL 表格名稱之 %s。"
#. module: base
#: view:ir.values:0
@ -103,7 +103,7 @@ msgstr "工作流程作用於"
#. module: base
#: field:ir.actions.act_window,display_menu_tip:0
msgid "Display Menu Tips"
msgstr "顯示選提示"
msgstr "顯示選提示"
#. module: base
#: view:ir.module.module:0
@ -129,7 +129,7 @@ msgstr ""
#. module: base
#: field:res.partner,ref:0
msgid "Reference"
msgstr "參"
msgstr "參"
#. module: base
#: field:ir.actions.act_window,target:0
@ -154,7 +154,7 @@ msgstr ""
#: code:addons/osv.py:133
#, python-format
msgid "Constraint Error"
msgstr ""
msgstr "約束錯誤"
#. module: base
#: model:ir.model,name:base.model_ir_ui_view_custom
@ -171,12 +171,12 @@ msgstr "史瓦濟蘭"
#: code:addons/orm.py:3653
#, python-format
msgid "created."
msgstr ""
msgstr "已建立。"
#. module: base
#: model:res.partner.category,name:base.res_partner_category_woodsuppliers0
msgid "Wood Suppliers"
msgstr ""
msgstr "木材供應商"
#. module: base
#: code:addons/base/module/module.py:303
@ -224,7 +224,7 @@ msgstr "新"
#. module: base
#: field:ir.actions.report.xml,multi:0
msgid "On multiple doc."
msgstr "作用於多重檔案。"
msgstr "作用於多重文件。"
#. module: base
#: field:ir.module.category,module_nr:0
@ -252,12 +252,12 @@ msgstr "聯絡人名稱"
msgid ""
"Save this document to a %s file and edit it with a specific software or a "
"text editor. The file encoding is UTF-8."
msgstr "將此檔案儲存為%s檔案後可以特定軟體或文字編輯程式修改。檔案編碼為 UTF-8。"
msgstr "將此檔案存為 %s 檔案,並以特定軟體或文字編輯程式修改。檔案編碼為 UTF-8。"
#. module: base
#: sql_constraint:res.lang:0
msgid "The name of the language must be unique !"
msgstr ""
msgstr "語言名稱須與其他不同 !"
#. module: base
#: selection:res.request,state:0
@ -329,7 +329,7 @@ msgstr "欄位名稱"
#: wizard_view:server.action.create,init:0
#: wizard_field:server.action.create,init,type:0
msgid "Select Action Type"
msgstr "選動作類型"
msgstr "選動作類型"
#. module: base
#: model:res.country,name:base.tv
@ -422,7 +422,7 @@ msgstr "哥倫比亞"
#. module: base
#: view:ir.module.module:0
msgid "Schedule Upgrade"
msgstr "排升級"
msgstr "排升級"
#. module: base
#: code:addons/orm.py:838
@ -436,7 +436,7 @@ msgid ""
"The ISO country code in two chars.\n"
"You can use this field for quick search."
msgstr ""
"ISO 國家代碼使用兩個字。\n"
"ISO 國家代碼使用兩個字。\n"
"您可以使用該欄位來快速搜索。"
#. module: base
@ -447,7 +447,7 @@ msgstr "帛琉"
#. module: base
#: view:res.partner:0
msgid "Sales & Purchases"
msgstr "銷售&採購"
msgstr "銷售 及 購貨"
#. module: base
#: view:ir.translation:0
@ -481,7 +481,7 @@ msgstr "自訂欄位名稱開頭必須是 'x_' !"
#. module: base
#: help:ir.actions.server,action_id:0
msgid "Select the Action Window, Report, Wizard to be executed."
msgstr "選擇要執行的動作視窗、報表或精靈。"
msgstr "選取要執行之動作視窗、報表或精靈。"
#. module: base
#: view:res.config.users:0
@ -502,7 +502,7 @@ msgstr "模型說明"
#: help:ir.actions.act_window,src_model:0
msgid ""
"Optional model name of the objects on which this action should be visible"
msgstr ""
msgstr "要可見動作的物件之模型名稱(可有可無)"
#. module: base
#: field:workflow.transition,trigger_expr_id:0
@ -548,7 +548,7 @@ msgstr "是否檢查 Ean "
#. module: base
#: field:ir.values,key2:0
msgid "Event Type"
msgstr "事件類型"
msgstr "活動類型"
#. module: base
#: view:base.language.export:0
@ -684,7 +684,7 @@ msgstr "西班牙文 (UY) / Español (UY)"
#: field:res.partner,mobile:0
#: field:res.partner.address,mobile:0
msgid "Mobile"
msgstr "手"
msgstr "手提電話"
#. module: base
#: model:res.country,name:base.om
@ -741,7 +741,7 @@ msgstr "印度"
#: model:ir.actions.act_window,name:base.res_request_link-act
#: model:ir.ui.menu,name:base.menu_res_request_link_act
msgid "Request Reference Types"
msgstr "請求參類型"
msgstr "請求參類型"
#. module: base
#: view:ir.values:0
@ -812,7 +812,7 @@ msgstr "人力資源儀錶板"
#: code:addons/base/res/res_user.py:507
#, python-format
msgid "Setting empty passwords is not allowed for security reasons!"
msgstr "因安理由密碼不能留空!"
msgstr "因安理由密碼不能留空!"
#. module: base
#: selection:ir.actions.server,state:0
@ -882,7 +882,7 @@ msgstr "刪除存取"
#. module: base
#: model:res.country,name:base.ne
msgid "Niger"
msgstr "尼日"
msgstr "尼日"
#. module: base
#: selection:base.language.install,lang:0
@ -998,7 +998,7 @@ msgstr "儀錶板"
#. module: base
#: model:ir.ui.menu,name:base.menu_purchase_root
msgid "Purchases"
msgstr "購"
msgstr "購"
#. module: base
#: model:res.country,name:base.md
@ -1120,7 +1120,7 @@ msgstr "報表"
msgid ""
"If set to true, the action will not be displayed on the right toolbar of a "
"form view."
msgstr "如設為,該動作不會顯示於表單檢視右側工具欄。"
msgstr "如設為,該動作不會顯示於表單檢視右側工具欄。"
#. module: base
#: field:workflow,on_create:0
@ -1239,7 +1239,7 @@ msgstr "馬爾地夫"
#. module: base
#: help:ir.values,res_id:0
msgid "Keep 0 if the action must appear on all resources."
msgstr "如該動作要顯示於所有資源的話請保持為「0」。"
msgstr "如該動作要顯示於所有資源的話請保持為「0」。"
#. module: base
#: model:ir.model,name:base.model_ir_rule
@ -1325,7 +1325,7 @@ msgstr "優先次序"
#. module: base
#: field:workflow.transition,act_from:0
msgid "Source Activity"
msgstr "來源活動"
msgstr "來源地動態"
#. module: base
#: view:ir.sequence:0
@ -1368,7 +1368,7 @@ msgstr "完整路徑"
#. module: base
#: view:res.request:0
msgid "References"
msgstr "參"
msgstr "參"
#. module: base
#: view:res.lang:0
@ -1510,7 +1510,7 @@ msgstr ""
#. module: base
#: view:workflow.activity:0
msgid "Workflow Activity"
msgstr "工作流程動"
msgstr "工作流程動"
#. module: base
#: view:ir.rule:0
@ -1694,7 +1694,7 @@ msgstr "原始檢視"
#. module: base
#: view:ir.values:0
msgid "Action To Launch"
msgstr "要啟動之動作"
msgstr "要執行之動作"
#. module: base
#: field:ir.actions.url,target:0
@ -1736,7 +1736,7 @@ msgstr ""
#. module: base
#: help:ir.values,action_id:0
msgid "This field is not used, it only helps you to select the right action."
msgstr "欄位並未使用,只是為了幫您選擇正確動作。"
msgstr "欄位並未使用,只是為了幫您選擇正確動作。"
#. module: base
#: field:ir.actions.server,email:0
@ -1916,7 +1916,7 @@ msgstr "諾福克島"
#. module: base
#: selection:base.language.install,lang:0
msgid "Korean (KR) / 한국어 (KR)"
msgstr "韓文 (KR) / 한국어 (KR)"
msgstr "韓文 (北韓) / 한국어 (KR)"
#. module: base
#: help:ir.model.fields,model:0
@ -1927,7 +1927,7 @@ msgstr ""
#: field:ir.actions.server,action_id:0
#: selection:ir.actions.server,state:0
msgid "Client Action"
msgstr "戶端動作"
msgstr "戶端動作"
#. module: base
#: model:res.country,name:base.bd
@ -2007,7 +2007,7 @@ msgstr "屬性"
#: model:ir.model,name:base.model_res_partner_bank_type
#: view:res.partner.bank.type:0
msgid "Bank Account Type"
msgstr "銀行帳類型"
msgstr "銀行帳類型"
#. module: base
#: field:base.language.export,config_logo:0
@ -2171,7 +2171,7 @@ msgstr "西班牙文 (DO) / Español (DO)"
#. module: base
#: model:ir.model,name:base.model_workflow_activity
msgid "workflow.activity"
msgstr "工作流程.動"
msgstr "工作流程.動"
#. module: base
#: help:ir.ui.view_sc,res_id:0
@ -2239,7 +2239,7 @@ msgstr ""
#. module: base
#: field:ir.default,ref_id:0
msgid "ID Ref."
msgstr "ID參"
msgstr "ID參"
#. module: base
#: model:ir.actions.server,name:base.action_start_configurator
@ -2313,7 +2313,7 @@ msgstr "分格格式"
#. module: base
#: selection:publisher_warranty.contract,state:0
msgid "Unvalidated"
msgstr "未驗"
msgstr "未驗"
#. module: base
#: model:ir.ui.menu,name:base.next_id_9
@ -2336,7 +2336,7 @@ msgstr "馬約特"
#: code:addons/base/ir/ir_actions.py:597
#, python-format
msgid "Please specify an action to launch !"
msgstr "請指定執行動作!"
msgstr "請指定執行動作!"
#. module: base
#: view:res.payterm:0
@ -2368,7 +2368,7 @@ msgstr "請檢查所有行數皆有%d個欄位。"
#: view:ir.cron:0
#: model:ir.ui.menu,name:base.menu_ir_cron_act
msgid "Scheduled Actions"
msgstr "計劃的動作"
msgstr "已安排動作"
#. module: base
#: field:res.partner.address,title:0
@ -2412,7 +2412,7 @@ msgstr "建立選單"
msgid ""
"Value Added Tax number. Check the box if the partner is subjected to the "
"VAT. Used by the VAT legal statement."
msgstr "增值稅編號。如該伙伴適用於增值稅,請選擇。用於增值稅申報。"
msgstr "增值稅編號。如該伙伴需要繳交增值稅,請選擇。用於申報增值稅。"
#. module: base
#: model:ir.model,name:base.model_maintenance_contract
@ -2710,7 +2710,7 @@ msgstr "基礎欄位"
#. module: base
#: view:publisher_warranty.contract:0
msgid "Validate"
msgstr "驗"
msgstr "驗"
#. module: base
#: field:ir.actions.todo,restart:0
@ -2810,7 +2810,7 @@ msgstr ""
#. module: base
#: model:res.partner.category,name:base.res_partner_category_16
msgid "Telecom sector"
msgstr "電範疇"
msgstr "電範疇"
#. module: base
#: field:workflow.transition,trigger_model:0
@ -2820,7 +2820,7 @@ msgstr "觸發器物件"
#. module: base
#: view:res.users:0
msgid "Current Activity"
msgstr "目前活動"
msgstr "當前動態"
#. module: base
#: view:workflow.activity:0
@ -2842,7 +2842,7 @@ msgstr "行銷"
#: view:res.partner.bank:0
#: model:res.partner.bank.type,name:base.bank_normal
msgid "Bank account"
msgstr "銀行帳"
msgstr "銀行帳"
#. module: base
#: selection:base.language.install,lang:0
@ -2857,7 +2857,7 @@ msgstr "序列類型"
#. module: base
#: view:ir.ui.view.custom:0
msgid "Customized Architecture"
msgstr "自訂架構"
msgstr "自訂架構"
#. module: base
#: field:ir.module.module,license:0
@ -2877,7 +2877,7 @@ msgstr "必定"
#. module: base
#: selection:ir.translation,type:0
msgid "SQL Constraint"
msgstr "SQL 限制"
msgstr "SQL 約束"
#. module: base
#: field:ir.actions.server,srcmodel_id:0
@ -3070,7 +3070,7 @@ msgstr "肯亞"
#. module: base
#: view:res.partner.event:0
msgid "Event"
msgstr "事件"
msgstr "活動"
#. module: base
#: model:ir.ui.menu,name:base.menu_custom_reports
@ -3080,7 +3080,7 @@ msgstr "自訂報表"
#. module: base
#: selection:base.language.install,lang:0
msgid "Abkhazian / аҧсуа"
msgstr "阿布哈文 / аҧсуа"
msgstr "阿布哈文 / аҧсуа"
#. module: base
#: view:base.module.configuration:0
@ -3106,7 +3106,7 @@ msgstr "聖馬利諾"
#. module: base
#: model:res.country,name:base.bm
msgid "Bermuda"
msgstr "百慕"
msgstr "百慕"
#. module: base
#: model:res.country,name:base.pe
@ -3180,7 +3180,7 @@ msgstr "完整存取"
#: view:ir.model.fields:0
#: model:ir.ui.menu,name:base.menu_security
msgid "Security"
msgstr "安"
msgstr "安"
#. module: base
#: model:res.widget,title:base.openerp_favorites_twitter_widget
@ -3481,8 +3481,8 @@ msgid ""
"plugin, don't forget to register emails to each contact so that the gateway "
"will automatically attach incoming emails to the right partner."
msgstr ""
"客戶是指您與其做生意者,例如公司或機構。客戶可有多個聯絡人或地址,均屬於其員工。您可用「歷史」分頁追蹤所有有關交易:訂單、電郵、機、退款要求等等。如您用"
"電郵閘道Outlook 或 Thunderbird 外掛程式,別忘了為聯絡人登記電郵,好讓閘道自動為合適伙伴寄送收到之電郵。"
"客戶是指您與其做生意者,例如公司或機構。客戶可有多個聯絡人或地址,均屬於其員工。您可用「歷史」分頁追蹤所有有關交易:訂單、電郵、機、退款要求等等。如您用"
"電郵閘道Outlook 或 Thunderbird 外掛程式,別忘了為聯絡人登記電郵,好讓閘道自動為合適伙伴寄送收到之電郵。"
#. module: base
#: field:ir.actions.report.xml,name:0
@ -3517,7 +3517,7 @@ msgstr "名稱"
msgid ""
"If set to true, the action will not be displayed on the right toolbar of a "
"form view"
msgstr "如設為,該動作不會顯示於表單檢視右側工具欄"
msgstr "如設為,該動作不會顯示於表單檢視右側工具欄"
#. module: base
#: model:res.country,name:base.ms
@ -3543,7 +3543,7 @@ msgstr "應用程式詞彙"
msgid ""
"The user's timezone, used to perform timezone conversions between the server "
"and the client."
msgstr ""
msgstr "用戶之時區,用以為伺服器及用戶端進行時區轉換。"
#. module: base
#: field:ir.module.module,demo:0
@ -3565,7 +3565,7 @@ msgstr "日文 / 日本語"
msgid ""
"Source activity. When this activity is over, the condition is tested to "
"determine if we can start the ACT_TO activity."
msgstr ""
msgstr "來源地動態。當再無動態,會測試條件以決定是否開始 ACT_TO 動態。"
#. module: base
#: model:res.partner.category,name:base.res_partner_category_3
@ -3618,7 +3618,7 @@ msgstr "冷岸及央麥恩群島"
#: model:ir.model,name:base.model_ir_actions_wizard
#: selection:ir.ui.menu,action:0
msgid "ir.actions.wizard"
msgstr ""
msgstr "ir.actions.wizard"
#. module: base
#: view:ir.actions.act_window:0
@ -3753,12 +3753,12 @@ msgstr "無法載入模組基礎!(提示:檢查附加元件路徑)"
#. module: base
#: view:res.partner.bank:0
msgid "Bank Account Owner"
msgstr "銀行帳所有者"
msgstr "銀行帳所有者"
#. module: base
#: model:ir.actions.act_window,name:base.act_values_form
msgid "Client Actions Connections"
msgstr "客戶端動作連接"
msgstr "用戶端動作連線"
#. module: base
#: field:ir.attachment,res_name:0
@ -3836,7 +3836,7 @@ msgstr ""
#. module: base
#: view:ir.actions.server:0
msgid "Client Action Configuration"
msgstr "客戶端動作設置"
msgstr "用戶端動作設定"
#. module: base
#: model:ir.model,name:base.model_res_partner_address
@ -3872,13 +3872,13 @@ msgstr "選取要匯入之模組套件 (.zip 檔)"
#: field:res.partner.event,name:0
#: model:res.widget,title:base.events_widget
msgid "Events"
msgstr "事件"
msgstr "活動"
#. module: base
#: model:ir.model,name:base.model_ir_actions_url
#: selection:ir.ui.menu,action:0
msgid "ir.actions.url"
msgstr ""
msgstr "ir.actions.url"
#. module: base
#: model:res.widget,title:base.currency_converter_widget
@ -4098,7 +4098,7 @@ msgstr "重複錯過的"
#. module: base
#: help:ir.actions.server,state:0
msgid "Type of the Action that is to be executed"
msgstr "要執行動作之類型"
msgstr "要執行動作之類型"
#. module: base
#: field:ir.server.object.lines,server_id:0
@ -4706,7 +4706,7 @@ msgid ""
"Track from where is coming your leads and opportunities by creating specific "
"channels that will be maintained at the creation of a document in the "
"system. Some examples of channels can be: Website, Phone Call, Reseller, etc."
msgstr ""
msgstr "於系統建立文件,以保持特定渠道追蹤潛在客戶及商機之來源。渠道例子有網站、電話查詢、零售商等等。"
#. module: base
#: model:res.partner.bank.type.field,name:base.bank_normal_field
@ -4765,7 +4765,7 @@ msgstr "變改我的偏好設定"
#: code:addons/base/ir/ir_actions.py:164
#, python-format
msgid "Invalid model name in the action definition."
msgstr "動作定義之模型名無效。"
msgstr "動作定義之模型名無效。"
#. module: base
#: field:partner.sms.send,text:0
@ -4900,7 +4900,7 @@ msgstr "如指定,此動作會於此用戶登入時於標準選單以外額外
#. module: base
#: view:ir.values:0
msgid "Client Actions"
msgstr "戶端動作"
msgstr "戶端動作"
#. module: base
#: code:addons/orm.py:1806
@ -4921,12 +4921,12 @@ msgstr ""
#. module: base
#: field:workflow.transition,act_to:0
msgid "Destination Activity"
msgstr "目的地動"
msgstr "目的地動"
#. module: base
#: view:ir.values:0
msgid "Connect Events to Actions"
msgstr "把事件連接動作"
msgstr "把活動關聯到動作"
#. module: base
#: model:ir.model,name:base.model_base_update_translations
@ -5041,7 +5041,7 @@ msgstr "網頁圖示影像"
#. module: base
#: view:ir.values:0
msgid "Values for Event Type"
msgstr "事件類型值"
msgstr "活動類型值"
#. module: base
#: selection:ir.model.fields,select_level:0
@ -5069,6 +5069,8 @@ msgid ""
"installed the CRM, with the history tab, you can track all the interactions "
"with a partner such as opportunities, emails, or sales orders issued."
msgstr ""
"客戶(於系統其他地方又稱「伙伴」)助您管理其他公司,包括潛在客戶、客戶及/或供應商,之通訊錄。「伙伴表單」讓您追蹤及紀錄所有所需資訊,以讓您處理公司地址、"
"聯絡人、報價單等等。如您安裝了客戶關係管理(CRM)模組,以歷史分頁您可追蹤與伙伴有關之所有來往,如商機、電郵或銷售訂單等。"
#. module: base
#: model:res.country,name:base.ph
@ -5552,7 +5554,7 @@ msgstr ""
#: code:addons/base/res/res_config.py:94
#, python-format
msgid "Couldn't find previous ir.actions.todo"
msgstr ""
msgstr "找不到之前的 ir.actions.todo"
#. module: base
#: view:ir.actions.act_window:0
@ -5792,7 +5794,7 @@ msgstr "宏都拉斯"
#: help:res.users,menu_tips:0
msgid ""
"Check out this box if you want to always display tips on each menu action"
msgstr ""
msgstr "如想於每個選單動作顯示提示,勾選此框"
#. module: base
#: model:res.country,name:base.eg
@ -6078,7 +6080,7 @@ msgstr "建立日期"
msgid ""
"Select the action that will be executed. Loop action will not be avaliable "
"inside loop."
msgstr "選擇將要執行的動作。循環動作在循環內不可用。"
msgstr "選取要執行之動作。循環動作在循環內不可用。"
#. module: base
#: selection:base.language.install,lang:0
@ -6358,7 +6360,7 @@ msgstr "模組更新結果"
#: view:workflow.activity:0
#: field:workflow.workitem,act_id:0
msgid "Activity"
msgstr "動"
msgstr "動"
#. module: base
#: view:res.partner:0
@ -6423,7 +6425,7 @@ msgstr ""
msgid ""
"Customized views are used when users reorganize the content of their "
"dashboard views (via web client)"
msgstr ""
msgstr "當用戶(以 web client重組其 dashboard 檢視即會使用自訂化檢視"
#. module: base
#: field:ir.model,name:0
@ -6494,7 +6496,7 @@ msgstr ""
#: model:ir.actions.act_window,name:base.res_log_act_window
#: model:ir.ui.menu,name:base.menu_res_log_act_window
msgid "Client Logs"
msgstr "戶端日誌"
msgstr "戶端日誌"
#. module: base
#: model:res.country,name:base.al
@ -6534,7 +6536,7 @@ msgstr ""
#: code:addons/base/ir/ir_actions.py:716
#, python-format
msgid "Problem in configuration `Record Id` in Server Action!"
msgstr "於伺服器動作之「Record Id」配置錯誤"
msgstr "伺服器動作之「紀錄 Id」配置有問題"
#. module: base
#: code:addons/orm.py:2306
@ -6587,7 +6589,7 @@ msgstr "電郵"
#: field:res.config.users,action_id:0
#: field:res.users,action_id:0
msgid "Home Action"
msgstr "家動作Home Action"
msgstr "家動作(Home Action)"
#. module: base
#: code:addons/custom.py:558
@ -7603,7 +7605,7 @@ msgstr "要更新模組"
msgid ""
"Important when you deal with multiple actions, the execution order will be "
"decided based on this, low number is higher priority."
msgstr "對於處理多個動作很重要,其決定動作執行順序;小的數字具較高優先次序。"
msgstr "對於處理多重動作很重要,其決定動作執行次序;小的數字具較高優先次序。"
#. module: base
#: field:ir.actions.report.xml,header:0
@ -7756,7 +7758,7 @@ msgstr "格陵蘭"
#. module: base
#: field:res.partner.bank,acc_number:0
msgid "Account Number"
msgstr "帳"
msgstr "帳號"
#. module: base
#: view:res.lang:0
@ -7969,7 +7971,7 @@ msgstr "斯洛伐克文 / Slovenský jazyk"
#: field:ir.ui.menu,icon_pict:0
#: field:publisher_warranty.contract.wizard,state:0
msgid "unknown"
msgstr "不"
msgstr "不"
#. module: base
#: field:res.currency,symbol:0
@ -8032,7 +8034,7 @@ msgstr "CSV 檔"
#. module: base
#: field:res.company,account_no:0
msgid "Account No."
msgstr "帳"
msgstr "帳號"
#. module: base
#: code:addons/base/res/res_lang.py:157
@ -8128,7 +8130,7 @@ msgstr "土耳其文 / Türkçe"
#: view:workflow:0
#: field:workflow,activities:0
msgid "Activities"
msgstr "動"
msgstr "動"
#. module: base
#: field:ir.actions.act_window,auto_refresh:0
@ -8166,7 +8168,7 @@ msgstr ""
#: model:ir.ui.menu,name:base.menu_event_association
#: model:ir.ui.menu,name:base.menu_event_main
msgid "Events Organisation"
msgstr "事件組織"
msgstr "活動組織"
#. module: base
#: model:ir.actions.act_window,name:base.ir_sequence_actions
@ -8247,7 +8249,7 @@ msgstr "添加公司 RML 頁首與否"
#. module: base
#: help:workflow.transition,act_to:0
msgid "The destination activity."
msgstr "目的地活地。"
msgstr "目的地動態。"
#. module: base
#: view:base.module.update:0
@ -8283,7 +8285,7 @@ msgstr "聖誕島"
#. module: base
#: view:ir.actions.server:0
msgid "Other Actions Configuration"
msgstr "其他動作置"
msgstr "其他動作置"
#. module: base
#: view:res.config.installer:0
@ -8307,7 +8309,7 @@ msgstr "額外資訊"
#: model:ir.actions.act_window,name:base.act_values_form_action
#: model:ir.ui.menu,name:base.menu_values_form_action
msgid "Client Events"
msgstr "客戶端事件"
msgstr "用戶端活動"
#. module: base
#: view:ir.module.module:0
@ -8429,7 +8431,7 @@ msgstr "關聯欄位"
#. module: base
#: view:res.partner.event:0
msgid "Event Logs"
msgstr "事件日誌"
msgstr "活動日誌"
#. module: base
#: code:addons/base/module/wizard/base_module_configuration.py:37
@ -8446,7 +8448,7 @@ msgstr "目的地實例"
#: field:ir.actions.act_window,multi:0
#: field:ir.actions.wizard,multi:0
msgid "Action on Multiple Doc."
msgstr "動作作用於多個文件"
msgstr "多重文件之動作。"
#. module: base
#: view:base.language.export:0
@ -8477,7 +8479,7 @@ msgstr "盧森堡"
#: help:ir.values,key2:0
msgid ""
"The kind of action or button in the client side that will trigger the action."
msgstr "客戶端的該類動作或按鈕將觸發此動作。"
msgstr "用戶端的該類動作或按鈕會觸發此動作。"
#. module: base
#: code:addons/base/ir/ir_ui_menu.py:285
@ -8899,7 +8901,7 @@ msgstr "目前視窗"
#. module: base
#: view:ir.values:0
msgid "Action Source"
msgstr "動作來源"
msgstr "動作來源"
#. module: base
#: view:res.config.view:0
@ -9048,7 +9050,7 @@ msgstr "本地化"
#. module: base
#: view:ir.actions.server:0
msgid "Action to Launch"
msgstr "要啟動動作"
msgstr "要執行之動作"
#. module: base
#: view:ir.cron:0
@ -9086,7 +9088,7 @@ msgstr "另存為附件前綴"
msgid ""
"Only one client action will be executed, last client action will be "
"considered in case of multiple client actions."
msgstr "只能執行一個客戶端動作,如有多重動作只考慮最後一個。"
msgstr "只會執行一個用戶端動作,如有多重用戶端動作只考慮最後一個。"
#. module: base
#: view:res.lang:0
@ -9131,7 +9133,7 @@ msgstr "塞席爾"
#: model:ir.model,name:base.model_res_partner_bank
#: view:res.partner.bank:0
msgid "Bank Accounts"
msgstr "銀行帳"
msgstr "銀行帳"
#. module: base
#: model:res.country,name:base.sl
@ -9152,7 +9154,7 @@ msgstr "土克斯及開科斯群島"
#. module: base
#: field:res.partner.bank,owner_name:0
msgid "Account Owner"
msgstr "帳所有者"
msgstr "帳所有者"
#. module: base
#: code:addons/base/res/res_user.py:256

View File

@ -1274,7 +1274,7 @@
<record id="action_model_model" model="ir.actions.act_window">
<field name="name">Objects</field>
<field name="name">Models</field>
<field name="res_model">ir.model</field>
<field name="view_type">form</field>
<field name="context">{'manual':True}</field>
@ -1740,7 +1740,7 @@
<field name="name">Property multi-company</field>
<field model="ir.model" name="model_id" ref="model_ir_property"/>
<field eval="True" name="global"/>
<field name="domain_force">['|',('company_id','=',user.company_id.id),('company_id','=',False)]</field>
<field name="domain_force">['|',('company_id','child_of',[user.company_id.id]),('company_id','=',False)]</field>
</record>
<!--server action view-->
@ -1766,7 +1766,9 @@
<page string="Trigger" attrs="{'invisible':[('state','!=','trigger')]}">
<separator colspan="4" string="Trigger Configuration"/>
<field name="wkf_model_id" attrs="{'required':[('state','=','trigger')]}"/>
<field name="trigger_obj_id" context="{'key':''}" domain="[('model_id','=',model_id)]" attrs="{'required':[('state','=','trigger')]}"/>
<field name="trigger_obj_id" context="{'key':''}"
domain="[('model_id','=',model_id),('ttype','in',['many2one','int'])]"
attrs="{'required':[('state','=','trigger')]}"/>
<field name="trigger_name" attrs="{'required':[('state','=','trigger')]}"/>
</page>
<page string="Action to Launch" attrs="{'invisible':[('state','!=','client_action')]}">

View File

@ -438,14 +438,15 @@ server_object_lines()
class actions_server(osv.osv):
def _select_signals(self, cr, uid, context=None):
cr.execute("SELECT distinct w.osv, t.signal FROM wkf w, wkf_activity a, wkf_transition t \
WHERE w.id = a.wkf_id AND t.act_from = a.id OR t.act_to = a.id AND t.signal!='' \
AND t.signal NOT IN (null, NULL)")
cr.execute("""SELECT distinct w.osv, t.signal FROM wkf w, wkf_activity a, wkf_transition t
WHERE w.id = a.wkf_id AND
(t.act_from = a.id OR t.act_to = a.id) AND
t.signal IS NOT NULL""")
result = cr.fetchall() or []
res = []
for rs in result:
if rs[0] is not None and rs[1] is not None:
line = rs[0], "%s - (%s)" % (rs[1], rs[0])
line = rs[1], "%s - (%s)" % (rs[1], rs[0])
res.append(line)
return res
@ -456,13 +457,15 @@ class actions_server(osv.osv):
return [(r['model'], r['name']) for r in res] + [('','')]
def change_object(self, cr, uid, ids, copy_object, state, context=None):
if state == 'object_copy':
if state == 'object_copy' and copy_object:
if context is None:
context = {}
model_pool = self.pool.get('ir.model')
model = copy_object.split(',')[0]
mid = model_pool.search(cr, uid, [('model','=',model)])
return {
'value':{'srcmodel_id':mid[0]},
'context':context
'value': {'srcmodel_id': mid[0]},
'context': context
}
else:
return {}
@ -503,9 +506,9 @@ class actions_server(osv.osv):
'sequence': fields.integer('Sequence', help="Important when you deal with multiple actions, the execution order will be decided based on this, low number is higher priority."),
'model_id': fields.many2one('ir.model', 'Object', required=True, help="Select the object on which the action will work (read, write, create)."),
'action_id': fields.many2one('ir.actions.actions', 'Client Action', help="Select the Action Window, Report, Wizard to be executed."),
'trigger_name': fields.selection(_select_signals, string='Trigger Name', size=128, help="Select the Signal name that is to be used as the trigger."),
'wkf_model_id': fields.many2one('ir.model', 'Workflow On', help="Workflow to be executed on this model."),
'trigger_obj_id': fields.many2one('ir.model.fields','Trigger On', help="Select the object from the model on which the workflow will executed."),
'trigger_name': fields.selection(_select_signals, string='Trigger Signal', size=128, help="The workflow signal to trigger"),
'wkf_model_id': fields.many2one('ir.model', 'Target Object', help="The object that should receive the workflow signal (must have an associated workflow)"),
'trigger_obj_id': fields.many2one('ir.model.fields','Relation Field', help="The field on the current object that links to the target object record (must be a many2one, or an integer field with the record ID)"),
'email': fields.char('Email Address', size=512, help="Expression that returns the email address to send to. Can be based on the same values as for the condition field.\n"
"Example: object.invoice_address_id.email, or 'me@example.com'"),
'subject': fields.char('Subject', size=1024, translate=True, help="Email subject, may contain expressions enclosed in double brackets based on the same values as those "
@ -532,7 +535,7 @@ class actions_server(osv.osv):
'sequence': lambda *a: 5,
'code': lambda *a: """# You can use the following variables:
# - self: ORM model of the record on which the action is triggered
# - object or obj: browse_record of the record on which the action is triggered
# - object: browse_record of the record on which the action is triggered if there is one, otherwise None
# - pool: ORM model pool (i.e. self.pool)
# - time: Python time module
# - cr: database cursor
@ -685,9 +688,10 @@ class actions_server(osv.osv):
if action.state == 'trigger':
wf_service = netsvc.LocalService("workflow")
model = action.wkf_model_id.model
res_id = obj_pool.read(cr, uid, [context.get('active_id')], [action.trigger_obj_id.name])
id = res_id [0][action.trigger_obj_id.name]
wf_service.trg_validate(uid, model, int(id), action.trigger_name, cr)
m2o_field_name = action.trigger_obj_id.name
target_id = obj_pool.read(cr, uid, context.get('active_id'), [m2o_field_name])[m2o_field_name]
target_id = target_id[0] if isinstance(target_id,tuple) else target_id
wf_service.trg_validate(uid, model, int(target_id), action.trigger_name, cr)
if action.state == 'sms':
#TODO: set the user and password from the system

View File

@ -21,13 +21,20 @@
import time
import logging
import threading
import psycopg2
from datetime import datetime
from dateutil.relativedelta import relativedelta
import netsvc
import tools
from tools.safe_eval import safe_eval as eval
import openerp
import pooler
import tools
from openerp.cron import WAKE_UP_NOW
from osv import fields, osv
from tools import DEFAULT_SERVER_DATETIME_FORMAT
from tools.safe_eval import safe_eval as eval
from tools.translate import _
def str2tuple(s):
return eval('tuple(%s)' % (s or ''))
@ -41,10 +48,15 @@ _intervalTypes = {
'minutes': lambda interval: relativedelta(minutes=interval),
}
class ir_cron(osv.osv, netsvc.Agent):
""" This is the ORM object that periodically executes actions.
Note that we use the netsvc.Agent()._logger member.
class ir_cron(osv.osv):
""" Model describing cron jobs (also called actions or tasks).
"""
# TODO: perhaps in the future we could consider a flag on ir.cron jobs
# that would cause database wake-up even if the database has not been
# loaded yet or was already unloaded (e.g. 'force_db_wakeup' or something)
# See also openerp.cron
_name = "ir.cron"
_order = 'name'
_columns = {
@ -54,17 +66,17 @@ class ir_cron(osv.osv, netsvc.Agent):
'interval_number': fields.integer('Interval Number',help="Repeat every x."),
'interval_type': fields.selection( [('minutes', 'Minutes'),
('hours', 'Hours'), ('work_days','Work Days'), ('days', 'Days'),('weeks', 'Weeks'), ('months', 'Months')], 'Interval Unit'),
'numbercall': fields.integer('Number of Calls', help='Number of time the function is called,\na negative number indicates no limit'),
'doall' : fields.boolean('Repeat Missed', help="Enable this if you want to execute missed occurences as soon as the server restarts."),
'nextcall' : fields.datetime('Next Execution Date', required=True, help="Next planned execution date for this scheduler"),
'model': fields.char('Object', size=64, help="Name of object whose function will be called when this scheduler will run. e.g. 'res.partener'"),
'function': fields.char('Function', size=64, help="Name of the method to be called on the object when this scheduler is executed."),
'args': fields.text('Arguments', help="Arguments to be passed to the method. e.g. (uid,)"),
'priority': fields.integer('Priority', help='0=Very Urgent\n10=Not urgent')
'numbercall': fields.integer('Number of Calls', help='How many times the method is called,\na negative number indicates no limit.'),
'doall' : fields.boolean('Repeat Missed', help="Specify if missed occurrences should be executed when the server restarts."),
'nextcall' : fields.datetime('Next Execution Date', required=True, help="Next planned execution date for this job."),
'model': fields.char('Object', size=64, help="Model name on which the method to be called is located, e.g. 'res.partner'."),
'function': fields.char('Method', size=64, help="Name of the method to be called when this job is processed."),
'args': fields.text('Arguments', help="Arguments to be passed to the method, e.g. (uid,)."),
'priority': fields.integer('Priority', help='The priority of the job, as an integer: 0 means higher priority, 10 means lower priority.')
}
_defaults = {
'nextcall' : lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
'nextcall' : lambda *a: time.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'priority' : lambda *a: 5,
'user_id' : lambda obj,cr,uid,context: uid,
'interval_number' : lambda *a: 1,
@ -74,6 +86,8 @@ class ir_cron(osv.osv, netsvc.Agent):
'doall' : lambda *a: 1
}
_logger = logging.getLogger('cron')
def _check_args(self, cr, uid, ids, context=None):
try:
for this in self.browse(cr, uid, ids, context):
@ -86,68 +100,164 @@ class ir_cron(osv.osv, netsvc.Agent):
(_check_args, 'Invalid arguments', ['args']),
]
def _handle_callback_exception(self, cr, uid, model, func, args, job_id, job_exception):
cr.rollback()
logger=logging.getLogger('cron')
logger.exception("Call of self.pool.get('%s').%s(cr, uid, *%r) failed in Job %s" % (model, func, args, job_id))
def _handle_callback_exception(self, cr, uid, model_name, method_name, args, job_id, job_exception):
""" Method called when an exception is raised by a job.
def _callback(self, cr, uid, model, func, args, job_id):
Simply logs the exception and rollback the transaction.
:param model_name: model name on which the job method is located.
:param method_name: name of the method to call when this job is processed.
:param args: arguments of the method (without the usual self, cr, uid).
:param job_id: job id.
:param job_exception: exception raised by the job.
"""
cr.rollback()
self._logger.exception("Call of self.pool.get('%s').%s(cr, uid, *%r) failed in Job %s" % (model_name, method_name, args, job_id))
def _callback(self, cr, uid, model_name, method_name, args, job_id):
""" Run the method associated to a given job
It takes care of logging and exception handling.
:param model_name: model name on which the job method is located.
:param method_name: name of the method to call when this job is processed.
:param args: arguments of the method (without the usual self, cr, uid).
:param job_id: job id.
"""
args = str2tuple(args)
m = self.pool.get(model)
if m and hasattr(m, func):
f = getattr(m, func)
model = self.pool.get(model_name)
if model and hasattr(model, method_name):
method = getattr(model, method_name)
try:
netsvc.log('cron', (cr.dbname,uid,'*',model,func)+tuple(args), channel=logging.DEBUG,
netsvc.log('cron', (cr.dbname,uid,'*',model_name,method_name)+tuple(args), channel=logging.DEBUG,
depth=(None if self._logger.isEnabledFor(logging.DEBUG_RPC_ANSWER) else 1), fn='object.execute')
logger = logging.getLogger('execution time')
if logger.isEnabledFor(logging.DEBUG):
start_time = time.time()
f(cr, uid, *args)
method(cr, uid, *args)
if logger.isEnabledFor(logging.DEBUG):
end_time = time.time()
logger.log(logging.DEBUG, '%.3fs (%s, %s)' % (end_time - start_time, model, func))
logger.log(logging.DEBUG, '%.3fs (%s, %s)' % (end_time - start_time, model_name, method_name))
except Exception, e:
self._handle_callback_exception(cr, uid, model, func, args, job_id, e)
self._handle_callback_exception(cr, uid, model_name, method_name, args, job_id, e)
def _poolJobs(self, db_name, check=False):
def _run_job(self, cr, job, now):
""" Run a given job taking care of the repetition.
The cursor has a lock on the job (aquired by _run_jobs_multithread()) and this
method is run in a worker thread (spawned by _run_jobs_multithread())).
:param job: job to be run (as a dictionary).
:param now: timestamp (result of datetime.now(), no need to call it multiple time).
"""
try:
db, pool = pooler.get_db_and_pool(db_name)
except:
return False
nextcall = datetime.strptime(job['nextcall'], DEFAULT_SERVER_DATETIME_FORMAT)
numbercall = job['numbercall']
ok = False
while nextcall < now and numbercall:
if numbercall > 0:
numbercall -= 1
if not ok or job['doall']:
self._callback(cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
if numbercall:
nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
ok = True
addsql = ''
if not numbercall:
addsql = ', active=False'
cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",
(nextcall.strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))
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())
openerp.cron.schedule_wakeup(nextcall, cr.dbname)
finally:
cr.commit()
cr.close()
openerp.cron.release_thread_slot()
def _run_jobs_multithread(self):
# TODO remove 'check' argument from addons/base_action_rule/base_action_rule.py
""" Process the cron jobs by spawning worker threads.
This selects in database all the jobs that should be processed. It then
tries to lock each of them and, if it succeeds, spawns a thread to run
the cron job (if it doesn't succeed, it means the job was already
locked to be taken care of by another thread).
The cursor used to lock the job in database is given to the worker
thread (which has to close it itself).
"""
db = self.pool.db
cr = db.cursor()
db_name = db.dbname
try:
if not pool._init:
now = datetime.now()
cr.execute('select * from ir_cron where numbercall<>0 and active and nextcall<=now() order by priority')
for job in cr.dictfetchall():
nextcall = datetime.strptime(job['nextcall'], '%Y-%m-%d %H:%M:%S')
numbercall = job['numbercall']
jobs = {} # mapping job ids to jobs for all jobs being processed.
now = datetime.now()
# Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1.
cr.execute("""SELECT * FROM ir_cron
WHERE numbercall != 0
AND active AND nextcall <= (now() at time zone 'UTC')
ORDER BY priority""")
for job in cr.dictfetchall():
if not openerp.cron.get_thread_slots():
break
jobs[job['id']] = job
ok = False
while nextcall < now and numbercall:
if numbercall > 0:
numbercall -= 1
if not ok or job['doall']:
self._callback(cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
if numbercall:
nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
ok = True
addsql = ''
if not numbercall:
addsql = ', active=False'
cr.execute("update ir_cron set nextcall=%s, numbercall=%s"+addsql+" where id=%s", (nextcall.strftime('%Y-%m-%d %H:%M:%S'), numbercall, job['id']))
cr.commit()
task_cr = db.cursor()
try:
# Try to grab an exclusive lock on the job row from within the task transaction
acquired_lock = False
task_cr.execute("""SELECT *
FROM ir_cron
WHERE id=%s
FOR UPDATE NOWAIT""",
(job['id'],), log_exceptions=False)
acquired_lock = True
except psycopg2.OperationalError, e:
if e.pgcode == '55P03':
# Class 55: Object not in prerequisite state; 55P03: lock_not_available
self._logger.debug('Another process/thread is already busy executing job `%s`, skipping it.', job['name'])
continue
else:
# Unexpected OperationalError
raise
finally:
if not acquired_lock:
# we're exiting due to an exception while acquiring the lot
task_cr.close()
# Got the lock on the job row, now spawn a thread to execute it in the transaction with the lock
task_thread = threading.Thread(target=self._run_job, name=job['name'], args=(task_cr, job, now))
# force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default)
task_thread.setDaemon(False)
openerp.cron.take_thread_slot()
task_thread.start()
self._logger.debug('Cron execution thread for job `%s` spawned', job['name'])
cr.execute('select min(nextcall) as min_next_call from ir_cron where numbercall<>0 and active')
next_call = cr.dictfetchone()['min_next_call']
if next_call:
next_call = time.mktime(time.strptime(next_call, '%Y-%m-%d %H:%M:%S'))
# Find next earliest job ignoring currently processed jobs (by this and other cron threads)
find_next_time_query = """SELECT min(nextcall) AS min_next_call
FROM ir_cron WHERE numbercall != 0 AND active"""
if jobs:
cr.execute(find_next_time_query + " AND id NOT IN %s", (tuple(jobs.keys()),))
else:
next_call = int(time.time()) + 3600 # if do not find active cron job from database, it will run again after 1 day
cr.execute(find_next_time_query)
next_call = cr.dictfetchone()['min_next_call']
if not check:
self.setAlarm(self._poolJobs, next_call, db_name, db_name)
if next_call:
next_call = time.mktime(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
next_call = time.time() + (24*3600)
openerp.cron.schedule_wakeup(next_call, db_name)
except Exception, ex:
self._logger.warning('Exception in cron:', exc_info=True)
@ -156,12 +266,8 @@ class ir_cron(osv.osv, netsvc.Agent):
cr.commit()
cr.close()
def restart(self, dbname):
self.cancel(dbname)
# Reschedule cron processing job asap, but not in the current thread
self.setAlarm(self._poolJobs, time.time(), dbname, dbname)
def update_running_cron(self, cr):
""" Schedule as soon as possible a wake-up for this database. """
# Verify whether the server is already started and thus whether we need to commit
# immediately our changes and restart the cron agent in order to apply the change
# immediately. The commit() is needed because as soon as the cron is (re)started it
@ -171,23 +277,37 @@ class ir_cron(osv.osv, netsvc.Agent):
# when the server is only starting or loading modules (hence the test on pool._init).
if not self.pool._init:
cr.commit()
self.restart(cr.dbname)
openerp.cron.schedule_wakeup(WAKE_UP_NOW, self.pool.db.dbname)
def _try_lock(self, cr, uid, ids, context=None):
"""Try to grab a dummy exclusive write-lock to the rows with the given ids,
to make sure a following write() or unlink() will not block due
to a process currently executing those cron tasks"""
try:
cr.execute("""SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table,
(tuple(ids),), log_exceptions=False)
except psycopg2.OperationalError:
cr.rollback() # early rollback to allow translations to work for the user feedback
raise osv.except_osv(_("Record cannot be modified right now"),
_("This cron task is currently being executed and may not be modified, "
"please try again in a few minutes"))
def create(self, cr, uid, vals, context=None):
res = super(ir_cron, self).create(cr, uid, vals, context=context)
self.update_running_cron(cr)
return res
def write(self, cr, user, ids, vals, context=None):
res = super(ir_cron, self).write(cr, user, ids, vals, context=context)
def write(self, cr, uid, ids, vals, context=None):
self._try_lock(cr, uid, ids, context)
res = super(ir_cron, self).write(cr, uid, ids, vals, context=context)
self.update_running_cron(cr)
return res
def unlink(self, cr, uid, ids, context=None):
self._try_lock(cr, uid, ids, context)
res = super(ir_cron, self).unlink(cr, uid, ids, context=context)
self.update_running_cron(cr)
return res
ir_cron()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -189,7 +189,7 @@ class ir_mail_server(osv.osv):
password=smtp_server.smtp_pass, encryption=smtp_server.smtp_encryption,
smtp_debug=smtp_server.smtp_debug)
except Exception, e:
raise osv.except_osv(_("Connection test failed!"), _("Here is what we got instead:\n %s") % e)
raise osv.except_osv(_("Connection test failed!"), _("Here is what we got instead:\n %s") % tools.ustr(e))
finally:
try:
if smtp: smtp.quit()

View File

@ -56,14 +56,14 @@ def _in_modules(self, cr, uid, ids, field_name, arg, context=None):
class ir_model(osv.osv):
_name = 'ir.model'
_description = "Objects"
_description = "Models"
_order = 'model'
def _is_osv_memory(self, cr, uid, ids, field_name, arg, context=None):
models = self.browse(cr, uid, ids, context=context)
res = dict.fromkeys(ids)
for model in models:
res[model.id] = isinstance(self.pool.get(model.model), osv.osv_memory)
res[model.id] = self.pool.get(model.model).is_transient()
return res
def _search_osv_memory(self, cr, uid, model, name, domain, context=None):
@ -85,8 +85,8 @@ class ir_model(osv.osv):
return res
_columns = {
'name': fields.char('Object Name', size=64, translate=True, required=True),
'model': fields.char('Object', size=64, required=True, select=1),
'name': fields.char('Model Description', size=64, translate=True, required=True),
'model': fields.char('Model', size=64, required=True, select=1),
'info': fields.text('Information'),
'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True),
'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Type',readonly=True),
@ -97,12 +97,12 @@ class ir_model(osv.osv):
'modules': fields.function(_in_modules, method=True, type='char', size=128, string='In modules', help='List of modules in which the object is defined or inherited'),
'view_ids': fields.function(_view_ids, method=True, type='one2many', obj='ir.ui.view', string='Views'),
}
_defaults = {
'model': lambda *a: 'x_',
'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
}
def _check_model_name(self, cr, uid, ids, context=None):
for model in self.browse(cr, uid, ids, context=context):
if model.state=='manual':
@ -114,9 +114,13 @@ class ir_model(osv.osv):
def _model_name_msg(self, cr, uid, ids, context=None):
return _('The Object name must start with x_ and not contain any special character !')
_constraints = [
(_check_model_name, _model_name_msg, ['model']),
]
_sql_constraints = [
('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),
]
# overridden to allow searching both on model name (model field)
# and model description (name field)
@ -161,7 +165,7 @@ class ir_model(osv.osv):
pass
x_custom_model._name = model
x_custom_model._module = False
a = x_custom_model.createInstance(self.pool, cr)
a = x_custom_model.create_instance(self.pool, cr)
if (not a._columns) or ('x_name' in a._columns.keys()):
x_name = 'x_name'
else:
@ -477,14 +481,12 @@ class ir_model_access(osv.osv):
if isinstance(model, browse_record):
assert model._table_name == 'ir.model', 'Invalid model object'
model_name = model.name
model_name = model.model
else:
model_name = model
# osv_memory objects can be read by everyone, as they only return
# results that belong to the current user (except for superuser)
model_obj = self.pool.get(model_name)
if isinstance(model_obj, osv.osv_memory):
# TransientModel records have no access rights, only an implicit access rule
if self.pool.get(model_name).is_transient():
return True
# We check if a specific rule exists
@ -519,7 +521,7 @@ class ir_model_access(osv.osv):
}
raise except_orm(_('AccessError'), msgs[mode] % (model_name, groups) )
return r
return r or False
__cache_clearing_methods = []
@ -616,7 +618,7 @@ class ir_model_data(osv.osv):
"""Returns the id of the ir.model.data record corresponding to a given module and xml_id (cached) or raise a ValueError if not found"""
ids = self.search(cr, uid, [('module','=',module), ('name','=', xml_id)])
if not ids:
raise ValueError('No references to %s.%s' % (module, xml_id))
raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
# the sql constraints ensure us we have only one result
return ids[0]
@ -626,7 +628,7 @@ class ir_model_data(osv.osv):
data_id = self._get_id(cr, uid, module, xml_id)
res = self.read(cr, uid, data_id, ['model', 'res_id'])
if not res['res_id']:
raise ValueError('No references to %s.%s' % (module, xml_id))
raise ValueError('No such external ID currently defined in the system: %s.%s' % (module, xml_id))
return (res['model'], res['res_id'])
def get_object(self, cr, uid, module, xml_id, context=None):

View File

@ -26,8 +26,7 @@ from functools import partial
import tools
from tools.safe_eval import safe_eval as eval
from tools.misc import unquote as unquote
SUPERUSER_UID = 1
from openerp import SUPERUSER_ID
class ir_rule(osv.osv):
_name = 'ir.rule'
@ -68,7 +67,7 @@ class ir_rule(osv.osv):
return res
def _check_model_obj(self, cr, uid, ids, context=None):
return not any(isinstance(self.pool.get(rule.model_id.model), osv.osv_memory) for rule in self.browse(cr, uid, ids, context))
return not any(self.pool.get(rule.model_id.model).is_transient() for rule in self.browse(cr, uid, ids, context))
_columns = {
'name': fields.char('Name', size=128, select=1),
@ -104,7 +103,7 @@ class ir_rule(osv.osv):
if mode not in self._MODES:
raise ValueError('Invalid mode: %r' % (mode,))
if uid == SUPERUSER_UID:
if uid == SUPERUSER_ID:
return None
cr.execute("""SELECT r.id
FROM ir_rule r
@ -117,10 +116,10 @@ class ir_rule(osv.osv):
rule_ids = [x[0] for x in cr.fetchall()]
if rule_ids:
# browse user as super-admin root to avoid access errors!
user = self.pool.get('res.users').browse(cr, SUPERUSER_UID, uid)
user = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid)
global_domains = [] # list of domains
group_domains = {} # map: group -> list of domains
for rule in self.browse(cr, SUPERUSER_UID, rule_ids):
for rule in self.browse(cr, SUPERUSER_ID, rule_ids):
# read 'domain' as UID to have the correct eval context for the rule.
rule_domain = self.read(cr, uid, rule.id, ['domain'])['domain']
dom = expression.normalize(rule_domain)

View File

@ -96,6 +96,19 @@ class view(osv.osv):
if not cr.fetchone():
cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, type, inherit_id)')
def get_inheriting_views_arch(self, cr, uid, view_id, model, context=None):
"""Retrieves the architecture of views that inherit from the given view.
:param int view_id: id of the view whose inheriting views should be retrieved
:param str model: model identifier of the view's related model (for double-checking)
:rtype: list of tuples
:return: [(view_arch,view_id), ...]
"""
cr.execute("""SELECT arch, id FROM ir_ui_view WHERE inherit_id=%s AND model=%s
ORDER BY priority""",
(view_id, model))
return cr.fetchall()
def write(self, cr, uid, ids, vals, context={}):
if not isinstance(ids, (list, tuple)):
ids = [ids]
@ -159,10 +172,10 @@ class view(osv.osv):
label_string = ""
if label:
for lbl in eval(label):
if t.has_key(str(lbl)) and str(t[lbl])=='False':
if t.has_key(tools.ustr(lbl)) and tools.ustr(t[lbl])=='False':
label_string = label_string + ' '
else:
label_string = label_string + " " + t[lbl]
label_string = label_string + " " + tools.ustr(t[lbl])
labels[str(t['id'])] = (a['id'],label_string)
g = graph(nodes, transitions, no_ancester)
g.process(start)

View File

@ -19,18 +19,15 @@
#
##############################################################################
from osv import osv
from osv.orm import orm_memory
import openerp
class osv_memory_autovacuum(osv.osv_memory):
class osv_memory_autovacuum(openerp.osv.osv.osv_memory):
""" Expose the osv_memory.vacuum() method to the cron jobs mechanism. """
_name = 'osv_memory.autovacuum'
def power_on(self, cr, uid, context=None):
for model in self.pool.obj_list():
obj = self.pool.get(model)
if isinstance(obj, orm_memory):
obj.vaccum(cr, uid)
for model in self.pool.models.values():
if model.is_transient():
model._transient_vacuum(cr, uid)
return True
osv_memory_autovacuum()

View File

@ -20,6 +20,5 @@
##############################################################################
import wizard_menu
import wizard_screen
import create_action
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -1,77 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import wizard
import pooler
import time
action_type = '''<?xml version="1.0"?>
<form string="Select Action Type">
<field name="type"/>
</form>'''
action_type_fields = {
'type': {'string':"Select Action Type",'type':'selection','required':True ,'selection':[('ir.actions.report.xml','Open Report')]},
}
report_action = '''<?xml version="1.0"?>
<form string="Select Report">
<field name="report" colspan="4"/>
</form>'''
report_action_fields = {
'report': {'string':"Select Report",'type':'many2one','relation':'ir.actions.report.xml', 'required':True},
}
class create_action(wizard.interface):
def _create_report_action(self, cr, uid, data, context={}):
pool = pooler.get_pool(cr.dbname)
reports = pool.get('ir.actions.report.xml')
form = data['form']
rpt = reports.browse(cr, uid, form['report'])
action = """action = {"type": "ir.actions.report.xml","model":"%s","report_name": "%s","ids": context["active_ids"]}""" % (rpt.model, rpt.report_name)
obj = pool.get('ir.actions.server')
obj.write(cr, uid, data['ids'], {'code':action})
return {}
states = {
'init': {
'actions': [],
'result': {'type':'form', 'arch':action_type,'fields':action_type_fields, 'state':[('step_1','Next'),('end','Close')]}
},
'step_1': {
'actions': [],
'result': {'type':'form', 'arch':report_action,'fields':report_action_fields, 'state':[('create','Create'),('end','Close')]}
},
'create': {
'actions': [_create_report_action],
'result': {'type':'state', 'state':'end'}
},
}
create_action('server.action.create')

View File

@ -21,12 +21,5 @@
</field>
</record>
<act_window context="{'model_id': active_id}" id="act_menu_create" name="Create Menu" res_model="wizard.ir.model.menu.create" target="new" view_mode="form"/>
<wizard
id="wizard_server_action_create"
model="ir.actions.server"
name="server.action.create"
string="Create Action"
menu="False"
/>
</data>
</openerp>

View File

@ -107,48 +107,59 @@ class module(osv.osv):
view_obj = self.pool.get('ir.ui.view')
report_obj = self.pool.get('ir.actions.report.xml')
menu_obj = self.pool.get('ir.ui.menu')
mlist = self.browse(cr, uid, ids, context=context)
mnames = {}
for m in mlist:
# skip uninstalled modules below,
# no data to find anyway
if m.state in ('installed', 'to upgrade', 'to remove'):
mnames[m.name] = m.id
res[m.id] = {
'menus_by_module':[],
'reports_by_module':[],
dmodels = []
if field_name is None or 'views_by_module' in field_name:
dmodels.append('ir.ui.view')
if field_name is None or 'reports_by_module' in field_name:
dmodels.append('ir.actions.report.xml')
if field_name is None or 'menus_by_module' in field_name:
dmodels.append('ir.ui.menu')
assert dmodels, "no models for %s" % field_name
for module_rec in self.browse(cr, uid, ids, context=context):
res[module_rec.id] = {
'menus_by_module': [],
'reports_by_module': [],
'views_by_module': []
}
if not mnames:
return res
# Skip uninstalled modules below, no data to find anyway.
if module_rec.state not in ('installed', 'to upgrade', 'to remove'):
continue
view_id = model_data_obj.search(cr,uid,[('module','in', mnames.keys()),
('model','in',('ir.ui.view','ir.actions.report.xml','ir.ui.menu'))])
for data_id in model_data_obj.browse(cr,uid,view_id,context):
# We use try except, because views or menus may not exist
# then, search and group ir.model.data records
imd_models = dict( [(m,[]) for m in dmodels])
imd_ids = model_data_obj.search(cr,uid,[('module','=', module_rec.name),
('model','in',tuple(dmodels))])
for imd_res in model_data_obj.read(cr, uid, imd_ids, ['model', 'res_id'], context=context):
imd_models[imd_res['model']].append(imd_res['res_id'])
# For each one of the models, get the names of these ids.
# We use try except, because views or menus may not exist.
try:
key = data_id.model
res_mod_dic = res[mnames[data_id.module]]
if key=='ir.ui.view':
v = view_obj.browse(cr,uid,data_id.res_id)
res_mod_dic = res[module_rec.id]
for v in view_obj.browse(cr, uid, imd_models.get('ir.ui.view', []), context=context):
aa = v.inherit_id and '* INHERIT ' or ''
res_mod_dic['views_by_module'].append(aa + v.name + '('+v.type+')')
elif key=='ir.actions.report.xml':
res_mod_dic['reports_by_module'].append(report_obj.browse(cr,uid,data_id.res_id).name)
elif key=='ir.ui.menu':
res_mod_dic['menus_by_module'].append(menu_obj.browse(cr,uid,data_id.res_id).complete_name)
for rx in report_obj.browse(cr, uid, imd_models.get('ir.actions.report.xml', []), context=context):
res_mod_dic['reports_by_module'].append(rx.name)
for um in menu_obj.browse(cr, uid, imd_models.get('ir.ui.menu', []), context=context):
res_mod_dic['menus_by_module'].append(um.complete_name)
except KeyError, e:
self.__logger.warning(
'Data not found for reference %s[%s:%s.%s]', data_id.model,
data_id.res_id, data_id.model, data_id.name, exc_info=True)
pass
'Data not found for items of %s', module_rec.name)
except AttributeError, e:
self.__logger.warning(
'Data not found for items of %s %s', module_rec.name, str(e))
except Exception, e:
self.__logger.warning('Unknown error while browsing %s[%s]',
data_id.model, data_id.res_id, exc_info=True)
pass
self.__logger.warning('Unknown error while fetching data of %s',
module_rec.name, exc_info=True)
for key, value in res.iteritems():
for k, v in res[key].iteritems() :
for k, v in res[key].iteritems():
res[key][k] = "\n".join(sorted(v))
return res
@ -437,12 +448,11 @@ class module(osv.osv):
res.append(mod.url)
if not download:
continue
zipfile = urllib.urlopen(mod.url).read()
zip_content = urllib.urlopen(mod.url).read()
fname = addons.get_module_path(str(mod.name)+'.zip', downloaded=True)
try:
fp = file(fname, 'wb')
fp.write(zipfile)
fp.close()
with open(fname, 'wb') as fp:
fp.write(zip_content)
except Exception:
self.__logger.exception('Error when trying to create module '
'file %s', fname)

View File

@ -35,9 +35,12 @@ class base_language_import(osv.osv_memory):
'name': fields.char('Language Name',size=64 , required=True),
'code': fields.char('Code (eg:en__US)',size=5 , required=True),
'data': fields.binary('File', required=True),
'overwrite': fields.boolean('Overwrite Existing Terms',
help="If you enable this option, existing translations (including custom ones) "
"will be overwritten and replaced by those in this file"),
}
def import_lang(self, cr, uid, ids, context):
def import_lang(self, cr, uid, ids, context=None):
"""
Import Language
@param cr: the current row, from the database cursor.
@ -45,8 +48,11 @@ class base_language_import(osv.osv_memory):
@param ids: the ID or list of IDs
@param context: A standard dictionary
"""
if context is None:
context = {}
import_data = self.browse(cr, uid, ids)[0]
if import_data.overwrite:
context.update(overwrite=True)
fileobj = TemporaryFile('w+')
fileobj.write(base64.decodestring(import_data.data))
@ -56,7 +62,7 @@ class base_language_import(osv.osv_memory):
fileformat = first_line.endswith("type,name,res_id,src,value") and 'csv' or 'po'
fileobj.seek(0)
tools.trans_load_data(cr, fileobj, fileformat, import_data.code, lang_name=import_data.name)
tools.trans_load_data(cr, fileobj, fileformat, import_data.code, lang_name=import_data.name, context=context)
tools.trans_update_res_ids(cr)
fileobj.close()
return {}

View File

@ -27,6 +27,7 @@
<field name="name" width="200"/>
<field name="code"/>
<field name="data" colspan="4"/>
<field name="overwrite"/>
</group>
<group colspan="8" col="8">
<separator string="" colspan="8"/>

View File

@ -28,6 +28,8 @@ import base64
from tools.translate import _
from osv import osv, fields
ADDONS_PATH = tools.config['addons_path'].split(",")[-1]
class base_module_import(osv.osv_memory):
""" Import Module """
@ -37,7 +39,8 @@ class base_module_import(osv.osv_memory):
_columns = {
'module_file': fields.binary('Module .ZIP file', required=True),
'state':fields.selection([('init','init'),('done','done')], 'state', readonly=True),
'state':fields.selection([('init','init'),('done','done')],
'state', readonly=True),
'module_name': fields.char('Module Name', size=128),
}
@ -48,26 +51,30 @@ class base_module_import(osv.osv_memory):
def importzip(self, cr, uid, ids, context):
(data,) = self.browse(cr, uid, ids , context=context)
module_data = data.module_file
val = base64.decodestring(module_data)
zip_data = base64.decodestring(module_data)
fp = StringIO()
fp.write(val)
fdata = zipfile.ZipFile(fp, 'r')
fname = fdata.namelist()[0]
module_name = os.path.split(fname)[0]
ad = tools.config['addons_path'].split(",")[-1]
fname = os.path.join(ad, module_name+'.zip')
fp.write(zip_data)
try:
fp = file(fname, 'wb')
fp.write(val)
fp.close()
except IOError:
raise osv.except_osv(_('Error !'), _('Can not create the module file: %s !') % (fname,) )
file_data = zipfile.ZipFile(fp, 'r')
except zipfile.BadZipfile:
raise osv.except_osv(_('Error !'), _('File is not a zip file!'))
init_file_name = sorted(file_data.namelist())[0]
module_name = os.path.split(init_file_name)[0]
self.pool.get('ir.module.module').update_list(cr, uid, {'module_name': module_name,})
self.write(cr, uid, ids, {'state':'done', 'module_name': module_name}, context)
file_path = os.path.join(ADDONS_PATH, '%s.zip' % module_name)
try:
zip_file = open(file_path, 'wb')
except IOError:
raise osv.except_osv(_('Error !'),
_('Can not create the module file: %s !') % \
(file_path,) )
zip_file.write(zip_data)
zip_file.close()
self.pool.get('ir.module.module').update_list(cr, uid,
{'module_name': module_name,})
self.write(cr, uid, ids, {'state':'done', 'module_name': module_name},
context)
return False
def action_module_open(self, cr, uid, ids, context):
@ -84,4 +91,4 @@ class base_module_import(osv.osv_memory):
base_module_import()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -12,9 +12,10 @@
<group colspan="3" col="1">
<field name="config_logo" widget="image" width="220" height="130" nolabel="1" colspan="1"/>
<newline/>
<label width="220" string="This wizard helps you add a new language to you OpenERP system. After loading a new language it becomes available as default interface language for users and partners."/>
<label width="220" string='This wizard helps you to import a new module to your OpenERP system.
After importing a new module you can install it by clicking on the button "Install" from the form view.'/>
<label width="220"/>
<label width="220" string="Please be patient, this operation may take a few minutes (depending on the number of modules currently installed)..."/>
<label width="220" string="Please be patient, this operation may take a few minutes..."/>
<field name="state" invisible="1"/>
</group>
<separator orientation="vertical" rowspan="5"/>

View File

@ -13,6 +13,7 @@
help="Parameters that are used by all resources."
domain="[('res_id','=',False)]"/>
<separator orientation="vertical"/>
<field name="fields_id" />
<field name="name"/>
<field name="company_id" groups="base.group_multi_company"/>
</search>

View File

@ -143,6 +143,9 @@ class res_company(osv.osv):
'vat': fields.related('partner_id', 'vat', string="Tax ID", type="char", size=32),
'company_registry': fields.char('Company Registry', size=64),
}
_sql_constraints = [
('name_uniq', 'unique (name)', 'The company name must be unique !')
]
def _search(self, cr, uid, args, offset=0, limit=None, order=None,
context=None, count=False, access_rights_uid=None):
@ -242,9 +245,7 @@ class res_company(osv.osv):
return False
def _get_logo(self, cr, uid, ids):
return open(os.path.join(
tools.config['root_path'], '..', 'pixmaps', 'your_logo.png'),
'rb') .read().encode('base64')
return open(os.path.join( tools.config['root_path'], 'addons', 'base', 'res', 'res_company_logo.png'), 'rb') .read().encode('base64')
_header = """
<header>

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -62,17 +62,36 @@ class res_currency(osv.osv):
'active': fields.boolean('Active'),
'company_id':fields.many2one('res.company', 'Company'),
'date': fields.date('Date'),
'base': fields.boolean('Base')
'base': fields.boolean('Base'),
'position': fields.selection([('after','After Amount'),('before','Before Amount')], 'Symbol position', help="Determines where the currency symbol should be placed after or before the amount.")
}
_defaults = {
'active': lambda *a: 1,
'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'res.currency', context=c)
'position' : 'after',
}
_sql_constraints = [
# this constraint does not cover all cases due to SQL NULL handling for company_id,
# so it is complemented with a unique index (see below). The constraint and index
# share the same prefix so that IntegrityError triggered by the index will be caught
# and reported to the user with the constraint's error message.
('unique_name_company_id', 'unique (name, company_id)', 'The currency code must be unique per company!'),
]
_order = "name"
def init(self, cr):
# CONSTRAINT/UNIQUE INDEX on (name,company_id)
# /!\ The unique constraint 'unique_name_company_id' is not sufficient, because SQL92
# only support field names in constraint definitions, and we need a function here:
# we need to special-case company_id to treat all NULL company_id as equal, otherwise
# we would allow duplicate "global" currencies (all having company_id == NULL)
cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'res_currency_unique_name_company_id_idx'""")
if not cr.fetchone():
cr.execute("""CREATE UNIQUE INDEX res_currency_unique_name_company_id_idx
ON res_currency
(name, (COALESCE(company_id,-1)))""")
def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
res = super(osv.osv, self).read(cr, user, ids, fields, context, load)
res = super(res_currency, self).read(cr, user, ids, fields, context, load)
currency_rate_obj = self.pool.get('res.currency.rate')
for r in res:
if r.__contains__('rate_ids'):
@ -150,7 +169,7 @@ res_currency()
class res_currency_rate_type(osv.osv):
_name = "res.currency.rate.type"
_description = "Used to define the type of Currency Rates"
_description = "Currency Rate Type"
_columns = {
'name': fields.char('Name', size=64, required=True, translate=True),
}

View File

@ -2,6 +2,18 @@
<openerp>
<data>
<record id="view_currency_search" model="ir.ui.view">
<field name="name">res.currency.search</field>
<field name="model">res.currency</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Currencies">
<field name="name"/>
<field name="active"/>
</search>
</field>
</record>
<record id="view_currency_tree" model="ir.ui.view">
<field name="name">res.currency.tree</field>
<field name="model">res.currency</field>
@ -9,12 +21,13 @@
<field name="arch" type="xml">
<tree string="Currencies">
<field name="name"/>
<field name="company_id" select="2" />
<field name="company_id" groups="base.group_multi_company"/>
<field name="rate_ids" invisible="1"/>
<field name="date"/>
<field name="rate"/>
<field name="rounding"/>
<field name="accuracy"/>
<field name="position"/>
<field name="active"/>
</tree>
</field>
@ -25,23 +38,30 @@
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Currency">
<group col="6" colspan="6">
<field name="name" select="1"/>
<group col="6" colspan="4">
<field name="name"/>
<field name="rate"/>
<field name="company_id" select="2" groups="base.group_multi_company" />
<field name="symbol"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group col="2" colspan="2">
<separator string="Price Accuracy" colspan="2"/>
<field name="rounding"/>
<field name="accuracy"/>
</group>
<group col="6" colspan="4">
<group col="2" colspan="2">
<separator string="Price Accuracy" colspan="2"/>
<field name="rounding"/>
<field name="accuracy"/>
</group>
<group col="2" colspan="2">
<separator string="Miscelleanous" colspan="2"/>
<field name="base"/>
<field name="active" select="1"/>
<group col="2" colspan="2">
<separator string="Display" colspan="2"/>
<field name="symbol"/>
<field name="position"/>
</group>
<group col="2" colspan="2">
<separator string="Miscelleanous" colspan="2"/>
<field name="base"/>
<field name="active" select="1"/>
</group>
</group>
<field colspan="4" mode="tree,form" name="rate_ids" nolabel="1" attrs="{'readonly':[('base','=',True)]}">
@ -62,6 +82,7 @@
<field name="res_model">res.currency</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_currency_search"/>
</record>
<menuitem action="action_currency_form" id="menu_action_currency_form" parent="menu_localisation" sequence="3"/>

View File

@ -90,36 +90,44 @@
<record id="res_partner_asus" model="res.partner">
<field name="name">ASUStek</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"/>
<field name="address" eval="[]"/>
<field name="opt_out" eval="True"/>
<field name="website">www.asustek.com</field>
</record>
<record id="res_partner_agrolait" model="res.partner">
<field name="name">Agrolait</field>
<field eval="[(6, 0, [ref('res_partner_category_8')])]" name="category_id"/>
<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="opt_out" eval="True"/>
<field name="website">www.agrolait.com</field>
</record>
<record id="res_partner_c2c" model="res.partner">
<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="opt_out" eval="True"/>
<field name="website">www.camptocamp.com</field>
</record>
<record id="res_partner_sednacom" model="res.partner">
<field name="website">http://www.syleam.fr</field>
<field name="website">www.syleam.fr</field>
<field name="name">Syleam</field>
<field eval="[(6, 0, [ref('res_partner_category_5')])]" name="category_id"/>
<field name="address" eval="[]"/>
<field name="opt_out" eval="True"/>
<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 eval="[(6, 0, [ref('res_partner_category_4')])]" name="category_id"/>
<field name="opt_out" eval="True"/>
<field name="website">www.thymbra.com/</field>
</record>
<record id="res_partner_desertic_hispafuentes" model="res.partner">
<field name="name">Axelor</field>
@ -127,41 +135,54 @@
<field name="supplier">1</field>
<field name="address" eval="[]"/>
<field name="opt_out" eval="True"/>
<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="opt_out" eval="True"/>
<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="opt_out" eval="True"/>
<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="opt_out" eval="True"/>
<field name="user_id" ref="base.user_root"/>
<field name="website">www.chinaexport.com/</field>
</record>
<record id="res_partner_4" model="res.partner">
<field name="name">Distrib PC</field>
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
<field name="supplier">1</field>
<field eval="0" name="customer"/>
<field name="address" eval="[]"/>
<field name="opt_out" eval="True"/>
<field name="website">www.distribpc.com/</field>
</record>
<record id="res_partner_5" model="res.partner">
<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="opt_out" eval="True"/>
<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"/>
<field name="address" eval="[]"/>
<field name="opt_out" eval="True"/>
</record>
@ -171,6 +192,7 @@
<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"/>
<field name="address" eval="[]"/>
<field name="opt_out" eval="True"/>
</record>
@ -192,11 +214,11 @@
<field name="opt_out" eval="True"/>
</record>
<record id="res_partner_9" model="res.partner">
<field name="website">http://balmerinc.com</field>
<field name="website">www.balmerinc.com</field>
<field name="name">BalmerInc S.A.</field>
<field eval="12000.00" name="credit_limit"/>
<field name="ref">or</field>
<field name="user_id" ref="user_demo"/>
<field name="user_id" ref="base.user_root"/>
<field eval="[(6, 0, [ref('res_partner_category_1')])]" name="category_id"/>
<field name="address" eval="[]"/>
<field name="opt_out" eval="True"/>
@ -207,6 +229,7 @@
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
<field name="address" eval="[]"/>
<field name="opt_out" eval="True"/>
<field name="user_id" ref="user_demo"/>
</record>
<record id="res_partner_11" model="res.partner">
<field name="name">Leclerc</field>
@ -224,6 +247,7 @@
<field eval="[(6, 0, [ref('res_partner_category_11')])]" name="category_id"/>
<field name="address" eval="[]"/>
<field name="opt_out" eval="True"/>
<field name="user_id" ref="user_demo"/>
</record>
<record id="res_partner_15" model="res.partner">
<field name="name">Magazin BML 1</field>
@ -240,6 +264,8 @@
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
<field name="address" eval="[]"/>
<field name="opt_out" eval="True"/>
<field name="user_id" ref="user_demo"/>
<field name="website">http://www.ulg.ac.be/</field>
</record>
<!--
@ -251,16 +277,19 @@
<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>
</record>
<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">
@ -271,32 +300,41 @@
<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>
<record id="res_partner_theshelvehouse0" model="res.partner">
<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">
<field name="name">Vicking Direct</field>
<field eval="[(6,0,[ref('res_partner_category_miscellaneoussuppliers0')])]" name="category_id"/>
<field name="supplier">1</field>
<field name="customer">0</field>
<field name="address" eval="[]"/>
<field name="website">vicking-direct.be</field>
</record>
<record id="res_partner_woodywoodpecker0" model="res.partner">
<field name="name">Wood y Wood Pecker</field>
<field eval="[(6,0,[ref('res_partner_category_woodsuppliers0')])]" name="category_id"/>
<field name="supplier">1</field>
<field eval="0" name="customer"/>
<field name="address" eval="[]"/>
<field name="website">woodywoodpecker.com</field>
</record>
<record id="res_partner_zerooneinc0" model="res.partner">
<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>
<!--
@ -333,6 +371,7 @@
<field name="email">info@axelor.com</field>
<field name="phone">+33 1 64 61 04 01</field>
<field name="street">12 rue Albert Einstein</field>
<field name="type">default</field>
<field name="partner_id" ref="res_partner_desertic_hispafuentes"/>
</record>
<record id="res_partner_address_3" model="res.partner.address">
@ -350,6 +389,8 @@
<field name="zip">23410</field>
<field model="res.country" name="country_id" search="[('name','=','Taiwan')]"/>
<field name="street">31 Hong Kong street</field>
<field name="email">info@asustek.com</field>
<field name="phone">+ 1 64 61 04 01</field>
<field name="type">default</field>
<field name="partner_id" ref="res_partner_asus"/>
</record>
@ -359,6 +400,8 @@
<field name="zip">23540</field>
<field model="res.country" name="country_id" search="[('name','=','China')]"/>
<field name="street">56 Beijing street</field>
<field name="email">info@maxtor.com</field>
<field name="phone">+ 11 8528 456 789</field>
<field name="type">default</field>
<field name="partner_id" ref="res_partner_maxtor"/>
</record>
@ -369,6 +412,8 @@
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
<field name="street">23 rue du Vieux Bruges</field>
<field name="type">default</field>
<field name="email">info@elecimport.com</field>
<field name="phone">+ 32 025 897 456</field>
<field name="partner_id" ref="res_partner_6"/>
</record>
<record id="res_partner_address_7" model="res.partner.address">
@ -378,6 +423,8 @@
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
<field name="street">42 rue de la Lesse</field>
<field name="type">default</field>
<field name="email">info@distribpc.com</field>
<field name="phone">+ 32 081256987</field>
<field name="partner_id" ref="res_partner_4"/>
</record>
<record id="res_partner_address_8" model="res.partner.address">
@ -387,7 +434,10 @@
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
<field name="street">69 rue de Chimay</field>
<field name="type">default</field>
<field name="email">s.l@agrolait.be</field>
<field name="phone">003281588558</field>
<field name="partner_id" ref="res_partner_agrolait"/>
<field name="title" ref="base.res_partner_title_madam"/>
</record>
<record id="res_partner_address_8delivery" model="res.partner.address">
<field name="city">Wavre</field>
@ -396,7 +446,10 @@
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
<field name="street">71 rue de Chimay</field>
<field name="type">delivery</field>
<field name="email">p.l@agrolait.be</field>
<field name="phone">003281588557</field>
<field name="partner_id" ref="res_partner_agrolait"/>
<field name="title" ref="base.res_partner_title_sir"/>
</record>
<record id="res_partner_address_8invoice" model="res.partner.address">
<field name="city">Wavre</field>
@ -405,7 +458,10 @@
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
<field name="street">69 rue de Chimay</field>
<field name="type">invoice</field>
<field name="email">serge.l@agrolait.be</field>
<field name="phone">003281588556</field>
<field name="partner_id" ref="res_partner_agrolait"/>
<field name="title" ref="base.res_partner_title_sir"/>
</record>
<record id="res_partner_address_9" model="res.partner.address">
<field name="city">Paris</field>
@ -414,7 +470,10 @@
<field model="res.country" name="country_id" search="[('name','=','France')]"/>
<field name="street">1 rue Rockfeller</field>
<field name="type">default</field>
<field name="email">a.g@wealthyandsons.com</field>
<field name="phone">003368978776</field>
<field name="partner_id" ref="res_partner_2"/>
<field name="title" ref="base.res_partner_title_sir"/>
</record>
<record id="res_partner_address_11" model="res.partner.address">
<field name="city">Alencon</field>
@ -433,48 +492,84 @@
<field name="zip">6985</field>
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
<field name="street">2 Impasse de la Soif</field>
<field name="email">k.lesbrouffe@eci-liege.info</field>
<field name="phone">+32 421 52571</field>
<field name="type">default</field>
<field name="partner_id" ref="res_partner_5"/>
</record>
<record id="res_partner_address_zen" model="res.partner.address">
<field name="city">Shanghai</field>
<field name="name">Zen</field>
<field name="zip">4785552</field>
<field name="zip">478552</field>
<field model="res.country" name="country_id" search="[('name','=','China')]"/>
<field name="street">52 Chop Suey street</field>
<field name="type">default</field>
<field name="email">zen@chinaexport.com</field>
<field name="phone">+86-751-64845671</field>
<field name="partner_id" ref="res_partner_3"/>
</record>
<record id="res_partner_address_12" model="res.partner.address">
<field name="type">default</field>
<field name="name">Centrale</field>
<field name="city">Grenoble</field>
<field name="name">Loïc Dupont</field>
<field name="zip">38100</field>
<field model="res.country" name="country_id" search="[('name','=','China')]"/>
<field name="street">Rue Lavoisier 145</field>
<field name="type">default</field>
<field name="email">l.dupont@tecsas.fr</field>
<field name="phone">+33-658-256545</field>
<field name="partner_id" ref="res_partner_10"/>
</record>
<record id="res_partner_address_13" model="res.partner.address">
<field name="type">default</field>
<field name="name">Centrale d'achats 1</field>
<field name="name">Carl François</field>
<field name="city">Bruxelles</field>
<field name="zip">1000</field>
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
<field name="street">89 Chaussée de Waterloo</field>
<field name="email">carl.françois@bml.be</field>
<field name="phone">+32-258-256545</field>
<field name="partner_id" ref="res_partner_14"/>
</record>
<record id="res_partner_address_14" model="res.partner.address">
<field name="type">default</field>
<field name="name">Shop 1</field>
<field name="name">Lucien Ferguson</field>
<field name="street">89 Chaussée de Liège</field>
<field name="city">Namur</field>
<field name="zip">5000</field>
<field name="email">lucien.ferguson@bml.be</field>
<field name="phone">+32-621-568978</field>
<field name="partner_id" ref="res_partner_15"/>
</record>
<record id="res_partner_address_15" model="res.partner.address">
<field name="type">default</field>
<field name="name">Shop 2</field>
<field name="name">Marine Leclerc</field>
<field name="street">rue Grande</field>
<field name="city">Brest</field>
<field name="zip">29200</field>
<field name="email">marine@leclerc.fr</field>
<field name="phone">+33-298.334558</field>
<field name="partner_id" ref="res_partner_11"/>
</record>
<record id="res_partner_address_16" model="res.partner.address">
<field name="type">default</field>
<field name="name">Shop 3</field>
<field name="type">invoice</field>
<field name="name">Claude Leclerc</field>
<field name="street">rue Grande</field>
<field name="city">Brest</field>
<field name="zip">29200</field>
<field name="email">claude@leclerc.fr</field>
<field name="phone">+33-298.334598</field>
<field name="partner_id" ref="res_partner_11"/>
</record>
<record id="res_partner_address_accent" model="res.partner.address">
<field name="type">default</field>
<field name="city">Liège</field>
<field name="street">Université de Liège</field>
<field name="name">Martine Ohio</field>
<field name="street">Place du 20Août</field>
<field name="city">Liège</field>
<field name="zip">4000</field>
<field name="email">martine.ohio@ulg.ac.be</field>
<field name="phone">+32-45895245</field>
<field name="partner_id" ref="res_partner_accent"/>
</record>
<record id="res_partner_address_Camptocamp" model="res.partner.address">
@ -493,6 +588,8 @@
<field name="zip">95014</field>
<field model="res.country" name="country_id" search="[('name','=','United States')]"/>
<field name="street">10200 S. De Anza Blvd</field>
<field name="email">info@seagate.com</field>
<field name="phone">+1 408 256987</field>
<field name="type">default</field>
<field name="partner_id" ref="res_partner_seagate"/>
</record>
@ -573,7 +670,12 @@
<record id="res_partner_address_brussels0" model="res.partner.address">
<field eval="'Brussels'" name="city"/>
<field eval="'Brussels'" name="name"/>
<field eval="'Leen Vandenloep'" name="name"/>
<field eval="'Puurs'" name="city"/>
<field eval="'2870'" name="zip"/>
<field name="country_id" ref="base.be"/>
<field eval="'(+32).70.12.85.00'" name="phone"/>
<field eval="'Schoonmansveld 28'" name="street"/>
<field name="partner_id" ref="res_partner_vickingdirect0"/>
<field name="country_id" ref="base.be"/>
</record>
@ -583,6 +685,7 @@
<field eval="'Kainuu'" name="city"/>
<field eval="'Roger Pecker'" name="name"/>
<field name="partner_id" ref="res_partner_woodywoodpecker0"/>
<field eval="'(+358).9.589 689'" name="phone"/>
<field name="country_id" ref="base.fi"/>
</record>
@ -618,10 +721,13 @@
<record id="res_partner_address_ericdubois0" model="res.partner.address">
<field eval="'Mons'" name="city"/>
<field eval="'Eric Dubois'" name="name"/>
<field eval="'7000'" name="zip"/>
<field name="partner_id" ref="res_partner_ericdubois0"/>
<field name="country_id" ref="base.be"/>
<field eval="'Chaussée de Binche, 27'" name="street"/>
<field eval="'e.dubois@gmail.com'" name="email"/>
<field eval="'(+32).758 958 789'" name="phone"/>
</record>

View File

@ -35,6 +35,7 @@ from osv import fields,osv
from osv.orm import browse_record
from service import security
from tools.translate import _
import openerp.exceptions
class groups(osv.osv):
_name = "res.groups"
@ -338,7 +339,7 @@ class users(osv.osv):
}
# User can write to a few of her own fields (but not her groups for example)
SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email']
SELF_WRITEABLE_FIELDS = ['menu_tips','view', 'password', 'signature', 'action_id', 'company_id', 'user_email', 'name']
def write(self, cr, uid, ids, values, context=None):
if not hasattr(ids, '__iter__'):
@ -437,14 +438,14 @@ class users(osv.osv):
if passwd == tools.config['admin_passwd']:
return True
else:
raise security.ExceptionNoTb('AccessDenied')
raise openerp.exceptions.AccessDenied()
def check(self, db, uid, passwd):
"""Verifies that the given (uid, password) pair is authorized for the database ``db`` and
raise an exception if it is not."""
if not passwd:
# empty passwords disallowed for obvious security reasons
raise security.ExceptionNoTb('AccessDenied')
raise openerp.exceptions.AccessDenied()
if self._uid_cache.get(db, {}).get(uid) == passwd:
return
cr = pooler.get_db(db).cursor()
@ -453,7 +454,7 @@ class users(osv.osv):
(int(uid), passwd, True))
res = cr.fetchone()[0]
if not res:
raise security.ExceptionNoTb('AccessDenied')
raise openerp.exceptions.AccessDenied()
if self._uid_cache.has_key(db):
ulist = self._uid_cache[db]
ulist[uid] = passwd
@ -470,7 +471,7 @@ class users(osv.osv):
cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
res = cr.fetchone()
if not res:
raise security.ExceptionNoTb('Bad username or password')
raise openerp.exceptions.AccessDenied()
return res[0]
finally:
cr.close()
@ -481,7 +482,7 @@ class users(osv.osv):
password is not used to authenticate requests.
:return: True
:raise: security.ExceptionNoTb when old password is wrong
:raise: openerp.exceptions.AccessDenied when old password is wrong
:raise: except_osv when new password is not set or empty
"""
self.check(cr.dbname, uid, old_passwd)
@ -553,7 +554,7 @@ class users_implied(osv.osv):
_inherit = 'res.users'
def create(self, cr, uid, values, context=None):
groups = values.pop('groups_id')
groups = values.pop('groups_id', None)
user_id = super(users_implied, self).create(cr, uid, values, context)
if groups:
# delegate addition of groups to add implied groups
@ -700,7 +701,7 @@ class users_view(osv.osv):
self._process_values_groups(cr, uid, values, context)
return super(users_view, self).write(cr, uid, ids, values, context)
def read(self, cr, uid, ids, fields, context=None, load='_classic_read'):
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).keys()
else:

View File

@ -42,28 +42,32 @@
<field name="groups_id" eval="[(6,0, [ref('group_system'), ref('group_erp_manager')])]"/>
</record>
<record model="ir.rule" id="res_widget_user_rule">
<field name="name">res.widget.user rule</field>
<field name="model_id" ref="model_res_widget_user"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('user_id','=',user.id),('user_id','=',False)]</field>
</record>
</data>
<record model="ir.rule" id="res_partner_rule">
<field name="name">res.partner company</field>
<field name="model_id" ref="model_res_partner"/>
<field name="global" eval="True"/>
<!-- Show partners from ancestors and descendants companies (or company-less), this is usually a better
default for multicompany setups. -->
<field name="domain_force">['|','|',('company_id.child_ids','child_of',[user.company_id.id]),('company_id','child_of',[user.company_id.id]),('company_id','=',False)]</field>
</record>
<data noupdate="1">
<record model="ir.rule" id="multi_company_default_rule">
<field name="name">Multi_company_default company</field>
<field name="model_id" ref="model_multi_company_default"/>
<field name="global" eval="True"/>
<field name="domain_force">[('company_id','child_of',[user.company_id.id])]</field>
</record>
<record model="ir.rule" id="res_widget_user_rule">
<field name="name">res.widget.user rule</field>
<field name="model_id" ref="model_res_widget_user"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('user_id','=',user.id),('user_id','=',False)]</field>
</record>
<record model="ir.rule" id="res_partner_rule">
<field name="name">res.partner company</field>
<field name="model_id" ref="model_res_partner"/>
<field name="global" eval="True"/>
<!-- Show partners from ancestors and descendants companies (or company-less), this is usually a better
default for multicompany setups. -->
<field name="domain_force">['|','|',('company_id.child_ids','child_of',[user.company_id.id]),('company_id','child_of',[user.company_id.id]),('company_id','=',False)]</field>
</record>
<record model="ir.rule" id="multi_company_default_rule">
<field name="name">Multi_company_default company</field>
<field name="model_id" ref="model_multi_company_default"/>
<field name="global" eval="True"/>
<field name="domain_force">[('company_id','child_of',[user.company_id.id])]</field>
</record>
</data>
</openerp>

View File

@ -48,6 +48,7 @@
"access_res_country_state_group_user","res_country_state group_user","model_res_country_state","group_partner_manager",1,1,1,1
"access_res_currency_group_all","res_currency group_all","model_res_currency",,1,0,0,0
"access_res_currency_rate_group_all","res_currency_rate group_all","model_res_currency_rate",,1,0,0,0
"access_res_currency_rate_type_group_all","res_currency_rate_type group_all","model_res_currency_rate_type",,1,0,0,0
"access_res_currency_group_system","res_currency group_system","model_res_currency","group_system",1,1,1,1
"access_res_currency_rate_group_system","res_currency_rate group_system","model_res_currency_rate","group_system",1,1,1,1
"access_res_groups_group_erp_manager","res_groups group_erp_manager","model_res_groups","group_erp_manager",1,1,1,1
@ -125,3 +126,4 @@
"access_ir_config_parameter","ir_config_parameter","model_ir_config_parameter",,1,0,0,0
"access_ir_mail_server_all","ir_mail_server","model_ir_mail_server",,1,0,0,0
"access_ir_actions_todo_category","ir_actions_todo_category","model_ir_actions_todo_category","group_system",1,1,1,1
"access_ir_actions_client","ir_actions_client all","model_ir_actions_client",,1,0,0,0

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
48 access_res_country_state_group_user res_country_state group_user model_res_country_state group_partner_manager 1 1 1 1
49 access_res_currency_group_all res_currency group_all model_res_currency 1 0 0 0
50 access_res_currency_rate_group_all res_currency_rate group_all model_res_currency_rate 1 0 0 0
51 access_res_currency_rate_type_group_all res_currency_rate_type group_all model_res_currency_rate_type 1 0 0 0
52 access_res_currency_group_system res_currency group_system model_res_currency group_system 1 1 1 1
53 access_res_currency_rate_group_system res_currency_rate group_system model_res_currency_rate group_system 1 1 1 1
54 access_res_groups_group_erp_manager res_groups group_erp_manager model_res_groups group_erp_manager 1 1 1 1
126 access_ir_config_parameter ir_config_parameter model_ir_config_parameter 1 0 0 0
127 access_ir_mail_server_all ir_mail_server model_ir_mail_server 1 0 0 0
128 access_ir_actions_todo_category ir_actions_todo_category model_ir_actions_todo_category group_system 1 1 1 1
129 access_ir_actions_client ir_actions_client all model_ir_actions_client 1 0 0 0

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2011-TODAY OpenERP S.A. <http://www.openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
# Useful for manual testing of cron jobs scheduling.
# This must be (un)commented with the corresponding yml file
# in ../__openerp__.py.
# import test_ir_cron
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -144,45 +144,3 @@
!python {model: res.partner.category}: |
self.pool._init = True
-
"OSV Memory: Verify that osv_memory properly handles large data allocation"
-
1. No "count-based" auto-vaccuum when max_count is disabled
-
!python {model: base.language.export}: |
# setup special limits for the test, these will be reset at next pool reload anyway
self._max_count = None
num_recs = 250
for i in xrange(num_recs):
self.create(cr, uid, {'format':'po'})
assert (len(self.datas) >= num_recs), "OSV Memory must not auto-vaccum records from the current transaction if max_count is not set"
-
2. Auto-vaccuum should be enabled when max_count is set
-
!python {model: base.language.export}: |
# setup special limits for the test, these will be reset at next pool reload anyway
self._max_count = 100
num_recs = 219
for i in xrange(num_recs):
self.create(cr, uid, {'name': i, 'format':'po'})
assert (self._max_count <= len(self.datas) < self._max_count + self._check_time), "OSV Memory must auto-expire records when max_count is reached"
for k,v in self.datas.iteritems():
assert (int(v['name']) >= (num_recs - (self._max_count + self._check_time))), "OSV Memory must auto-expire records based on age"
-
3. Auto-vaccuum should be based on age only when max_count is not set
-
!python {model: base.language.export}: |
# setup special limits for the test, these will be reset at next pool reload anyway
self._max_count = None
self._max_hours = 0.01 #36 seconds
num_recs = 200
for i in xrange(num_recs):
self.create(cr, uid, {'format':'po'})
assert (len(self.datas) >= num_recs), "OSV Memory must not auto-expire records from the current transaction"
# expire all records
for k,v in self.datas.iteritems():
v['internal.date_access'] = 0
self.vaccum(cr, 1, force=True)
assert (len(self.datas) == 0), "OSV Memory must expire old records after vaccuum"

View File

@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2011-TODAY OpenERP S.A. <http://www.openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import time
from datetime import datetime
from dateutil.relativedelta import relativedelta
import openerp
JOB = {
'function': u'_0_seconds',
'interval_type': u'minutes',
'user_id': 1,
'name': u'test',
'args': False,
'numbercall': 1,
'nextcall': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'priority': 5,
'doall': True,
'active': True,
'interval_number': 1,
'model': u'ir.cron'
}
class test_ir_cron(openerp.osv.osv.osv):
""" Add a few handy methods to test cron jobs scheduling. """
_inherit = "ir.cron"
def _0_seconds(a, b, c):
print ">>> _0_seconds"
def _20_seconds(self, cr, uid):
print ">>> in _20_seconds"
time.sleep(20)
print ">>> out _20_seconds"
def _80_seconds(self, cr, uid):
print ">>> in _80_seconds"
time.sleep(80)
print ">>> out _80_seconds"
def test_0(self, cr, uid):
now = datetime.now()
t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S')
t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S')
t3 = (now + relativedelta(minutes=1, seconds=10)).strftime('%Y-%m-%d %H:%M:%S')
self.create(cr, uid, dict(JOB, name='test_0 _20_seconds A', function='_20_seconds', nextcall=t1))
self.create(cr, uid, dict(JOB, name='test_0 _20_seconds B', function='_20_seconds', nextcall=t2))
self.create(cr, uid, dict(JOB, name='test_0 _20_seconds C', function='_20_seconds', nextcall=t3))
def test_1(self, cr, uid):
now = datetime.now()
t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S')
self.create(cr, uid, dict(JOB, name='test_1 _20_seconds * 3', function='_20_seconds', nextcall=t1, numbercall=3))
def test_2(self, cr, uid):
now = datetime.now()
t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S')
self.create(cr, uid, dict(JOB, name='test_2 _80_seconds * 2', function='_80_seconds', nextcall=t1, numbercall=2))
def test_3(self, cr, uid):
now = datetime.now()
t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S')
t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S')
t3 = (now + relativedelta(minutes=1, seconds=10)).strftime('%Y-%m-%d %H:%M:%S')
self.create(cr, uid, dict(JOB, name='test_3 _80_seconds A', function='_80_seconds', nextcall=t1))
self.create(cr, uid, dict(JOB, name='test_3 _20_seconds B', function='_20_seconds', nextcall=t2))
self.create(cr, uid, dict(JOB, name='test_3 _20_seconds C', function='_20_seconds', nextcall=t3))
# This test assumes 4 cron threads.
def test_00(self, cr, uid):
self.test_00_set = set()
now = datetime.now()
t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S')
t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S')
t3 = (now + relativedelta(minutes=1, seconds=10)).strftime('%Y-%m-%d %H:%M:%S')
self.create(cr, uid, dict(JOB, name='test_00 _20_seconds_A', function='_20_seconds_A', nextcall=t1))
self.create(cr, uid, dict(JOB, name='test_00 _20_seconds_B', function='_20_seconds_B', nextcall=t2))
self.create(cr, uid, dict(JOB, name='test_00 _20_seconds_C', function='_20_seconds_C', nextcall=t3))
def _expect(self, cr, uid, to_add, to_sleep, to_expect_in, to_expect_out):
assert self.test_00_set == to_expect_in
self.test_00_set.add(to_add)
time.sleep(to_sleep)
self.test_00_set.discard(to_add)
assert self.test_00_set == to_expect_out
def _20_seconds_A(self, cr, uid):
self._expect(cr, uid, 'A', 20, set(), set(['B', 'C']))
def _20_seconds_B(self, cr, uid):
self._expect(cr, uid, 'B', 20, set('A'), set('C'))
def _20_seconds_C(self, cr, uid):
self._expect(cr, uid, 'C', 20, set(['A', 'B']), set())
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,61 @@
-
Test the cron jobs scheduling.
-
Disable the existing cron jobs if any during the tests.
-
!python {model: ir.cron }: |
# For this test to work, as it involves multiple database cursors,
# we have to commit changes. But YAML tests must be rollbacked, so
# the final database state is left untouched. So we have to be a bit
# ugly here: use our own cursor, commit, and clean after ourselves.
# We also pass around some ids using setattr/delattr, and we have to
# rollback the previous tests otherwise we won't be able to touch the
# db.
# Well, this should probably be a standalone, or regular unit test,
# instead of using the YAML infrastructure.
cr.rollback()
our_cr = self.pool.db.cursor()
try:
ids = self.search(our_cr, uid, [], {})
setattr(self, 'saved_ids', ids)
self.write(our_cr, uid, ids, {'active': False}, {})
our_cr.commit()
finally:
our_cr.close()
-
Three concurrent jobs started with a slight time gap. Assume 4 cron threads.
This will take about 2 minutes.
-
!python {model: ir.cron }: |
# Pretend initialization is already done. We the use a try/finally
# to reset _init correctly.
self.pool._init = False
our_cr = self.pool.db.cursor()
try:
self.test_00(our_cr, uid) # this will commit using the passed cursor
import openerp.cron
openerp.cron._thread_slots = 4
# Wake up this db as soon as the master cron thread starts.
openerp.cron.schedule_wakeup(1, self.pool.db.dbname)
# Pretend to be the master thread, for 4 iterations.
openerp.cron.runner_body()
openerp.cron.runner_body()
openerp.cron.runner_body()
openerp.cron.runner_body()
finally:
self.pool._init = True
our_cr.close()
-
Clean after ourselves.
-
!python {model: ir.cron }: |
our_cr = self.pool.db.cursor()
try:
ids = [x for x in self.search(our_cr, uid, ['|', ('active', '=', True), ('active', '=', False)], {}) if x not in self.saved_ids]
self.unlink(our_cr, uid, ids, {})
ids = self.saved_ids
delattr(self, 'saved_ids')
self.write(our_cr, uid, ids, {'active': True}, {})
our_cr.commit()
finally:
our_cr.close()

View File

@ -177,6 +177,251 @@
res_ids = self.search(cr, uid, [('company_id.partner_id', 'not in', [])])
res_ids.sort()
assert res_ids == all_ids, "Searching against empty set failed, returns %r" % res_ids
-
Test the '(not) like/in' behavior. res.partner and its parent_id column are used because
parent_id is a many2one, allowing to test the Null value, and there are actually some
null and non-null values in the demo data.
-
!python {model: res.partner }: |
partner_ids = self.search(cr, uid, [])
partner_ids.sort()
max_partner_id = max(partner_ids)
# Grab test sample data without using a normal
# search domain, because we want to test these later,
# so we can't rely on them!
partners = self.browse(cr, uid, partner_ids)
with_parent = []
without_parent = []
with_website = []
for x in partners:
if x.parent_id:
with_parent.append(x.id)
else:
without_parent.append(x.id)
if x.website:
with_website.append(x.id)
with_parent.sort()
without_parent.sort()
with_website.sort()
# We treat null values differently than in SQL. For instance in SQL:
# SELECT id FROM res_partner WHERE parent_id NOT IN (0)
# will return only the records with non-null parent_id.
# SELECT id FROM res_partner WHERE parent_id IN (0)
# will return expectedly nothing (our ids always begin at 1).
# This means the union of those two results will give only some
# records, but not all present in database.
#
# When using domains and the ORM's search method, we think it is
# more intuitive that the union returns all the records, and that
# a domain like ('parent_id', 'not in', [0]) will return all
# the records. For instance, if you perform a search for the companies
# that don't have OpenERP has a parent company, you expect to find,
# among others, the companies that don't have parent company.
#
# ('parent_id', 'not in', [0]) must give the same result than
# ('parent_id', 'not in', []), i.e. a empty set or a set with non-
# existing values be treated similarly if we simply check that some
# existing value belongs to them.
res_0 = self.search(cr, uid, [('parent_id', 'not like', 'probably_unexisting_name')]) # get all rows, included null parent_id
res_0.sort()
res_1 = self.search(cr, uid, [('parent_id', 'not in', [max_partner_id + 1])]) # get all rows, included null parent_id
res_1.sort()
res_2 = self.search(cr, uid, [('parent_id', 'not in', False)]) # get rows with not null parent_id, deprecated syntax
res_2.sort()
res_3 = self.search(cr, uid, [('parent_id', 'not in', [])]) # get all rows, included null parent_id
res_3.sort()
res_4 = self.search(cr, uid, [('parent_id', 'not in', [False])]) # get rows with not null parent_id
res_4.sort()
assert res_0 == partner_ids
assert res_1 == partner_ids
assert res_2 == with_parent
assert res_3 == partner_ids
assert res_4 == with_parent
# The results of these queries, when combined with queries 0..4 must
# give the whole set of ids.
res_5 = self.search(cr, uid, [('parent_id', 'like', 'probably_unexisting_name')])
res_5.sort()
res_6 = self.search(cr, uid, [('parent_id', 'in', [max_partner_id + 1])])
res_6.sort()
res_7 = self.search(cr, uid, [('parent_id', 'in', False)])
res_7.sort()
res_8 = self.search(cr, uid, [('parent_id', 'in', [])])
res_8.sort()
res_9 = self.search(cr, uid, [('parent_id', 'in', [False])])
res_9.sort()
assert res_5 == []
assert res_6 == []
assert res_7 == without_parent
assert res_8 == []
assert res_9 == without_parent
# These queries must return exactly the results than the queries 0..4,
# i.e. not ... in ... must be the same as ... not in ... .
res_10 = self.search(cr, uid, ['!', ('parent_id', 'like', 'probably_unexisting_name')])
res_10.sort()
res_11 = self.search(cr, uid, ['!', ('parent_id', 'in', [max_partner_id + 1])])
res_11.sort()
res_12 = self.search(cr, uid, ['!', ('parent_id', 'in', False)])
res_12.sort()
res_13 = self.search(cr, uid, ['!', ('parent_id', 'in', [])])
res_13.sort()
res_14 = self.search(cr, uid, ['!', ('parent_id', 'in', [False])])
res_14.sort()
assert res_0 == res_10
assert res_1 == res_11
assert res_2 == res_12
assert res_3 == res_13
assert res_4 == res_14
# Testing many2one field is not enough, a regular char field is tested
# with in [] and must not return any result.
res_15 = self.search(cr, uid, [('website', 'in', [])])
assert res_15 == []
# not in [] must return everything.
res_16 = self.search(cr, uid, [('website', 'not in', [])])
res_16.sort()
assert res_16 == partner_ids
res_17 = self.search(cr, uid, [('website', 'not in', False)])
res_17.sort()
assert res_17 == with_website
-
Property of the query (one2many not in False).
-
!python {model: res.currency }: |
ids = self.search(cr, uid, [])
referenced_companies = set([x.company_id.id for x in self.browse(cr, uid, ids)])
companies = set(self.pool.get('res.company').search(cr, uid, [('currency_ids', 'not in', False)]))
assert referenced_companies == companies
-
Property of the query (one2many in False).
-
!python {model: res.currency }: |
ids = self.search(cr, uid, [])
referenced_companies = set([x.company_id.id for x in self.browse(cr, uid, ids)])
unreferenced_companies = set(self.pool.get('res.company').search(cr, uid, [])).difference(referenced_companies)
companies = set(self.pool.get('res.company').search(cr, uid, [('currency_ids', 'in', False)]))
assert unreferenced_companies == companies
-
Equivalent queries.
-
!python {model: res.currency }: |
max_currency_id = max(self.search(cr, uid, []))
res_0 = self.search(cr, uid, [])
res_1 = self.search(cr, uid, [('name', 'not like', 'probably_unexisting_name')])
res_2 = self.search(cr, uid, [('id', 'not in', [max_currency_id + 1003])])
res_3 = self.search(cr, uid, [('id', 'not in', [])])
res_4 = self.search(cr, uid, [('id', 'not in', False)])
res_0.sort()
res_1.sort()
res_2.sort()
res_3.sort()
res_4.sort()
assert res_0 == res_1
assert res_0 == res_2
assert res_0 == res_3
assert res_0 == res_4
-
Equivalent queries, integer and string.
-
!python {model: res.partner }: |
all_ids = self.search(cr, uid, [])
if len(all_ids) > 1:
one = all_ids[0]
record = self.browse(cr, uid, one)
others = all_ids[1:]
res_1 = self.search(cr, uid, [('id', '=', one)])
# self.search(cr, uid, [('id', '!=', others)]) # not permitted
res_2 = self.search(cr, uid, [('id', 'not in', others)])
res_3 = self.search(cr, uid, ['!', ('id', '!=', one)])
res_4 = self.search(cr, uid, ['!', ('id', 'in', others)])
# res_5 = self.search(cr, uid, [('id', 'in', one)]) # TODO make it permitted, just like for child_of
res_6 = self.search(cr, uid, [('id', 'in', [one])])
res_7 = self.search(cr, uid, [('name', '=', record.name)])
res_8 = self.search(cr, uid, [('name', 'in', [record.name])])
# res_9 = self.search(cr, uid, [('name', 'in', record.name)]) # TODO
assert [one] == res_1
assert [one] == res_2
assert [one] == res_3
assert [one] == res_4
#assert [one] == res_5
assert [one] == res_6
assert [one] == res_7
-
Need a company with a parent_id.
-
!record {model: res.company, id: ymltest_company3}:
name: Acme 3
-
Need a company with a parent_id.
-
!record {model: res.company, id: ymltest_company4}:
name: Acme 4
parent_id: ymltest_company3
-
Equivalent queries, one2many.
-
!python {model: res.company }: |
# Search the company via its one2many (the one2many must point back at the company).
company = self.browse(cr, uid, ref('ymltest_company3'))
max_currency_id = max(self.pool.get('res.currency').search(cr, uid, []))
currency_ids1 = self.pool.get('res.currency').search(cr, uid, [('name', 'not like', 'probably_unexisting_name')])
currency_ids2 = self.pool.get('res.currency').search(cr, uid, [('id', 'not in', [max_currency_id + 1003])])
currency_ids3 = self.pool.get('res.currency').search(cr, uid, [('id', 'not in', [])])
assert currency_ids1 == currency_ids2 == currency_ids3, 'All 3 results should have be the same: all currencies'
default_company = self.browse(cr, uid, 1)
# one2many towards same model
res_1 = self.search(cr, uid, [('child_ids', 'in', [x.id for x in company.child_ids])]) # any company having a child of company3 as child
res_2 = self.search(cr, uid, [('child_ids', 'in', [company.child_ids[0].id])]) # any company having the first child of company3 as child
# one2many towards another model
res_3 = self.search(cr, uid, [('currency_ids', 'in', [x.id for x in default_company.currency_ids])]) # companies having a currency of main company
res_4 = self.search(cr, uid, [('currency_ids', 'in', [default_company.currency_ids[0].id])]) # companies having first currency of main company
res_5 = self.search(cr, uid, [('currency_ids', 'in', default_company.currency_ids[0].id)]) # companies having first currency of main company
# res_6 = self.search(cr, uid, [('currency_ids', 'in', [default_company.currency_ids[0].name])]) # TODO
res_7 = self.search(cr, uid, [('currency_ids', '=', default_company.currency_ids[0].name)])
res_8 = self.search(cr, uid, [('currency_ids', 'like', default_company.currency_ids[0].name)])
res_9 = self.search(cr, uid, [('currency_ids', 'like', 'probably_unexisting_name')])
# self.search(cr, uid, [('currency_ids', 'unexisting_op', 'probably_unexisting_name')]) # TODO expected exception
assert res_1 == [ref('ymltest_company3')]
assert res_2 == [ref('ymltest_company3')]
assert res_3 == [1]
assert res_4 == [1]
assert res_5 == [1]
assert res_7 == [1]
assert res_8 == [1]
assert res_9 == []
# get the companies referenced by some currency (this is normally the main company)
res_10 = self.search(cr, uid, [('currency_ids', 'not like', 'probably_unexisting_name')])
res_11 = self.search(cr, uid, [('currency_ids', 'not in', [max_currency_id + 1])])
res_12 = self.search(cr, uid, [('currency_ids', 'not in', False)])
res_13 = self.search(cr, uid, [('currency_ids', 'not in', [])])
res_10.sort()
res_11.sort()
res_12.sort()
res_13.sort()
assert res_10 == res_11
assert res_10 == res_12
assert res_10 == res_13
# child_of x returns x and its children (direct or not).
company = self.browse(cr, uid, ref('ymltest_company3'))
expected = [ref('ymltest_company3'), ref('ymltest_company4')]
expected.sort()
res_1 = self.search(cr, uid, [('id', 'child_of', [ref('ymltest_company3')])])
res_1.sort()
res_2 = self.search(cr, uid, [('id', 'child_of', ref('ymltest_company3'))])
res_2.sort()
res_3 = self.search(cr, uid, [('id', 'child_of', [company.name])])
res_3.sort()
res_4 = self.search(cr, uid, [('id', 'child_of', company.name)])
res_4.sort()
assert res_1 == expected
assert res_2 == expected
assert res_3 == expected
assert res_4 == expected
-
Verify that normalize_domain() works.
-
@ -187,6 +432,72 @@
domain = [('x','in',['y','z']),('a.v','=','e'),'|','|',('a','=','b'),'!',('c','>','d'),('e','!=','f'),('g','=','h')]
norm_domain = ['&','&','&'] + domain
assert norm_domain == expression.normalize(domain), "Non-normalized domains should be properly normalized"
-
Unaccent. Create a company with an accent in its name.
-
!record {model: res.company, id: ymltest_unaccent_company}:
name: Hélène
-
Test the unaccent-enabled 'ilike'.
-
!python {model: res.company}: |
if self.pool.has_unaccent:
ids = self.search(cr, uid, [('name','ilike','Helene')], {})
assert ids == [ref('ymltest_unaccent_company')]
ids = self.search(cr, uid, [('name','ilike','hélène')], {})
assert ids == [ref('ymltest_unaccent_company')]
ids = self.search(cr, uid, [('name','not ilike','Helene')], {})
assert ref('ymltest_unaccent_company') not in ids
ids = self.search(cr, uid, [('name','not ilike','hélène')], {})
assert ref('ymltest_unaccent_company') not in ids
-
Check that =like/=ilike expressions (no wildcard variants of like/ilike) are working on an untranslated field.
-
!python {model: res.partner }: |
all_ids = self.search(cr, uid, [('name', '=like', 'A_e_or')])
assert len(all_ids) == 1, "Must match one partner (Axelor), got %r"%all_ids
all_ids = self.search(cr, uid, [('name', '=ilike', 'm_____')])
assert len(all_ids) == 1, "Must match *only* one partner (Maxtor), got %r"%all_ids
-
Check that =like/=ilike expressions (no wildcard variants of like/ilike) are working on translated field.
-
!python {model: res.country }: |
all_ids = self.search(cr, uid, [('name', '=like', 'Ind__')])
assert len(all_ids) == 1, "Must match India only, got %r"%all_ids
all_ids = self.search(cr, uid, [('name', '=ilike', 'z%')])
assert len(all_ids) == 3, "Must match only countries with names starting with Z (currently 3), got %r"%all_ids
-
Use the create_date column on res.country (which doesn't declare it in _columns).
-
!python {model: res.country }: |
ids = self.search(cr, uid, [('create_date', '<', '2001-01-01 12:00:00')])
-
Verify that invalid expressions are refused, even for magic fields
-
!python {model: res.country }: |
try:
self.search(cr, uid, [('does_not_exist', '=', 'foo')])
raise AssertionError('Invalid fields should not be accepted')
except ValueError:
pass
try:
self.search(cr, uid, [('create_date', '>>', 'foo')])
raise AssertionError('Invalid operators should not be accepted')
except ValueError:
pass
import psycopg2
try:
cr._default_log_exceptions = False
cr.execute('SAVEPOINT expression_failure_test')
self.search(cr, uid, [('create_date', '=', "1970-01-01'); --")])
# if the above search gives no error, the operand was not escaped!
cr.execute('RELEASE SAVEPOINT expression_failure_test')
raise AssertionError('Operands should always be SQL escaped')
except psycopg2.DataError:
# Should give: 'DataError: invalid input syntax for type timestamp' or similar
cr.execute('ROLLBACK TO SAVEPOINT expression_failure_test')

View File

@ -28,8 +28,25 @@ parsing, configuration file loading and saving, ...) in this module
and provide real Python variables, e.g. addons_paths is really a list
of paths.
To initialize properly this module, openerp.tools.config.parse_config()
must be used.
"""
import deprecation
# Maximum number of threads processing concurrently cron jobs.
max_cron_threads = 4 # Actually the default value here is meaningless,
# look at tools.config for the default value.
# Paths to search for OpenERP addons.
addons_paths = []
# List of server-wide modules to load. Those modules are supposed to provide
# features not necessarily tied to a particular database. This is in contrast
# to modules that are always bound to a specific database when they are
# installed (i.e. the majority of OpenERP addons). This is set with the --load
# command-line option.
server_wide_modules = []
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

212
openerp/cron.py Normal file
View File

@ -0,0 +1,212 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2011 OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
""" Cron jobs scheduling
Cron jobs are defined in the ir_cron table/model. This module deals with all
cron jobs, for all databases of a single OpenERP server instance.
It defines a single master thread that will spawn (a bounded number of)
threads to process individual cron jobs.
The thread runs forever, checking every 60 seconds for new
'database wake-ups'. It maintains a heapq of database wake-ups. At each
wake-up, it will call ir_cron._run_jobs_multithread() for the given database. _run_jobs_multithread
will check the jobs defined in the ir_cron table and spawn accordingly threads
to process them.
This module's behavior depends on the following configuration variable:
openerp.conf.max_cron_threads.
"""
import heapq
import logging
import threading
import time
import openerp
import tools
# Heapq of database wake-ups. Note that 'database wake-up' meaning is in
# the context of the cron management. This is not originally about loading
# a database, although having the database name in the queue will
# cause it to be loaded when the schedule time is reached, even if it was
# unloaded in the mean time. Normally a database's wake-up is cancelled by
# the RegistryManager when the database is unloaded - so this should not
# cause it to be reloaded.
#
# TODO: perhaps in the future we could consider a flag on ir.cron jobs
# that would cause database wake-up even if the database has not been
# loaded yet or was already unloaded (e.g. 'force_db_wakeup' or something)
#
# Each element is a triple (timestamp, database-name, boolean). The boolean
# specifies if the wake-up is canceled (so a wake-up can be canceled without
# relying on the heapq implementation detail; no need to remove the job from
# the heapq).
_wakeups = []
# Mapping of database names to the wake-up defined in the heapq,
# so that we can cancel the wake-up without messing with the heapq
# invariant: lookup the wake-up by database-name, then set
# its third element to True.
_wakeup_by_db = {}
# Re-entrant lock to protect the above _wakeups and _wakeup_by_db variables.
# We could use a simple (non-reentrant) lock if the runner function below
# was more fine-grained, but we are fine with the loop owning the lock
# while spawning a few threads.
_wakeups_lock = threading.RLock()
# Maximum number of threads allowed to process cron jobs concurrently. This
# variable is set by start_master_thread using openerp.conf.max_cron_threads.
_thread_slots = None
# A (non re-entrant) lock to protect the above _thread_slots variable.
_thread_slots_lock = threading.Lock()
_logger = logging.getLogger('cron')
# Sleep duration limits - must not loop too quickly, but can't sleep too long
# either, because a new job might be inserted in ir_cron with a much sooner
# execution date than current known ones. We won't see it until we wake!
MAX_SLEEP = 60 # 1 min
MIN_SLEEP = 1 # 1 sec
# Dummy wake-up timestamp that can be used to force a database wake-up asap
WAKE_UP_NOW = 1
def get_thread_slots():
""" Return the number of available thread slots """
return _thread_slots
def release_thread_slot():
""" Increment the number of available thread slots """
global _thread_slots
with _thread_slots_lock:
_thread_slots += 1
def take_thread_slot():
""" Decrement the number of available thread slots """
global _thread_slots
with _thread_slots_lock:
_thread_slots -= 1
def cancel(db_name):
""" Cancel the next wake-up of a given database, if any.
:param db_name: database name for which the wake-up is canceled.
"""
_logger.debug("Cancel next wake-up for database '%s'.", db_name)
with _wakeups_lock:
if db_name in _wakeup_by_db:
_wakeup_by_db[db_name][2] = True
def cancel_all():
""" Cancel all database wake-ups. """
_logger.debug("Cancel all database wake-ups")
global _wakeups
global _wakeup_by_db
with _wakeups_lock:
_wakeups = []
_wakeup_by_db = {}
def schedule_wakeup(timestamp, db_name):
""" Schedule a new wake-up for a database.
If an earlier wake-up is already defined, the new wake-up is discarded.
If another wake-up is defined, that wake-up is discarded and the new one
is scheduled.
:param db_name: database name for which a new wake-up is scheduled.
:param timestamp: when the wake-up is scheduled.
"""
if not timestamp:
return
with _wakeups_lock:
if db_name in _wakeup_by_db:
task = _wakeup_by_db[db_name]
if not task[2] and timestamp > task[0]:
# existing wakeup is valid and occurs earlier than new one
return
task[2] = True # cancel existing task
task = [timestamp, db_name, False]
heapq.heappush(_wakeups, task)
_wakeup_by_db[db_name] = task
_logger.debug("Wake-up scheduled for database '%s' @ %s", db_name,
'NOW' if timestamp == WAKE_UP_NOW else timestamp)
def runner():
"""Neverending function (intended to be run in a dedicated thread) that
checks every 60 seconds the next database wake-up. TODO: make configurable
"""
while True:
runner_body()
def runner_body():
with _wakeups_lock:
while _wakeups and _wakeups[0][0] < time.time() and get_thread_slots():
task = heapq.heappop(_wakeups)
timestamp, db_name, canceled = task
if canceled:
continue
del _wakeup_by_db[db_name]
registry = openerp.pooler.get_pool(db_name)
if not registry._init:
_logger.debug("Database '%s' wake-up! Firing multi-threaded cron job processing", db_name)
registry['ir.cron']._run_jobs_multithread()
amount = MAX_SLEEP
with _wakeups_lock:
# Sleep less than MAX_SLEEP if the next known wake-up will happen before that.
if _wakeups and get_thread_slots():
amount = min(MAX_SLEEP, max(MIN_SLEEP, _wakeups[0][0] - time.time()))
_logger.debug("Going to sleep for %ss", amount)
time.sleep(amount)
def start_master_thread():
""" Start the above runner function in a daemon thread.
The thread is a typical daemon thread: it will never quit and must be
terminated when the main process exits - with no consequence (the processing
threads it spawns are not marked daemon).
"""
global _thread_slots
_thread_slots = openerp.conf.max_cron_threads
db_maxconn = tools.config['db_maxconn']
if _thread_slots >= tools.config.get('db_maxconn', 64):
_logger.warning("Connection pool size (%s) is set lower than max number of cron threads (%s), "
"this may cause trouble if you reach that number of parallel cron tasks.",
db_maxconn, _thread_slots)
t = threading.Thread(target=runner, name="openerp.cron.master_thread")
t.setDaemon(True)
t.start()
_logger.debug("Master cron daemon started!")
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

57
openerp/exceptions.py Normal file
View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
""" OpenERP core exceptions.
This module defines a few exception types. Those types are understood by the
RPC layer. Any other exception type bubbling until the RPC layer will be
treated as a 'Server error'.
"""
class Warning(Exception):
pass
class AccessDenied(Exception):
""" Login/password error. No message, no traceback. """
def __init__(self):
super(AccessDenied, self).__init__('AccessDenied.')
self.traceback = ('', '', '')
class AccessError(Exception):
""" Access rights error. """
class DeferredException(Exception):
""" Exception object holding a traceback for asynchronous reporting.
Some RPC calls (database creation and report generation) happen with
an initial request followed by multiple, polling requests. This class
is used to store the possible exception occuring in the thread serving
the first request, and is then sent to a polling request.
('Traceback' is misleading, this is really a exc_info() triple.)
"""
def __init__(self, msg, tb):
self.message = msg
self.traceback = tb
self.args = (msg, tb)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -21,6 +21,7 @@
##############################################################################
import openerp.modules
import logging
def is_initialized(cr):
""" Check if a database has been initialized for the ORM.
@ -40,6 +41,10 @@ def initialize(cr):
"""
f = openerp.modules.get_module_resource('base', 'base.sql')
if not f:
m = "File not found: 'base.sql' (provided by module 'base')."
logging.getLogger('init').critical(m)
raise IOError(m)
base_sql_file = openerp.tools.misc.file_open(f)
try:
cr.execute(base_sql_file.read())
@ -118,4 +123,14 @@ def create_categories(cr, categories):
categories = categories[1:]
return p_id
def has_unaccent(cr):
""" Test if the database has an unaccent function.
The unaccent is supposed to be provided by the PostgreSQL unaccent contrib
module but any similar function will be picked by OpenERP.
"""
cr.execute("SELECT proname FROM pg_proc WHERE proname='unaccent'")
return len(cr.fetchall()) > 0
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -58,16 +58,16 @@ class Graph(dict):
"""
def add_node(self, name, deps):
def add_node(self, name, info):
max_depth, father = 0, None
for n in [Node(x, self) for x in deps]:
for n in [Node(x, self, None) for x in info['depends']]:
if n.depth >= max_depth:
father = n
max_depth = n.depth
if father:
return father.add_child(name)
return father.add_child(name, info)
else:
return Node(name, self)
return Node(name, self, info)
def update_from_db(self, cr):
if not len(self):
@ -120,7 +120,7 @@ class Graph(dict):
continue
later.clear()
current.remove(package)
node = self.add_node(package, deps)
node = self.add_node(package, info)
node.data = info
for kind in ('init', 'demo', 'update'):
if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force:
@ -154,12 +154,13 @@ class Graph(dict):
class Singleton(object):
def __new__(cls, name, graph):
def __new__(cls, name, graph, info):
if name in graph:
inst = graph[name]
else:
inst = object.__new__(cls)
inst.name = name
inst.info = info
graph[name] = inst
return inst
@ -167,19 +168,21 @@ class Singleton(object):
class Node(Singleton):
""" One module in the modules dependency graph.
Node acts as a per-module singleton.
Node acts as a per-module singleton. A node is constructed via
Graph.add_module() or Graph.add_modules(). Some of its fields are from
ir_module_module (setted by Graph.update_from_db()).
"""
def __init__(self, name, graph):
def __init__(self, name, graph, info):
self.graph = graph
if not hasattr(self, 'children'):
self.children = []
if not hasattr(self, 'depth'):
self.depth = 0
def add_child(self, name):
node = Node(name, self.graph)
def add_child(self, name, info):
node = Node(name, self.graph, info)
node.depth = self.depth + 1
if node not in self.children:
self.children.append(node)

View File

@ -158,7 +158,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=
logger.info('module %s: loading objects', package.name)
migrations.migrate_module(package, 'pre')
register_module_classes(package.name)
models = pool.instanciate(package.name, cr)
models = pool.load(cr, package)
loaded_modules.append(package.name)
if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
init_module_models(cr, package.name, models)
@ -273,8 +273,6 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
# This is a brand new pool, just created in pooler.get_db_and_pool()
pool = pooler.get_pool(cr.dbname)
processed_modules = [] # for cleanup step after install
loaded_modules = [] # to avoid double loading
report = tools.assertion_report()
if 'base' in tools.config['update'] or 'all' in tools.config['update']:
cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed'))
@ -285,8 +283,10 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
if not graph:
logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)')
raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)'))
loaded, processed = load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)
processed_modules.extend(processed)
# processed_modules: for cleanup step after install
# loaded_modules: to avoid double loading
loaded_modules, processed_modules = load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)
if tools.config['load_language']:
for lang in tools.config['load_language'].split(','):
@ -339,16 +339,16 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""")
for (model, name) in cr.fetchall():
model_obj = pool.get(model)
if model_obj and not isinstance(model_obj, osv.osv.osv_memory):
logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name))
if model_obj and not model_obj.is_transient():
logger.notifyChannel('init', netsvc.LOG_WARNING, 'Model %s (%s) has no access rules!' % (model, name))
# Temporary warning while we remove access rights on osv_memory objects, as they have
# been replaced by owner-only access rights
cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""")
for (model, name) in cr.fetchall():
model_obj = pool.get(model)
if isinstance(model_obj, osv.osv.osv_memory) and not isinstance(model_obj, osv.osv.osv):
logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name))
if model_obj and model_obj.is_transient():
logger.notifyChannel('init', netsvc.LOG_WARNING, 'The transient model %s (%s) should not have explicit access rules!' % (model, name))
cr.execute("SELECT model from ir_model")
for (model,) in cr.fetchall():
@ -356,7 +356,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
if obj:
obj._check_removed_columns(cr, log=True)
else:
logger.notifyChannel('init', netsvc.LOG_WARNING, "Model %s is referenced but not present in the orm pool!" % model)
logger.notifyChannel('init', netsvc.LOG_WARNING, "Model %s is declared but cannot be loaded! (Perhaps a module was partially removed or renamed)" % model)
# Cleanup orphan records
pool.get('ir.model.data')._process_end(cr, 1, processed_modules)

View File

@ -31,7 +31,6 @@ import openerp.osv as osv
import openerp.tools as tools
import openerp.tools.osutil as osutil
from openerp.tools.safe_eval import safe_eval as eval
import openerp.pooler as pooler
from openerp.tools.translate import _
import openerp.netsvc as netsvc
@ -58,6 +57,11 @@ loaded = []
logger = netsvc.Logger()
def initialize_sys_path():
""" Add all addons paths in sys.path.
This ensures something like ``import crm`` works even if the addons are
not in the PYTHONPATH.
"""
global ad_paths
if ad_paths:
@ -250,6 +254,8 @@ def load_information_from_description_file(module):
info['license'] = info.get('license') or 'AGPL-3'
info.setdefault('installable', True)
info.setdefault('active', False)
# If the following is provided, it is called after the module is --loaded.
info.setdefault('post_load', None)
for kind in ['data', 'demo', 'test',
'init_xml', 'update_xml', 'demo_xml']:
info.setdefault(kind, [])
@ -271,7 +277,6 @@ def init_module_models(cr, module_name, obj_list):
TODO better explanation of _auto_init and init.
"""
logger.notifyChannel('init', netsvc.LOG_INFO,
'module %s: creating or updating database tables' % module_name)
todo = []
@ -290,23 +295,22 @@ def init_module_models(cr, module_name, obj_list):
t[1](cr, *t[2])
cr.commit()
# Import hook to write a addon m in both sys.modules['m'] and
# sys.modules['openerp.addons.m']. Otherwise it could be loaded twice
# if imported twice using different names.
#class MyImportHook(object):
# def find_module(self, module_name, package_path):
# print ">>>", module_name, package_path
# def load_module(self, module_name):
# raise ImportError("Restricted")
def load_module(module_name):
""" Load a Python module found on the addons paths."""
fm = imp.find_module(module_name, ad_paths)
try:
imp.load_module(module_name, *fm)
finally:
if fm[0]:
fm[0].close()
#sys.meta_path.append(MyImportHook())
def register_module_classes(m):
""" Register module named m, if not already registered.
This will load the module and register all of its models. (Actually, the
explicit constructor call of each of the models inside the module will
register them.)
This loads the module and register all of its models, thanks to either
the MetaModel metaclass, or the explicit instantiation of the model.
"""
@ -326,7 +330,7 @@ def register_module_classes(m):
try:
zip_mod_path = mod_path + '.zip'
if not os.path.isfile(zip_mod_path):
load_module(m)
__import__(m)
else:
zimp = zipimport.zipimporter(zip_mod_path)
zimp.load_module(m)

View File

@ -22,10 +22,16 @@
""" Models registries.
"""
import threading
import logging
import openerp.sql_db
import openerp.osv.orm
import openerp.cron
import openerp.tools
import openerp.modules.db
import openerp.tools.config
class Registry(object):
""" Model registry for a particular database.
@ -44,6 +50,14 @@ class Registry(object):
self.db_name = db_name
self.db = openerp.sql_db.db_connect(db_name)
cr = self.db.cursor()
has_unaccent = openerp.modules.db.has_unaccent(cr)
if openerp.tools.config['unaccent'] and not has_unaccent:
logger = logging.getLogger('unaccent')
logger.warning("The option --unaccent was given but no unaccent() function was found in database.")
self.has_unaccent = openerp.tools.config['unaccent'] and has_unaccent
cr.close()
def do_parent_store(self, cr):
for o in self._init_parent:
self.get(o)._parent_store_compute(cr)
@ -65,28 +79,42 @@ class Registry(object):
""" Return a model for a given name or raise KeyError if it doesn't exist."""
return self.models[model_name]
def instanciate(self, module, cr):
""" Instanciate all the classes of a given module for a particular db."""
def load(self, cr, module):
""" Load a given module in the registry.
At the Python level, the modules are already loaded, but not yet on a
per-registry level. This method populates a registry with the given
modules, i.e. it instanciates all the classes of a the given module
and registers them in the registry.
"""
res = []
# Instantiate registered classes (via metamodel discovery or via explicit
# constructor call), and add them to the pool.
for cls in openerp.osv.orm.MetaModel.module_to_models.get(module, []):
res.append(cls.createInstance(self, cr))
# Instantiate registered classes (via the MetaModel automatic discovery
# or via explicit constructor call), and add them to the pool.
for cls in openerp.osv.orm.MetaModel.module_to_models.get(module.name, []):
res.append(cls.create_instance(self, cr))
return res
def schedule_cron_jobs(self):
""" Make the cron thread care about this registry/database jobs.
This will initiate the cron thread to check for any pending jobs for
this registry/database as soon as possible. Then it will continuously
monitor the ir.cron model for future jobs. See openerp.cron for
details.
"""
openerp.cron.schedule_wakeup(openerp.cron.WAKE_UP_NOW, self.db.dbname)
def clear_caches(self):
""" Clear the caches
This clears the caches associated to methods decorated with
``tools.ormcache`` or ``tools.ormcache_multi`` for all the models.
"""
for model in self.models.itervalues():
model.clear_caches()
class RegistryManager(object):
""" Model registries manager.
@ -94,24 +122,22 @@ class RegistryManager(object):
registries (essentially database connection/model registry pairs).
"""
# Mapping between db name and model registry.
# Accessed through the methods below.
registries = {}
registries_lock = threading.RLock()
@classmethod
def get(cls, db_name, force_demo=False, status=None, update_module=False,
pooljobs=True):
""" Return a registry for a given database name."""
if db_name in cls.registries:
registry = cls.registries[db_name]
else:
registry = cls.new(db_name, force_demo, status,
update_module, pooljobs)
return registry
with cls.registries_lock:
if db_name in cls.registries:
registry = cls.registries[db_name]
else:
registry = cls.new(db_name, force_demo, status,
update_module, pooljobs)
return registry
@classmethod
def new(cls, db_name, force_demo=False, status=None,
@ -121,47 +147,64 @@ class RegistryManager(object):
The (possibly) previous registry for that database name is discarded.
"""
import openerp.modules
registry = Registry(db_name)
with cls.registries_lock:
registry = Registry(db_name)
# Initializing a registry will call general code which will in turn
# call registries.get (this object) to obtain the registry being
# initialized. Make it available in the registries dictionary then
# remove it if an exception is raised.
cls.delete(db_name)
cls.registries[db_name] = registry
try:
# This should be a method on Registry
openerp.modules.load_modules(registry.db, force_demo, status, update_module)
except Exception:
del cls.registries[db_name]
raise
# Initializing a registry will call general code which will in turn
# call registries.get (this object) to obtain the registry being
# initialized. Make it available in the registries dictionary then
# remove it if an exception is raised.
cls.delete(db_name)
cls.registries[db_name] = registry
try:
# This should be a method on Registry
openerp.modules.load_modules(registry.db, force_demo, status, update_module)
except Exception:
del cls.registries[db_name]
raise
cr = registry.db.cursor()
try:
registry.do_parent_store(cr)
registry.get('ir.actions.report.xml').register_all(cr)
cr.commit()
finally:
cr.close()
cr = registry.db.cursor()
try:
registry.do_parent_store(cr)
registry.get('ir.actions.report.xml').register_all(cr)
cr.commit()
finally:
cr.close()
if pooljobs:
registry.get('ir.cron').restart(registry.db.dbname)
return registry
if pooljobs:
registry.schedule_cron_jobs()
return registry
@classmethod
def delete(cls, db_name):
""" Delete the registry linked to a given database. """
if db_name in cls.registries:
del cls.registries[db_name]
"""Delete the registry linked to a given database.
This also cleans the associated caches. For good measure this also
cancels the associated cron job. But please note that the cron job can
be running and take some time before ending, and that you should not
remove a registry if it can still be used by some thread. So it might
be necessary to call yourself openerp.cron.Agent.cancel(db_name) and
and join (i.e. wait for) the thread.
"""
with cls.registries_lock:
if db_name in cls.registries:
cls.registries[db_name].clear_caches()
del cls.registries[db_name]
openerp.cron.cancel(db_name)
@classmethod
def delete_all(cls):
"""Delete all the registries. """
with cls.registries_lock:
for db_name in cls.registries.keys():
cls.delete(db_name)
@classmethod
def clear_caches(cls, db_name):
""" Clear the caches
"""Clear caches
This clears the caches associated to methods decorated with
``tools.ormcache`` or ``tools.ormcache_multi`` for all the models
@ -170,8 +213,9 @@ class RegistryManager(object):
This method is given to spare you a ``RegistryManager.get(db_name)``
that would loads the given database if it was not already loaded.
"""
if db_name in cls.registries:
cls.registries[db_name].clear_caches()
with cls.registries_lock:
if db_name in cls.registries:
cls.registries[db_name].clear_caches()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -21,7 +21,6 @@
##############################################################################
import errno
import heapq
import logging
import logging.handlers
import os
@ -31,12 +30,14 @@ import socket
import sys
import threading
import time
import traceback
import types
from pprint import pformat
# TODO modules that import netsvc only for things from loglevels must be changed to use loglevels.
from loglevels import *
import tools
import openerp
def close_socket(sock):
""" Closes a socket instance cleanly
@ -60,20 +61,22 @@ def close_socket(sock):
#.apidoc title: Common Services: netsvc
#.apidoc module-mods: member-order: bysource
def abort_response(dummy_1, description, dummy_2, details):
# TODO Replace except_{osv,orm} with these directly.
if description == 'AccessError':
raise openerp.exceptions.AccessError(details)
else:
raise openerp.exceptions.Warning(details)
class Service(object):
""" Base class for *Local* services
Functionality here is trusted, no authentication.
"""
_services = {}
def __init__(self, name, audience=''):
def __init__(self, name):
Service._services[name] = self
self.__name = name
self._methods = {}
def joinGroup(self, name):
raise Exception("No group for local services")
#GROUPS.setdefault(name, {})[self.__name] = self
@classmethod
def exists(cls, name):
@ -84,75 +87,39 @@ class Service(object):
if cls.exists(name):
cls._services.pop(name)
def exportMethod(self, method):
if callable(method):
self._methods[method.__name__] = method
def LocalService(name):
# Special case for addons support, will be removed in a few days when addons
# are updated to directly use openerp.osv.osv.service.
if name == 'object_proxy':
return openerp.osv.osv.service
def abortResponse(self, error, description, origin, details):
if not tools.config['debug_mode']:
raise Exception("%s -- %s\n\n%s"%(origin, description, details))
else:
raise
class LocalService(object):
""" Proxy for local services.
Any instance of this class will behave like the single instance
of Service(name)
"""
__logger = logging.getLogger('service')
def __init__(self, name):
self.__name = name
try:
self._service = Service._services[name]
for method_name, method_definition in self._service._methods.items():
setattr(self, method_name, method_definition)
except KeyError, keyError:
self.__logger.error('This service does not exist: %s' % (str(keyError),) )
raise
def __call__(self, method, *params):
return getattr(self, method)(*params)
return Service._services[name]
class ExportService(object):
""" Proxy for exported services.
All methods here should take an AuthProxy as their first parameter. It
will be appended by the calling framework.
Note that this class has no direct proxy, capable of calling
eservice.method(). Rather, the proxy should call
dispatch(method,auth,params)
dispatch(method, params)
"""
_services = {}
_groups = {}
_logger = logging.getLogger('web-services')
def __init__(self, name, audience=''):
def __init__(self, name):
ExportService._services[name] = self
self.__name = name
self._logger.debug("Registered an exported service: %s" % name)
def joinGroup(self, name):
ExportService._groups.setdefault(name, {})[self.__name] = self
@classmethod
def getService(cls,name):
return cls._services[name]
def dispatch(self, method, auth, params):
# Dispatch a RPC call w.r.t. the method name. The dispatching
# w.r.t. the service (this class) is done by OpenERPDispatcher.
def dispatch(self, method, params):
raise Exception("stub dispatch at %s" % self.__name)
def new_dispatch(self,method,auth,params):
raise Exception("stub dispatch at %s" % self.__name)
def abortResponse(self, error, description, origin, details):
if not tools.config['debug_mode']:
raise Exception("%s -- %s\n\n%s"%(origin, description, details))
else:
raise
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, _NOTHING, DEFAULT = range(10)
#The background is set with 40 plus the number of the color, and the foreground with 30
#These are the sequences need to get colored ouput
@ -244,83 +211,6 @@ def init_alternative_logger():
logger.addHandler(handler)
logger.setLevel(logging.ERROR)
class Agent(object):
""" Singleton that keeps track of cancellable tasks to run at a given
timestamp.
The tasks are characterised by:
* a timestamp
* the database on which the task run
* the function to call
* the arguments and keyword arguments to pass to the function
Implementation details:
- Tasks are stored as list, allowing the cancellation by setting
the timestamp to 0.
- A heapq is used to store tasks, so we don't need to sort
tasks ourself.
"""
__tasks = []
__tasks_by_db = {}
_logger = logging.getLogger('netsvc.agent')
@classmethod
def setAlarm(cls, function, timestamp, db_name, *args, **kwargs):
task = [timestamp, db_name, function, args, kwargs]
heapq.heappush(cls.__tasks, task)
cls.__tasks_by_db.setdefault(db_name, []).append(task)
@classmethod
def cancel(cls, db_name):
"""Cancel all tasks for a given database. If None is passed, all tasks are cancelled"""
cls._logger.debug("Cancel timers for %s db", db_name or 'all')
if db_name is None:
cls.__tasks, cls.__tasks_by_db = [], {}
else:
if db_name in cls.__tasks_by_db:
for task in cls.__tasks_by_db[db_name]:
task[0] = 0
@classmethod
def quit(cls):
cls.cancel(None)
@classmethod
def runner(cls):
"""Neverending function (intended to be ran in a dedicated thread) that
checks every 60 seconds tasks to run. TODO: make configurable
"""
current_thread = threading.currentThread()
while True:
while cls.__tasks and cls.__tasks[0][0] < time.time():
task = heapq.heappop(cls.__tasks)
timestamp, dbname, function, args, kwargs = task
cls.__tasks_by_db[dbname].remove(task)
if not timestamp:
# null timestamp -> cancelled task
continue
current_thread.dbname = dbname # hack hack
cls._logger.debug("Run %s.%s(*%s, **%s)", function.im_class.__name__, function.func_name, args, kwargs)
delattr(current_thread, 'dbname')
task_thread = threading.Thread(target=function, name='netsvc.Agent.task', args=args, kwargs=kwargs)
# force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default)
task_thread.setDaemon(False)
task_thread.start()
time.sleep(1)
time.sleep(60)
def start_agent():
agent_runner = threading.Thread(target=Agent.runner, name="netsvc.Agent.runner")
# the agent runner is a typical daemon thread, that will never quit and must be
# terminated when the main process exits - with no consequence (the processing
# threads it spawns are not marked daemon)
agent_runner.setDaemon(True)
agent_runner.start()
import traceback
class Server:
""" Generic interface for all servers with an event loop etc.
Override this to impement http, net-rpc etc. servers.
@ -403,11 +293,6 @@ class Server:
def _close_socket(self):
close_socket(self.socket)
class OpenERPDispatcherException(Exception):
def __init__(self, exception, traceback):
self.exception = exception
self.traceback = traceback
def replace_request_password(args):
# password is always 3rd argument in a request, we replace it in RPC logs
# so it's easier to forward logs for diagnostics/debugging purposes...
@ -425,33 +310,47 @@ def log(title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""):
logger.log(channel, indent+line)
indent=indent_after
class OpenERPDispatcher:
def log(self, title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""):
def dispatch_rpc(service_name, method, params):
""" Handle a RPC call.
This is pure Python code, the actual marshalling (from/to XML-RPC or
NET-RPC) is done in a upper layer.
"""
def _log(title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""):
log(title, msg, channel=channel, depth=depth, fn=fn)
def dispatch(self, service_name, method, params):
try:
auth = getattr(self, 'auth_provider', None)
logger = logging.getLogger('result')
start_time = end_time = 0
if logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
self.log('service', tuple(replace_request_password(params)), depth=None, fn='%s.%s'%(service_name,method))
if logger.isEnabledFor(logging.DEBUG_RPC):
start_time = time.time()
result = ExportService.getService(service_name).dispatch(method, auth, params)
if logger.isEnabledFor(logging.DEBUG_RPC):
end_time = time.time()
if not logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
self.log('service (%.3fs)' % (end_time - start_time), tuple(replace_request_password(params)), depth=1, fn='%s.%s'%(service_name,method))
self.log('execution time', '%.3fs' % (end_time - start_time), channel=logging.DEBUG_RPC_ANSWER)
self.log('result', result, channel=logging.DEBUG_RPC_ANSWER)
return result
except Exception, e:
self.log('exception', tools.exception_to_unicode(e))
tb = getattr(e, 'traceback', sys.exc_info())
tb_s = "".join(traceback.format_exception(*tb))
if tools.config['debug_mode'] and isinstance(tb[2], types.TracebackType):
import pdb
pdb.post_mortem(tb[2])
raise OpenERPDispatcherException(e, tb_s)
try:
logger = logging.getLogger('result')
start_time = end_time = 0
if logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
_log('service', tuple(replace_request_password(params)), depth=None, fn='%s.%s'%(service_name,method))
if logger.isEnabledFor(logging.DEBUG_RPC):
start_time = time.time()
result = ExportService.getService(service_name).dispatch(method, params)
if logger.isEnabledFor(logging.DEBUG_RPC):
end_time = time.time()
if not logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
_log('service (%.3fs)' % (end_time - start_time), tuple(replace_request_password(params)), depth=1, fn='%s.%s'%(service_name,method))
_log('execution time', '%.3fs' % (end_time - start_time), channel=logging.DEBUG_RPC_ANSWER)
_log('result', result, channel=logging.DEBUG_RPC_ANSWER)
return result
except openerp.exceptions.AccessError:
raise
except openerp.exceptions.AccessDenied:
raise
except openerp.exceptions.Warning:
raise
except openerp.exceptions.DeferredException, e:
_log('exception', tools.exception_to_unicode(e))
post_mortem(e.traceback)
raise
except Exception, e:
_log('exception', tools.exception_to_unicode(e))
post_mortem(sys.exc_info())
raise
def post_mortem(info):
if tools.config['debug_mode'] and isinstance(info[2], types.TracebackType):
import pdb
pdb.post_mortem(info[2])
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -20,17 +20,146 @@
#
##############################################################################
""" Domain expression processing
The main duty of this module is to compile a domain expression into a SQL
query. A lot of things should be documented here, but as a first step in the
right direction, some tests in test_osv_expression.yml might give you some
additional information.
For legacy reasons, a domain uses an inconsistent two-levels abstract syntax
(domains are regular Python data structures). At the first level, a domain
is an expression made of terms (sometimes called leaves) and (domain) operators
used in prefix notation. The available operators at this level are '!', '&',
and '|'. '!' is a unary 'not', '&' is a binary 'and', and '|' is a binary 'or'.
For instance, here is a possible domain. (<term> stands for an arbitrary term,
more on this later.)
['&', '!', <term1>, '|', <term2>, <term3>]
It is equivalent to this pseudo code using infix notation:
(not <term1>) and (<term2> or <term3>)
The second level of syntax deals with the term representation. A term is
a triple of the form (left, operator, right). That is, a term uses an infix
notation, and the available operators, and possible left and right operands
differ with those of the previous level. Here is a possible term:
('company_id.name', '=', 'OpenERP')
The left and right operand don't have the same possible values. The left
operand is field name (related to the model for which the domain applies).
Actually, the field name can use the dot-notation to traverse relationships.
The right operand is a Python value whose type should match the used operator
and field type. In the above example, a string is used because the name field
of a company has type string, and because we use the '=' operator. When
appropriate, a 'in' operator can be used, and thus the right operand should be
a list.
Note: the non-uniform syntax could have been more uniform, but this would hide
an important limitation of the domain syntax. Say that the term representation
was ['=', 'company_id.name', 'OpenERP']. Used in a complete domain, this would
look like:
['!', ['=', 'company_id.name', 'OpenERP']]
and you would be tempted to believe something like this would be possible:
['!', ['=', 'company_id.name', ['&', ..., ...]]]
That is, a domain could be a valid operand. But this is not the case. A domain
is really limited to a two-level nature, and can not takes a recursive form: a
domain is not a valid second-level operand.
Unaccent - Accent-insensitive search
OpenERP will use the SQL function 'unaccent' when available for the 'ilike' and
'not ilike' operators, and enabled in the configuration.
Normally the 'unaccent' function is obtained from the PostgreSQL 'unaccent'
contrib module[0].
..todo: The following explanation should be moved in some external installation
guide
The steps to install the module might differ on specific PostgreSQL versions.
We give here some instruction for PostgreSQL 9.x on a Ubuntu system.
Ubuntu doesn't come yet with PostgreSQL 9.x, so an alternative package source
is used. We use Martin Pitt's PPA available at ppa:pitti/postgresql[1]. See
[2] for instructions. Basically:
> sudo add-apt-repository ppa:pitti/postgresql
> sudo apt-get update
Once the package list is up-to-date, you have to install PostgreSQL 9.0 and
its contrib modules.
> sudo apt-get install postgresql-9.0 postgresql-contrib-9.0
When you want to enable unaccent on some database:
> psql9 <database> -f /usr/share/postgresql/9.0/contrib/unaccent.sql
Here 'psql9' is an alias for the newly installed PostgreSQL 9.0 tool, together
with the correct port if necessary (for instance if PostgreSQL 8.4 is running
on 5432). (Other aliases can be used for createdb and dropdb.)
> alias psql9='/usr/lib/postgresql/9.0/bin/psql -p 5433'
You can check unaccent is working:
> psql9 <database> -c"select unaccent('hélène')"
Finally, to instruct OpenERP to really use the unaccent function, you have to
start the server specifying the --unaccent flag.
[0] http://developer.postgresql.org/pgdocs/postgres/unaccent.html
[1] https://launchpad.net/~pitti/+archive/postgresql
[2] https://launchpad.net/+help/soyuz/ppa-sources-list.html
"""
import logging
from openerp.tools import flatten, reverse_enumerate
import fields
import openerp.modules
from openerp.osv.orm import MAGIC_COLUMNS
#.apidoc title: Domain Expressions
# Domain operators.
NOT_OPERATOR = '!'
OR_OPERATOR = '|'
AND_OPERATOR = '&'
DOMAIN_OPERATORS = (NOT_OPERATOR, OR_OPERATOR, AND_OPERATOR)
TRUE_DOMAIN = [(1,'=',1)]
FALSE_DOMAIN = [(0,'=',1)]
# List of available term operators. It is also possible to use the '<>'
# operator, which is strictly the same as '!='; the later should be prefered
# for consistency. This list doesn't contain '<>' as it is simpified to '!='
# by the normalize_operator() function (so later part of the code deals with
# only one representation).
# An internal (i.e. not available to the user) 'inselect' operator is also
# used. In this case its right operand has the form (subselect, params).
TERM_OPERATORS = ('=', '!=', '<=', '<', '>', '>=', '=?', '=like', '=ilike',
'like', 'not like', 'ilike', 'not ilike', 'in', 'not in',
'child_of')
# A subset of the above operators, with a 'negative' semantic. When the
# expressions 'in NEGATIVE_TERM_OPERATORS' or 'not in NEGATIVE_TERM_OPERATORS' are used in the code
# below, this doesn't necessarily mean that any of those NEGATIVE_TERM_OPERATORS is
# legal in the processed term.
NEGATIVE_TERM_OPERATORS = ('!=', 'not like', 'not ilike', 'not in')
TRUE_LEAF = (1, '=', 1)
FALSE_LEAF = (0, '=', 1)
TRUE_DOMAIN = [TRUE_LEAF]
FALSE_DOMAIN = [FALSE_LEAF]
_logger = logging.getLogger('expression')
def normalize(domain):
"""Returns a normalized version of ``domain_expr``, where all implicit '&' operators
@ -45,10 +174,10 @@ def normalize(domain):
op_arity = {NOT_OPERATOR: 1, AND_OPERATOR: 2, OR_OPERATOR: 2}
for token in domain:
if expected == 0: # more than expected, like in [A, B]
result[0:0] = ['&'] # put an extra '&' in front
result[0:0] = [AND_OPERATOR] # put an extra '&' in front
expected = 1
result.append(token)
if isinstance(token, (list,tuple)): # domain term
if isinstance(token, (list, tuple)): # domain term
expected -= 1
else:
expected += op_arity.get(token, 0) - 1
@ -57,7 +186,8 @@ def normalize(domain):
def combine(operator, unit, zero, domains):
"""Returns a new domain expression where all domain components from ``domains``
have been added together using the binary operator ``operator``.
have been added together using the binary operator ``operator``. The given
domains must be normalized.
:param unit: the identity element of the domains "set" with regard to the operation
performed by ``operator``, i.e the domain component ``i`` which, when
@ -69,6 +199,7 @@ def combine(operator, unit, zero, domains):
combined with any domain ``x`` via ``operator``, yields ``z``.
E.g. [(1,'=',1)] is the typical zero for OR_OPERATOR: as soon as
you see it in a domain component the resulting domain is the zero.
:param domains: a list of normalized domains.
"""
result = []
count = 0
@ -84,13 +215,130 @@ def combine(operator, unit, zero, domains):
return result
def AND(domains):
""" AND([D1,D2,...]) returns a domain representing D1 and D2 and ... """
"""AND([D1,D2,...]) returns a domain representing D1 and D2 and ... """
return combine(AND_OPERATOR, TRUE_DOMAIN, FALSE_DOMAIN, domains)
def OR(domains):
""" OR([D1,D2,...]) returns a domain representing D1 or D2 or ... """
"""OR([D1,D2,...]) returns a domain representing D1 or D2 or ... """
return combine(OR_OPERATOR, FALSE_DOMAIN, TRUE_DOMAIN, domains)
def is_operator(element):
"""Test whether an object is a valid domain operator. """
return isinstance(element, basestring) and element in DOMAIN_OPERATORS
# TODO change the share wizard to use this function.
def is_leaf(element, internal=False):
""" Test whether an object is a valid domain term.
:param internal: allow or not the 'inselect' internal operator in the term.
This normally should be always left to False.
"""
INTERNAL_OPS = TERM_OPERATORS + ('inselect',)
return (isinstance(element, tuple) or isinstance(element, list)) \
and len(element) == 3 \
and (((not internal) and element[1] in TERM_OPERATORS + ('<>',)) \
or (internal and element[1] in INTERNAL_OPS + ('<>',)))
def normalize_leaf(left, operator, right):
""" Change a term's operator to some canonical form, simplifying later
processing.
"""
original = operator
operator = operator.lower()
if operator == '<>':
operator = '!='
if isinstance(right, bool) and operator in ('in', 'not in'):
_logger.warning("The domain term '%s' should use the '=' or '!=' operator." % ((left, original, right),))
operator = '=' if operator == 'in' else '!='
if isinstance(right, (list, tuple)) and operator in ('=', '!='):
_logger.warning("The domain term '%s' should use the 'in' or 'not in' operator." % ((left, original, right),))
operator = 'in' if operator == '=' else 'not in'
return left, operator, right
def distribute_not(domain):
""" Distribute any '!' domain operators found inside a normalized domain.
Because we don't use SQL semantic for processing a 'left not in right'
query (i.e. our 'not in' is not simply translated to a SQL 'not in'),
it means that a '! left in right' can not be simply processed
by __leaf_to_sql by first emitting code for 'left in right' then wrapping
the result with 'not (...)', as it would result in a 'not in' at the SQL
level.
This function is thus responsible for pushing any '!' domain operators
inside the terms themselves. For example::
['!','&',('user_id','=',4),('partner_id','in',[1,2])]
will be turned into:
['|',('user_id','!=',4),('partner_id','not in',[1,2])]
"""
def negate(leaf):
"""Negates and returns a single domain leaf term,
using the opposite operator if possible"""
left, operator, right = leaf
mapping = {
'<': '>=',
'>': '<=',
'<=': '>',
'>=': '<',
'=': '!=',
'!=': '=',
}
if operator in ('in', 'like', 'ilike'):
operator = 'not ' + operator
return [(left, operator, right)]
if operator in ('not in', 'not like', 'not ilike'):
operator = operator[4:]
return [(left, operator, right)]
if operator in mapping:
operator = mapping[operator]
return [(left, operator, right)]
return [NOT_OPERATOR, (left, operator, right)]
def distribute_negate(domain):
"""Negate the domain ``subtree`` rooted at domain[0],
leaving the rest of the domain intact, and return
(negated_subtree, untouched_domain_rest)
"""
if is_leaf(domain[0]):
return negate(domain[0]), domain[1:]
if domain[0] == AND_OPERATOR:
done1, todo1 = distribute_negate(domain[1:])
done2, todo2 = distribute_negate(todo1)
return [OR_OPERATOR] + done1 + done2, todo2
if domain[0] == OR_OPERATOR:
done1, todo1 = distribute_negate(domain[1:])
done2, todo2 = distribute_negate(todo1)
return [AND_OPERATOR] + done1 + done2, todo2
if not domain:
return []
if domain[0] != NOT_OPERATOR:
return [domain[0]] + distribute_not(domain[1:])
if domain[0] == NOT_OPERATOR:
done, todo = distribute_negate(domain[1:])
return done + distribute_not(todo)
def select_from_where(cr, select_field, from_table, where_field, where_ids, where_operator):
# todo: merge into parent query as sub-query
res = []
if where_ids:
if where_operator in ['<','>','>=','<=']:
cr.execute('SELECT "%s" FROM "%s" WHERE "%s" %s %%s' % \
(select_field, from_table, where_field, where_operator),
(where_ids[0],)) # TODO shouldn't this be min/max(where_ids) ?
res = [r[0] for r in cr.fetchall()]
else: # TODO where_operator is supposed to be 'in'? It is called with child_of...
for i in range(0, len(where_ids), cr.IN_MAX):
subids = where_ids[i:i+cr.IN_MAX]
cr.execute('SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % \
(select_field, from_table, where_field), (tuple(subids),))
res.extend([r[0] for r in cr.fetchall()])
return res
def select_distinct_from_where_not_null(cr, select_field, from_table):
cr.execute('SELECT distinct("%s") FROM "%s" where "%s" is not null' % \
(select_field, from_table, select_field))
return [r[0] for r in cr.fetchall()]
class expression(object):
"""
@ -100,148 +348,124 @@ class expression(object):
For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html
"""
@classmethod
def _is_operator(cls, element):
return isinstance(element, (str, unicode)) and element in [AND_OPERATOR, OR_OPERATOR, NOT_OPERATOR]
@classmethod
def _is_leaf(cls, element, internal=False):
OPS = ('=', '!=', '<>', '<=', '<', '>', '>=', '=?', '=like', '=ilike', 'like', 'not like', 'ilike', 'not ilike', 'in', 'not in', 'child_of')
INTERNAL_OPS = OPS + ('inselect',)
return (isinstance(element, tuple) or isinstance(element, list)) \
and len(element) == 3 \
and (((not internal) and element[1] in OPS) \
or (internal and element[1] in INTERNAL_OPS))
def __execute_recursive_in(self, cr, s, f, w, ids, op, type):
# todo: merge into parent query as sub-query
res = []
if ids:
if op in ['<','>','>=','<=']:
cr.execute('SELECT "%s"' \
' FROM "%s"' \
' WHERE "%s" %s %%s' % (s, f, w, op), (ids[0],))
res.extend([r[0] for r in cr.fetchall()])
else:
for i in range(0, len(ids), cr.IN_MAX):
subids = ids[i:i+cr.IN_MAX]
cr.execute('SELECT "%s"' \
' FROM "%s"' \
' WHERE "%s" IN %%s' % (s, f, w),(tuple(subids),))
res.extend([r[0] for r in cr.fetchall()])
else:
cr.execute('SELECT distinct("%s")' \
' FROM "%s" where "%s" is not null' % (s, f, s)),
res.extend([r[0] for r in cr.fetchall()])
return res
def __init__(self, exp):
# check if the expression is valid
if not reduce(lambda acc, val: acc and (self._is_operator(val) or self._is_leaf(val)), exp, True):
raise ValueError('Bad domain expression: %r' % (exp,))
self.__exp = exp
def __init__(self, cr, uid, exp, table, context):
self.has_unaccent = openerp.modules.registry.RegistryManager.get(cr.dbname).has_unaccent
self.__field_tables = {} # used to store the table to use for the sql generation. key = index of the leaf
self.__all_tables = set()
self.__joins = []
self.__main_table = None # 'root' table. set by parse()
self.__DUMMY_LEAF = (1, '=', 1) # a dummy leaf that must not be parsed or sql generated
# assign self.__exp with the normalized, parsed domain.
self.parse(cr, uid, distribute_not(normalize(exp)), table, context)
# TODO used only for osv_memory
@property
def exp(self):
return self.__exp[:]
def parse(self, cr, uid, table, context):
""" transform the leafs of the expression """
if not self.__exp:
return self
def parse(self, cr, uid, exp, table, context):
""" transform the leaves of the expression """
self.__exp = exp
self.__main_table = table
self.__all_tables.add(table)
def _rec_get(ids, table, parent=None, left='id', prefix=''):
if table._parent_store and (not table.pool._init):
# TODO: Improve where joins are implemented for many with '.', replace by:
# doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)]
def child_of_domain(left, ids, left_model, parent=None, prefix=''):
"""Returns a domain implementing the child_of operator for [(left,child_of,ids)],
either as a range using the parent_left/right tree lookup fields (when available),
or as an expanded [(left,in,child_ids)]"""
if left_model._parent_store and (not left_model.pool._init):
# TODO: Improve where joins are implemented for many with '.', replace by:
# doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)]
doms = []
for o in table.browse(cr, uid, ids, context=context):
for o in left_model.browse(cr, uid, ids, context=context):
if doms:
doms.insert(0, OR_OPERATOR)
doms += [AND_OPERATOR, ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)]
if prefix:
return [(left, 'in', table.search(cr, uid, doms, context=context))]
return [(left, 'in', left_model.search(cr, uid, doms, context=context))]
return doms
else:
def rg(ids, table, parent):
def recursive_children(ids, model, parent_field):
if not ids:
return []
ids2 = table.search(cr, uid, [(parent, 'in', ids)], context=context)
return ids + rg(ids2, table, parent)
return [(left, 'in', rg(ids, table, parent or table._parent_name))]
ids2 = model.search(cr, uid, [(parent_field, 'in', ids)], context=context)
return ids + recursive_children(ids2, model, parent_field)
return [(left, 'in', recursive_children(ids, left_model, parent or left_model._parent_name))]
def child_of_right_to_ids(value):
""" Normalize a single id, or a string, or a list of ids to a list of ids.
This function is always used with _rec_get() above, so it should be
called directly from _rec_get instead of repeatedly before _rec_get.
"""
def to_ids(value, field_obj):
"""Normalize a single id or name, or a list of those, into a list of ids"""
names = []
if isinstance(value, basestring):
return [x[0] for x in field_obj.name_search(cr, uid, value, [], 'ilike', context=context, limit=None)]
names = [value]
if value and isinstance(value, (tuple, list)) and isinstance(value[0], basestring):
names = value
if names:
return flatten([[x[0] for x in field_obj.name_search(cr, uid, n, [], 'ilike', context=context, limit=None)] \
for n in names])
elif isinstance(value, (int, long)):
return [value]
else:
return list(value)
self.__main_table = table
self.__all_tables.add(table)
return list(value)
i = -1
while i + 1<len(self.__exp):
i += 1
e = self.__exp[i]
if self._is_operator(e) or e == self.__DUMMY_LEAF:
if is_operator(e) or e == TRUE_LEAF or e == FALSE_LEAF:
continue
# check if the expression is valid
if not is_leaf(e):
raise ValueError("Invalid term %r in domain expression %r" % (e, exp))
# normalize the leaf's operator
e = normalize_leaf(*e)
self.__exp[i] = e
left, operator, right = e
operator = operator.lower()
working_table = table
main_table = table
fargs = left.split('.', 1)
if fargs[0] in table._inherit_fields:
working_table = table # The table containing the field (the name provided in the left operand)
field_path = left.split('.', 1)
# If the field is _inherits'd, search for the working_table,
# and extract the field.
if field_path[0] in table._inherit_fields:
while True:
field = main_table._columns.get(fargs[0], False)
field = working_table._columns.get(field_path[0])
if field:
working_table = main_table
self.__field_tables[i] = working_table
break
working_table = main_table.pool.get(main_table._inherit_fields[fargs[0]][0])
if working_table not in self.__all_tables:
self.__joins.append('%s.%s=%s.%s' % (working_table._table, 'id', main_table._table, main_table._inherits[working_table._name]))
self.__all_tables.add(working_table)
main_table = working_table
next_table = working_table.pool.get(working_table._inherit_fields[field_path[0]][0])
if next_table not in self.__all_tables:
self.__joins.append('%s."%s"=%s."%s"' % (next_table._table, 'id', working_table._table, working_table._inherits[next_table._name]))
self.__all_tables.add(next_table)
working_table = next_table
# Or (try to) directly extract the field.
else:
field = working_table._columns.get(field_path[0])
field = working_table._columns.get(fargs[0], False)
if not field:
if left == 'id' and operator == 'child_of':
ids2 = child_of_right_to_ids(right)
dom = _rec_get(ids2, working_table)
ids2 = to_ids(right, table)
dom = child_of_domain(left, ids2, working_table)
self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
else:
# field could not be found in model columns, it's probably invalid, unless
# it's one of the _log_access special fields
# TODO: make these fields explicitly available in self.columns instead!
if field_path[0] not in MAGIC_COLUMNS:
raise ValueError("Invalid field %r in domain expression %r" % (left, exp))
continue
field_obj = table.pool.get(field._obj)
if len(fargs) > 1:
if len(field_path) > 1:
if field._type == 'many2one':
right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context)
if right == []:
self.__exp[i] = ( 'id', '=', 0 )
else:
self.__exp[i] = (fargs[0], 'in', right)
right = field_obj.search(cr, uid, [(field_path[1], operator, right)], context=context)
self.__exp[i] = (field_path[0], 'in', right)
# Making search easier when there is a left operand as field.o2m or field.m2m
if field._type in ['many2many','one2many']:
right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context)
right1 = table.search(cr, uid, [(fargs[0],'in', right)], context=context)
if right1 == []:
self.__exp[i] = ( 'id', '=', 0 )
else:
self.__exp[i] = ('id', 'in', right1)
if field._type in ['many2many', 'one2many']:
right = field_obj.search(cr, uid, [(field_path[1], operator, right)], context=context)
right1 = table.search(cr, uid, [(field_path[0], 'in', right)], context=context)
self.__exp[i] = ('id', 'in', right1)
if not isinstance(field,fields.property):
if not isinstance(field, fields.property):
continue
if field._properties and not field.store:
@ -249,16 +473,16 @@ class expression(object):
if not field._fnct_search:
# the function field doesn't provide a search function and doesn't store
# values in the database, so we must ignore it : we generate a dummy leaf
self.__exp[i] = self.__DUMMY_LEAF
self.__exp[i] = TRUE_LEAF
else:
subexp = field.search(cr, uid, table, left, [self.__exp[i]], context=context)
if not subexp:
self.__exp[i] = self.__DUMMY_LEAF
self.__exp[i] = TRUE_LEAF
else:
# we assume that the expression is valid
# we create a dummy leaf for forcing the parsing of the resulting expression
self.__exp[i] = AND_OPERATOR
self.__exp.insert(i + 1, self.__DUMMY_LEAF)
self.__exp.insert(i + 1, TRUE_LEAF)
for j, se in enumerate(subexp):
self.__exp.insert(i + 2 + j, se)
# else, the value of the field is store in the database, so we search on it
@ -266,11 +490,11 @@ class expression(object):
elif field._type == 'one2many':
# Applying recursivity on field(one2many)
if operator == 'child_of':
ids2 = child_of_right_to_ids(right)
ids2 = to_ids(right, field_obj)
if field._obj != working_table._name:
dom = _rec_get(ids2, field_obj, left=left, prefix=field._obj)
dom = child_of_domain(left, ids2, field_obj, prefix=field._obj)
else:
dom = _rec_get(ids2, working_table, parent=left)
dom = child_of_domain('id', ids2, working_table, parent=left)
self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
else:
@ -282,7 +506,7 @@ class expression(object):
if ids2:
operator = 'in'
else:
if not isinstance(right,list):
if not isinstance(right, list):
ids2 = [right]
else:
ids2 = right
@ -290,22 +514,16 @@ class expression(object):
if operator in ['like','ilike','in','=']:
#no result found with given search criteria
call_null = False
self.__exp[i] = ('id','=',0)
else:
call_null = True
operator = 'in' # operator changed because ids are directly related to main object
self.__exp[i] = FALSE_LEAF
else:
call_null = False
o2m_op = 'in'
if operator in ['not like','not ilike','not in','<>','!=']:
o2m_op = 'not in'
self.__exp[i] = ('id', o2m_op, self.__execute_recursive_in(cr, field._fields_id, field_obj._table, 'id', ids2, operator, field._type))
ids2 = select_from_where(cr, field._fields_id, field_obj._table, 'id', ids2, operator)
if ids2:
call_null = False
self.__exp[i] = ('id', 'in', ids2)
if call_null:
o2m_op = 'not in'
if operator in ['not like','not ilike','not in','<>','!=']:
o2m_op = 'in'
self.__exp[i] = ('id', o2m_op, self.__execute_recursive_in(cr, field._fields_id, field_obj._table, 'id', [], operator, field._type) or [0])
o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
self.__exp[i] = ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, field_obj._table))
elif field._type == 'many2many':
#FIXME
@ -313,10 +531,10 @@ class expression(object):
def _rec_convert(ids):
if field_obj == table:
return ids
return self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, ids, operator, field._type)
return select_from_where(cr, field._id1, field._rel, field._id2, ids, operator)
ids2 = child_of_right_to_ids(right)
dom = _rec_get(ids2, field_obj)
ids2 = to_ids(right, field_obj)
dom = child_of_domain('id', ids2, field_obj)
ids2 = field_obj.search(cr, uid, dom, context=context)
self.__exp[i] = ('id', 'in', _rec_convert(ids2))
else:
@ -335,34 +553,28 @@ class expression(object):
if operator in ['like','ilike','in','=']:
#no result found with given search criteria
call_null_m2m = False
self.__exp[i] = ('id','=',0)
self.__exp[i] = FALSE_LEAF
else:
call_null_m2m = True
operator = 'in' # operator changed because ids are directly related to main object
else:
call_null_m2m = False
m2m_op = 'in'
if operator in ['not like','not ilike','not in','<>','!=']:
m2m_op = 'not in'
m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
self.__exp[i] = ('id', m2m_op, select_from_where(cr, field._id1, field._rel, field._id2, res_ids, operator) or [0])
self.__exp[i] = ('id', m2m_op, self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, res_ids, operator, field._type) or [0])
if call_null_m2m:
m2m_op = 'not in'
if operator in ['not like','not ilike','not in','<>','!=']:
m2m_op = 'in'
self.__exp[i] = ('id', m2m_op, self.__execute_recursive_in(cr, field._id1, field._rel, field._id2, [], operator, field._type) or [0])
m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
self.__exp[i] = ('id', m2m_op, select_distinct_from_where_not_null(cr, field._id1, field._rel))
elif field._type == 'many2one':
if operator == 'child_of':
ids2 = child_of_right_to_ids(right)
self.__operator = 'in'
ids2 = to_ids(right, field_obj)
if field._obj != working_table._name:
dom = _rec_get(ids2, field_obj, left=left, prefix=field._obj)
dom = child_of_domain(left, ids2, field_obj, prefix=field._obj)
else:
dom = _rec_get(ids2, working_table, parent=left)
dom = child_of_domain('id', ids2, working_table, parent=left)
self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
else:
def _get_expression(field_obj,cr, uid, left, right, operator, context=None):
def _get_expression(field_obj, cr, uid, left, right, operator, context=None):
if context is None:
context = {}
c = context.copy()
@ -370,46 +582,35 @@ class expression(object):
#Special treatment to ill-formed domains
operator = ( operator in ['<','>','<=','>='] ) and 'in' or operator
dict_op = {'not in':'!=','in':'=','=':'in','!=':'not in','<>':'not in'}
if isinstance(right,tuple):
dict_op = {'not in':'!=','in':'=','=':'in','!=':'not in'}
if isinstance(right, tuple):
right = list(right)
if (not isinstance(right,list)) and operator in ['not in','in']:
if (not isinstance(right, list)) and operator in ['not in','in']:
operator = dict_op[operator]
elif isinstance(right,list) and operator in ['<>','!=','=']: #for domain (FIELD,'=',['value1','value2'])
elif isinstance(right, list) and operator in ['!=','=']: #for domain (FIELD,'=',['value1','value2'])
operator = dict_op[operator]
res_ids = field_obj.name_search(cr, uid, right, [], operator, limit=None, context=c)
if not res_ids:
return ('id','=',0)
else:
right = map(lambda x: x[0], res_ids)
return (left, 'in', right)
res_ids = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, limit=None, context=c)]
if operator in NEGATIVE_TERM_OPERATORS:
res_ids.append(False) # TODO this should not be appended if False was in 'right'
return (left, 'in', res_ids)
m2o_str = False
if right:
if isinstance(right, basestring): # and not isinstance(field, fields.related):
m2o_str = True
elif isinstance(right,(list,tuple)):
elif isinstance(right, (list, tuple)):
m2o_str = True
for ele in right:
if not isinstance(ele, basestring):
m2o_str = False
break
if m2o_str:
self.__exp[i] = _get_expression(field_obj, cr, uid, left, right, operator, context=context)
elif right == []:
m2o_str = False
if operator in ('not in', '!=', '<>'):
# (many2one not in []) should return all records
self.__exp[i] = self.__DUMMY_LEAF
else:
self.__exp[i] = ('id','=',0)
else:
new_op = '='
if operator in ['not like','not ilike','not in','<>','!=']:
new_op = '!='
#Is it ok to put 'left' and not 'id' ?
self.__exp[i] = (left,new_op,False)
pass # Handled by __leaf_to_sql().
else: # right is False
pass # Handled by __leaf_to_sql().
if m2o_str:
self.__exp[i] = _get_expression(field_obj,cr, uid, left, right, operator, context=context)
else:
# other field type
# add the time part to datetime field when it's not there:
@ -425,127 +626,160 @@ class expression(object):
self.__exp[i] = tuple(self.__exp[i])
if field.translate:
if operator in ('like', 'ilike', 'not like', 'not ilike'):
need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
sql_operator = {'=like':'like','=ilike':'ilike'}.get(operator,operator)
if need_wildcard:
right = '%%%s%%' % right
operator = operator == '=like' and 'like' or operator
query1 = '( SELECT res_id' \
subselect = '( SELECT res_id' \
' FROM ir_translation' \
' WHERE name = %s' \
' AND lang = %s' \
' AND type = %s'
instr = ' %s'
#Covering in,not in operators with operands (%s,%s) ,etc.
if operator in ['in','not in']:
if sql_operator in ['in','not in']:
instr = ','.join(['%s'] * len(right))
query1 += ' AND value ' + operator + ' ' +" (" + instr + ")" \
subselect += ' AND value ' + sql_operator + ' ' +" (" + instr + ")" \
') UNION (' \
' SELECT id' \
' FROM "' + working_table._table + '"' \
' WHERE "' + left + '" ' + operator + ' ' +" (" + instr + "))"
' WHERE "' + left + '" ' + sql_operator + ' ' +" (" + instr + "))"
else:
query1 += ' AND value ' + operator + instr + \
subselect += ' AND value ' + sql_operator + instr + \
') UNION (' \
' SELECT id' \
' FROM "' + working_table._table + '"' \
' WHERE "' + left + '" ' + operator + instr + ")"
' WHERE "' + left + '" ' + sql_operator + instr + ")"
query2 = [working_table._name + ',' + left,
params = [working_table._name + ',' + left,
context.get('lang', False) or 'en_US',
'model',
right,
right,
]
self.__exp[i] = ('id', 'inselect', (query1, query2))
return self
self.__exp[i] = ('id', 'inselect', (subselect, params))
def __leaf_to_sql(self, leaf, table):
if leaf == self.__DUMMY_LEAF:
return ('(1=1)', [])
left, operator, right = leaf
if operator == 'inselect':
query = '(%s.%s in (%s))' % (table._table, left, right[0])
params = right[1]
elif operator in ['in', 'not in']:
params = right and right[:] or []
len_before = len(params)
for i in range(len_before)[::-1]:
if params[i] == False:
del params[i]
# final sanity checks - should never fail
assert operator in (TERM_OPERATORS + ('inselect',)), \
"Invalid operator %r in domain term %r" % (operator, leaf)
assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in table._all_columns \
or left in MAGIC_COLUMNS, "Invalid field %r in domain term %r" % (left, leaf)
len_after = len(params)
check_nulls = len_after != len_before
query = '(1=0)'
if len_after:
if left == 'id':
instr = ','.join(['%s'] * len_after)
else:
instr = ','.join([table._columns[left]._symbol_set[0]] * len_after)
query = '(%s.%s %s (%s))' % (table._table, left, operator, instr)
else:
# the case for [field, 'in', []] or [left, 'not in', []]
if operator == 'in':
query = '(%s.%s IS NULL)' % (table._table, left)
else:
query = '(%s.%s IS NOT NULL)' % (table._table, left)
if check_nulls:
query = '(%s OR %s.%s IS NULL)' % (query, table._table, left)
else:
if leaf == TRUE_LEAF:
query = 'TRUE'
params = []
if right == False and (leaf[0] in table._columns) and table._columns[leaf[0]]._type=="boolean" and (operator == '='):
query = '(%s.%s IS NULL or %s.%s = false )' % (table._table, left,table._table, left)
elif (((right == False) and (type(right)==bool)) or (right is None)) and (operator == '='):
query = '%s.%s IS NULL ' % (table._table, left)
elif right == False and (leaf[0] in table._columns) and table._columns[leaf[0]]._type=="boolean" and (operator in ['<>', '!=']):
query = '(%s.%s IS NOT NULL and %s.%s != false)' % (table._table, left,table._table, left)
elif (((right == False) and (type(right)==bool)) or right is None) and (operator in ['<>', '!=']):
query = '%s.%s IS NOT NULL' % (table._table, left)
elif (operator == '=?'):
op = '='
if (right is False or right is None):
return ( 'TRUE',[])
if left in table._columns:
format = table._columns[left]._symbol_set[0]
query = '(%s.%s %s %s)' % (table._table, left, op, format)
params = table._columns[left]._symbol_set[1](right)
else:
query = "(%s.%s %s '%%s')" % (table._table, left, op)
params = right
elif leaf == FALSE_LEAF:
query = 'FALSE'
params = []
else:
if left == 'id':
query = '%s.id %s %%s' % (table._table, operator)
params = right
else:
like = operator in ('like', 'ilike', 'not like', 'not ilike')
elif operator == 'inselect':
query = '(%s."%s" in (%s))' % (table._table, left, right[0])
params = right[1]
op = {'=like':'like','=ilike':'ilike'}.get(operator,operator)
if left in table._columns:
format = like and '%s' or table._columns[left]._symbol_set[0]
query = '(%s.%s %s %s)' % (table._table, left, op, format)
elif operator in ['in', 'not in']:
# Two cases: right is a boolean or a list. The boolean case is an
# abuse and handled for backward compatibility.
if isinstance(right, bool):
_logger.warning("The domain term '%s' should use the '=' or '!=' operator." % (leaf,))
if operator == 'in':
r = 'NOT NULL' if right else 'NULL'
else:
r = 'NULL' if right else 'NOT NULL'
query = '(%s."%s" IS %s)' % (table._table, left, r)
params = []
elif isinstance(right, (list, tuple)):
params = right[:]
check_nulls = False
for i in range(len(params))[::-1]:
if params[i] == False:
check_nulls = True
del params[i]
if params:
if left == 'id':
instr = ','.join(['%s'] * len(params))
else:
query = "(%s.%s %s '%s')" % (table._table, left, op, right)
instr = ','.join([table._columns[left]._symbol_set[0]] * len(params))
query = '(%s."%s" %s (%s))' % (table._table, left, operator, instr)
else:
# The case for (left, 'in', []) or (left, 'not in', []).
query = 'FALSE' if operator == 'in' else 'TRUE'
add_null = False
if like:
if isinstance(right, str):
str_utf8 = right
elif isinstance(right, unicode):
str_utf8 = right.encode('utf-8')
else:
str_utf8 = str(right)
params = '%%%s%%' % str_utf8
add_null = not str_utf8
elif left in table._columns:
params = table._columns[left]._symbol_set[1](right)
if check_nulls and operator == 'in':
query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left)
elif not check_nulls and operator == 'not in':
query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left)
elif check_nulls and operator == 'not in':
query = '(%s AND %s."%s" IS NOT NULL)' % (query, table._table, left) # needed only for TRUE.
else: # Must not happen
raise ValueError("Invalid domain term %r" % (leaf,))
if add_null:
query = '(%s OR %s IS NULL)' % (query, left)
elif right == False and (left in table._columns) and table._columns[left]._type=="boolean" and (operator == '='):
query = '(%s."%s" IS NULL or %s."%s" = false )' % (table._table, left, table._table, left)
params = []
elif (right is False or right is None) and (operator == '='):
query = '%s."%s" IS NULL ' % (table._table, left)
params = []
elif right == False and (left in table._columns) and table._columns[left]._type=="boolean" and (operator == '!='):
query = '(%s."%s" IS NOT NULL and %s."%s" != false)' % (table._table, left, table._table, left)
params = []
elif (right is False or right is None) and (operator == '!='):
query = '%s."%s" IS NOT NULL' % (table._table, left)
params = []
elif (operator == '=?'):
if (right is False or right is None):
# '=?' is a short-circuit that makes the term TRUE if right is None or False
query = 'TRUE'
params = []
else:
# '=?' behaves like '=' in other cases
query, params = self.__leaf_to_sql((left, '=', right), table)
elif left == 'id':
query = '%s.id %s %%s' % (table._table, operator)
params = right
else:
need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
sql_operator = {'=like':'like','=ilike':'ilike'}.get(operator,operator)
if left in table._columns:
format = need_wildcard and '%s' or table._columns[left]._symbol_set[0]
if self.has_unaccent and sql_operator in ('ilike', 'not ilike'):
query = '(unaccent(%s."%s") %s unaccent(%s))' % (table._table, left, sql_operator, format)
else:
query = '(%s."%s" %s %s)' % (table._table, left, sql_operator, format)
elif left in MAGIC_COLUMNS:
query = "(%s.\"%s\" %s %%s)" % (table._table, left, sql_operator)
params = right
else: # Must not happen
raise ValueError("Invalid field %r in domain term %r" % (left, leaf))
add_null = False
if need_wildcard:
if isinstance(right, str):
str_utf8 = right
elif isinstance(right, unicode):
str_utf8 = right.encode('utf-8')
else:
str_utf8 = str(right)
params = '%%%s%%' % str_utf8
add_null = not str_utf8
elif left in table._columns:
params = table._columns[left]._symbol_set[1](right)
if add_null:
query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left)
if isinstance(params, basestring):
params = [params]
@ -555,25 +789,26 @@ class expression(object):
def to_sql(self):
stack = []
params = []
# Process the domain from right to left, using a stack, to generate a SQL expression.
for i, e in reverse_enumerate(self.__exp):
if self._is_leaf(e, internal=True):
if is_leaf(e, internal=True):
table = self.__field_tables.get(i, self.__main_table)
q, p = self.__leaf_to_sql(e, table)
params.insert(0, p)
stack.append(q)
elif e == NOT_OPERATOR:
stack.append('(NOT (%s))' % (stack.pop(),))
else:
if e == NOT_OPERATOR:
stack.append('(NOT (%s))' % (stack.pop(),))
else:
ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '}
q1 = stack.pop()
q2 = stack.pop()
stack.append('(%s %s %s)' % (q1, ops[e], q2,))
ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '}
q1 = stack.pop()
q2 = stack.pop()
stack.append('(%s %s %s)' % (q1, ops[e], q2,))
query = ' AND '.join(reversed(stack))
assert len(stack) == 1
query = stack[0]
joins = ' AND '.join(self.__joins)
if joins:
query = '(%s) AND (%s)' % (joins, query)
query = '(%s) AND %s' % (joins, query)
return (query, flatten(params))
def get_tables(self):

View File

@ -55,7 +55,7 @@ def _symbol_set(symb):
class _column(object):
""" Base of all fields, a database column
An instance of this object is a *description* of a database column. It will
not hold any data, but only provide the methods to manipulate data of an
ORM record or even prepare/update the database to hold such a field of data.
@ -72,7 +72,7 @@ class _column(object):
_symbol_set = (_symbol_c, _symbol_f)
_symbol_get = None
def __init__(self, string='unknown', required=False, readonly=False, domain=None, context=None, states=None, priority=0, change_default=False, size=None, ondelete="set null", translate=False, select=False, manual=False, **args):
def __init__(self, string='unknown', required=False, readonly=False, domain=None, context=None, states=None, priority=0, change_default=False, size=None, ondelete=None, translate=False, select=False, manual=False, **args):
"""
The 'manual' keyword argument specifies if the field is a custom one.
@ -91,7 +91,7 @@ class _column(object):
self.help = args.get('help', '')
self.priority = priority
self.change_default = change_default
self.ondelete = ondelete
self.ondelete = ondelete.lower() if ondelete else None # defaults to 'set null' in ORM
self.translate = translate
self._domain = domain
self._context = context
@ -112,12 +112,6 @@ class _column(object):
def set(self, cr, obj, id, name, value, user=None, context=None):
cr.execute('update '+obj._table+' set '+name+'='+self._symbol_set[0]+' where id=%s', (self._symbol_set[1](value), id))
def set_memory(self, cr, obj, id, name, value, user=None, context=None):
raise Exception(_('Not implemented set_memory method !'))
def get_memory(self, cr, obj, ids, name, user=None, context=None, values=None):
raise Exception(_('Not implemented get_memory method !'))
def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
raise Exception(_('undefined get method !'))
@ -126,9 +120,6 @@ class _column(object):
res = obj.read(cr, uid, ids, [name], context=context)
return [x[name] for x in res]
def search_memory(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, context=None):
raise Exception(_('Not implemented search_memory method !'))
# ---------------------------------------------------------
# Simple fields
@ -269,7 +260,7 @@ class binary(_column):
_column.__init__(self, string=string, **args)
self.filters = filters
def get_memory(self, cr, obj, ids, name, user=None, context=None, values=None):
def get(self, cr, obj, ids, name, user=None, context=None, values=None):
if not context:
context = {}
if not values:
@ -293,9 +284,6 @@ class binary(_column):
res[i] = val
return res
get = get_memory
class selection(_column):
_type = 'selection'
@ -355,30 +343,6 @@ class many2one(_column):
_column.__init__(self, string=string, **args)
self._obj = obj
def set_memory(self, cr, obj, id, field, values, user=None, context=None):
obj.datas.setdefault(id, {})
obj.datas[id][field] = values
def get_memory(self, cr, obj, ids, name, user=None, context=None, values=None):
result = {}
for id in ids:
result[id] = obj.datas[id].get(name, False)
# build a dictionary of the form {'id_of_distant_resource': name_of_distant_resource}
# we use uid=1 because the visibility of a many2one field value (just id and name)
# must be the access right of the parent form and not the linked object itself.
obj = obj.pool.get(self._obj)
records = dict(obj.name_get(cr, 1,
list(set([x for x in result.values() if x and isinstance(x, (int,long))])),
context=context))
for id in ids:
if result[id] in records:
result[id] = (result[id], records[result[id]])
else:
result[id] = False
return result
def get(self, cr, obj, ids, name, user=None, context=None, values=None):
if context is None:
context = {}
@ -447,55 +411,6 @@ class one2many(_column):
#one2many can't be used as condition for defaults
assert(self.change_default != True)
def get_memory(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
if context is None:
context = {}
if self._context:
context = context.copy()
context.update(self._context)
if not values:
values = {}
res = {}
for id in ids:
res[id] = []
ids2 = obj.pool.get(self._obj).search(cr, user, [(self._fields_id, 'in', ids)], limit=self._limit, context=context)
for r in obj.pool.get(self._obj).read(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
if r[self._fields_id] in res:
res[r[self._fields_id]].append(r['id'])
return res
def set_memory(self, cr, obj, id, field, values, user=None, context=None):
if not context:
context = {}
if self._context:
context = context.copy()
context.update(self._context)
if not values:
return
obj = obj.pool.get(self._obj)
for act in values:
if act[0] == 0:
act[2][self._fields_id] = id
obj.create(cr, user, act[2], context=context)
elif act[0] == 1:
obj.write(cr, user, [act[1]], act[2], context=context)
elif act[0] == 2:
obj.unlink(cr, user, [act[1]], context=context)
elif act[0] == 3:
obj.datas[act[1]][self._fields_id] = False
elif act[0] == 4:
obj.datas[act[1]][self._fields_id] = id
elif act[0] == 5:
for o in obj.datas.values():
if o[self._fields_id] == id:
o[self._fields_id] = False
elif act[0] == 6:
for id2 in (act[2] or []):
obj.datas[id2][self._fields_id] = id
def search_memory(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
raise _('Not Implemented')
def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
if context is None:
context = {}
@ -578,14 +493,34 @@ class one2many(_column):
# (6, ?, ids) set a list of links
#
class many2many(_column):
"""Encapsulates the logic of a many-to-many bidirectional relationship, handling the
low-level details of the intermediary relationship table transparently.
:param str obj: destination model
:param str rel: optional name of the intermediary relationship table. If not specified,
a canonical name will be derived based on the alphabetically-ordered
model names of the source and destination (in the form: ``amodel_bmodel_rel``).
Automatic naming is not possible when the source and destination are
the same, for obvious ambiguity reasons.
:param str id1: optional name for the column holding the foreign key to the current
model in the relationship table. If not specified, a canonical name
will be derived based on the model name (in the form: `src_model_id`).
:param str id2: optional name for the column holding the foreign key to the destination
model in the relationship table. If not specified, a canonical name
will be derived based on the model name (in the form: `dest_model_id`)
:param str string: field label
"""
_classic_read = False
_classic_write = False
_prefetch = False
_type = 'many2many'
def __init__(self, obj, rel, id1, id2, string='unknown', limit=None, **args):
def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
"""
"""
_column.__init__(self, string=string, **args)
self._obj = obj
if '.' in rel:
if rel and '.' in rel:
raise Exception(_('The second argument of the many2many field %s must be a SQL table !'\
'You used %s, which is not a valid SQL table name.')% (string,rel))
self._rel = rel
@ -593,7 +528,30 @@ class many2many(_column):
self._id2 = id2
self._limit = limit
def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
def _sql_names(self, source_model):
"""Return the SQL names defining the structure of the m2m relationship table
:return: (m2m_table, local_col, dest_col) where m2m_table is the table name,
local_col is the name of the column holding the current model's FK, and
dest_col is the name of the column holding the destination model's FK, and
"""
tbl, col1, col2 = self._rel, self._id1, self._id2
if not all((tbl, col1, col2)):
# the default table name is based on the stable alphabetical order of tables
dest_model = source_model.pool.get(self._obj)
tables = tuple(sorted([source_model._table, dest_model._table]))
if not tbl:
assert tables[0] != tables[1], 'Implicit/Canonical naming of m2m relationship table '\
'is not possible when source and destination models are '\
'the same'
tbl = '%s_%s_rel' % tables
if not col1:
col1 = '%s_id' % source_model._table
if not col2:
col2 = '%s_id' % dest_model._table
return (tbl, col1, col2)
def get(self, cr, model, ids, name, user=None, offset=0, context=None, values=None):
if not context:
context = {}
if not values:
@ -606,7 +564,8 @@ class many2many(_column):
if offset:
warnings.warn("Specifying offset at a many2many.get() may produce unpredictable results.",
DeprecationWarning, stacklevel=2)
obj = obj.pool.get(self._obj)
obj = model.pool.get(self._obj)
rel, id1, id2 = self._sql_names(model)
# static domains are lists, and are evaluated both here and on client-side, while string
# domains supposed by dynamic and evaluated on client-side only (thus ignored here)
@ -636,11 +595,11 @@ class many2many(_column):
%(order_by)s \
%(limit)s \
OFFSET %(offset)d' \
% {'rel': self._rel,
% {'rel': rel,
'from_c': from_c,
'tbl': obj._table,
'id1': self._id1,
'id2': self._id2,
'id1': id1,
'id2': id2,
'where_c': where_c,
'limit': limit_str,
'order_by': order_by,
@ -651,31 +610,32 @@ class many2many(_column):
res[r[1]].append(r[0])
return res
def set(self, cr, obj, id, name, values, user=None, context=None):
def set(self, cr, model, id, name, values, user=None, context=None):
if not context:
context = {}
if not values:
return
obj = obj.pool.get(self._obj)
rel, id1, id2 = self._sql_names(model)
obj = model.pool.get(self._obj)
for act in values:
if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
continue
if act[0] == 0:
idnew = obj.create(cr, user, act[2], context=context)
cr.execute('insert into '+self._rel+' ('+self._id1+','+self._id2+') values (%s,%s)', (id, idnew))
cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, idnew))
elif act[0] == 1:
obj.write(cr, user, [act[1]], act[2], context=context)
elif act[0] == 2:
obj.unlink(cr, user, [act[1]], context=context)
elif act[0] == 3:
cr.execute('delete from '+self._rel+' where ' + self._id1 + '=%s and '+ self._id2 + '=%s', (id, act[1]))
cr.execute('delete from '+rel+' where ' + id1 + '=%s and '+ id2 + '=%s', (id, act[1]))
elif act[0] == 4:
# following queries are in the same transaction - so should be relatively safe
cr.execute('SELECT 1 FROM '+self._rel+' WHERE '+self._id1+' = %s and '+self._id2+' = %s', (id, act[1]))
cr.execute('SELECT 1 FROM '+rel+' WHERE '+id1+' = %s and '+id2+' = %s', (id, act[1]))
if not cr.fetchone():
cr.execute('insert into '+self._rel+' ('+self._id1+','+self._id2+') values (%s,%s)', (id, act[1]))
cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s,%s)', (id, act[1]))
elif act[0] == 5:
cr.execute('update '+self._rel+' set '+self._id2+'=null where '+self._id2+'=%s', (id,))
cr.execute('delete from '+rel+' where ' + id1 + ' = %s', (id,))
elif act[0] == 6:
d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
@ -683,10 +643,10 @@ class many2many(_column):
d1 = ' and ' + ' and '.join(d1)
else:
d1 = ''
cr.execute('delete from '+self._rel+' where '+self._id1+'=%s AND '+self._id2+' IN (SELECT '+self._rel+'.'+self._id2+' FROM '+self._rel+', '+','.join(tables)+' WHERE '+self._rel+'.'+self._id1+'=%s AND '+self._rel+'.'+self._id2+' = '+obj._table+'.id '+ d1 +')', [id, id]+d2)
cr.execute('delete from '+rel+' where '+id1+'=%s AND '+id2+' IN (SELECT '+rel+'.'+id2+' FROM '+rel+', '+','.join(tables)+' WHERE '+rel+'.'+id1+'=%s AND '+rel+'.'+id2+' = '+obj._table+'.id '+ d1 +')', [id, id]+d2)
for act_nbr in act[2]:
cr.execute('insert into '+self._rel+' ('+self._id1+','+self._id2+') values (%s, %s)', (id, act_nbr))
cr.execute('insert into '+rel+' ('+id1+','+id2+') values (%s, %s)', (id, act_nbr))
#
# TODO: use a name_search
@ -694,32 +654,6 @@ class many2many(_column):
def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):
return obj.pool.get(self._obj).search(cr, uid, args+self._domain+[('name', operator, value)], offset, limit, context=context)
def get_memory(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
result = {}
for id in ids:
result[id] = obj.datas[id].get(name, [])
return result
def set_memory(self, cr, obj, id, name, values, user=None, context=None):
if not values:
return
for act in values:
# TODO: use constants instead of these magic numbers
if act[0] == 0:
raise _('Not Implemented')
elif act[0] == 1:
raise _('Not Implemented')
elif act[0] == 2:
raise _('Not Implemented')
elif act[0] == 3:
raise _('Not Implemented')
elif act[0] == 4:
raise _('Not Implemented')
elif act[0] == 5:
raise _('Not Implemented')
elif act[0] == 6:
obj.datas[id][name] = act[2]
def get_nice_size(value):
size = 0
@ -801,8 +735,8 @@ class function(_column):
Implements the function field.
:param orm_template model: model to which the field belongs (should be ``self`` for
a model method)
:param orm model: model to which the field belongs (should be ``self`` for
a model method)
:param field_name(s): name of the field to compute, or if ``multi`` is provided,
list of field names to compute.
:type field_name(s): str | [str]
@ -865,8 +799,8 @@ class function(_column):
Callable that implements the ``write`` operation for the function field.
:param orm_template model: model to which the field belongs (should be ``self`` for
a model method)
:param orm model: model to which the field belongs (should be ``self`` for
a model method)
:param int id: the identifier of the object to write on
:param str field_name: name of the field to set
:param fnct_inv_arg: arbitrary value passed when declaring the function field
@ -887,10 +821,10 @@ class function(_column):
a search criterion based on the function field into a new domain based only on
columns that are stored in the database.
:param orm_template model: model to which the field belongs (should be ``self`` for
a model method)
:param orm_template model_again: same value as ``model`` (seriously! this is for backwards
compatibility)
:param orm model: model to which the field belongs (should be ``self`` for
a model method)
:param orm model_again: same value as ``model`` (seriously! this is for backwards
compatibility)
:param str field_name: name of the field to search on
:param list criterion: domain component specifying the search criterion on the field.
:rtype: list
@ -935,7 +869,7 @@ class function(_column):
corresponding records in the source model (whose field values
need to be recomputed).
:param orm_template model: trigger_model
:param orm model: trigger_model
:param list trigger_ids: ids of the records of trigger_model that were
modified
:rtype: list
@ -1064,14 +998,11 @@ class function(_column):
result[id] = self.postprocess(cr, uid, obj, name, result[id], context)
return result
get_memory = get
def set(self, cr, obj, id, name, value, user=None, context=None):
if not context:
context = {}
if self._fnct_inv:
self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
set_memory = set
# ---------------------------------------------------------
# Related fields
@ -1295,7 +1226,7 @@ class property(function):
def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
prop = obj.pool.get('ir.property')
# get the default values (for res_id = False) for the property fields
# get the default values (for res_id = False) for the property fields
default_val = self._get_defaults(obj, cr, uid, prop_names, context)
# build the dictionary that will be returned
@ -1417,12 +1348,16 @@ class column_info(object):
:attr parent_column: the name of the column containing the m2o
relationship to the parent model that contains
this column, None for local columns.
:attr original_parent: if the column is inherited, name of the original
parent model that contains it i.e in case of multilevel
inheritence, None for local columns.
"""
def __init__(self, name, column, parent_model=None, parent_column=None):
def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
self.name = name
self.column = column
self.parent_model = parent_model
self.parent_column = parent_column
self.original_parent = original_parent
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

File diff suppressed because it is too large Load Diff

View File

@ -21,31 +21,29 @@
#.apidoc title: Objects Services (OSV)
import logging
from psycopg2 import IntegrityError, errorcodes
import orm
import openerp
import openerp.netsvc as netsvc
import openerp.pooler as pooler
import openerp.sql_db as sql_db
import logging
from psycopg2 import IntegrityError, errorcodes
from openerp.tools.func import wraps
from openerp.tools.translate import translate
from openerp.osv.orm import MetaModel
from openerp.osv.orm import MetaModel, Model, TransientModel, AbstractModel
import openerp.exceptions
# For backward compatibility
except_osv = openerp.exceptions.Warning
class except_osv(Exception):
def __init__(self, name, value, exc_type='warning'):
self.name = name
self.exc_type = exc_type
self.value = value
self.args = (exc_type, name)
service = None
class object_proxy(netsvc.Service):
class object_proxy():
def __init__(self):
self.logger = logging.getLogger('web-services')
netsvc.Service.__init__(self, 'object_proxy', audience='')
self.exportMethod(self.exec_workflow)
self.exportMethod(self.execute)
global service
service = self
def check(f):
@wraps(f)
@ -119,14 +117,14 @@ class object_proxy(netsvc.Service):
except orm.except_orm, inst:
if inst.name == 'AccessError':
self.logger.debug("AccessError", exc_info=True)
self.abortResponse(1, inst.name, 'warning', inst.value)
netsvc.abort_response(1, inst.name, 'warning', inst.value)
except except_osv, inst:
self.abortResponse(1, inst.name, inst.exc_type, inst.value)
netsvc.abort_response(1, inst.name, 'warning', inst.value)
except IntegrityError, inst:
osv_pool = pooler.get_pool(dbname)
for key in osv_pool._sql_error.keys():
if key in inst[0]:
self.abortResponse(1, _('Constraint Error'), 'warning',
netsvc.abort_response(1, _('Constraint Error'), 'warning',
tr(osv_pool._sql_error[key], 'sql_constraint') or inst[0])
if inst.pgcode in (errorcodes.NOT_NULL_VIOLATION, errorcodes.FOREIGN_KEY_VIOLATION, errorcodes.RESTRICT_VIOLATION):
msg = _('The operation cannot be completed, probably due to the following:\n- deletion: you may be trying to delete a record while other records still reference it\n- creation/update: a mandatory field is not correctly set')
@ -147,9 +145,9 @@ class object_proxy(netsvc.Service):
msg += _('\n\n[object with reference: %s - %s]') % (model_name, model)
except Exception:
pass
self.abortResponse(1, _('Integrity Error'), 'warning', msg)
netsvc.abort_response(1, _('Integrity Error'), 'warning', msg)
else:
self.abortResponse(1, _('Integrity Error'), 'warning', inst[0])
netsvc.abort_response(1, _('Integrity Error'), 'warning', inst[0])
except Exception:
self.logger.exception("Uncaught exception")
raise
@ -198,17 +196,10 @@ class object_proxy(netsvc.Service):
cr.close()
return res
class osv_memory(orm.orm_memory):
""" Deprecated class. """
__metaclass__ = MetaModel
_register = False # Set to false if the model shouldn't be automatically discovered.
class osv(orm.orm):
""" Deprecated class. """
__metaclass__ = MetaModel
_register = False # Set to false if the model shouldn't be automatically discovered.
# deprecated - for backward compatibility.
osv = Model
osv_memory = TransientModel
osv_abstract = AbstractModel # ;-)
def start_object_proxy():

View File

@ -34,11 +34,6 @@ def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False,
return registry.db, registry
def delete_pool(db_name):
"""Delete an existing registry."""
RegistryManager.delete(db_name)
def restart_pool(db_name, force_demo=False, status=None, update_module=False):
"""Delete an existing registry and return a database connection and a newly initialized registry."""
registry = RegistryManager.new(db_name, force_demo, status, update_module, True)

View File

@ -136,16 +136,15 @@ class report_custom(report_int):
ids = self.pool.get(report.model_id.model).search(cr, uid, [])
datas['ids'] = ids
service = netsvc.LocalService("object_proxy")
report_id = datas['report_id']
report = service.execute(cr.dbname, uid, 'ir.report.custom', 'read', [report_id], context=context)[0]
fields = service.execute(cr.dbname, uid, 'ir.report.custom.fields', 'read', report['fields_child0'], context=context)
report = self.pool.get('ir.report.custom').read(cr, uid, [report_id], context=context)[0]
fields = self.pool.get('ir.report.custom.fields').read(cr, uid, report['fields_child0'], context=context)
fields.sort(lambda x,y : x['sequence'] - y['sequence'])
if report['field_parent']:
parent_field = service.execute(cr.dbname, uid, 'ir.model.fields', 'read', [report['field_parent'][0]],['model'])
model_name = service.execute(cr.dbname, uid, 'ir.model', 'read', [report['model_id'][0]], ['model'],context=context)[0]['model']
parent_field = self.pool.get('ir.model.fields').read(cr, uid, [report['field_parent'][0]], ['model'])
model_name = self.pool.get('ir.model').read(cr, uid, [report['model_id'][0]], ['model'], context=context)[0]['model']
fct = {}
fct['id'] = lambda x : x
@ -160,9 +159,7 @@ class report_custom(report_int):
field_child = f['field_child'+str(i)]
if field_child:
row.append(
service.execute(cr.dbname, uid,
'ir.model.fields', 'read', [field_child[0]],
['name'], context=context)[0]['name']
self.pool.get('ir.model.fields').read(cr, uid, [field_child[0]], ['name'], context=context)[0]['name']
)
if f['fc'+str(i)+'_operande']:
fct_name = 'id'
@ -346,7 +343,7 @@ class report_custom(report_int):
def _create_lines(self, cr, uid, ids, report, fields, results, context):
service = netsvc.LocalService("object_proxy")
pool = pooler.get_pool(cr.dbname)
pdf_string = cStringIO.StringIO()
can = canvas.init(fname=pdf_string, format='pdf')
@ -376,7 +373,7 @@ class report_custom(report_int):
for f in fields:
field_id = (f['field_child3'] and f['field_child3'][0]) or (f['field_child2'] and f['field_child2'][0]) or (f['field_child1'] and f['field_child1'][0]) or (f['field_child0'] and f['field_child0'][0])
if field_id:
type = service.execute(cr.dbname, uid, 'ir.model.fields', 'read', [field_id],['ttype'])
type = pool.get('ir.model.fields').read(cr, uid, [field_id],['ttype'])
if type[0]['ttype'] == 'date':
date_idx = idx
fct[idx] = process_date[report['frequency']]
@ -449,7 +446,7 @@ class report_custom(report_int):
def _create_bars(self, cr, uid, ids, report, fields, results, context):
service = netsvc.LocalService("object_proxy")
pool = pooler.get_pool(cr.dbname)
pdf_string = cStringIO.StringIO()
can = canvas.init(fname=pdf_string, format='pdf')
@ -475,7 +472,7 @@ class report_custom(report_int):
for f in fields:
field_id = (f['field_child3'] and f['field_child3'][0]) or (f['field_child2'] and f['field_child2'][0]) or (f['field_child1'] and f['field_child1'][0]) or (f['field_child0'] and f['field_child0'][0])
if field_id:
type = service.execute(cr.dbname, uid, 'ir.model.fields', 'read', [field_id],['ttype'])
type = pool.get('ir.model.fields').read(cr, uid, [field_id],['ttype'])
if type[0]['ttype'] == 'date':
date_idx = idx
fct[idx] = process_date[report['frequency']]

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