[MERGE] sync w/ latest trunk
bzr revid: odo@openerp.com-20110929100200-cq90mv3d3tymluyw
12
MANIFEST.in
|
@ -1,18 +1,12 @@
|
||||||
include rpminstall_sh.txt # TODO do we need this file ?
|
|
||||||
include README
|
include README
|
||||||
include LICENSE
|
include LICENSE
|
||||||
include MANIFEST.in
|
include MANIFEST.in
|
||||||
include setup.nsi
|
include setup.nsi
|
||||||
include setup.cfg
|
include setup.cfg
|
||||||
#include openerp/server.cert
|
include setup_rpm.sh
|
||||||
#include openerp/server.pkey
|
|
||||||
#include openerp/gpl.txt
|
|
||||||
include man/openerp-server.1
|
|
||||||
include man/openerp_serverrc.5
|
|
||||||
recursive-include pixmaps *bmp *ico *png
|
|
||||||
recursive-include win32 *.py *.bat
|
recursive-include win32 *.py *.bat
|
||||||
recursive-include openerp *css *csv *html *png *po *pot
|
recursive-include openerp *css *csv *html *png *po *pot *rml *rng *sql *sxw *xml *xsl *yml
|
||||||
recursive-include openerp *rml *rng *sql *sxw *xml *xsl *yml
|
graft install
|
||||||
graft debian
|
graft debian
|
||||||
graft doc
|
graft doc
|
||||||
global-exclude *pyc *~ # Exclude possible garbage from previous graft.
|
global-exclude *pyc *~ # Exclude possible garbage from previous graft.
|
||||||
|
|
155
README
|
@ -1,17 +1,138 @@
|
||||||
About OpenERP
|
About OpenERP
|
||||||
---------------
|
-------------
|
||||||
|
|
||||||
OpenERP is a free Enterprise Resource Planning and Customer Relationship
|
OpenERP is a free Enterprise Resource Planning and Customer Relationship
|
||||||
Management software. It is mainly developed to meet changing needs.
|
Management software. It is mainly developed to meet changing needs.
|
||||||
|
|
||||||
The main functional features are: CRM & SRM, analytic and financial accounting,
|
The main functional features are: CRM & SRM, analytic and financial accounting,
|
||||||
double-entry stock management, sales and purchases management, tasks automation,
|
double-entry stock management, sales and purchases management, tasks automation,
|
||||||
help desk, marketing campaign, ... and vertical modules for very specific
|
help desk, marketing campaign, ... and vertical modules for very specific
|
||||||
businesses.
|
businesses.
|
||||||
|
|
||||||
Technical features include a distributed server, flexible workflows, an object
|
Technical features include a distributed server, flexible workflows, an object
|
||||||
database, dynamic GUIs, customizable reports, NET-RPC and XML-RPC interfaces, ...
|
database, dynamic GUIs, customizable reports, NET-RPC and XML-RPC interfaces, ...
|
||||||
|
|
||||||
For more information, please visit:
|
For more information, please visit:
|
||||||
http://www.openerp.com
|
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
|
||||||
|
|
||||||
|
|
|
@ -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
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
@ -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
|
The whole source code is distributed under the terms of the
|
||||||
GNU Public Licence.
|
GNU Public Licence.
|
||||||
|
|
||||||
(c) 2003-TODAY, Fabien Pinckaers - OpenERP s.a.
|
(c) 2003-TODAY, Fabien Pinckaers - OpenERP SA
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
@ -88,19 +88,27 @@ def setup_pid_file():
|
||||||
|
|
||||||
def preload_registry(dbname):
|
def preload_registry(dbname):
|
||||||
""" Preload a registry, and start the cron."""
|
""" 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)
|
try:
|
||||||
pool.get('ir.cron').restart(db.dbname)
|
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):
|
def run_test_file(dbname, test_file):
|
||||||
""" Preload a registry, possibly run a test file, and start the cron."""
|
""" 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():
|
def export_translation():
|
||||||
config = openerp.tools.config
|
config = openerp.tools.config
|
||||||
|
@ -136,27 +144,6 @@ def import_translation():
|
||||||
cr.commit()
|
cr.commit()
|
||||||
cr.close()
|
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
|
# Variable keeping track of the number of calls to the signal handler defined
|
||||||
# below. This variable is monitored by ``quit_on_signals()``.
|
# below. This variable is monitored by ``quit_on_signals()``.
|
||||||
quit_signals_received = 0
|
quit_signals_received = 0
|
||||||
|
@ -208,30 +195,16 @@ def quit_on_signals():
|
||||||
while quit_signals_received == 0:
|
while quit_signals_received == 0:
|
||||||
time.sleep(60)
|
time.sleep(60)
|
||||||
|
|
||||||
openerp.netsvc.Agent.quit()
|
|
||||||
openerp.netsvc.Server.quitAll()
|
|
||||||
config = openerp.tools.config
|
|
||||||
if config['pidfile']:
|
if config['pidfile']:
|
||||||
os.unlink(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
|
openerp.service.stop_services()
|
||||||
# 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)
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
os.environ["TZ"] = "UTC"
|
||||||
|
|
||||||
check_root_user()
|
check_root_user()
|
||||||
openerp.tools.config.parse_config(sys.argv[1:])
|
openerp.tools.config.parse_config(sys.argv[1:])
|
||||||
check_postgres_user()
|
check_postgres_user()
|
||||||
|
@ -257,7 +230,7 @@ if __name__ == "__main__":
|
||||||
if not config["stop_after_init"]:
|
if not config["stop_after_init"]:
|
||||||
# Some module register themselves when they are loaded so we need the
|
# Some module register themselves when they are loaded so we need the
|
||||||
# services to be running before loading any registry.
|
# services to be running before loading any registry.
|
||||||
start_services()
|
openerp.service.start_services()
|
||||||
|
|
||||||
if config['db_name']:
|
if config['db_name']:
|
||||||
for dbname in config['db_name'].split(','):
|
for dbname in config['db_name'].split(','):
|
||||||
|
@ -266,6 +239,16 @@ if __name__ == "__main__":
|
||||||
if config["stop_after_init"]:
|
if config["stop_after_init"]:
|
||||||
sys.exit(0)
|
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()
|
setup_pid_file()
|
||||||
logger = logging.getLogger('server')
|
logger = logging.getLogger('server')
|
||||||
logger.info('OpenERP server is running, waiting for connections...')
|
logger.info('OpenERP server is running, waiting for connections...')
|
||||||
|
|
|
@ -22,6 +22,8 @@
|
||||||
""" OpenERP core library.
|
""" OpenERP core library.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# The hard-coded super-user id (a.k.a. administrator, or root user).
|
||||||
|
SUPERUSER_ID = 1
|
||||||
|
|
||||||
import addons
|
import addons
|
||||||
import conf
|
import conf
|
||||||
|
@ -41,6 +43,7 @@ import tiny_socket
|
||||||
import tools
|
import tools
|
||||||
import wizard
|
import wizard
|
||||||
import workflow
|
import workflow
|
||||||
|
import wsgi
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
||||||
|
|
|
@ -22,10 +22,15 @@
|
||||||
|
|
||||||
""" Addons module.
|
""" Addons module.
|
||||||
|
|
||||||
This module only serves to contain OpenERP addons. For the code to
|
This module serves to contain all OpenERP addons, across all configured addons
|
||||||
manage those addons, see openerp.modules. This module conveniently
|
paths. For the code to manage those addons, see openerp.modules.
|
||||||
reexports some symbols from openerp.modules. Importing them from here
|
|
||||||
is deprecated.
|
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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import module
|
||||||
import res
|
import res
|
||||||
import publisher_warranty
|
import publisher_warranty
|
||||||
import report
|
import report
|
||||||
|
import test
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,10 @@
|
||||||
'test/test_ir_rule.yml', # <-- These tests modify/add/delete ir_rules.
|
'test/test_ir_rule.yml', # <-- These tests modify/add/delete ir_rules.
|
||||||
'test/test_ir_values.yml',
|
'test/test_ir_values.yml',
|
||||||
'test/test_edi_documents.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,
|
'installable': True,
|
||||||
'active': True,
|
'active': True,
|
||||||
|
|
|
@ -1006,7 +1006,7 @@
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="main_partner" model="res.partner">
|
<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 -->
|
<!-- Address and Company ID will be set later -->
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
<field name="company_id" eval="None"/>
|
<field name="company_id" eval="None"/>
|
||||||
|
@ -1014,13 +1014,13 @@
|
||||||
</record>
|
</record>
|
||||||
<record id="main_address" model="res.partner.address">
|
<record id="main_address" model="res.partner.address">
|
||||||
<field name="partner_id" ref="main_partner"/>
|
<field name="partner_id" ref="main_partner"/>
|
||||||
<field name="name">Fabien Pinckaers</field>
|
<field name="name">Company contact name</field>
|
||||||
<field name="street">Chaussee de Namur 40</field>
|
<field name="street">Company street, number</field>
|
||||||
<field name="zip">1367</field>
|
<field name="zip">Company zip</field>
|
||||||
<field name="city">Gerompont</field>
|
<field name="city">Company city</field>
|
||||||
<field name="phone">(+32).81.81.37.00</field>
|
<field name="phone">+1-212-555-12345</field>
|
||||||
<field name="type">default</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 -->
|
<!-- Company ID will be set later -->
|
||||||
<field name="company_id" eval="None"/>
|
<field name="company_id" eval="None"/>
|
||||||
</record>
|
</record>
|
||||||
|
@ -1042,19 +1042,14 @@
|
||||||
|
|
||||||
<!-- Basic Company -->
|
<!-- Basic Company -->
|
||||||
<record id="main_company" model="res.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="partner_id" ref="main_partner"/>
|
||||||
<field name="rml_header1">Free Business Solutions</field>
|
<field name="rml_header1">Company business slogan</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_footer1">Web: www.companyname.com - Tel: +1-212-555-12345</field>
|
||||||
<field name="rml_footer2">IBAN: BE74 1262 0132 6907 - SWIFT: CPHBBE75 - VAT: BE0477.472.701</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"/>
|
<field name="currency_id" ref="base.EUR"/>
|
||||||
</record>
|
</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">
|
<record model="res.users" id="base.user_root">
|
||||||
<field name="signature">Administrator</field>
|
<field name="signature">Administrator</field>
|
||||||
<field name="company_id" ref="main_company"/>
|
<field name="company_id" ref="main_company"/>
|
||||||
|
@ -1090,16 +1085,17 @@
|
||||||
<field eval="time.strftime('%Y-01-01')" name="name"/>
|
<field eval="time.strftime('%Y-01-01')" name="name"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="VEB" model="res.currency">
|
<!-- VEF was previously VEB -->
|
||||||
<field name="name">VEB</field>
|
<record id="VEF" model="res.currency">
|
||||||
<field name="symbol">Bs</field>
|
<field name="name">VEF</field>
|
||||||
<field name="rounding">2.95</field>
|
<field name="symbol">Bs.F</field>
|
||||||
|
<field name="rounding">0.0001</field>
|
||||||
<field name="accuracy">4</field>
|
<field name="accuracy">4</field>
|
||||||
<field name="company_id" ref="main_company"/>
|
<field name="company_id" ref="main_company"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="rateVEB" model="res.currency.rate">
|
<record id="rateVEF" model="res.currency.rate">
|
||||||
<field name="rate">2768.45</field>
|
<field name="rate">5.864</field>
|
||||||
<field name="currency_id" ref="VEB"/>
|
<field name="currency_id" ref="VEF"/>
|
||||||
<field eval="time.strftime('%Y-01-01')" name="name"/>
|
<field eval="time.strftime('%Y-01-01')" name="name"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
@ -1603,6 +1599,7 @@
|
||||||
<field name="rounding">0.01</field>
|
<field name="rounding">0.01</field>
|
||||||
<field name="accuracy">4</field>
|
<field name="accuracy">4</field>
|
||||||
<field name="symbol">¢</field>
|
<field name="symbol">¢</field>
|
||||||
|
<field name="company_id" ref="main_company"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="rateCRC" model="res.currency.rate">
|
<record id="rateCRC" model="res.currency.rate">
|
||||||
<field name="rate">691.3153</field>
|
<field name="rate">691.3153</field>
|
||||||
|
|
|
@ -75,29 +75,29 @@
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<record id="view_users_form_simple_modif" model="ir.ui.view">
|
<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="model">res.users</field>
|
||||||
<field name="type">form</field>
|
<field name="type">form</field>
|
||||||
<field eval="18" name="priority"/>
|
<field eval="18" name="priority"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="Users">
|
<form string="Users">
|
||||||
<field name="name"/>
|
<field name="name" readonly="1"/>
|
||||||
<newline/>
|
<newline/>
|
||||||
<group colspan="2" col="2">
|
<group colspan="2" col="2">
|
||||||
<separator string="Preferences" colspan="2"/>
|
<separator string="Preferences" colspan="2"/>
|
||||||
<field name="view"/>
|
<field name="view" readonly="0"/>
|
||||||
<field name="context_lang"/>
|
<field name="context_lang" readonly="0"/>
|
||||||
<field name="context_tz"/>
|
<field name="context_tz" readonly="0"/>
|
||||||
<field name="menu_tips"/>
|
<field name="menu_tips" readonly="0"/>
|
||||||
</group>
|
</group>
|
||||||
<group name="default_filters" colspan="2" col="2">
|
<group name="default_filters" colspan="2" col="2">
|
||||||
<separator string="Default Filters" colspan="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)"/>
|
groups="base.group_multi_company" on_change="on_change_company_id(company_id)"/>
|
||||||
</group>
|
</group>
|
||||||
<separator string="Email Preferences" colspan="4"/>
|
<separator string="Email Preferences" colspan="4"/>
|
||||||
<field colspan="4" name="user_email" widget="email"/>
|
<field colspan="4" name="user_email" widget="email" readonly="0"/>
|
||||||
<field colspan="4" name="signature"/>
|
<field colspan="4" name="signature" readonly="0"/>
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
@ -147,7 +147,7 @@
|
||||||
<page string="Access Rights">
|
<page string="Access Rights">
|
||||||
<field nolabel="1" name="groups_id"/>
|
<field nolabel="1" name="groups_id"/>
|
||||||
</page>
|
</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"/>
|
<field colspan="4" nolabel="1" name="company_ids" select="1"/>
|
||||||
</page>
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
|
|
|
@ -7,14 +7,14 @@ msgstr ""
|
||||||
"Project-Id-Version: openobject-server\n"
|
"Project-Id-Version: openobject-server\n"
|
||||||
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
||||||
"POT-Creation-Date: 2011-01-11 11:14+0000\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"
|
"Last-Translator: Jiří Hajda <robie@centrum.cz>\n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"X-Launchpad-Export-Date: 2011-09-14 04:40+0000\n"
|
"X-Launchpad-Export-Date: 2011-09-17 04:54+0000\n"
|
||||||
"X-Generator: Launchpad (build 13921)\n"
|
"X-Generator: Launchpad (build 13955)\n"
|
||||||
"X-Poedit-Language: Czech\n"
|
"X-Poedit-Language: Czech\n"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
|
@ -3812,7 +3812,7 @@ msgstr ""
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:publisher_warranty.contract.wizard:0
|
#: view:publisher_warranty.contract.wizard:0
|
||||||
msgid "Please enter the serial key provided in your contract document:"
|
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
|
#. module: base
|
||||||
#: view:workflow.activity:0
|
#: view:workflow.activity:0
|
||||||
|
|
|
@ -8,14 +8,14 @@ msgstr ""
|
||||||
"Project-Id-Version: openobject-server\n"
|
"Project-Id-Version: openobject-server\n"
|
||||||
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"POT-Creation-Date: 2011-01-11 11:14+0000\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"
|
"Last-Translator: John Bradshaw <Unknown>\n"
|
||||||
"Language-Team: English (United Kingdom) <en_GB@li.org>\n"
|
"Language-Team: English (United Kingdom) <en_GB@li.org>\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"X-Launchpad-Export-Date: 2011-09-01 04:44+0000\n"
|
"X-Launchpad-Export-Date: 2011-09-23 04:38+0000\n"
|
||||||
"X-Generator: Launchpad (build 13827)\n"
|
"X-Generator: Launchpad (build 14012)\n"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.filters:0
|
#: view:ir.filters:0
|
||||||
|
@ -1972,7 +1972,7 @@ msgstr "Iteration Actions"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:multi_company.default,company_id:0
|
#: help:multi_company.default,company_id:0
|
||||||
msgid "Company where the user is connected"
|
msgid "Company where the user is connected"
|
||||||
msgstr ""
|
msgstr "Company where the user is connected"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:publisher_warranty.contract,date_stop:0
|
#: 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
|
#: model:ir.actions.act_window,name:base.action_view_partner_wizard_ean_check
|
||||||
#: view:partner.wizard.ean.check:0
|
#: view:partner.wizard.ean.check:0
|
||||||
msgid "Ean check"
|
msgid "Ean check"
|
||||||
msgstr ""
|
msgstr "Ean check"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:res.partner,vat:0
|
#: field:res.partner,vat:0
|
||||||
|
@ -2623,7 +2623,7 @@ msgstr "GPL-2 or later version"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.partner.title,shortcut:base.res_partner_title_sir
|
#: model:res.partner.title,shortcut:base.res_partner_title_sir
|
||||||
msgid "M."
|
msgid "M."
|
||||||
msgstr ""
|
msgstr "M."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/base/module/module.py:429
|
#: code:addons/base/module/module.py:429
|
||||||
|
@ -3001,7 +3001,7 @@ msgstr "License"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.attachment,url:0
|
#: field:ir.attachment,url:0
|
||||||
msgid "Url"
|
msgid "Url"
|
||||||
msgstr ""
|
msgstr "Url"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:ir.actions.todo,restart:0
|
#: selection:ir.actions.todo,restart:0
|
||||||
|
@ -3153,7 +3153,7 @@ msgstr "Workflows"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.translation,xml_id:0
|
#: field:ir.translation,xml_id:0
|
||||||
msgid "XML Id"
|
msgid "XML Id"
|
||||||
msgstr ""
|
msgstr "XML Id"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.actions.act_window,name:base.action_config_user_form
|
#: model:ir.actions.act_window,name:base.action_config_user_form
|
||||||
|
@ -3224,7 +3224,7 @@ msgstr "Abkhazian / аҧсуа"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:base.module.configuration:0
|
#: view:base.module.configuration:0
|
||||||
msgid "System Configuration Done"
|
msgid "System Configuration Done"
|
||||||
msgstr ""
|
msgstr "System Configuration Done"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/orm.py:929
|
#: code:addons/orm.py:929
|
||||||
|
@ -3271,7 +3271,7 @@ msgstr "That contract is already registered in the system."
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:ir.sequence,suffix:0
|
#: help:ir.sequence,suffix:0
|
||||||
msgid "Suffix value of the record for the sequence"
|
msgid "Suffix value of the record for the sequence"
|
||||||
msgstr ""
|
msgstr "Suffix value of the record for the sequence"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
|
@ -3343,7 +3343,7 @@ msgstr "Installed"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
msgid "Ukrainian / українська"
|
msgid "Ukrainian / українська"
|
||||||
msgstr ""
|
msgstr "Ukrainian / українська"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.actions.act_window,name:base.action_translation
|
#: model:ir.actions.act_window,name:base.action_translation
|
||||||
|
@ -3389,7 +3389,7 @@ msgstr "Next Number"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:workflow.transition,condition:0
|
#: help:workflow.transition,condition:0
|
||||||
msgid "Expression to be satisfied if we want the transition done."
|
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
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
|
@ -3518,6 +3518,12 @@ msgid ""
|
||||||
"Would your payment have been carried out after this mail was sent, please "
|
"Would your payment have been carried out after this mail was sent, please "
|
||||||
"consider the present one as void."
|
"consider the present one as void."
|
||||||
msgstr ""
|
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
|
#. module: base
|
||||||
#: model:res.country,name:base.mx
|
#: 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 "
|
"If set to true, the action will not be displayed on the right toolbar of a "
|
||||||
"form view"
|
"form view"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"If set to true, the action will not be displayed on the right toolbar of a "
|
||||||
|
"form view"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.ms
|
#: model:res.country,name:base.ms
|
||||||
|
@ -3717,7 +3725,7 @@ msgstr "English (UK)"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
msgid "Japanese / 日本語"
|
msgid "Japanese / 日本語"
|
||||||
msgstr ""
|
msgstr "Japanese / 日本語"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:workflow.transition,act_from:0
|
#: help:workflow.transition,act_from:0
|
||||||
|
@ -3725,6 +3733,8 @@ msgid ""
|
||||||
"Source activity. When this activity is over, the condition is tested to "
|
"Source activity. When this activity is over, the condition is tested to "
|
||||||
"determine if we can start the ACT_TO activity."
|
"determine if we can start the ACT_TO activity."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Source activity. When this activity is over, the condition is tested to "
|
||||||
|
"determine if we can start the ACT_TO activity."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.partner.category,name:base.res_partner_category_3
|
#: model:res.partner.category,name:base.res_partner_category_3
|
||||||
|
@ -3885,7 +3895,7 @@ msgstr "Init Date"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
msgid "Gujarati / ગુજરાતી"
|
msgid "Gujarati / ગુજરાતી"
|
||||||
msgstr ""
|
msgstr "Gujarati / ગુજરાતી"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/base/module/module.py:257
|
#: 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 "
|
"form, signal tests the name of the pressed button. If signal is NULL, no "
|
||||||
"button is necessary to validate this transition."
|
"button is necessary to validate this transition."
|
||||||
msgstr ""
|
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
|
#. module: base
|
||||||
#: help:multi_company.default,object_id:0
|
#: help:multi_company.default,object_id:0
|
||||||
|
@ -3972,7 +3985,7 @@ msgstr "Menu Name"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.module.module:0
|
#: view:ir.module.module:0
|
||||||
msgid "Author Website"
|
msgid "Author Website"
|
||||||
msgstr ""
|
msgstr "Author Website"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.attachment:0
|
#: view:ir.attachment:0
|
||||||
|
@ -4012,6 +4025,8 @@ msgid ""
|
||||||
"Whether values for this field can be translated (enables the translation "
|
"Whether values for this field can be translated (enables the translation "
|
||||||
"mechanism for that field)"
|
"mechanism for that field)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Whether values for this field can be translated (enables the translation "
|
||||||
|
"mechanism for that field)"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.lang:0
|
#: view:res.lang:0
|
||||||
|
@ -4072,13 +4087,13 @@ msgstr "Price Accuracy"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
msgid "Latvian / latviešu valoda"
|
msgid "Latvian / latviešu valoda"
|
||||||
msgstr ""
|
msgstr "Latvian / latviešu valoda"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.config:0
|
#: view:res.config:0
|
||||||
#: view:res.config.installer:0
|
#: view:res.config.installer:0
|
||||||
msgid "vsep"
|
msgid "vsep"
|
||||||
msgstr ""
|
msgstr "vsep"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
|
@ -4099,7 +4114,7 @@ msgstr "Workitem"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.actions.todo:0
|
#: view:ir.actions.todo:0
|
||||||
msgid "Set as Todo"
|
msgid "Set as Todo"
|
||||||
msgstr ""
|
msgstr "Set as Todo"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.actions.act_window.view,act_window_id:0
|
#: field:ir.actions.act_window.view,act_window_id:0
|
||||||
|
@ -4177,7 +4192,7 @@ msgstr "Menus"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
msgid "Serbian (Latin) / srpski"
|
msgid "Serbian (Latin) / srpski"
|
||||||
msgstr ""
|
msgstr "Serbian (Latin) / srpski"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.il
|
#: model:res.country,name:base.il
|
||||||
|
@ -4353,7 +4368,7 @@ msgstr ""
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:base.language.import:0
|
#: view:base.language.import:0
|
||||||
msgid "- module,type,name,res_id,src,value"
|
msgid "- module,type,name,res_id,src,value"
|
||||||
msgstr ""
|
msgstr "- module,type,name,res_id,src,value"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
|
@ -4372,7 +4387,7 @@ msgstr ""
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:ir.model.fields,relation:0
|
#: help:ir.model.fields,relation:0
|
||||||
msgid "For relationship fields, the technical name of the target model"
|
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
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
|
@ -4387,7 +4402,7 @@ msgstr "Inherited View"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.translation:0
|
#: view:ir.translation:0
|
||||||
msgid "Source Term"
|
msgid "Source Term"
|
||||||
msgstr ""
|
msgstr "Source Term"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.ui.menu,name:base.menu_main_pm
|
#: model:ir.ui.menu,name:base.menu_main_pm
|
||||||
|
@ -4397,7 +4412,7 @@ msgstr "Project"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.ui.menu,web_icon_hover_data:0
|
#: field:ir.ui.menu,web_icon_hover_data:0
|
||||||
msgid "Web Icon Image (hover)"
|
msgid "Web Icon Image (hover)"
|
||||||
msgstr ""
|
msgstr "Web Icon Image (hover)"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:base.module.import:0
|
#: view:base.module.import:0
|
||||||
|
@ -4417,7 +4432,7 @@ msgstr "Create User"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:partner.clear.ids:0
|
#: view:partner.clear.ids:0
|
||||||
msgid "Want to Clear Ids ? "
|
msgid "Want to Clear Ids ? "
|
||||||
msgstr ""
|
msgstr "Want to Clear Ids ? "
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:publisher_warranty.contract,name:0
|
#: field:publisher_warranty.contract,name:0
|
||||||
|
@ -4469,17 +4484,17 @@ msgstr "Fed. State"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.actions.server,copy_object:0
|
#: field:ir.actions.server,copy_object:0
|
||||||
msgid "Copy Of"
|
msgid "Copy Of"
|
||||||
msgstr ""
|
msgstr "Copy Of"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.model,osv_memory:0
|
#: field:ir.model,osv_memory:0
|
||||||
msgid "In-memory model"
|
msgid "In-memory model"
|
||||||
msgstr ""
|
msgstr "In-memory model"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:partner.clear.ids:0
|
#: view:partner.clear.ids:0
|
||||||
msgid "Clear Ids"
|
msgid "Clear Ids"
|
||||||
msgstr ""
|
msgstr "Clear Ids"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.io
|
#: model:res.country,name:base.io
|
||||||
|
@ -4501,7 +4516,7 @@ msgstr "Field Mapping"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:publisher_warranty.contract:0
|
#: view:publisher_warranty.contract:0
|
||||||
msgid "Refresh Validation Dates"
|
msgid "Refresh Validation Dates"
|
||||||
msgstr ""
|
msgstr "Refresh Validation Dates"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.model:0
|
#: view:ir.model:0
|
||||||
|
@ -4572,7 +4587,7 @@ msgstr "_Ok"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:ir.filters,user_id:0
|
#: help:ir.filters,user_id:0
|
||||||
msgid "False means for every user"
|
msgid "False means for every user"
|
||||||
msgstr ""
|
msgstr "False means for every user"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/base/module/module.py:198
|
#: code:addons/base/module/module.py:198
|
||||||
|
@ -4621,6 +4636,7 @@ msgstr "Contacts"
|
||||||
msgid ""
|
msgid ""
|
||||||
"Unable to delete this document because it is used as a default property"
|
"Unable to delete this document because it is used as a default property"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Unable to delete this document because it is used as a default property"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.widget.wizard:0
|
#: view:res.widget.wizard:0
|
||||||
|
@ -4674,7 +4690,7 @@ msgstr ""
|
||||||
#: code:addons/orm.py:1350
|
#: code:addons/orm.py:1350
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Insufficient fields for Calendar View!"
|
msgid "Insufficient fields for Calendar View!"
|
||||||
msgstr ""
|
msgstr "Insufficient fields for Calendar View!"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:ir.property,type:0
|
#: 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 "
|
"The path to the main report file (depending on Report Type) or NULL if the "
|
||||||
"content is in another data field"
|
"content is in another data field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"The path to the main report file (depending on Report Type) or NULL if the "
|
||||||
|
"content is in another data field"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:res.config.users,company_id:0
|
#: help:res.config.users,company_id:0
|
||||||
|
@ -4748,7 +4766,7 @@ msgstr "Close"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
msgid "Spanish (MX) / Español (MX)"
|
msgid "Spanish (MX) / Español (MX)"
|
||||||
msgstr ""
|
msgstr "Spanish (MX) / Español (MX)"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.log:0
|
#: view:res.log:0
|
||||||
|
@ -4783,7 +4801,7 @@ msgstr "Publisher Warranty Contracts"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:res.log,name:0
|
#: help:res.log,name:0
|
||||||
msgid "The logging message."
|
msgid "The logging message."
|
||||||
msgstr ""
|
msgstr "The logging message."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:base.language.export,format:0
|
#: field:base.language.export,format:0
|
||||||
|
@ -5018,7 +5036,7 @@ msgstr ""
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:ir.cron,interval_number:0
|
#: help:ir.cron,interval_number:0
|
||||||
msgid "Repeat every x."
|
msgid "Repeat every x."
|
||||||
msgstr ""
|
msgstr "Repeat every x."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: wizard_view:server.action.create,step_1:0
|
#: 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 "
|
"If specified, this action will be opened at logon for this user, in addition "
|
||||||
"to the standard menu."
|
"to the standard menu."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"If specified, this action will be opened at logon for this user, in addition "
|
||||||
|
"to the standard menu."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.values:0
|
#: view:ir.values:0
|
||||||
|
@ -5088,7 +5108,7 @@ msgstr "Client Actions"
|
||||||
#: code:addons/orm.py:1806
|
#: code:addons/orm.py:1806
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "The exists method is not implemented on this object !"
|
msgid "The exists method is not implemented on this object !"
|
||||||
msgstr ""
|
msgstr "The exists method is not implemented on this object !"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/base/module/module.py:336
|
#: code:addons/base/module/module.py:336
|
||||||
|
@ -5113,7 +5133,7 @@ msgstr "Connect Events to Actions"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.model,name:base.model_base_update_translations
|
#: model:ir.model,name:base.model_base_update_translations
|
||||||
msgid "base.update.translations"
|
msgid "base.update.translations"
|
||||||
msgstr ""
|
msgstr "base.update.translations"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.module.category,parent_id:0
|
#: field:ir.module.category,parent_id:0
|
||||||
|
@ -5124,7 +5144,7 @@ msgstr "Parent Category"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:ir.property,type:0
|
#: selection:ir.property,type:0
|
||||||
msgid "Integer Big"
|
msgid "Integer Big"
|
||||||
msgstr ""
|
msgstr "Integer Big"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:res.partner.address,type:0
|
#: selection:res.partner.address,type:0
|
||||||
|
@ -5158,7 +5178,7 @@ msgstr "Communication"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.actions.report.xml:0
|
#: view:ir.actions.report.xml:0
|
||||||
msgid "RML Report"
|
msgid "RML Report"
|
||||||
msgstr ""
|
msgstr "RML Report"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.model,name:base.model_ir_server_object_lines
|
#: model:ir.model,name:base.model_ir_server_object_lines
|
||||||
|
@ -5206,7 +5226,7 @@ msgstr "Nigeria"
|
||||||
#: code:addons/base/ir/ir_model.py:250
|
#: code:addons/base/ir/ir_model.py:250
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "For selection fields, the Selection Options must be given!"
|
msgid "For selection fields, the Selection Options must be given!"
|
||||||
msgstr ""
|
msgstr "For selection fields, the Selection Options must be given!"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.actions.act_window,name:base.action_partner_sms_send
|
#: 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 "
|
"installed the CRM, with the history tab, you can track all the interactions "
|
||||||
"with a partner such as opportunities, emails, or sales orders issued."
|
"with a partner such as opportunities, emails, or sales orders issued."
|
||||||
msgstr ""
|
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
|
#. module: base
|
||||||
#: model:res.country,name:base.ph
|
#: model:res.country,name:base.ph
|
||||||
|
@ -5278,7 +5305,7 @@ msgstr "Content"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:ir.rule,global:0
|
#: help:ir.rule,global:0
|
||||||
msgid "If no group is specified the rule is global and applied to everyone"
|
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
|
#. module: base
|
||||||
#: model:res.country,name:base.td
|
#: model:res.country,name:base.td
|
||||||
|
@ -5355,6 +5382,9 @@ msgid ""
|
||||||
"groups. If this field is empty, OpenERP will compute visibility based on the "
|
"groups. If this field is empty, OpenERP will compute visibility based on the "
|
||||||
"related object's read access."
|
"related object's read access."
|
||||||
msgstr ""
|
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
|
#. module: base
|
||||||
#: model:ir.actions.act_window,name:base.action_ui_view_custom
|
#: model:ir.actions.act_window,name:base.action_ui_view_custom
|
||||||
|
@ -5496,7 +5526,7 @@ msgstr "Spanish (EC) / Español (EC)"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:ir.ui.view,xml_id:0
|
#: help:ir.ui.view,xml_id:0
|
||||||
msgid "ID of the view defined in xml file"
|
msgid "ID of the view defined in xml file"
|
||||||
msgstr ""
|
msgstr "ID of the view defined in xml file"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.model,name:base.model_base_module_import
|
#: model:ir.model,name:base.model_base_module_import
|
||||||
|
@ -5512,7 +5542,7 @@ msgstr "American Samoa"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:ir.actions.act_window,res_model:0
|
#: help:ir.actions.act_window,res_model:0
|
||||||
msgid "Model name of the object to open in the view window"
|
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
|
#. module: base
|
||||||
#: field:res.log,secondary:0
|
#: field:res.log,secondary:0
|
||||||
|
@ -5692,11 +5722,15 @@ msgid ""
|
||||||
"Warning: if \"email_from\" and \"smtp_server\" aren't configured, it won't "
|
"Warning: if \"email_from\" and \"smtp_server\" aren't configured, it won't "
|
||||||
"be possible to email new users."
|
"be possible to email new users."
|
||||||
msgstr ""
|
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
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
msgid "Flemish (BE) / Vlaams (BE)"
|
msgid "Flemish (BE) / Vlaams (BE)"
|
||||||
msgstr ""
|
msgstr "Flemish (BE) / Vlaams (BE)"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.cron,interval_number:0
|
#: field:ir.cron,interval_number:0
|
||||||
|
@ -5746,7 +5780,7 @@ msgstr "ir.actions.todo"
|
||||||
#: code:addons/base/res/res_config.py:94
|
#: code:addons/base/res/res_config.py:94
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Couldn't find previous ir.actions.todo"
|
msgid "Couldn't find previous ir.actions.todo"
|
||||||
msgstr ""
|
msgstr "Couldn't find previous ir.actions.todo"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.actions.act_window:0
|
#: view:ir.actions.act_window:0
|
||||||
|
@ -5761,7 +5795,7 @@ msgstr "Custom Shortcuts"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
msgid "Vietnamese / Tiếng Việt"
|
msgid "Vietnamese / Tiếng Việt"
|
||||||
msgstr ""
|
msgstr "Vietnamese / Tiếng Việt"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.dz
|
#: model:res.country,name:base.dz
|
||||||
|
@ -5776,7 +5810,7 @@ msgstr "Belgium"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.model,name:base.model_osv_memory_autovacuum
|
#: model:ir.model,name:base.model_osv_memory_autovacuum
|
||||||
msgid "osv_memory.autovacuum"
|
msgid "osv_memory.autovacuum"
|
||||||
msgstr ""
|
msgstr "osv_memory.autovacuum"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:base.language.export,lang:0
|
#: field:base.language.export,lang:0
|
||||||
|
@ -5809,30 +5843,30 @@ msgstr "Companies"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.lang:0
|
#: view:res.lang:0
|
||||||
msgid "%H - Hour (24-hour clock) [00,23]."
|
msgid "%H - Hour (24-hour clock) [00,23]."
|
||||||
msgstr ""
|
msgstr "%H - Hour (24-hour clock) [00,23]."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.model,name:base.model_res_widget
|
#: model:ir.model,name:base.model_res_widget
|
||||||
msgid "res.widget"
|
msgid "res.widget"
|
||||||
msgstr ""
|
msgstr "res.widget"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/base/ir/ir_model.py:258
|
#: code:addons/base/ir/ir_model.py:258
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Model %s does not exist!"
|
msgid "Model %s does not exist!"
|
||||||
msgstr ""
|
msgstr "Model %s does not exist!"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/base/res/res_lang.py:159
|
#: code:addons/base/res/res_lang.py:159
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "You cannot delete the language which is User's Preferred Language !"
|
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
|
#. module: base
|
||||||
#: code:addons/fields.py:103
|
#: code:addons/fields.py:103
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Not implemented get_memory method !"
|
msgid "Not implemented get_memory method !"
|
||||||
msgstr ""
|
msgstr "Not implemented get_memory method !"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.actions.server:0
|
#: view:ir.actions.server:0
|
||||||
|
@ -5879,7 +5913,7 @@ msgstr "Neutral Zone"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
msgid "Hindi / हिंदी"
|
msgid "Hindi / हिंदी"
|
||||||
msgstr ""
|
msgstr "Hindi / हिंदी"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.model:0
|
#: view:ir.model:0
|
||||||
|
@ -5926,7 +5960,7 @@ msgstr "Window Actions"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.lang:0
|
#: view:res.lang:0
|
||||||
msgid "%I - Hour (12-hour clock) [01,12]."
|
msgid "%I - Hour (12-hour clock) [01,12]."
|
||||||
msgstr ""
|
msgstr "%I - Hour (12-hour clock) [01,12]."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:publisher_warranty.contract.wizard,state:0
|
#: 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 "
|
"View type: set to 'tree' for a hierarchical tree view, or 'form' for other "
|
||||||
"views"
|
"views"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"View type: set to 'tree' for a hierarchical tree view, or 'form' for other "
|
||||||
|
"views"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/base/res/res_config.py:421
|
#: code:addons/base/res/res_config.py:421
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Click 'Continue' to configure the next addon..."
|
msgid "Click 'Continue' to configure the next addon..."
|
||||||
msgstr ""
|
msgstr "Click 'Continue' to configure the next addon..."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.actions.server,record_id:0
|
#: field:ir.actions.server,record_id:0
|
||||||
|
@ -6010,7 +6046,7 @@ msgstr ""
|
||||||
#: code:addons/base/ir/ir_actions.py:629
|
#: code:addons/base/ir/ir_actions.py:629
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Please specify server option --email-from !"
|
msgid "Please specify server option --email-from !"
|
||||||
msgstr ""
|
msgstr "Please specify server option --email-from !"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:base.language.import,name:0
|
#: 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 "
|
"It gives the status if the tip has to be displayed or not when a user "
|
||||||
"executes an action"
|
"executes an action"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"It shows if the tip is to be displayed or not when a user executes an action"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.model:0
|
#: view:ir.model:0
|
||||||
|
@ -6126,7 +6163,7 @@ msgstr "Code"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.model,name:base.model_res_config_installer
|
#: model:ir.model,name:base.model_res_config_installer
|
||||||
msgid "res.config.installer"
|
msgid "res.config.installer"
|
||||||
msgstr ""
|
msgstr "res.config.installer"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.mc
|
#: model:res.country,name:base.mc
|
||||||
|
@ -6170,7 +6207,7 @@ msgstr "Sequence Codes"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
msgid "Spanish (CO) / Español (CO)"
|
msgid "Spanish (CO) / Español (CO)"
|
||||||
msgstr ""
|
msgstr "Spanish (CO) / Español (CO)"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:base.module.configuration:0
|
#: view:base.module.configuration:0
|
||||||
|
@ -6178,6 +6215,8 @@ msgid ""
|
||||||
"All pending configuration wizards have been executed. You may restart "
|
"All pending configuration wizards have been executed. You may restart "
|
||||||
"individual wizards via the list of configuration wizards."
|
"individual wizards via the list of configuration wizards."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"All pending configuration wizards have been executed. You may restart "
|
||||||
|
"individual wizards via the list of configuration wizards."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: wizard_button:server.action.create,step_1,create:0
|
#: wizard_button:server.action.create,step_1,create:0
|
||||||
|
@ -6187,7 +6226,7 @@ msgstr "Create"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.sequence:0
|
#: view:ir.sequence:0
|
||||||
msgid "Current Year with Century: %(year)s"
|
msgid "Current Year with Century: %(year)s"
|
||||||
msgstr ""
|
msgstr "Current Year with Century: %(year)s"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.exports,export_fields:0
|
#: field:ir.exports,export_fields:0
|
||||||
|
@ -6202,13 +6241,13 @@ msgstr "France"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.model,name:base.model_res_log
|
#: model:ir.model,name:base.model_res_log
|
||||||
msgid "res.log"
|
msgid "res.log"
|
||||||
msgstr ""
|
msgstr "res.log"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:ir.translation,module:0
|
#: help:ir.translation,module:0
|
||||||
#: help:ir.translation,xml_id:0
|
#: help:ir.translation,xml_id:0
|
||||||
msgid "Maps to the ir_model_data for which this translation is provided."
|
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
|
#. module: base
|
||||||
#: view:workflow.activity:0
|
#: view:workflow.activity:0
|
||||||
|
@ -6302,7 +6341,7 @@ msgstr "Todo"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.attachment,datas:0
|
#: field:ir.attachment,datas:0
|
||||||
msgid "File Content"
|
msgid "File Content"
|
||||||
msgstr ""
|
msgstr "File Content"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.pa
|
#: model:res.country,name:base.pa
|
||||||
|
@ -6319,12 +6358,13 @@ msgstr "Ltd"
|
||||||
msgid ""
|
msgid ""
|
||||||
"The group that a user must have to be authorized to validate this transition."
|
"The group that a user must have to be authorized to validate this transition."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"The group that a user must have to be authorized to validate this transition."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: constraint:res.config.users:0
|
#: constraint:res.config.users:0
|
||||||
#: constraint:res.users:0
|
#: constraint:res.users:0
|
||||||
msgid "The chosen company is not in the allowed companies for this user"
|
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
|
#. module: base
|
||||||
#: model:res.country,name:base.gi
|
#: model:res.country,name:base.gi
|
||||||
|
@ -6346,6 +6386,7 @@ msgstr "Pitcairn Island"
|
||||||
msgid ""
|
msgid ""
|
||||||
"We suggest to reload the menu tab to see the new menus (Ctrl+T then Ctrl+R)."
|
"We suggest to reload the menu tab to see the new menus (Ctrl+T then Ctrl+R)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"We suggest reloading the menu tab to see the new menus (Ctrl+T then Ctrl+R)."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.actions.act_window,name:base.action_rule
|
#: model:ir.actions.act_window,name:base.action_rule
|
||||||
|
@ -6398,7 +6439,7 @@ msgstr "Search View"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: sql_constraint:res.lang:0
|
#: sql_constraint:res.lang:0
|
||||||
msgid "The code of the language must be unique !"
|
msgid "The code of the language must be unique !"
|
||||||
msgstr ""
|
msgstr "The code of the language must be unique !"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.actions.act_window,name:base.action_attachment
|
#: model:ir.actions.act_window,name:base.action_attachment
|
||||||
|
@ -6441,7 +6482,7 @@ msgstr "Write Access"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.lang:0
|
#: view:res.lang:0
|
||||||
msgid "%m - Month number [01,12]."
|
msgid "%m - Month number [01,12]."
|
||||||
msgstr ""
|
msgstr "%m - Month number [01,12]."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:res.bank,city:0
|
#: field:res.bank,city:0
|
||||||
|
@ -6499,7 +6540,7 @@ msgstr "English (US)"
|
||||||
#: view:ir.model.data:0
|
#: view:ir.model.data:0
|
||||||
#: model:ir.ui.menu,name:base.ir_model_data_menu
|
#: model:ir.ui.menu,name:base.ir_model_data_menu
|
||||||
msgid "Object Identifiers"
|
msgid "Object Identifiers"
|
||||||
msgstr ""
|
msgstr "Object Identifiers"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.actions.act_window,help:base.action_partner_title_partner
|
#: 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 "
|
"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."
|
"partner titles is the legal status of the company: Private Limited, SA, etc."
|
||||||
msgstr ""
|
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
|
#. module: base
|
||||||
#: view:base.language.export:0
|
#: view:base.language.export:0
|
||||||
msgid "To browse official translations, you can start with these links:"
|
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
|
#. module: base
|
||||||
#: code:addons/base/ir/ir_model.py:484
|
#: 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 "
|
"You can not read this document (%s) ! Be sure your user belongs to one of "
|
||||||
"these groups: %s."
|
"these groups: %s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"You can not read this document (%s) ! Be sure your user belongs to one of "
|
||||||
|
"these groups: %s."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.bank:0
|
#: view:res.bank:0
|
||||||
|
@ -6538,7 +6583,7 @@ msgstr "Installed version"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
msgid "Mongolian / монгол"
|
msgid "Mongolian / монгол"
|
||||||
msgstr ""
|
msgstr "Mongolian / монгол"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.mr
|
#: model:res.country,name:base.mr
|
||||||
|
@ -6553,7 +6598,7 @@ msgstr "ir.translation"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:base.module.update:0
|
#: view:base.module.update:0
|
||||||
msgid "Module update result"
|
msgid "Module update result"
|
||||||
msgstr ""
|
msgstr "Module update result"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:workflow.activity:0
|
#: view:workflow.activity:0
|
||||||
|
@ -6575,7 +6620,7 @@ msgstr "Parent Company"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
msgid "Spanish (CR) / Español (CR)"
|
msgid "Spanish (CR) / Español (CR)"
|
||||||
msgstr ""
|
msgstr "Spanish (CR) / Español (CR)"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:res.currency.rate,rate:0
|
#: field:res.currency.rate,rate:0
|
||||||
|
@ -6615,6 +6660,9 @@ msgid ""
|
||||||
"for the currency: %s \n"
|
"for the currency: %s \n"
|
||||||
"at the date: %s"
|
"at the date: %s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"No rate found \n"
|
||||||
|
"for the currency: %s \n"
|
||||||
|
"at the date: %s"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.actions.act_window,help:base.action_ui_view_custom
|
#: 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 "
|
"Customized views are used when users reorganize the content of their "
|
||||||
"dashboard views (via web client)"
|
"dashboard views (via web client)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Customised views are used when users reorganise the content of their "
|
||||||
|
"dashboard views (via web client)"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.model,name:0
|
#: field:ir.model,name:0
|
||||||
|
@ -6660,7 +6710,7 @@ msgstr "Icon"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:ir.model.fields,model_id:0
|
#: help:ir.model.fields,model_id:0
|
||||||
msgid "The model this field belongs to"
|
msgid "The model this field belongs to"
|
||||||
msgstr ""
|
msgstr "The model this field belongs to"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.mq
|
#: model:res.country,name:base.mq
|
||||||
|
@ -6670,7 +6720,7 @@ msgstr "Martinique (French)"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.sequence.type:0
|
#: view:ir.sequence.type:0
|
||||||
msgid "Sequences Type"
|
msgid "Sequences Type"
|
||||||
msgstr ""
|
msgstr "Sequences Type"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.actions.act_window,name:base.res_request-act
|
#: 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.actions.act_window,name:base.res_log_act_window
|
||||||
#: model:ir.ui.menu,name:base.menu_res_log_act_window
|
#: model:ir.ui.menu,name:base.menu_res_log_act_window
|
||||||
msgid "Client Logs"
|
msgid "Client Logs"
|
||||||
msgstr ""
|
msgstr "Client Logs"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.al
|
#: model:res.country,name:base.al
|
||||||
|
@ -6713,6 +6763,8 @@ msgid ""
|
||||||
"You cannot delete the language which is Active !\n"
|
"You cannot delete the language which is Active !\n"
|
||||||
"Please de-activate the language first."
|
"Please de-activate the language first."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"You cannot delete a language which is active !\n"
|
||||||
|
"Please de-activate the language first."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:base.language.install:0
|
#: view:base.language.install:0
|
||||||
|
@ -6721,6 +6773,8 @@ msgid ""
|
||||||
"Please be patient, this operation may take a few minutes (depending on the "
|
"Please be patient, this operation may take a few minutes (depending on the "
|
||||||
"number of modules currently installed)..."
|
"number of modules currently installed)..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Please be patient, this operation may take a few minutes (depending on the "
|
||||||
|
"number of modules currently installed)..."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.ui.menu,child_id:0
|
#: 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
|
#: code:addons/orm.py:2316
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "ValidateError"
|
msgid "ValidateError"
|
||||||
msgstr ""
|
msgstr "ValidateError"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:base.module.import:0
|
#: view:base.module.import:0
|
||||||
#: view:base.module.update:0
|
#: view:base.module.update:0
|
||||||
msgid "Open Modules"
|
msgid "Open Modules"
|
||||||
msgstr ""
|
msgstr "Open Modules"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.actions.act_window,help:base.action_res_bank_form
|
#: model:ir.actions.act_window,help:base.action_res_bank_form
|
||||||
msgid "Manage bank records you want to be used in the system."
|
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
|
#. module: base
|
||||||
#: view:base.module.import:0
|
#: 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 "
|
"The path to the main report file (depending on Report Type) or NULL if the "
|
||||||
"content is in another field"
|
"content is in another field"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"The path to the main report file (depending on Report Type) or NULL if the "
|
||||||
|
"content is in another field"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.la
|
#: model:res.country,name:base.la
|
||||||
|
@ -6794,6 +6850,8 @@ msgid ""
|
||||||
"The sum of the data (2nd field) is null.\n"
|
"The sum of the data (2nd field) is null.\n"
|
||||||
"We can't draw a pie chart !"
|
"We can't draw a pie chart !"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"The sum of the data (2nd field) is null.\n"
|
||||||
|
"We can't draw a pie chart !"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.ui.menu,name:base.menu_lunch_reporting
|
#: model:ir.ui.menu,name:base.menu_lunch_reporting
|
||||||
|
@ -6815,7 +6873,7 @@ msgstr "Togo"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:ir.module.module,license:0
|
#: selection:ir.module.module,license:0
|
||||||
msgid "Other Proprietary"
|
msgid "Other Proprietary"
|
||||||
msgstr ""
|
msgstr "Other Proprietary"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:workflow.activity,kind:0
|
#: selection:workflow.activity,kind:0
|
||||||
|
@ -6826,7 +6884,7 @@ msgstr "Stop All"
|
||||||
#: code:addons/orm.py:412
|
#: code:addons/orm.py:412
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "The read_group method is not implemented on this object !"
|
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
|
#. module: base
|
||||||
#: view:ir.model.data:0
|
#: view:ir.model.data:0
|
||||||
|
@ -6846,7 +6904,7 @@ msgstr "Cascade"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:workflow.transition,group_id:0
|
#: field:workflow.transition,group_id:0
|
||||||
msgid "Group Required"
|
msgid "Group Required"
|
||||||
msgstr ""
|
msgstr "Group Required"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.actions.configuration.wizard:0
|
#: 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 "
|
"Enable this if you want to execute missed occurences as soon as the server "
|
||||||
"restarts."
|
"restarts."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Enable this if you want to execute missed occurences as soon as the server "
|
||||||
|
"restarts."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:base.module.upgrade:0
|
#: view:base.module.upgrade:0
|
||||||
msgid "Start update"
|
msgid "Start update"
|
||||||
msgstr ""
|
msgstr "Start update"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/base/publisher_warranty/publisher_warranty.py:144
|
#: code:addons/base/publisher_warranty/publisher_warranty.py:144
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Contract validation error"
|
msgid "Contract validation error"
|
||||||
msgstr ""
|
msgstr "Contract validation error"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:res.country.state,name:0
|
#: field:res.country.state,name:0
|
||||||
|
@ -6906,7 +6966,7 @@ msgstr "ir.actions.report.xml"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.partner.title,shortcut:base.res_partner_title_miss
|
#: model:res.partner.title,shortcut:base.res_partner_title_miss
|
||||||
msgid "Mss"
|
msgid "Mss"
|
||||||
msgstr ""
|
msgstr "Mss"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.model,name:base.model_ir_ui_view
|
#: model:ir.model,name:base.model_ir_ui_view
|
||||||
|
@ -6916,7 +6976,7 @@ msgstr "ir.ui.view"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: constraint:res.partner:0
|
#: constraint:res.partner:0
|
||||||
msgid "Error ! You can not create recursive associated members."
|
msgid "Error ! You can not create recursive associated members."
|
||||||
msgstr ""
|
msgstr "Error ! You can not create recursive associated members."
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:res.lang,code:0
|
#: help:res.lang,code:0
|
||||||
|
@ -6931,7 +6991,7 @@ msgstr "OpenERP Partners"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.ui.menu,name:base.menu_hr_manager
|
#: model:ir.ui.menu,name:base.menu_hr_manager
|
||||||
msgid "HR Manager Dashboard"
|
msgid "HR Manager Dashboard"
|
||||||
msgstr ""
|
msgstr "HR Manager Dashboard"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/base/module/module.py:253
|
#: code:addons/base/module/module.py:253
|
||||||
|
@ -6939,11 +6999,12 @@ msgstr ""
|
||||||
msgid ""
|
msgid ""
|
||||||
"Unable to install module \"%s\" because an external dependency is not met: %s"
|
"Unable to install module \"%s\" because an external dependency is not met: %s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Unable to install module \"%s\" because an external dependency is not met: %s"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.module.module:0
|
#: view:ir.module.module:0
|
||||||
msgid "Search modules"
|
msgid "Search modules"
|
||||||
msgstr ""
|
msgstr "Search modules"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.by
|
#: 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 "
|
"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."
|
"specific access to the applications they need to use in the system."
|
||||||
msgstr ""
|
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
|
#. module: base
|
||||||
#: selection:res.request,priority:0
|
#: selection:res.request,priority:0
|
||||||
|
@ -6983,13 +7048,13 @@ msgstr "Street2"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.actions.act_window,name:base.action_view_base_module_update
|
#: model:ir.actions.act_window,name:base.action_view_base_module_update
|
||||||
msgid "Module Update"
|
msgid "Module Update"
|
||||||
msgstr ""
|
msgstr "Module Update"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/base/module/wizard/base_module_upgrade.py:95
|
#: code:addons/base/module/wizard/base_module_upgrade.py:95
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Following modules are not installed or unknown: %s"
|
msgid "Following modules are not installed or unknown: %s"
|
||||||
msgstr ""
|
msgstr "Following modules are not installed or unknown: %s"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.cron:0
|
#: view:ir.cron:0
|
||||||
|
@ -7018,7 +7083,7 @@ msgstr "Open Window"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.actions.act_window,auto_search:0
|
#: field:ir.actions.act_window,auto_search:0
|
||||||
msgid "Auto Search"
|
msgid "Auto Search"
|
||||||
msgstr ""
|
msgstr "Auto Search"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.actions.act_window,filter:0
|
#: field:ir.actions.act_window,filter:0
|
||||||
|
@ -7064,25 +7129,25 @@ msgstr "Load"
|
||||||
#: help:res.config.users,name:0
|
#: help:res.config.users,name:0
|
||||||
#: help:res.users,name:0
|
#: help:res.users,name:0
|
||||||
msgid "The new user's real name, used for searching and most listings"
|
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
|
#. module: base
|
||||||
#: code:addons/osv.py:154
|
#: code:addons/osv.py:154
|
||||||
#: code:addons/osv.py:156
|
#: code:addons/osv.py:156
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Integrity Error"
|
msgid "Integrity Error"
|
||||||
msgstr ""
|
msgstr "Integrity Error"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.model,name:base.model_ir_wizard_screen
|
#: model:ir.model,name:base.model_ir_wizard_screen
|
||||||
msgid "ir.wizard.screen"
|
msgid "ir.wizard.screen"
|
||||||
msgstr ""
|
msgstr "ir.wizard.screen"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/base/ir/ir_model.py:223
|
#: code:addons/base/ir/ir_model.py:223
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Size of the field can never be less than 1 !"
|
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
|
#. module: base
|
||||||
#: model:res.country,name:base.so
|
#: model:res.country,name:base.so
|
||||||
|
@ -7092,7 +7157,7 @@ msgstr "Somalia"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:publisher_warranty.contract,state:0
|
#: selection:publisher_warranty.contract,state:0
|
||||||
msgid "Terminated"
|
msgid "Terminated"
|
||||||
msgstr ""
|
msgstr "Terminated"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.partner.category,name:base.res_partner_category_13
|
#: model:res.partner.category,name:base.res_partner_category_13
|
||||||
|
@ -7102,7 +7167,7 @@ msgstr "Important customers"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.lang:0
|
#: view:res.lang:0
|
||||||
msgid "Update Terms"
|
msgid "Update Terms"
|
||||||
msgstr ""
|
msgstr "Update Terms"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:partner.sms.send,mobile_to:0
|
#: field:partner.sms.send,mobile_to:0
|
||||||
|
@ -7121,7 +7186,7 @@ msgstr "Arguments"
|
||||||
#: code:addons/orm.py:716
|
#: code:addons/orm.py:716
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Database ID doesn't exist: %s : %s"
|
msgid "Database ID doesn't exist: %s : %s"
|
||||||
msgstr ""
|
msgstr "Database ID doesn't exist: %s : %s"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:ir.module.module,license:0
|
#: selection:ir.module.module,license:0
|
||||||
|
@ -7137,7 +7202,7 @@ msgstr "GPL Version 3"
|
||||||
#: code:addons/orm.py:836
|
#: code:addons/orm.py:836
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "key '%s' not found in selection field '%s'"
|
msgid "key '%s' not found in selection field '%s'"
|
||||||
msgstr ""
|
msgstr "key '%s' not found in selection field '%s'"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:partner.wizard.ean.check:0
|
#: view:partner.wizard.ean.check:0
|
||||||
|
@ -7148,7 +7213,7 @@ msgstr "Correct EAN13"
|
||||||
#: code:addons/orm.py:2317
|
#: code:addons/orm.py:2317
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "The value \"%s\" for the field \"%s\" is not in the selection"
|
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
|
#. module: base
|
||||||
#: field:res.partner,customer:0
|
#: field:res.partner,customer:0
|
||||||
|
|
|
@ -7,14 +7,14 @@ msgstr ""
|
||||||
"Project-Id-Version: OpenERP Server 5.0.0\n"
|
"Project-Id-Version: OpenERP Server 5.0.0\n"
|
||||||
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
||||||
"POT-Creation-Date: 2011-01-11 11:14+0000\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"
|
"Last-Translator: Walter Cheuk <wwycheuk@gmail.com>\n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"X-Launchpad-Export-Date: 2011-09-01 04:45+0000\n"
|
"X-Launchpad-Export-Date: 2011-09-28 05:19+0000\n"
|
||||||
"X-Generator: Launchpad (build 13827)\n"
|
"X-Generator: Launchpad (build 14049)\n"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.filters:0
|
#: view:ir.filters:0
|
||||||
|
@ -46,7 +46,7 @@ msgstr "日期時間"
|
||||||
msgid ""
|
msgid ""
|
||||||
"The second argument of the many2many field %s must be a SQL table !You used "
|
"The second argument of the many2many field %s must be a SQL table !You used "
|
||||||
"%s, which is not a valid SQL table name."
|
"%s, which is not a valid SQL table name."
|
||||||
msgstr "many2many 欄位 %s 之第二個引數須為 SQL 表格!您用了並非有效 SQL 表格名稱之 %s。"
|
msgstr "「多對多(many2many)」欄位 %s 之第二個引數須為 SQL 表格!您用了並非有效 SQL 表格名稱之 %s。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.values:0
|
#: view:ir.values:0
|
||||||
|
@ -103,7 +103,7 @@ msgstr "工作流程作用於"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.actions.act_window,display_menu_tip:0
|
#: field:ir.actions.act_window,display_menu_tip:0
|
||||||
msgid "Display Menu Tips"
|
msgid "Display Menu Tips"
|
||||||
msgstr "顯示選表提示"
|
msgstr "顯示選單提示"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.module.module:0
|
#: view:ir.module.module:0
|
||||||
|
@ -129,7 +129,7 @@ msgstr ""
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:res.partner,ref:0
|
#: field:res.partner,ref:0
|
||||||
msgid "Reference"
|
msgid "Reference"
|
||||||
msgstr "參照"
|
msgstr "參考"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.actions.act_window,target:0
|
#: field:ir.actions.act_window,target:0
|
||||||
|
@ -154,7 +154,7 @@ msgstr ""
|
||||||
#: code:addons/osv.py:133
|
#: code:addons/osv.py:133
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Constraint Error"
|
msgid "Constraint Error"
|
||||||
msgstr ""
|
msgstr "約束錯誤"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.model,name:base.model_ir_ui_view_custom
|
#: model:ir.model,name:base.model_ir_ui_view_custom
|
||||||
|
@ -171,12 +171,12 @@ msgstr "史瓦濟蘭"
|
||||||
#: code:addons/orm.py:3653
|
#: code:addons/orm.py:3653
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "created."
|
msgid "created."
|
||||||
msgstr ""
|
msgstr "已建立。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.partner.category,name:base.res_partner_category_woodsuppliers0
|
#: model:res.partner.category,name:base.res_partner_category_woodsuppliers0
|
||||||
msgid "Wood Suppliers"
|
msgid "Wood Suppliers"
|
||||||
msgstr ""
|
msgstr "木材供應商"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/base/module/module.py:303
|
#: code:addons/base/module/module.py:303
|
||||||
|
@ -224,7 +224,7 @@ msgstr "新"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.actions.report.xml,multi:0
|
#: field:ir.actions.report.xml,multi:0
|
||||||
msgid "On multiple doc."
|
msgid "On multiple doc."
|
||||||
msgstr "作用於多重檔案。"
|
msgstr "作用於多重文件。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.module.category,module_nr:0
|
#: field:ir.module.category,module_nr:0
|
||||||
|
@ -252,12 +252,12 @@ msgstr "聯絡人名稱"
|
||||||
msgid ""
|
msgid ""
|
||||||
"Save this document to a %s file and edit it with a specific software or a "
|
"Save this document to a %s file and edit it with a specific software or a "
|
||||||
"text editor. The file encoding is UTF-8."
|
"text editor. The file encoding is UTF-8."
|
||||||
msgstr "將此檔案儲存為%s檔案後可以特定軟體或文字編輯程式修改。檔案編碼為 UTF-8。"
|
msgstr "將此檔案存為 %s 檔案,並以特定軟體或文字編輯程式修改。檔案編碼為 UTF-8。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: sql_constraint:res.lang:0
|
#: sql_constraint:res.lang:0
|
||||||
msgid "The name of the language must be unique !"
|
msgid "The name of the language must be unique !"
|
||||||
msgstr ""
|
msgstr "語言名稱須與其他不同 !"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:res.request,state:0
|
#: selection:res.request,state:0
|
||||||
|
@ -329,7 +329,7 @@ msgstr "欄位名稱"
|
||||||
#: wizard_view:server.action.create,init:0
|
#: wizard_view:server.action.create,init:0
|
||||||
#: wizard_field:server.action.create,init,type:0
|
#: wizard_field:server.action.create,init,type:0
|
||||||
msgid "Select Action Type"
|
msgid "Select Action Type"
|
||||||
msgstr "選擇動作類型"
|
msgstr "選取動作類型"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.tv
|
#: model:res.country,name:base.tv
|
||||||
|
@ -422,7 +422,7 @@ msgstr "哥倫比亞"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.module.module:0
|
#: view:ir.module.module:0
|
||||||
msgid "Schedule Upgrade"
|
msgid "Schedule Upgrade"
|
||||||
msgstr "排程升級"
|
msgstr "安排升級"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/orm.py:838
|
#: code:addons/orm.py:838
|
||||||
|
@ -436,7 +436,7 @@ msgid ""
|
||||||
"The ISO country code in two chars.\n"
|
"The ISO country code in two chars.\n"
|
||||||
"You can use this field for quick search."
|
"You can use this field for quick search."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"ISO 國家代碼使用兩個字符。\n"
|
"ISO 國家代碼使用兩個字元。\n"
|
||||||
"您可以使用該欄位來快速搜索。"
|
"您可以使用該欄位來快速搜索。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
|
@ -447,7 +447,7 @@ msgstr "帛琉"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.partner:0
|
#: view:res.partner:0
|
||||||
msgid "Sales & Purchases"
|
msgid "Sales & Purchases"
|
||||||
msgstr "銷售&採購"
|
msgstr "銷售 及 購貨"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.translation:0
|
#: view:ir.translation:0
|
||||||
|
@ -481,7 +481,7 @@ msgstr "自訂欄位名稱開頭必須是 'x_' !"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:ir.actions.server,action_id:0
|
#: help:ir.actions.server,action_id:0
|
||||||
msgid "Select the Action Window, Report, Wizard to be executed."
|
msgid "Select the Action Window, Report, Wizard to be executed."
|
||||||
msgstr "選擇要執行的動作視窗、報表或精靈。"
|
msgstr "選取要執行之動作視窗、報表或精靈。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.config.users:0
|
#: view:res.config.users:0
|
||||||
|
@ -502,7 +502,7 @@ msgstr "模型說明"
|
||||||
#: help:ir.actions.act_window,src_model:0
|
#: help:ir.actions.act_window,src_model:0
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optional model name of the objects on which this action should be visible"
|
"Optional model name of the objects on which this action should be visible"
|
||||||
msgstr ""
|
msgstr "要可見動作的物件之模型名稱(可有可無)"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:workflow.transition,trigger_expr_id:0
|
#: field:workflow.transition,trigger_expr_id:0
|
||||||
|
@ -548,7 +548,7 @@ msgstr "是否檢查 Ean? "
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.values,key2:0
|
#: field:ir.values,key2:0
|
||||||
msgid "Event Type"
|
msgid "Event Type"
|
||||||
msgstr "事件類型"
|
msgstr "活動類型"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:base.language.export:0
|
#: view:base.language.export:0
|
||||||
|
@ -684,7 +684,7 @@ msgstr "西班牙文 (UY) / Español (UY)"
|
||||||
#: field:res.partner,mobile:0
|
#: field:res.partner,mobile:0
|
||||||
#: field:res.partner.address,mobile:0
|
#: field:res.partner.address,mobile:0
|
||||||
msgid "Mobile"
|
msgid "Mobile"
|
||||||
msgstr "手機"
|
msgstr "手提電話"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.om
|
#: model:res.country,name:base.om
|
||||||
|
@ -741,7 +741,7 @@ msgstr "印度"
|
||||||
#: model:ir.actions.act_window,name:base.res_request_link-act
|
#: model:ir.actions.act_window,name:base.res_request_link-act
|
||||||
#: model:ir.ui.menu,name:base.menu_res_request_link_act
|
#: model:ir.ui.menu,name:base.menu_res_request_link_act
|
||||||
msgid "Request Reference Types"
|
msgid "Request Reference Types"
|
||||||
msgstr "請求參照類型"
|
msgstr "請求參考類型"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.values:0
|
#: view:ir.values:0
|
||||||
|
@ -812,7 +812,7 @@ msgstr "人力資源儀錶板"
|
||||||
#: code:addons/base/res/res_user.py:507
|
#: code:addons/base/res/res_user.py:507
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Setting empty passwords is not allowed for security reasons!"
|
msgid "Setting empty passwords is not allowed for security reasons!"
|
||||||
msgstr "因保安理由密碼不能留空!"
|
msgstr "因安全理由密碼不能留空!"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:ir.actions.server,state:0
|
#: selection:ir.actions.server,state:0
|
||||||
|
@ -882,7 +882,7 @@ msgstr "刪除存取"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.ne
|
#: model:res.country,name:base.ne
|
||||||
msgid "Niger"
|
msgid "Niger"
|
||||||
msgstr "尼日爾"
|
msgstr "尼日"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
|
@ -998,7 +998,7 @@ msgstr "儀錶板"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.ui.menu,name:base.menu_purchase_root
|
#: model:ir.ui.menu,name:base.menu_purchase_root
|
||||||
msgid "Purchases"
|
msgid "Purchases"
|
||||||
msgstr "採購"
|
msgstr "購貨"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.md
|
#: model:res.country,name:base.md
|
||||||
|
@ -1120,7 +1120,7 @@ msgstr "報表"
|
||||||
msgid ""
|
msgid ""
|
||||||
"If set to true, the action will not be displayed on the right toolbar of a "
|
"If set to true, the action will not be displayed on the right toolbar of a "
|
||||||
"form view."
|
"form view."
|
||||||
msgstr "如設為是,該動作不會顯示於表單檢視右側工具欄。"
|
msgstr "如設為真,該動作不會顯示於表單檢視右側工具欄。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:workflow,on_create:0
|
#: field:workflow,on_create:0
|
||||||
|
@ -1239,7 +1239,7 @@ msgstr "馬爾地夫"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:ir.values,res_id:0
|
#: help:ir.values,res_id:0
|
||||||
msgid "Keep 0 if the action must appear on all resources."
|
msgid "Keep 0 if the action must appear on all resources."
|
||||||
msgstr "如果該動作要顯示於所有資源上的話請保持為「0」。"
|
msgstr "如該動作要顯示於所有資源的話請保持為「0」。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.model,name:base.model_ir_rule
|
#: model:ir.model,name:base.model_ir_rule
|
||||||
|
@ -1325,7 +1325,7 @@ msgstr "優先次序"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:workflow.transition,act_from:0
|
#: field:workflow.transition,act_from:0
|
||||||
msgid "Source Activity"
|
msgid "Source Activity"
|
||||||
msgstr "來源活動"
|
msgstr "來源地動態"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.sequence:0
|
#: view:ir.sequence:0
|
||||||
|
@ -1368,7 +1368,7 @@ msgstr "完整路徑"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.request:0
|
#: view:res.request:0
|
||||||
msgid "References"
|
msgid "References"
|
||||||
msgstr "參照"
|
msgstr "參考"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.lang:0
|
#: view:res.lang:0
|
||||||
|
@ -1510,7 +1510,7 @@ msgstr ""
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:workflow.activity:0
|
#: view:workflow.activity:0
|
||||||
msgid "Workflow Activity"
|
msgid "Workflow Activity"
|
||||||
msgstr "工作流程活動"
|
msgstr "工作流程動態"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.rule:0
|
#: view:ir.rule:0
|
||||||
|
@ -1694,7 +1694,7 @@ msgstr "原始檢視"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.values:0
|
#: view:ir.values:0
|
||||||
msgid "Action To Launch"
|
msgid "Action To Launch"
|
||||||
msgstr "要啟動之動作"
|
msgstr "要執行之動作"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.actions.url,target:0
|
#: field:ir.actions.url,target:0
|
||||||
|
@ -1736,7 +1736,7 @@ msgstr ""
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:ir.values,action_id:0
|
#: help:ir.values,action_id:0
|
||||||
msgid "This field is not used, it only helps you to select the right action."
|
msgid "This field is not used, it only helps you to select the right action."
|
||||||
msgstr "該欄位並未使用,只是為了幫您選擇正確的動作。"
|
msgstr "此欄位並未使用,只是為了幫您選擇正確動作。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.actions.server,email:0
|
#: field:ir.actions.server,email:0
|
||||||
|
@ -1916,7 +1916,7 @@ msgstr "諾福克島"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
msgid "Korean (KR) / 한국어 (KR)"
|
msgid "Korean (KR) / 한국어 (KR)"
|
||||||
msgstr "韓文 (KR) / 한국어 (KR)"
|
msgstr "韓文 (北韓) / 한국어 (KR)"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:ir.model.fields,model:0
|
#: help:ir.model.fields,model:0
|
||||||
|
@ -1927,7 +1927,7 @@ msgstr ""
|
||||||
#: field:ir.actions.server,action_id:0
|
#: field:ir.actions.server,action_id:0
|
||||||
#: selection:ir.actions.server,state:0
|
#: selection:ir.actions.server,state:0
|
||||||
msgid "Client Action"
|
msgid "Client Action"
|
||||||
msgstr "客戶端動作"
|
msgstr "用戶端動作"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.bd
|
#: model:res.country,name:base.bd
|
||||||
|
@ -2007,7 +2007,7 @@ msgstr "屬性"
|
||||||
#: model:ir.model,name:base.model_res_partner_bank_type
|
#: model:ir.model,name:base.model_res_partner_bank_type
|
||||||
#: view:res.partner.bank.type:0
|
#: view:res.partner.bank.type:0
|
||||||
msgid "Bank Account Type"
|
msgid "Bank Account Type"
|
||||||
msgstr "銀行帳戶類型"
|
msgstr "銀行帳號類型"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:base.language.export,config_logo:0
|
#: field:base.language.export,config_logo:0
|
||||||
|
@ -2171,7 +2171,7 @@ msgstr "西班牙文 (DO) / Español (DO)"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.model,name:base.model_workflow_activity
|
#: model:ir.model,name:base.model_workflow_activity
|
||||||
msgid "workflow.activity"
|
msgid "workflow.activity"
|
||||||
msgstr "工作流程.活動"
|
msgstr "工作流程.動態"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:ir.ui.view_sc,res_id:0
|
#: help:ir.ui.view_sc,res_id:0
|
||||||
|
@ -2239,7 +2239,7 @@ msgstr ""
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.default,ref_id:0
|
#: field:ir.default,ref_id:0
|
||||||
msgid "ID Ref."
|
msgid "ID Ref."
|
||||||
msgstr "ID參照"
|
msgstr "ID參考"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.actions.server,name:base.action_start_configurator
|
#: model:ir.actions.server,name:base.action_start_configurator
|
||||||
|
@ -2313,7 +2313,7 @@ msgstr "分格格式"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:publisher_warranty.contract,state:0
|
#: selection:publisher_warranty.contract,state:0
|
||||||
msgid "Unvalidated"
|
msgid "Unvalidated"
|
||||||
msgstr "未檢驗"
|
msgstr "未驗證"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.ui.menu,name:base.next_id_9
|
#: model:ir.ui.menu,name:base.next_id_9
|
||||||
|
@ -2336,7 +2336,7 @@ msgstr "馬約特"
|
||||||
#: code:addons/base/ir/ir_actions.py:597
|
#: code:addons/base/ir/ir_actions.py:597
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Please specify an action to launch !"
|
msgid "Please specify an action to launch !"
|
||||||
msgstr "請指定執行動作!"
|
msgstr "請指定要執行之動作!"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.payterm:0
|
#: view:res.payterm:0
|
||||||
|
@ -2368,7 +2368,7 @@ msgstr "請檢查所有行數皆有%d個欄位。"
|
||||||
#: view:ir.cron:0
|
#: view:ir.cron:0
|
||||||
#: model:ir.ui.menu,name:base.menu_ir_cron_act
|
#: model:ir.ui.menu,name:base.menu_ir_cron_act
|
||||||
msgid "Scheduled Actions"
|
msgid "Scheduled Actions"
|
||||||
msgstr "計劃的動作"
|
msgstr "已安排動作"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:res.partner.address,title:0
|
#: field:res.partner.address,title:0
|
||||||
|
@ -2412,7 +2412,7 @@ msgstr "建立選單"
|
||||||
msgid ""
|
msgid ""
|
||||||
"Value Added Tax number. Check the box if the partner is subjected to the "
|
"Value Added Tax number. Check the box if the partner is subjected to the "
|
||||||
"VAT. Used by the VAT legal statement."
|
"VAT. Used by the VAT legal statement."
|
||||||
msgstr "增值稅編號。如該伙伴適用於增值稅,請選擇。用於增值稅申報。"
|
msgstr "增值稅編號。如該伙伴需要繳交增值稅,請選擇。用於申報增值稅。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.model,name:base.model_maintenance_contract
|
#: model:ir.model,name:base.model_maintenance_contract
|
||||||
|
@ -2710,7 +2710,7 @@ msgstr "基礎欄位"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:publisher_warranty.contract:0
|
#: view:publisher_warranty.contract:0
|
||||||
msgid "Validate"
|
msgid "Validate"
|
||||||
msgstr "檢驗"
|
msgstr "驗證"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.actions.todo,restart:0
|
#: field:ir.actions.todo,restart:0
|
||||||
|
@ -2810,7 +2810,7 @@ msgstr ""
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.partner.category,name:base.res_partner_category_16
|
#: model:res.partner.category,name:base.res_partner_category_16
|
||||||
msgid "Telecom sector"
|
msgid "Telecom sector"
|
||||||
msgstr "電訊範疇"
|
msgstr "電信範疇"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:workflow.transition,trigger_model:0
|
#: field:workflow.transition,trigger_model:0
|
||||||
|
@ -2820,7 +2820,7 @@ msgstr "觸發器物件"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.users:0
|
#: view:res.users:0
|
||||||
msgid "Current Activity"
|
msgid "Current Activity"
|
||||||
msgstr "目前活動"
|
msgstr "當前動態"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:workflow.activity:0
|
#: view:workflow.activity:0
|
||||||
|
@ -2842,7 +2842,7 @@ msgstr "行銷"
|
||||||
#: view:res.partner.bank:0
|
#: view:res.partner.bank:0
|
||||||
#: model:res.partner.bank.type,name:base.bank_normal
|
#: model:res.partner.bank.type,name:base.bank_normal
|
||||||
msgid "Bank account"
|
msgid "Bank account"
|
||||||
msgstr "銀行帳戶"
|
msgstr "銀行帳號"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
|
@ -2857,7 +2857,7 @@ msgstr "序列類型"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.ui.view.custom:0
|
#: view:ir.ui.view.custom:0
|
||||||
msgid "Customized Architecture"
|
msgid "Customized Architecture"
|
||||||
msgstr "自訂架構"
|
msgstr "自訂化架構"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.module.module,license:0
|
#: field:ir.module.module,license:0
|
||||||
|
@ -2877,7 +2877,7 @@ msgstr "必定"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:ir.translation,type:0
|
#: selection:ir.translation,type:0
|
||||||
msgid "SQL Constraint"
|
msgid "SQL Constraint"
|
||||||
msgstr "SQL 限制"
|
msgstr "SQL 約束"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.actions.server,srcmodel_id:0
|
#: field:ir.actions.server,srcmodel_id:0
|
||||||
|
@ -3070,7 +3070,7 @@ msgstr "肯亞"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.partner.event:0
|
#: view:res.partner.event:0
|
||||||
msgid "Event"
|
msgid "Event"
|
||||||
msgstr "事件"
|
msgstr "活動"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.ui.menu,name:base.menu_custom_reports
|
#: model:ir.ui.menu,name:base.menu_custom_reports
|
||||||
|
@ -3080,7 +3080,7 @@ msgstr "自訂報表"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
msgid "Abkhazian / аҧсуа"
|
msgid "Abkhazian / аҧсуа"
|
||||||
msgstr "阿布哈茲文 / аҧсуа"
|
msgstr "阿布哈兹文 / аҧсуа"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:base.module.configuration:0
|
#: view:base.module.configuration:0
|
||||||
|
@ -3106,7 +3106,7 @@ msgstr "聖馬利諾"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.bm
|
#: model:res.country,name:base.bm
|
||||||
msgid "Bermuda"
|
msgid "Bermuda"
|
||||||
msgstr "百慕達"
|
msgstr "百慕大"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.pe
|
#: model:res.country,name:base.pe
|
||||||
|
@ -3180,7 +3180,7 @@ msgstr "完整存取"
|
||||||
#: view:ir.model.fields:0
|
#: view:ir.model.fields:0
|
||||||
#: model:ir.ui.menu,name:base.menu_security
|
#: model:ir.ui.menu,name:base.menu_security
|
||||||
msgid "Security"
|
msgid "Security"
|
||||||
msgstr "保安"
|
msgstr "安全"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.widget,title:base.openerp_favorites_twitter_widget
|
#: 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 "
|
"plugin, don't forget to register emails to each contact so that the gateway "
|
||||||
"will automatically attach incoming emails to the right partner."
|
"will automatically attach incoming emails to the right partner."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"客戶是指您與其做生意者,例如公司或機構。客戶可有多個聯絡人或地址,均屬於其員工。您可用「歷史」分頁追蹤所有有關交易:訂單、電郵、機會、退款要求等等。如您用"
|
"客戶是指您與其做生意者,例如公司或機構。客戶可有多個聯絡人或地址,均屬於其員工。您可用「歷史」分頁追蹤所有有關交易:訂單、電郵、商機、退款要求等等。如您用"
|
||||||
"電郵閘道和 Outlook 或 Thunderbird 外掛程式,別忘了為聯絡人登記電郵,好讓閘道自動為合適伙伴寄送收到之電郵。"
|
"電郵閘道、Outlook 或 Thunderbird 外掛程式,別忘了為聯絡人登記電郵,好讓閘道自動為合適伙伴寄送收到之電郵。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.actions.report.xml,name:0
|
#: field:ir.actions.report.xml,name:0
|
||||||
|
@ -3517,7 +3517,7 @@ msgstr "名稱"
|
||||||
msgid ""
|
msgid ""
|
||||||
"If set to true, the action will not be displayed on the right toolbar of a "
|
"If set to true, the action will not be displayed on the right toolbar of a "
|
||||||
"form view"
|
"form view"
|
||||||
msgstr "如設為是,該動作不會顯示於表單檢視右側工具欄"
|
msgstr "如設為真,該動作不會顯示於表單檢視右側工具欄"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.ms
|
#: model:res.country,name:base.ms
|
||||||
|
@ -3543,7 +3543,7 @@ msgstr "應用程式詞彙"
|
||||||
msgid ""
|
msgid ""
|
||||||
"The user's timezone, used to perform timezone conversions between the server "
|
"The user's timezone, used to perform timezone conversions between the server "
|
||||||
"and the client."
|
"and the client."
|
||||||
msgstr ""
|
msgstr "用戶之時區,用以為伺服器及用戶端進行時區轉換。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.module.module,demo:0
|
#: field:ir.module.module,demo:0
|
||||||
|
@ -3565,7 +3565,7 @@ msgstr "日文 / 日本語"
|
||||||
msgid ""
|
msgid ""
|
||||||
"Source activity. When this activity is over, the condition is tested to "
|
"Source activity. When this activity is over, the condition is tested to "
|
||||||
"determine if we can start the ACT_TO activity."
|
"determine if we can start the ACT_TO activity."
|
||||||
msgstr ""
|
msgstr "來源地動態。當再無動態,會測試條件以決定是否開始 ACT_TO 動態。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.partner.category,name:base.res_partner_category_3
|
#: model:res.partner.category,name:base.res_partner_category_3
|
||||||
|
@ -3618,7 +3618,7 @@ msgstr "冷岸及央麥恩群島"
|
||||||
#: model:ir.model,name:base.model_ir_actions_wizard
|
#: model:ir.model,name:base.model_ir_actions_wizard
|
||||||
#: selection:ir.ui.menu,action:0
|
#: selection:ir.ui.menu,action:0
|
||||||
msgid "ir.actions.wizard"
|
msgid "ir.actions.wizard"
|
||||||
msgstr ""
|
msgstr "ir.actions.wizard"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.actions.act_window:0
|
#: view:ir.actions.act_window:0
|
||||||
|
@ -3753,12 +3753,12 @@ msgstr "無法載入模組基礎!(提示:檢查附加元件路徑)"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.partner.bank:0
|
#: view:res.partner.bank:0
|
||||||
msgid "Bank Account Owner"
|
msgid "Bank Account Owner"
|
||||||
msgstr "銀行帳戶所有者"
|
msgstr "銀行帳號所有者"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.actions.act_window,name:base.act_values_form
|
#: model:ir.actions.act_window,name:base.act_values_form
|
||||||
msgid "Client Actions Connections"
|
msgid "Client Actions Connections"
|
||||||
msgstr "客戶端動作連接"
|
msgstr "用戶端動作連線"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.attachment,res_name:0
|
#: field:ir.attachment,res_name:0
|
||||||
|
@ -3836,7 +3836,7 @@ msgstr ""
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.actions.server:0
|
#: view:ir.actions.server:0
|
||||||
msgid "Client Action Configuration"
|
msgid "Client Action Configuration"
|
||||||
msgstr "客戶端動作設置"
|
msgstr "用戶端動作設定"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.model,name:base.model_res_partner_address
|
#: model:ir.model,name:base.model_res_partner_address
|
||||||
|
@ -3872,13 +3872,13 @@ msgstr "選取要匯入之模組套件 (.zip 檔):"
|
||||||
#: field:res.partner.event,name:0
|
#: field:res.partner.event,name:0
|
||||||
#: model:res.widget,title:base.events_widget
|
#: model:res.widget,title:base.events_widget
|
||||||
msgid "Events"
|
msgid "Events"
|
||||||
msgstr "事件"
|
msgstr "活動"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.model,name:base.model_ir_actions_url
|
#: model:ir.model,name:base.model_ir_actions_url
|
||||||
#: selection:ir.ui.menu,action:0
|
#: selection:ir.ui.menu,action:0
|
||||||
msgid "ir.actions.url"
|
msgid "ir.actions.url"
|
||||||
msgstr ""
|
msgstr "ir.actions.url"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.widget,title:base.currency_converter_widget
|
#: model:res.widget,title:base.currency_converter_widget
|
||||||
|
@ -4098,7 +4098,7 @@ msgstr "重複錯過的"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:ir.actions.server,state:0
|
#: help:ir.actions.server,state:0
|
||||||
msgid "Type of the Action that is to be executed"
|
msgid "Type of the Action that is to be executed"
|
||||||
msgstr "將要執行動作之類型"
|
msgstr "要執行動作之類型"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.server.object.lines,server_id:0
|
#: 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 "
|
"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 "
|
"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."
|
"system. Some examples of channels can be: Website, Phone Call, Reseller, etc."
|
||||||
msgstr ""
|
msgstr "於系統建立文件,以保持特定渠道追蹤潛在客戶及商機之來源。渠道例子有網站、電話查詢、零售商等等。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.partner.bank.type.field,name:base.bank_normal_field
|
#: model:res.partner.bank.type.field,name:base.bank_normal_field
|
||||||
|
@ -4765,7 +4765,7 @@ msgstr "變改我的偏好設定"
|
||||||
#: code:addons/base/ir/ir_actions.py:164
|
#: code:addons/base/ir/ir_actions.py:164
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Invalid model name in the action definition."
|
msgid "Invalid model name in the action definition."
|
||||||
msgstr "動作定義之模型名無效。"
|
msgstr "動作定義之模型名稱無效。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:partner.sms.send,text:0
|
#: field:partner.sms.send,text:0
|
||||||
|
@ -4900,7 +4900,7 @@ msgstr "如指定,此動作會於此用戶登入時於標準選單以外額外
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.values:0
|
#: view:ir.values:0
|
||||||
msgid "Client Actions"
|
msgid "Client Actions"
|
||||||
msgstr "客戶端動作"
|
msgstr "用戶端動作"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/orm.py:1806
|
#: code:addons/orm.py:1806
|
||||||
|
@ -4921,12 +4921,12 @@ msgstr ""
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:workflow.transition,act_to:0
|
#: field:workflow.transition,act_to:0
|
||||||
msgid "Destination Activity"
|
msgid "Destination Activity"
|
||||||
msgstr "目的地活動"
|
msgstr "目的地動態"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.values:0
|
#: view:ir.values:0
|
||||||
msgid "Connect Events to Actions"
|
msgid "Connect Events to Actions"
|
||||||
msgstr "把事件連接動作"
|
msgstr "把活動關聯到動作"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.model,name:base.model_base_update_translations
|
#: model:ir.model,name:base.model_base_update_translations
|
||||||
|
@ -5041,7 +5041,7 @@ msgstr "網頁圖示影像"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.values:0
|
#: view:ir.values:0
|
||||||
msgid "Values for Event Type"
|
msgid "Values for Event Type"
|
||||||
msgstr "事件類型值"
|
msgstr "活動類型值"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:ir.model.fields,select_level:0
|
#: 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 "
|
"installed the CRM, with the history tab, you can track all the interactions "
|
||||||
"with a partner such as opportunities, emails, or sales orders issued."
|
"with a partner such as opportunities, emails, or sales orders issued."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"客戶(於系統其他地方又稱「伙伴」)助您管理其他公司,包括潛在客戶、客戶及/或供應商,之通訊錄。「伙伴表單」讓您追蹤及紀錄所有所需資訊,以讓您處理公司地址、"
|
||||||
|
"聯絡人、報價單等等。如您安裝了客戶關係管理(CRM)模組,以歷史分頁您可追蹤與伙伴有關之所有來往,如商機、電郵或銷售訂單等。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.ph
|
#: model:res.country,name:base.ph
|
||||||
|
@ -5552,7 +5554,7 @@ msgstr ""
|
||||||
#: code:addons/base/res/res_config.py:94
|
#: code:addons/base/res/res_config.py:94
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Couldn't find previous ir.actions.todo"
|
msgid "Couldn't find previous ir.actions.todo"
|
||||||
msgstr ""
|
msgstr "找不到之前的 ir.actions.todo"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.actions.act_window:0
|
#: view:ir.actions.act_window:0
|
||||||
|
@ -5792,7 +5794,7 @@ msgstr "宏都拉斯"
|
||||||
#: help:res.users,menu_tips:0
|
#: help:res.users,menu_tips:0
|
||||||
msgid ""
|
msgid ""
|
||||||
"Check out this box if you want to always display tips on each menu action"
|
"Check out this box if you want to always display tips on each menu action"
|
||||||
msgstr ""
|
msgstr "如想於每個選單動作顯示提示,勾選此框"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.eg
|
#: model:res.country,name:base.eg
|
||||||
|
@ -6078,7 +6080,7 @@ msgstr "建立日期"
|
||||||
msgid ""
|
msgid ""
|
||||||
"Select the action that will be executed. Loop action will not be avaliable "
|
"Select the action that will be executed. Loop action will not be avaliable "
|
||||||
"inside loop."
|
"inside loop."
|
||||||
msgstr "選擇將要執行的動作。循環動作在循環內不可用。"
|
msgstr "選取要執行之動作。循環動作在循環內不可用。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: selection:base.language.install,lang:0
|
#: selection:base.language.install,lang:0
|
||||||
|
@ -6358,7 +6360,7 @@ msgstr "模組更新結果"
|
||||||
#: view:workflow.activity:0
|
#: view:workflow.activity:0
|
||||||
#: field:workflow.workitem,act_id:0
|
#: field:workflow.workitem,act_id:0
|
||||||
msgid "Activity"
|
msgid "Activity"
|
||||||
msgstr "活動"
|
msgstr "動態"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.partner:0
|
#: view:res.partner:0
|
||||||
|
@ -6423,7 +6425,7 @@ msgstr ""
|
||||||
msgid ""
|
msgid ""
|
||||||
"Customized views are used when users reorganize the content of their "
|
"Customized views are used when users reorganize the content of their "
|
||||||
"dashboard views (via web client)"
|
"dashboard views (via web client)"
|
||||||
msgstr ""
|
msgstr "當用戶(以 web client)重組其 dashboard 檢視即會使用自訂化檢視"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.model,name:0
|
#: field:ir.model,name:0
|
||||||
|
@ -6494,7 +6496,7 @@ msgstr ""
|
||||||
#: model:ir.actions.act_window,name:base.res_log_act_window
|
#: model:ir.actions.act_window,name:base.res_log_act_window
|
||||||
#: model:ir.ui.menu,name:base.menu_res_log_act_window
|
#: model:ir.ui.menu,name:base.menu_res_log_act_window
|
||||||
msgid "Client Logs"
|
msgid "Client Logs"
|
||||||
msgstr "客戶端日誌"
|
msgstr "用戶端日誌"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.al
|
#: model:res.country,name:base.al
|
||||||
|
@ -6534,7 +6536,7 @@ msgstr ""
|
||||||
#: code:addons/base/ir/ir_actions.py:716
|
#: code:addons/base/ir/ir_actions.py:716
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Problem in configuration `Record Id` in Server Action!"
|
msgid "Problem in configuration `Record Id` in Server Action!"
|
||||||
msgstr "於伺服器動作之「Record Id」配置錯誤!"
|
msgstr "伺服器動作之「紀錄 Id」配置有問題!"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/orm.py:2306
|
#: code:addons/orm.py:2306
|
||||||
|
@ -6587,7 +6589,7 @@ msgstr "電郵"
|
||||||
#: field:res.config.users,action_id:0
|
#: field:res.config.users,action_id:0
|
||||||
#: field:res.users,action_id:0
|
#: field:res.users,action_id:0
|
||||||
msgid "Home Action"
|
msgid "Home Action"
|
||||||
msgstr "家動作(Home Action)"
|
msgstr "家動作(Home Action)"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/custom.py:558
|
#: code:addons/custom.py:558
|
||||||
|
@ -7603,7 +7605,7 @@ msgstr "要更新模組"
|
||||||
msgid ""
|
msgid ""
|
||||||
"Important when you deal with multiple actions, the execution order will be "
|
"Important when you deal with multiple actions, the execution order will be "
|
||||||
"decided based on this, low number is higher priority."
|
"decided based on this, low number is higher priority."
|
||||||
msgstr "對於處理多個動作很重要,其決定動作執行順序;小的數字具較高優先次序。"
|
msgstr "對於處理多重動作很重要,其決定動作執行次序;小的數字具較高優先次序。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.actions.report.xml,header:0
|
#: field:ir.actions.report.xml,header:0
|
||||||
|
@ -7756,7 +7758,7 @@ msgstr "格陵蘭"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:res.partner.bank,acc_number:0
|
#: field:res.partner.bank,acc_number:0
|
||||||
msgid "Account Number"
|
msgid "Account Number"
|
||||||
msgstr "帳戶號碼"
|
msgstr "帳號"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.lang:0
|
#: view:res.lang:0
|
||||||
|
@ -7969,7 +7971,7 @@ msgstr "斯洛伐克文 / Slovenský jazyk"
|
||||||
#: field:ir.ui.menu,icon_pict:0
|
#: field:ir.ui.menu,icon_pict:0
|
||||||
#: field:publisher_warranty.contract.wizard,state:0
|
#: field:publisher_warranty.contract.wizard,state:0
|
||||||
msgid "unknown"
|
msgid "unknown"
|
||||||
msgstr "不明"
|
msgstr "不詳"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:res.currency,symbol:0
|
#: field:res.currency,symbol:0
|
||||||
|
@ -8032,7 +8034,7 @@ msgstr "CSV 檔"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:res.company,account_no:0
|
#: field:res.company,account_no:0
|
||||||
msgid "Account No."
|
msgid "Account No."
|
||||||
msgstr "帳戶號碼"
|
msgstr "帳號"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/base/res/res_lang.py:157
|
#: code:addons/base/res/res_lang.py:157
|
||||||
|
@ -8128,7 +8130,7 @@ msgstr "土耳其文 / Türkçe"
|
||||||
#: view:workflow:0
|
#: view:workflow:0
|
||||||
#: field:workflow,activities:0
|
#: field:workflow,activities:0
|
||||||
msgid "Activities"
|
msgid "Activities"
|
||||||
msgstr "活動"
|
msgstr "動態"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:ir.actions.act_window,auto_refresh:0
|
#: 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_association
|
||||||
#: model:ir.ui.menu,name:base.menu_event_main
|
#: model:ir.ui.menu,name:base.menu_event_main
|
||||||
msgid "Events Organisation"
|
msgid "Events Organisation"
|
||||||
msgstr "事件組織"
|
msgstr "活動組織"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:ir.actions.act_window,name:base.ir_sequence_actions
|
#: model:ir.actions.act_window,name:base.ir_sequence_actions
|
||||||
|
@ -8247,7 +8249,7 @@ msgstr "添加公司 RML 頁首與否"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: help:workflow.transition,act_to:0
|
#: help:workflow.transition,act_to:0
|
||||||
msgid "The destination activity."
|
msgid "The destination activity."
|
||||||
msgstr "目的地活地。"
|
msgstr "目的地動態。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:base.module.update:0
|
#: view:base.module.update:0
|
||||||
|
@ -8283,7 +8285,7 @@ msgstr "聖誕島"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.actions.server:0
|
#: view:ir.actions.server:0
|
||||||
msgid "Other Actions Configuration"
|
msgid "Other Actions Configuration"
|
||||||
msgstr "其他動作設置"
|
msgstr "其他動作配置"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.config.installer:0
|
#: view:res.config.installer:0
|
||||||
|
@ -8307,7 +8309,7 @@ msgstr "額外資訊"
|
||||||
#: model:ir.actions.act_window,name:base.act_values_form_action
|
#: model:ir.actions.act_window,name:base.act_values_form_action
|
||||||
#: model:ir.ui.menu,name:base.menu_values_form_action
|
#: model:ir.ui.menu,name:base.menu_values_form_action
|
||||||
msgid "Client Events"
|
msgid "Client Events"
|
||||||
msgstr "客戶端事件"
|
msgstr "用戶端活動"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.module.module:0
|
#: view:ir.module.module:0
|
||||||
|
@ -8429,7 +8431,7 @@ msgstr "關聯欄位"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.partner.event:0
|
#: view:res.partner.event:0
|
||||||
msgid "Event Logs"
|
msgid "Event Logs"
|
||||||
msgstr "事件日誌"
|
msgstr "活動日誌"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/base/module/wizard/base_module_configuration.py:37
|
#: 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.act_window,multi:0
|
||||||
#: field:ir.actions.wizard,multi:0
|
#: field:ir.actions.wizard,multi:0
|
||||||
msgid "Action on Multiple Doc."
|
msgid "Action on Multiple Doc."
|
||||||
msgstr "動作作用於多個文件"
|
msgstr "多重文件之動作。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:base.language.export:0
|
#: view:base.language.export:0
|
||||||
|
@ -8477,7 +8479,7 @@ msgstr "盧森堡"
|
||||||
#: help:ir.values,key2:0
|
#: help:ir.values,key2:0
|
||||||
msgid ""
|
msgid ""
|
||||||
"The kind of action or button in the client side that will trigger the action."
|
"The kind of action or button in the client side that will trigger the action."
|
||||||
msgstr "客戶端的該類動作或按鈕將觸發此動作。"
|
msgstr "用戶端的該類動作或按鈕會觸發此動作。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/base/ir/ir_ui_menu.py:285
|
#: code:addons/base/ir/ir_ui_menu.py:285
|
||||||
|
@ -8899,7 +8901,7 @@ msgstr "目前視窗"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.values:0
|
#: view:ir.values:0
|
||||||
msgid "Action Source"
|
msgid "Action Source"
|
||||||
msgstr "動作來源"
|
msgstr "動作來源地"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.config.view:0
|
#: view:res.config.view:0
|
||||||
|
@ -9048,7 +9050,7 @@ msgstr "本地化"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.actions.server:0
|
#: view:ir.actions.server:0
|
||||||
msgid "Action to Launch"
|
msgid "Action to Launch"
|
||||||
msgstr "要啟動動作"
|
msgstr "要執行之動作"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:ir.cron:0
|
#: view:ir.cron:0
|
||||||
|
@ -9086,7 +9088,7 @@ msgstr "另存為附件前綴"
|
||||||
msgid ""
|
msgid ""
|
||||||
"Only one client action will be executed, last client action will be "
|
"Only one client action will be executed, last client action will be "
|
||||||
"considered in case of multiple client actions."
|
"considered in case of multiple client actions."
|
||||||
msgstr "只能執行一個客戶端動作,如有多重動作只考慮最後一個。"
|
msgstr "只會執行一個用戶端動作,如有多重用戶端動作只考慮最後一個。"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: view:res.lang:0
|
#: view:res.lang:0
|
||||||
|
@ -9131,7 +9133,7 @@ msgstr "塞席爾"
|
||||||
#: model:ir.model,name:base.model_res_partner_bank
|
#: model:ir.model,name:base.model_res_partner_bank
|
||||||
#: view:res.partner.bank:0
|
#: view:res.partner.bank:0
|
||||||
msgid "Bank Accounts"
|
msgid "Bank Accounts"
|
||||||
msgstr "銀行帳戶"
|
msgstr "銀行帳號"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: model:res.country,name:base.sl
|
#: model:res.country,name:base.sl
|
||||||
|
@ -9152,7 +9154,7 @@ msgstr "土克斯及開科斯群島"
|
||||||
#. module: base
|
#. module: base
|
||||||
#: field:res.partner.bank,owner_name:0
|
#: field:res.partner.bank,owner_name:0
|
||||||
msgid "Account Owner"
|
msgid "Account Owner"
|
||||||
msgstr "帳戶所有者"
|
msgstr "帳號所有者"
|
||||||
|
|
||||||
#. module: base
|
#. module: base
|
||||||
#: code:addons/base/res/res_user.py:256
|
#: code:addons/base/res/res_user.py:256
|
||||||
|
|
|
@ -1274,7 +1274,7 @@
|
||||||
|
|
||||||
|
|
||||||
<record id="action_model_model" model="ir.actions.act_window">
|
<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="res_model">ir.model</field>
|
||||||
<field name="view_type">form</field>
|
<field name="view_type">form</field>
|
||||||
<field name="context">{'manual':True}</field>
|
<field name="context">{'manual':True}</field>
|
||||||
|
@ -1740,7 +1740,7 @@
|
||||||
<field name="name">Property multi-company</field>
|
<field name="name">Property multi-company</field>
|
||||||
<field model="ir.model" name="model_id" ref="model_ir_property"/>
|
<field model="ir.model" name="model_id" ref="model_ir_property"/>
|
||||||
<field eval="True" name="global"/>
|
<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>
|
</record>
|
||||||
|
|
||||||
<!--server action view-->
|
<!--server action view-->
|
||||||
|
@ -1766,7 +1766,9 @@
|
||||||
<page string="Trigger" attrs="{'invisible':[('state','!=','trigger')]}">
|
<page string="Trigger" attrs="{'invisible':[('state','!=','trigger')]}">
|
||||||
<separator colspan="4" string="Trigger Configuration"/>
|
<separator colspan="4" string="Trigger Configuration"/>
|
||||||
<field name="wkf_model_id" attrs="{'required':[('state','=','trigger')]}"/>
|
<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')]}"/>
|
<field name="trigger_name" attrs="{'required':[('state','=','trigger')]}"/>
|
||||||
</page>
|
</page>
|
||||||
<page string="Action to Launch" attrs="{'invisible':[('state','!=','client_action')]}">
|
<page string="Action to Launch" attrs="{'invisible':[('state','!=','client_action')]}">
|
||||||
|
|
|
@ -438,14 +438,15 @@ server_object_lines()
|
||||||
class actions_server(osv.osv):
|
class actions_server(osv.osv):
|
||||||
|
|
||||||
def _select_signals(self, cr, uid, context=None):
|
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 \
|
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!='' \
|
WHERE w.id = a.wkf_id AND
|
||||||
AND t.signal NOT IN (null, NULL)")
|
(t.act_from = a.id OR t.act_to = a.id) AND
|
||||||
|
t.signal IS NOT NULL""")
|
||||||
result = cr.fetchall() or []
|
result = cr.fetchall() or []
|
||||||
res = []
|
res = []
|
||||||
for rs in result:
|
for rs in result:
|
||||||
if rs[0] is not None and rs[1] is not None:
|
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)
|
res.append(line)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@ -456,13 +457,15 @@ class actions_server(osv.osv):
|
||||||
return [(r['model'], r['name']) for r in res] + [('','')]
|
return [(r['model'], r['name']) for r in res] + [('','')]
|
||||||
|
|
||||||
def change_object(self, cr, uid, ids, copy_object, state, context=None):
|
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_pool = self.pool.get('ir.model')
|
||||||
model = copy_object.split(',')[0]
|
model = copy_object.split(',')[0]
|
||||||
mid = model_pool.search(cr, uid, [('model','=',model)])
|
mid = model_pool.search(cr, uid, [('model','=',model)])
|
||||||
return {
|
return {
|
||||||
'value':{'srcmodel_id':mid[0]},
|
'value': {'srcmodel_id': mid[0]},
|
||||||
'context':context
|
'context': context
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {}
|
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."),
|
'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)."),
|
'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."),
|
'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."),
|
'trigger_name': fields.selection(_select_signals, string='Trigger Signal', size=128, help="The workflow signal to trigger"),
|
||||||
'wkf_model_id': fields.many2one('ir.model', 'Workflow On', help="Workflow to be executed on this model."),
|
'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','Trigger On', help="Select the object from the model on which the workflow will executed."),
|
'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"
|
'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'"),
|
"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 "
|
'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,
|
'sequence': lambda *a: 5,
|
||||||
'code': lambda *a: """# You can use the following variables:
|
'code': lambda *a: """# You can use the following variables:
|
||||||
# - self: ORM model of the record on which the action is triggered
|
# - 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)
|
# - pool: ORM model pool (i.e. self.pool)
|
||||||
# - time: Python time module
|
# - time: Python time module
|
||||||
# - cr: database cursor
|
# - cr: database cursor
|
||||||
|
@ -685,9 +688,10 @@ class actions_server(osv.osv):
|
||||||
if action.state == 'trigger':
|
if action.state == 'trigger':
|
||||||
wf_service = netsvc.LocalService("workflow")
|
wf_service = netsvc.LocalService("workflow")
|
||||||
model = action.wkf_model_id.model
|
model = action.wkf_model_id.model
|
||||||
res_id = obj_pool.read(cr, uid, [context.get('active_id')], [action.trigger_obj_id.name])
|
m2o_field_name = action.trigger_obj_id.name
|
||||||
id = res_id [0][action.trigger_obj_id.name]
|
target_id = obj_pool.read(cr, uid, context.get('active_id'), [m2o_field_name])[m2o_field_name]
|
||||||
wf_service.trg_validate(uid, model, int(id), action.trigger_name, cr)
|
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':
|
if action.state == 'sms':
|
||||||
#TODO: set the user and password from the system
|
#TODO: set the user and password from the system
|
||||||
|
|
|
@ -21,13 +21,20 @@
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
|
import psycopg2
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
import netsvc
|
import netsvc
|
||||||
import tools
|
import openerp
|
||||||
from tools.safe_eval import safe_eval as eval
|
|
||||||
import pooler
|
import pooler
|
||||||
|
import tools
|
||||||
|
from openerp.cron import WAKE_UP_NOW
|
||||||
from osv import fields, osv
|
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):
|
def str2tuple(s):
|
||||||
return eval('tuple(%s)' % (s or ''))
|
return eval('tuple(%s)' % (s or ''))
|
||||||
|
@ -41,10 +48,15 @@ _intervalTypes = {
|
||||||
'minutes': lambda interval: relativedelta(minutes=interval),
|
'minutes': lambda interval: relativedelta(minutes=interval),
|
||||||
}
|
}
|
||||||
|
|
||||||
class ir_cron(osv.osv, netsvc.Agent):
|
class ir_cron(osv.osv):
|
||||||
""" This is the ORM object that periodically executes actions.
|
""" Model describing cron jobs (also called actions or tasks).
|
||||||
Note that we use the netsvc.Agent()._logger member.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# 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"
|
_name = "ir.cron"
|
||||||
_order = 'name'
|
_order = 'name'
|
||||||
_columns = {
|
_columns = {
|
||||||
|
@ -54,17 +66,17 @@ class ir_cron(osv.osv, netsvc.Agent):
|
||||||
'interval_number': fields.integer('Interval Number',help="Repeat every x."),
|
'interval_number': fields.integer('Interval Number',help="Repeat every x."),
|
||||||
'interval_type': fields.selection( [('minutes', 'Minutes'),
|
'interval_type': fields.selection( [('minutes', 'Minutes'),
|
||||||
('hours', 'Hours'), ('work_days','Work Days'), ('days', 'Days'),('weeks', 'Weeks'), ('months', 'Months')], 'Interval Unit'),
|
('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'),
|
'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="Enable this if you want to execute missed occurences as soon as the server restarts."),
|
'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 scheduler"),
|
'nextcall' : fields.datetime('Next Execution Date', required=True, help="Next planned execution date for this job."),
|
||||||
'model': fields.char('Object', size=64, help="Name of object whose function will be called when this scheduler will run. e.g. 'res.partener'"),
|
'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('Function', size=64, help="Name of the method to be called on the object when this scheduler is executed."),
|
'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,)"),
|
'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')
|
'priority': fields.integer('Priority', help='The priority of the job, as an integer: 0 means higher priority, 10 means lower priority.')
|
||||||
}
|
}
|
||||||
|
|
||||||
_defaults = {
|
_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,
|
'priority' : lambda *a: 5,
|
||||||
'user_id' : lambda obj,cr,uid,context: uid,
|
'user_id' : lambda obj,cr,uid,context: uid,
|
||||||
'interval_number' : lambda *a: 1,
|
'interval_number' : lambda *a: 1,
|
||||||
|
@ -74,6 +86,8 @@ class ir_cron(osv.osv, netsvc.Agent):
|
||||||
'doall' : lambda *a: 1
|
'doall' : lambda *a: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger = logging.getLogger('cron')
|
||||||
|
|
||||||
def _check_args(self, cr, uid, ids, context=None):
|
def _check_args(self, cr, uid, ids, context=None):
|
||||||
try:
|
try:
|
||||||
for this in self.browse(cr, uid, ids, context):
|
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']),
|
(_check_args, 'Invalid arguments', ['args']),
|
||||||
]
|
]
|
||||||
|
|
||||||
def _handle_callback_exception(self, cr, uid, model, func, args, job_id, job_exception):
|
def _handle_callback_exception(self, cr, uid, model_name, method_name, args, job_id, job_exception):
|
||||||
cr.rollback()
|
""" Method called when an exception is raised by a job.
|
||||||
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 _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)
|
args = str2tuple(args)
|
||||||
m = self.pool.get(model)
|
model = self.pool.get(model_name)
|
||||||
if m and hasattr(m, func):
|
if model and hasattr(model, method_name):
|
||||||
f = getattr(m, func)
|
method = getattr(model, method_name)
|
||||||
try:
|
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')
|
depth=(None if self._logger.isEnabledFor(logging.DEBUG_RPC_ANSWER) else 1), fn='object.execute')
|
||||||
logger = logging.getLogger('execution time')
|
logger = logging.getLogger('execution time')
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
f(cr, uid, *args)
|
method(cr, uid, *args)
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
end_time = time.time()
|
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:
|
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:
|
try:
|
||||||
db, pool = pooler.get_db_and_pool(db_name)
|
nextcall = datetime.strptime(job['nextcall'], DEFAULT_SERVER_DATETIME_FORMAT)
|
||||||
except:
|
numbercall = job['numbercall']
|
||||||
return False
|
|
||||||
|
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()
|
cr = db.cursor()
|
||||||
|
db_name = db.dbname
|
||||||
try:
|
try:
|
||||||
if not pool._init:
|
jobs = {} # mapping job ids to jobs for all jobs being processed.
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
cr.execute('select * from ir_cron where numbercall<>0 and active and nextcall<=now() order by priority')
|
# Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1.
|
||||||
for job in cr.dictfetchall():
|
cr.execute("""SELECT * FROM ir_cron
|
||||||
nextcall = datetime.strptime(job['nextcall'], '%Y-%m-%d %H:%M:%S')
|
WHERE numbercall != 0
|
||||||
numbercall = job['numbercall']
|
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
|
task_cr = db.cursor()
|
||||||
while nextcall < now and numbercall:
|
try:
|
||||||
if numbercall > 0:
|
# Try to grab an exclusive lock on the job row from within the task transaction
|
||||||
numbercall -= 1
|
acquired_lock = False
|
||||||
if not ok or job['doall']:
|
task_cr.execute("""SELECT *
|
||||||
self._callback(cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
|
FROM ir_cron
|
||||||
if numbercall:
|
WHERE id=%s
|
||||||
nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
|
FOR UPDATE NOWAIT""",
|
||||||
ok = True
|
(job['id'],), log_exceptions=False)
|
||||||
addsql = ''
|
acquired_lock = True
|
||||||
if not numbercall:
|
except psycopg2.OperationalError, e:
|
||||||
addsql = ', active=False'
|
if e.pgcode == '55P03':
|
||||||
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']))
|
# Class 55: Object not in prerequisite state; 55P03: lock_not_available
|
||||||
cr.commit()
|
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')
|
# Find next earliest job ignoring currently processed jobs (by this and other cron threads)
|
||||||
next_call = cr.dictfetchone()['min_next_call']
|
find_next_time_query = """SELECT min(nextcall) AS min_next_call
|
||||||
if next_call:
|
FROM ir_cron WHERE numbercall != 0 AND active"""
|
||||||
next_call = time.mktime(time.strptime(next_call, '%Y-%m-%d %H:%M:%S'))
|
if jobs:
|
||||||
|
cr.execute(find_next_time_query + " AND id NOT IN %s", (tuple(jobs.keys()),))
|
||||||
else:
|
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:
|
if next_call:
|
||||||
self.setAlarm(self._poolJobs, next_call, db_name, db_name)
|
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:
|
except Exception, ex:
|
||||||
self._logger.warning('Exception in cron:', exc_info=True)
|
self._logger.warning('Exception in cron:', exc_info=True)
|
||||||
|
@ -156,12 +266,8 @@ class ir_cron(osv.osv, netsvc.Agent):
|
||||||
cr.commit()
|
cr.commit()
|
||||||
cr.close()
|
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):
|
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
|
# 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 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
|
# 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).
|
# when the server is only starting or loading modules (hence the test on pool._init).
|
||||||
if not self.pool._init:
|
if not self.pool._init:
|
||||||
cr.commit()
|
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):
|
def create(self, cr, uid, vals, context=None):
|
||||||
res = super(ir_cron, self).create(cr, uid, vals, context=context)
|
res = super(ir_cron, self).create(cr, uid, vals, context=context)
|
||||||
self.update_running_cron(cr)
|
self.update_running_cron(cr)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def write(self, cr, user, ids, vals, context=None):
|
def write(self, cr, uid, ids, vals, context=None):
|
||||||
res = super(ir_cron, self).write(cr, user, ids, vals, context=context)
|
self._try_lock(cr, uid, ids, context)
|
||||||
|
res = super(ir_cron, self).write(cr, uid, ids, vals, context=context)
|
||||||
self.update_running_cron(cr)
|
self.update_running_cron(cr)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def unlink(self, cr, uid, ids, context=None):
|
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)
|
res = super(ir_cron, self).unlink(cr, uid, ids, context=context)
|
||||||
self.update_running_cron(cr)
|
self.update_running_cron(cr)
|
||||||
return res
|
return res
|
||||||
ir_cron()
|
ir_cron()
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
||||||
|
|
|
@ -189,7 +189,7 @@ class ir_mail_server(osv.osv):
|
||||||
password=smtp_server.smtp_pass, encryption=smtp_server.smtp_encryption,
|
password=smtp_server.smtp_pass, encryption=smtp_server.smtp_encryption,
|
||||||
smtp_debug=smtp_server.smtp_debug)
|
smtp_debug=smtp_server.smtp_debug)
|
||||||
except Exception, e:
|
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:
|
finally:
|
||||||
try:
|
try:
|
||||||
if smtp: smtp.quit()
|
if smtp: smtp.quit()
|
||||||
|
|
|
@ -56,14 +56,14 @@ def _in_modules(self, cr, uid, ids, field_name, arg, context=None):
|
||||||
|
|
||||||
class ir_model(osv.osv):
|
class ir_model(osv.osv):
|
||||||
_name = 'ir.model'
|
_name = 'ir.model'
|
||||||
_description = "Objects"
|
_description = "Models"
|
||||||
_order = 'model'
|
_order = 'model'
|
||||||
|
|
||||||
def _is_osv_memory(self, cr, uid, ids, field_name, arg, context=None):
|
def _is_osv_memory(self, cr, uid, ids, field_name, arg, context=None):
|
||||||
models = self.browse(cr, uid, ids, context=context)
|
models = self.browse(cr, uid, ids, context=context)
|
||||||
res = dict.fromkeys(ids)
|
res = dict.fromkeys(ids)
|
||||||
for model in models:
|
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
|
return res
|
||||||
|
|
||||||
def _search_osv_memory(self, cr, uid, model, name, domain, context=None):
|
def _search_osv_memory(self, cr, uid, model, name, domain, context=None):
|
||||||
|
@ -85,8 +85,8 @@ class ir_model(osv.osv):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
_columns = {
|
_columns = {
|
||||||
'name': fields.char('Object Name', size=64, translate=True, required=True),
|
'name': fields.char('Model Description', size=64, translate=True, required=True),
|
||||||
'model': fields.char('Object', size=64, required=True, select=1),
|
'model': fields.char('Model', size=64, required=True, select=1),
|
||||||
'info': fields.text('Information'),
|
'info': fields.text('Information'),
|
||||||
'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True),
|
'field_id': fields.one2many('ir.model.fields', 'model_id', 'Fields', required=True),
|
||||||
'state': fields.selection([('manual','Custom Object'),('base','Base Object')],'Type',readonly=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'),
|
'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'),
|
'view_ids': fields.function(_view_ids, method=True, type='one2many', obj='ir.ui.view', string='Views'),
|
||||||
}
|
}
|
||||||
|
|
||||||
_defaults = {
|
_defaults = {
|
||||||
'model': lambda *a: 'x_',
|
'model': lambda *a: 'x_',
|
||||||
'state': lambda self,cr,uid,ctx=None: (ctx and ctx.get('manual',False)) and 'manual' or 'base',
|
'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):
|
def _check_model_name(self, cr, uid, ids, context=None):
|
||||||
for model in self.browse(cr, uid, ids, context=context):
|
for model in self.browse(cr, uid, ids, context=context):
|
||||||
if model.state=='manual':
|
if model.state=='manual':
|
||||||
|
@ -114,9 +114,13 @@ class ir_model(osv.osv):
|
||||||
|
|
||||||
def _model_name_msg(self, cr, uid, ids, context=None):
|
def _model_name_msg(self, cr, uid, ids, context=None):
|
||||||
return _('The Object name must start with x_ and not contain any special character !')
|
return _('The Object name must start with x_ and not contain any special character !')
|
||||||
|
|
||||||
_constraints = [
|
_constraints = [
|
||||||
(_check_model_name, _model_name_msg, ['model']),
|
(_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)
|
# overridden to allow searching both on model name (model field)
|
||||||
# and model description (name field)
|
# and model description (name field)
|
||||||
|
@ -161,7 +165,7 @@ class ir_model(osv.osv):
|
||||||
pass
|
pass
|
||||||
x_custom_model._name = model
|
x_custom_model._name = model
|
||||||
x_custom_model._module = False
|
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()):
|
if (not a._columns) or ('x_name' in a._columns.keys()):
|
||||||
x_name = 'x_name'
|
x_name = 'x_name'
|
||||||
else:
|
else:
|
||||||
|
@ -477,14 +481,12 @@ class ir_model_access(osv.osv):
|
||||||
|
|
||||||
if isinstance(model, browse_record):
|
if isinstance(model, browse_record):
|
||||||
assert model._table_name == 'ir.model', 'Invalid model object'
|
assert model._table_name == 'ir.model', 'Invalid model object'
|
||||||
model_name = model.name
|
model_name = model.model
|
||||||
else:
|
else:
|
||||||
model_name = model
|
model_name = model
|
||||||
|
|
||||||
# osv_memory objects can be read by everyone, as they only return
|
# TransientModel records have no access rights, only an implicit access rule
|
||||||
# results that belong to the current user (except for superuser)
|
if self.pool.get(model_name).is_transient():
|
||||||
model_obj = self.pool.get(model_name)
|
|
||||||
if isinstance(model_obj, osv.osv_memory):
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# We check if a specific rule exists
|
# 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) )
|
raise except_orm(_('AccessError'), msgs[mode] % (model_name, groups) )
|
||||||
return r
|
return r or False
|
||||||
|
|
||||||
__cache_clearing_methods = []
|
__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"""
|
"""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)])
|
ids = self.search(cr, uid, [('module','=',module), ('name','=', xml_id)])
|
||||||
if not ids:
|
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
|
# the sql constraints ensure us we have only one result
|
||||||
return ids[0]
|
return ids[0]
|
||||||
|
|
||||||
|
@ -626,7 +628,7 @@ class ir_model_data(osv.osv):
|
||||||
data_id = self._get_id(cr, uid, module, xml_id)
|
data_id = self._get_id(cr, uid, module, xml_id)
|
||||||
res = self.read(cr, uid, data_id, ['model', 'res_id'])
|
res = self.read(cr, uid, data_id, ['model', 'res_id'])
|
||||||
if not res['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'])
|
return (res['model'], res['res_id'])
|
||||||
|
|
||||||
def get_object(self, cr, uid, module, xml_id, context=None):
|
def get_object(self, cr, uid, module, xml_id, context=None):
|
||||||
|
|
|
@ -26,8 +26,7 @@ from functools import partial
|
||||||
import tools
|
import tools
|
||||||
from tools.safe_eval import safe_eval as eval
|
from tools.safe_eval import safe_eval as eval
|
||||||
from tools.misc import unquote as unquote
|
from tools.misc import unquote as unquote
|
||||||
|
from openerp import SUPERUSER_ID
|
||||||
SUPERUSER_UID = 1
|
|
||||||
|
|
||||||
class ir_rule(osv.osv):
|
class ir_rule(osv.osv):
|
||||||
_name = 'ir.rule'
|
_name = 'ir.rule'
|
||||||
|
@ -68,7 +67,7 @@ class ir_rule(osv.osv):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _check_model_obj(self, cr, uid, ids, context=None):
|
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 = {
|
_columns = {
|
||||||
'name': fields.char('Name', size=128, select=1),
|
'name': fields.char('Name', size=128, select=1),
|
||||||
|
@ -104,7 +103,7 @@ class ir_rule(osv.osv):
|
||||||
if mode not in self._MODES:
|
if mode not in self._MODES:
|
||||||
raise ValueError('Invalid mode: %r' % (mode,))
|
raise ValueError('Invalid mode: %r' % (mode,))
|
||||||
|
|
||||||
if uid == SUPERUSER_UID:
|
if uid == SUPERUSER_ID:
|
||||||
return None
|
return None
|
||||||
cr.execute("""SELECT r.id
|
cr.execute("""SELECT r.id
|
||||||
FROM ir_rule r
|
FROM ir_rule r
|
||||||
|
@ -117,10 +116,10 @@ class ir_rule(osv.osv):
|
||||||
rule_ids = [x[0] for x in cr.fetchall()]
|
rule_ids = [x[0] for x in cr.fetchall()]
|
||||||
if rule_ids:
|
if rule_ids:
|
||||||
# browse user as super-admin root to avoid access errors!
|
# 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
|
global_domains = [] # list of domains
|
||||||
group_domains = {} # map: group -> 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.
|
# read 'domain' as UID to have the correct eval context for the rule.
|
||||||
rule_domain = self.read(cr, uid, rule.id, ['domain'])['domain']
|
rule_domain = self.read(cr, uid, rule.id, ['domain'])['domain']
|
||||||
dom = expression.normalize(rule_domain)
|
dom = expression.normalize(rule_domain)
|
||||||
|
|
|
@ -96,6 +96,19 @@ class view(osv.osv):
|
||||||
if not cr.fetchone():
|
if not cr.fetchone():
|
||||||
cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, type, inherit_id)')
|
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={}):
|
def write(self, cr, uid, ids, vals, context={}):
|
||||||
if not isinstance(ids, (list, tuple)):
|
if not isinstance(ids, (list, tuple)):
|
||||||
ids = [ids]
|
ids = [ids]
|
||||||
|
@ -159,10 +172,10 @@ class view(osv.osv):
|
||||||
label_string = ""
|
label_string = ""
|
||||||
if label:
|
if label:
|
||||||
for lbl in eval(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 + ' '
|
label_string = label_string + ' '
|
||||||
else:
|
else:
|
||||||
label_string = label_string + " " + t[lbl]
|
label_string = label_string + " " + tools.ustr(t[lbl])
|
||||||
labels[str(t['id'])] = (a['id'],label_string)
|
labels[str(t['id'])] = (a['id'],label_string)
|
||||||
g = graph(nodes, transitions, no_ancester)
|
g = graph(nodes, transitions, no_ancester)
|
||||||
g.process(start)
|
g.process(start)
|
||||||
|
|
|
@ -19,18 +19,15 @@
|
||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
from osv import osv
|
import openerp
|
||||||
from osv.orm import orm_memory
|
|
||||||
|
|
||||||
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'
|
_name = 'osv_memory.autovacuum'
|
||||||
|
|
||||||
def power_on(self, cr, uid, context=None):
|
def power_on(self, cr, uid, context=None):
|
||||||
for model in self.pool.obj_list():
|
for model in self.pool.models.values():
|
||||||
obj = self.pool.get(model)
|
if model.is_transient():
|
||||||
if isinstance(obj, orm_memory):
|
model._transient_vacuum(cr, uid)
|
||||||
obj.vaccum(cr, uid)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
osv_memory_autovacuum()
|
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,5 @@
|
||||||
##############################################################################
|
##############################################################################
|
||||||
import wizard_menu
|
import wizard_menu
|
||||||
import wizard_screen
|
import wizard_screen
|
||||||
import create_action
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
||||||
|
|
|
@ -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')
|
|
||||||
|
|
||||||
|
|
|
@ -21,12 +21,5 @@
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</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"/>
|
<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>
|
</data>
|
||||||
</openerp>
|
</openerp>
|
||||||
|
|
|
@ -107,48 +107,59 @@ class module(osv.osv):
|
||||||
view_obj = self.pool.get('ir.ui.view')
|
view_obj = self.pool.get('ir.ui.view')
|
||||||
report_obj = self.pool.get('ir.actions.report.xml')
|
report_obj = self.pool.get('ir.actions.report.xml')
|
||||||
menu_obj = self.pool.get('ir.ui.menu')
|
menu_obj = self.pool.get('ir.ui.menu')
|
||||||
mlist = self.browse(cr, uid, ids, context=context)
|
|
||||||
mnames = {}
|
dmodels = []
|
||||||
for m in mlist:
|
if field_name is None or 'views_by_module' in field_name:
|
||||||
# skip uninstalled modules below,
|
dmodels.append('ir.ui.view')
|
||||||
# no data to find anyway
|
if field_name is None or 'reports_by_module' in field_name:
|
||||||
if m.state in ('installed', 'to upgrade', 'to remove'):
|
dmodels.append('ir.actions.report.xml')
|
||||||
mnames[m.name] = m.id
|
if field_name is None or 'menus_by_module' in field_name:
|
||||||
res[m.id] = {
|
dmodels.append('ir.ui.menu')
|
||||||
'menus_by_module':[],
|
assert dmodels, "no models for %s" % field_name
|
||||||
'reports_by_module':[],
|
|
||||||
|
for module_rec in self.browse(cr, uid, ids, context=context):
|
||||||
|
res[module_rec.id] = {
|
||||||
|
'menus_by_module': [],
|
||||||
|
'reports_by_module': [],
|
||||||
'views_by_module': []
|
'views_by_module': []
|
||||||
}
|
}
|
||||||
|
|
||||||
if not mnames:
|
# Skip uninstalled modules below, no data to find anyway.
|
||||||
return res
|
if module_rec.state not in ('installed', 'to upgrade', 'to remove'):
|
||||||
|
continue
|
||||||
|
|
||||||
view_id = model_data_obj.search(cr,uid,[('module','in', mnames.keys()),
|
# then, search and group ir.model.data records
|
||||||
('model','in',('ir.ui.view','ir.actions.report.xml','ir.ui.menu'))])
|
imd_models = dict( [(m,[]) for m in dmodels])
|
||||||
for data_id in model_data_obj.browse(cr,uid,view_id,context):
|
imd_ids = model_data_obj.search(cr,uid,[('module','=', module_rec.name),
|
||||||
# We use try except, because views or menus may not exist
|
('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:
|
try:
|
||||||
key = data_id.model
|
res_mod_dic = res[module_rec.id]
|
||||||
res_mod_dic = res[mnames[data_id.module]]
|
for v in view_obj.browse(cr, uid, imd_models.get('ir.ui.view', []), context=context):
|
||||||
if key=='ir.ui.view':
|
|
||||||
v = view_obj.browse(cr,uid,data_id.res_id)
|
|
||||||
aa = v.inherit_id and '* INHERIT ' or ''
|
aa = v.inherit_id and '* INHERIT ' or ''
|
||||||
res_mod_dic['views_by_module'].append(aa + v.name + '('+v.type+')')
|
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)
|
for rx in report_obj.browse(cr, uid, imd_models.get('ir.actions.report.xml', []), context=context):
|
||||||
elif key=='ir.ui.menu':
|
res_mod_dic['reports_by_module'].append(rx.name)
|
||||||
res_mod_dic['menus_by_module'].append(menu_obj.browse(cr,uid,data_id.res_id).complete_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:
|
except KeyError, e:
|
||||||
self.__logger.warning(
|
self.__logger.warning(
|
||||||
'Data not found for reference %s[%s:%s.%s]', data_id.model,
|
'Data not found for items of %s', module_rec.name)
|
||||||
data_id.res_id, data_id.model, data_id.name, exc_info=True)
|
except AttributeError, e:
|
||||||
pass
|
self.__logger.warning(
|
||||||
|
'Data not found for items of %s %s', module_rec.name, str(e))
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
self.__logger.warning('Unknown error while browsing %s[%s]',
|
self.__logger.warning('Unknown error while fetching data of %s',
|
||||||
data_id.model, data_id.res_id, exc_info=True)
|
module_rec.name, exc_info=True)
|
||||||
pass
|
|
||||||
for key, value in res.iteritems():
|
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))
|
res[key][k] = "\n".join(sorted(v))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@ -437,12 +448,11 @@ class module(osv.osv):
|
||||||
res.append(mod.url)
|
res.append(mod.url)
|
||||||
if not download:
|
if not download:
|
||||||
continue
|
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)
|
fname = addons.get_module_path(str(mod.name)+'.zip', downloaded=True)
|
||||||
try:
|
try:
|
||||||
fp = file(fname, 'wb')
|
with open(fname, 'wb') as fp:
|
||||||
fp.write(zipfile)
|
fp.write(zip_content)
|
||||||
fp.close()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.__logger.exception('Error when trying to create module '
|
self.__logger.exception('Error when trying to create module '
|
||||||
'file %s', fname)
|
'file %s', fname)
|
||||||
|
|
|
@ -35,9 +35,12 @@ class base_language_import(osv.osv_memory):
|
||||||
'name': fields.char('Language Name',size=64 , required=True),
|
'name': fields.char('Language Name',size=64 , required=True),
|
||||||
'code': fields.char('Code (eg:en__US)',size=5 , required=True),
|
'code': fields.char('Code (eg:en__US)',size=5 , required=True),
|
||||||
'data': fields.binary('File', 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
|
Import Language
|
||||||
@param cr: the current row, from the database cursor.
|
@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 ids: the ID or list of IDs
|
||||||
@param context: A standard dictionary
|
@param context: A standard dictionary
|
||||||
"""
|
"""
|
||||||
|
if context is None:
|
||||||
|
context = {}
|
||||||
import_data = self.browse(cr, uid, ids)[0]
|
import_data = self.browse(cr, uid, ids)[0]
|
||||||
|
if import_data.overwrite:
|
||||||
|
context.update(overwrite=True)
|
||||||
fileobj = TemporaryFile('w+')
|
fileobj = TemporaryFile('w+')
|
||||||
fileobj.write(base64.decodestring(import_data.data))
|
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'
|
fileformat = first_line.endswith("type,name,res_id,src,value") and 'csv' or 'po'
|
||||||
fileobj.seek(0)
|
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)
|
tools.trans_update_res_ids(cr)
|
||||||
fileobj.close()
|
fileobj.close()
|
||||||
return {}
|
return {}
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
<field name="name" width="200"/>
|
<field name="name" width="200"/>
|
||||||
<field name="code"/>
|
<field name="code"/>
|
||||||
<field name="data" colspan="4"/>
|
<field name="data" colspan="4"/>
|
||||||
|
<field name="overwrite"/>
|
||||||
</group>
|
</group>
|
||||||
<group colspan="8" col="8">
|
<group colspan="8" col="8">
|
||||||
<separator string="" colspan="8"/>
|
<separator string="" colspan="8"/>
|
||||||
|
|
|
@ -28,6 +28,8 @@ import base64
|
||||||
from tools.translate import _
|
from tools.translate import _
|
||||||
from osv import osv, fields
|
from osv import osv, fields
|
||||||
|
|
||||||
|
ADDONS_PATH = tools.config['addons_path'].split(",")[-1]
|
||||||
|
|
||||||
class base_module_import(osv.osv_memory):
|
class base_module_import(osv.osv_memory):
|
||||||
""" Import Module """
|
""" Import Module """
|
||||||
|
|
||||||
|
@ -37,7 +39,8 @@ class base_module_import(osv.osv_memory):
|
||||||
|
|
||||||
_columns = {
|
_columns = {
|
||||||
'module_file': fields.binary('Module .ZIP file', required=True),
|
'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),
|
'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):
|
def importzip(self, cr, uid, ids, context):
|
||||||
(data,) = self.browse(cr, uid, ids , context=context)
|
(data,) = self.browse(cr, uid, ids , context=context)
|
||||||
module_data = data.module_file
|
module_data = data.module_file
|
||||||
|
zip_data = base64.decodestring(module_data)
|
||||||
val = base64.decodestring(module_data)
|
|
||||||
fp = StringIO()
|
fp = StringIO()
|
||||||
fp.write(val)
|
fp.write(zip_data)
|
||||||
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')
|
|
||||||
try:
|
try:
|
||||||
fp = file(fname, 'wb')
|
file_data = zipfile.ZipFile(fp, 'r')
|
||||||
fp.write(val)
|
except zipfile.BadZipfile:
|
||||||
fp.close()
|
raise osv.except_osv(_('Error !'), _('File is not a zip file!'))
|
||||||
except IOError:
|
init_file_name = sorted(file_data.namelist())[0]
|
||||||
raise osv.except_osv(_('Error !'), _('Can not create the module file: %s !') % (fname,) )
|
module_name = os.path.split(init_file_name)[0]
|
||||||
|
|
||||||
self.pool.get('ir.module.module').update_list(cr, uid, {'module_name': module_name,})
|
file_path = os.path.join(ADDONS_PATH, '%s.zip' % module_name)
|
||||||
self.write(cr, uid, ids, {'state':'done', 'module_name': module_name}, context)
|
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
|
return False
|
||||||
|
|
||||||
def action_module_open(self, cr, uid, ids, context):
|
def action_module_open(self, cr, uid, ids, context):
|
||||||
|
@ -84,4 +91,4 @@ class base_module_import(osv.osv_memory):
|
||||||
base_module_import()
|
base_module_import()
|
||||||
|
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
|
@ -12,9 +12,10 @@
|
||||||
<group colspan="3" col="1">
|
<group colspan="3" col="1">
|
||||||
<field name="config_logo" widget="image" width="220" height="130" nolabel="1" colspan="1"/>
|
<field name="config_logo" widget="image" width="220" height="130" nolabel="1" colspan="1"/>
|
||||||
<newline/>
|
<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"/>
|
||||||
<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"/>
|
<field name="state" invisible="1"/>
|
||||||
</group>
|
</group>
|
||||||
<separator orientation="vertical" rowspan="5"/>
|
<separator orientation="vertical" rowspan="5"/>
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
help="Parameters that are used by all resources."
|
help="Parameters that are used by all resources."
|
||||||
domain="[('res_id','=',False)]"/>
|
domain="[('res_id','=',False)]"/>
|
||||||
<separator orientation="vertical"/>
|
<separator orientation="vertical"/>
|
||||||
|
<field name="fields_id" />
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="company_id" groups="base.group_multi_company"/>
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
</search>
|
</search>
|
||||||
|
|
|
@ -143,6 +143,9 @@ class res_company(osv.osv):
|
||||||
'vat': fields.related('partner_id', 'vat', string="Tax ID", type="char", size=32),
|
'vat': fields.related('partner_id', 'vat', string="Tax ID", type="char", size=32),
|
||||||
'company_registry': fields.char('Company Registry', size=64),
|
'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,
|
def _search(self, cr, uid, args, offset=0, limit=None, order=None,
|
||||||
context=None, count=False, access_rights_uid=None):
|
context=None, count=False, access_rights_uid=None):
|
||||||
|
@ -242,9 +245,7 @@ class res_company(osv.osv):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _get_logo(self, cr, uid, ids):
|
def _get_logo(self, cr, uid, ids):
|
||||||
return open(os.path.join(
|
return open(os.path.join( tools.config['root_path'], 'addons', 'base', 'res', 'res_company_logo.png'), 'rb') .read().encode('base64')
|
||||||
tools.config['root_path'], '..', 'pixmaps', 'your_logo.png'),
|
|
||||||
'rb') .read().encode('base64')
|
|
||||||
|
|
||||||
_header = """
|
_header = """
|
||||||
<header>
|
<header>
|
||||||
|
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
@ -62,17 +62,36 @@ class res_currency(osv.osv):
|
||||||
'active': fields.boolean('Active'),
|
'active': fields.boolean('Active'),
|
||||||
'company_id':fields.many2one('res.company', 'Company'),
|
'company_id':fields.many2one('res.company', 'Company'),
|
||||||
'date': fields.date('Date'),
|
'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 = {
|
_defaults = {
|
||||||
'active': lambda *a: 1,
|
'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"
|
_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'):
|
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')
|
currency_rate_obj = self.pool.get('res.currency.rate')
|
||||||
for r in res:
|
for r in res:
|
||||||
if r.__contains__('rate_ids'):
|
if r.__contains__('rate_ids'):
|
||||||
|
@ -150,7 +169,7 @@ res_currency()
|
||||||
|
|
||||||
class res_currency_rate_type(osv.osv):
|
class res_currency_rate_type(osv.osv):
|
||||||
_name = "res.currency.rate.type"
|
_name = "res.currency.rate.type"
|
||||||
_description = "Used to define the type of Currency Rates"
|
_description = "Currency Rate Type"
|
||||||
_columns = {
|
_columns = {
|
||||||
'name': fields.char('Name', size=64, required=True, translate=True),
|
'name': fields.char('Name', size=64, required=True, translate=True),
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,18 @@
|
||||||
<openerp>
|
<openerp>
|
||||||
<data>
|
<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">
|
<record id="view_currency_tree" model="ir.ui.view">
|
||||||
<field name="name">res.currency.tree</field>
|
<field name="name">res.currency.tree</field>
|
||||||
<field name="model">res.currency</field>
|
<field name="model">res.currency</field>
|
||||||
|
@ -9,12 +21,13 @@
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<tree string="Currencies">
|
<tree string="Currencies">
|
||||||
<field name="name"/>
|
<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="rate_ids" invisible="1"/>
|
||||||
<field name="date"/>
|
<field name="date"/>
|
||||||
<field name="rate"/>
|
<field name="rate"/>
|
||||||
<field name="rounding"/>
|
<field name="rounding"/>
|
||||||
<field name="accuracy"/>
|
<field name="accuracy"/>
|
||||||
|
<field name="position"/>
|
||||||
<field name="active"/>
|
<field name="active"/>
|
||||||
</tree>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
|
@ -25,23 +38,30 @@
|
||||||
<field name="type">form</field>
|
<field name="type">form</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="Currency">
|
<form string="Currency">
|
||||||
<group col="6" colspan="6">
|
<group col="6" colspan="4">
|
||||||
<field name="name" select="1"/>
|
<field name="name"/>
|
||||||
<field name="rate"/>
|
<field name="rate"/>
|
||||||
<field name="company_id" select="2" groups="base.group_multi_company" />
|
<field name="company_id" groups="base.group_multi_company"/>
|
||||||
<field name="symbol"/>
|
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group col="2" colspan="2">
|
<group col="6" colspan="4">
|
||||||
<separator string="Price Accuracy" colspan="2"/>
|
<group col="2" colspan="2">
|
||||||
<field name="rounding"/>
|
<separator string="Price Accuracy" colspan="2"/>
|
||||||
<field name="accuracy"/>
|
<field name="rounding"/>
|
||||||
</group>
|
<field name="accuracy"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
<group col="2" colspan="2">
|
<group col="2" colspan="2">
|
||||||
<separator string="Miscelleanous" colspan="2"/>
|
<separator string="Display" colspan="2"/>
|
||||||
<field name="base"/>
|
<field name="symbol"/>
|
||||||
<field name="active" select="1"/>
|
<field name="position"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group col="2" colspan="2">
|
||||||
|
<separator string="Miscelleanous" colspan="2"/>
|
||||||
|
<field name="base"/>
|
||||||
|
<field name="active" select="1"/>
|
||||||
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<field colspan="4" mode="tree,form" name="rate_ids" nolabel="1" attrs="{'readonly':[('base','=',True)]}">
|
<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="res_model">res.currency</field>
|
||||||
<field name="view_type">form</field>
|
<field name="view_type">form</field>
|
||||||
<field name="view_mode">tree,form</field>
|
<field name="view_mode">tree,form</field>
|
||||||
|
<field name="search_view_id" ref="view_currency_search"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<menuitem action="action_currency_form" id="menu_action_currency_form" parent="menu_localisation" sequence="3"/>
|
<menuitem action="action_currency_form" id="menu_action_currency_form" parent="menu_localisation" sequence="3"/>
|
||||||
|
|
|
@ -90,36 +90,44 @@
|
||||||
|
|
||||||
<record id="res_partner_asus" model="res.partner">
|
<record id="res_partner_asus" model="res.partner">
|
||||||
<field name="name">ASUStek</field>
|
<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 eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
||||||
<field name="supplier">1</field>
|
<field name="supplier">1</field>
|
||||||
|
<field eval="0" name="customer"/>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
|
<field name="website">www.asustek.com</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_agrolait" model="res.partner">
|
<record id="res_partner_agrolait" model="res.partner">
|
||||||
<field name="name">Agrolait</field>
|
<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="address" eval="[]"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
|
<field name="website">www.agrolait.com</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_c2c" model="res.partner">
|
<record id="res_partner_c2c" model="res.partner">
|
||||||
<field name="name">Camptocamp</field>
|
<field name="name">Camptocamp</field>
|
||||||
<field eval="[(6, 0, [ref('res_partner_category_10'), ref('res_partner_category_5')])]" name="category_id"/>
|
<field eval="[(6, 0, [ref('res_partner_category_10'), ref('res_partner_category_5')])]" name="category_id"/>
|
||||||
<field name="supplier">1</field>
|
<field name="supplier">1</field>
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
|
<field name="website">www.camptocamp.com</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_sednacom" model="res.partner">
|
<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 name="name">Syleam</field>
|
||||||
<field eval="[(6, 0, [ref('res_partner_category_5')])]" name="category_id"/>
|
<field eval="[(6, 0, [ref('res_partner_category_5')])]" name="category_id"/>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
|
<field name="user_id" ref="user_demo"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_thymbra" model="res.partner">
|
<record id="res_partner_thymbra" model="res.partner">
|
||||||
<field name="name">Thymbra</field>
|
<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 eval="[(6, 0, [ref('res_partner_category_4')])]" name="category_id"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
|
<field name="website">www.thymbra.com/</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_desertic_hispafuentes" model="res.partner">
|
<record id="res_partner_desertic_hispafuentes" model="res.partner">
|
||||||
<field name="name">Axelor</field>
|
<field name="name">Axelor</field>
|
||||||
|
@ -127,41 +135,54 @@
|
||||||
<field name="supplier">1</field>
|
<field name="supplier">1</field>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
|
<field name="user_id" ref="user_demo"/>
|
||||||
|
<field name="website">www.axelor.com/</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_tinyatwork" model="res.partner">
|
<record id="res_partner_tinyatwork" model="res.partner">
|
||||||
<field name="name">Tiny AT Work</field>
|
<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 eval="[(6, 0, [ref('res_partner_category_5'), ref('res_partner_category_10')])]" name="category_id"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
|
<field name="website">www.tinyatwork.com/</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_2" model="res.partner">
|
<record id="res_partner_2" model="res.partner">
|
||||||
<field name="name">Bank Wealthy and sons</field>
|
<field name="name">Bank Wealthy and sons</field>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
|
<field name="website">www.wealthyandsons.com/</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_3" model="res.partner">
|
<record id="res_partner_3" model="res.partner">
|
||||||
<field name="name">China Export</field>
|
<field name="name">China Export</field>
|
||||||
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
|
<field name="website">www.chinaexport.com/</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_4" model="res.partner">
|
<record id="res_partner_4" model="res.partner">
|
||||||
<field name="name">Distrib PC</field>
|
<field name="name">Distrib PC</field>
|
||||||
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
||||||
<field name="supplier">1</field>
|
<field name="supplier">1</field>
|
||||||
|
<field eval="0" name="customer"/>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
|
<field name="website">www.distribpc.com/</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_5" model="res.partner">
|
<record id="res_partner_5" model="res.partner">
|
||||||
<field name="name">Ecole de Commerce de Liege</field>
|
<field name="name">Ecole de Commerce de Liege</field>
|
||||||
<field eval="[(6, 0, [ref('res_partner_category_1')])]" name="category_id"/>
|
<field eval="[(6, 0, [ref('res_partner_category_1')])]" name="category_id"/>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
|
<field name="user_id" ref="user_demo"/>
|
||||||
|
<field name="website">www.eci-liege.info//</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_6" model="res.partner">
|
<record id="res_partner_6" model="res.partner">
|
||||||
<field name="name">Elec Import</field>
|
<field name="name">Elec Import</field>
|
||||||
<field name="user_id" ref="user_demo"/>
|
<field name="user_id" ref="user_demo"/>
|
||||||
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
||||||
<field name="supplier">1</field>
|
<field name="supplier">1</field>
|
||||||
|
<field eval="0" name="customer"/>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
</record>
|
</record>
|
||||||
|
@ -171,6 +192,7 @@
|
||||||
<field name="user_id" ref="user_demo"/>
|
<field name="user_id" ref="user_demo"/>
|
||||||
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
||||||
<field name="supplier">1</field>
|
<field name="supplier">1</field>
|
||||||
|
<field eval="0" name="customer"/>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
</record>
|
</record>
|
||||||
|
@ -192,11 +214,11 @@
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_9" model="res.partner">
|
<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 name="name">BalmerInc S.A.</field>
|
||||||
<field eval="12000.00" name="credit_limit"/>
|
<field eval="12000.00" name="credit_limit"/>
|
||||||
<field name="ref">or</field>
|
<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 eval="[(6, 0, [ref('res_partner_category_1')])]" name="category_id"/>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
|
@ -207,6 +229,7 @@
|
||||||
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
|
<field name="user_id" ref="user_demo"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_11" model="res.partner">
|
<record id="res_partner_11" model="res.partner">
|
||||||
<field name="name">Leclerc</field>
|
<field name="name">Leclerc</field>
|
||||||
|
@ -224,6 +247,7 @@
|
||||||
<field eval="[(6, 0, [ref('res_partner_category_11')])]" name="category_id"/>
|
<field eval="[(6, 0, [ref('res_partner_category_11')])]" name="category_id"/>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
|
<field name="user_id" ref="user_demo"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_15" model="res.partner">
|
<record id="res_partner_15" model="res.partner">
|
||||||
<field name="name">Magazin BML 1</field>
|
<field name="name">Magazin BML 1</field>
|
||||||
|
@ -240,6 +264,8 @@
|
||||||
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
<field eval="[(6, 0, [ref('res_partner_category_9')])]" name="category_id"/>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
<field name="opt_out" eval="True"/>
|
<field name="opt_out" eval="True"/>
|
||||||
|
<field name="user_id" ref="user_demo"/>
|
||||||
|
<field name="website">http://www.ulg.ac.be/</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
@ -251,16 +277,19 @@
|
||||||
<field model="res.users" name="user_id" search="[('name', '=', u'Thomas Lebrun')]"/>
|
<field model="res.users" name="user_id" search="[('name', '=', u'Thomas Lebrun')]"/>
|
||||||
<field name="name">Dubois sprl</field>
|
<field name="name">Dubois sprl</field>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
|
<field name="website">http://www.dubois.be/</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="res_partner_ericdubois0" model="res.partner">
|
<record id="res_partner_ericdubois0" model="res.partner">
|
||||||
<field name="name">Eric Dubois</field>
|
<field name="name">Eric Dubois</field>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
|
<field name="user_id" ref="user_demo"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="res_partner_fabiendupont0" model="res.partner">
|
<record id="res_partner_fabiendupont0" model="res.partner">
|
||||||
<field name="name">Fabien Dupont</field>
|
<field name="name">Fabien Dupont</field>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="res_partner_lucievonck0" model="res.partner">
|
<record id="res_partner_lucievonck0" model="res.partner">
|
||||||
|
@ -271,32 +300,41 @@
|
||||||
<record id="res_partner_notsotinysarl0" model="res.partner">
|
<record id="res_partner_notsotinysarl0" model="res.partner">
|
||||||
<field name="name">NotSoTiny SARL</field>
|
<field name="name">NotSoTiny SARL</field>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
|
<field name="website">notsotiny.be</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="res_partner_theshelvehouse0" model="res.partner">
|
<record id="res_partner_theshelvehouse0" model="res.partner">
|
||||||
<field name="name">The Shelve House</field>
|
<field name="name">The Shelve House</field>
|
||||||
<field eval="[(6,0,[ref('res_partner_category_retailers0')])]" name="category_id"/>
|
<field eval="[(6,0,[ref('res_partner_category_retailers0')])]" name="category_id"/>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="res_partner_vickingdirect0" model="res.partner">
|
<record id="res_partner_vickingdirect0" model="res.partner">
|
||||||
<field name="name">Vicking Direct</field>
|
<field name="name">Vicking Direct</field>
|
||||||
<field eval="[(6,0,[ref('res_partner_category_miscellaneoussuppliers0')])]" name="category_id"/>
|
<field eval="[(6,0,[ref('res_partner_category_miscellaneoussuppliers0')])]" name="category_id"/>
|
||||||
<field name="supplier">1</field>
|
<field name="supplier">1</field>
|
||||||
|
<field name="customer">0</field>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
|
<field name="website">vicking-direct.be</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="res_partner_woodywoodpecker0" model="res.partner">
|
<record id="res_partner_woodywoodpecker0" model="res.partner">
|
||||||
<field name="name">Wood y Wood Pecker</field>
|
<field name="name">Wood y Wood Pecker</field>
|
||||||
<field eval="[(6,0,[ref('res_partner_category_woodsuppliers0')])]" name="category_id"/>
|
<field eval="[(6,0,[ref('res_partner_category_woodsuppliers0')])]" name="category_id"/>
|
||||||
<field name="supplier">1</field>
|
<field name="supplier">1</field>
|
||||||
|
<field eval="0" name="customer"/>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
|
<field name="website">woodywoodpecker.com</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="res_partner_zerooneinc0" model="res.partner">
|
<record id="res_partner_zerooneinc0" model="res.partner">
|
||||||
<field name="name">ZeroOne Inc</field>
|
<field name="name">ZeroOne Inc</field>
|
||||||
<field eval="[(6,0,[ref('res_partner_category_consumers0')])]" name="category_id"/>
|
<field eval="[(6,0,[ref('res_partner_category_consumers0')])]" name="category_id"/>
|
||||||
<field name="address" eval="[]"/>
|
<field name="address" eval="[]"/>
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
|
<field name="website">http://www.zerooneinc.com/</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
@ -333,6 +371,7 @@
|
||||||
<field name="email">info@axelor.com</field>
|
<field name="email">info@axelor.com</field>
|
||||||
<field name="phone">+33 1 64 61 04 01</field>
|
<field name="phone">+33 1 64 61 04 01</field>
|
||||||
<field name="street">12 rue Albert Einstein</field>
|
<field name="street">12 rue Albert Einstein</field>
|
||||||
|
<field name="type">default</field>
|
||||||
<field name="partner_id" ref="res_partner_desertic_hispafuentes"/>
|
<field name="partner_id" ref="res_partner_desertic_hispafuentes"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_address_3" model="res.partner.address">
|
<record id="res_partner_address_3" model="res.partner.address">
|
||||||
|
@ -350,6 +389,8 @@
|
||||||
<field name="zip">23410</field>
|
<field name="zip">23410</field>
|
||||||
<field model="res.country" name="country_id" search="[('name','=','Taiwan')]"/>
|
<field model="res.country" name="country_id" search="[('name','=','Taiwan')]"/>
|
||||||
<field name="street">31 Hong Kong street</field>
|
<field name="street">31 Hong Kong street</field>
|
||||||
|
<field name="email">info@asustek.com</field>
|
||||||
|
<field name="phone">+ 1 64 61 04 01</field>
|
||||||
<field name="type">default</field>
|
<field name="type">default</field>
|
||||||
<field name="partner_id" ref="res_partner_asus"/>
|
<field name="partner_id" ref="res_partner_asus"/>
|
||||||
</record>
|
</record>
|
||||||
|
@ -359,6 +400,8 @@
|
||||||
<field name="zip">23540</field>
|
<field name="zip">23540</field>
|
||||||
<field model="res.country" name="country_id" search="[('name','=','China')]"/>
|
<field model="res.country" name="country_id" search="[('name','=','China')]"/>
|
||||||
<field name="street">56 Beijing street</field>
|
<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="type">default</field>
|
||||||
<field name="partner_id" ref="res_partner_maxtor"/>
|
<field name="partner_id" ref="res_partner_maxtor"/>
|
||||||
</record>
|
</record>
|
||||||
|
@ -369,6 +412,8 @@
|
||||||
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
||||||
<field name="street">23 rue du Vieux Bruges</field>
|
<field name="street">23 rue du Vieux Bruges</field>
|
||||||
<field name="type">default</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"/>
|
<field name="partner_id" ref="res_partner_6"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_address_7" model="res.partner.address">
|
<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 model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
||||||
<field name="street">42 rue de la Lesse</field>
|
<field name="street">42 rue de la Lesse</field>
|
||||||
<field name="type">default</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"/>
|
<field name="partner_id" ref="res_partner_4"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_address_8" model="res.partner.address">
|
<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 model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
||||||
<field name="street">69 rue de Chimay</field>
|
<field name="street">69 rue de Chimay</field>
|
||||||
<field name="type">default</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="partner_id" ref="res_partner_agrolait"/>
|
||||||
|
<field name="title" ref="base.res_partner_title_madam"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_address_8delivery" model="res.partner.address">
|
<record id="res_partner_address_8delivery" model="res.partner.address">
|
||||||
<field name="city">Wavre</field>
|
<field name="city">Wavre</field>
|
||||||
|
@ -396,7 +446,10 @@
|
||||||
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
||||||
<field name="street">71 rue de Chimay</field>
|
<field name="street">71 rue de Chimay</field>
|
||||||
<field name="type">delivery</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="partner_id" ref="res_partner_agrolait"/>
|
||||||
|
<field name="title" ref="base.res_partner_title_sir"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_address_8invoice" model="res.partner.address">
|
<record id="res_partner_address_8invoice" model="res.partner.address">
|
||||||
<field name="city">Wavre</field>
|
<field name="city">Wavre</field>
|
||||||
|
@ -405,7 +458,10 @@
|
||||||
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
||||||
<field name="street">69 rue de Chimay</field>
|
<field name="street">69 rue de Chimay</field>
|
||||||
<field name="type">invoice</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="partner_id" ref="res_partner_agrolait"/>
|
||||||
|
<field name="title" ref="base.res_partner_title_sir"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_address_9" model="res.partner.address">
|
<record id="res_partner_address_9" model="res.partner.address">
|
||||||
<field name="city">Paris</field>
|
<field name="city">Paris</field>
|
||||||
|
@ -414,7 +470,10 @@
|
||||||
<field model="res.country" name="country_id" search="[('name','=','France')]"/>
|
<field model="res.country" name="country_id" search="[('name','=','France')]"/>
|
||||||
<field name="street">1 rue Rockfeller</field>
|
<field name="street">1 rue Rockfeller</field>
|
||||||
<field name="type">default</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="partner_id" ref="res_partner_2"/>
|
||||||
|
<field name="title" ref="base.res_partner_title_sir"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_address_11" model="res.partner.address">
|
<record id="res_partner_address_11" model="res.partner.address">
|
||||||
<field name="city">Alencon</field>
|
<field name="city">Alencon</field>
|
||||||
|
@ -433,48 +492,84 @@
|
||||||
<field name="zip">6985</field>
|
<field name="zip">6985</field>
|
||||||
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
<field model="res.country" name="country_id" search="[('name','=','Belgium')]"/>
|
||||||
<field name="street">2 Impasse de la Soif</field>
|
<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="type">default</field>
|
||||||
<field name="partner_id" ref="res_partner_5"/>
|
<field name="partner_id" ref="res_partner_5"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_address_zen" model="res.partner.address">
|
<record id="res_partner_address_zen" model="res.partner.address">
|
||||||
<field name="city">Shanghai</field>
|
<field name="city">Shanghai</field>
|
||||||
<field name="name">Zen</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 model="res.country" name="country_id" search="[('name','=','China')]"/>
|
||||||
<field name="street">52 Chop Suey street</field>
|
<field name="street">52 Chop Suey street</field>
|
||||||
<field name="type">default</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"/>
|
<field name="partner_id" ref="res_partner_3"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_address_12" model="res.partner.address">
|
<record id="res_partner_address_12" model="res.partner.address">
|
||||||
<field name="type">default</field>
|
<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"/>
|
<field name="partner_id" ref="res_partner_10"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_address_13" model="res.partner.address">
|
<record id="res_partner_address_13" model="res.partner.address">
|
||||||
<field name="type">default</field>
|
<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"/>
|
<field name="partner_id" ref="res_partner_14"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_address_14" model="res.partner.address">
|
<record id="res_partner_address_14" model="res.partner.address">
|
||||||
<field name="type">default</field>
|
<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"/>
|
<field name="partner_id" ref="res_partner_15"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_address_15" model="res.partner.address">
|
<record id="res_partner_address_15" model="res.partner.address">
|
||||||
<field name="type">default</field>
|
<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"/>
|
<field name="partner_id" ref="res_partner_11"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_address_16" model="res.partner.address">
|
<record id="res_partner_address_16" model="res.partner.address">
|
||||||
<field name="type">default</field>
|
<field name="type">invoice</field>
|
||||||
<field name="name">Shop 3</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"/>
|
<field name="partner_id" ref="res_partner_11"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="res_partner_address_accent" model="res.partner.address">
|
<record id="res_partner_address_accent" model="res.partner.address">
|
||||||
<field name="type">default</field>
|
<field name="type">default</field>
|
||||||
<field name="city">Liège</field>
|
<field name="name">Martine Ohio</field>
|
||||||
<field name="street">Université de Liège</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"/>
|
<field name="partner_id" ref="res_partner_accent"/>
|
||||||
</record>
|
</record>
|
||||||
<record id="res_partner_address_Camptocamp" model="res.partner.address">
|
<record id="res_partner_address_Camptocamp" model="res.partner.address">
|
||||||
|
@ -493,6 +588,8 @@
|
||||||
<field name="zip">95014</field>
|
<field name="zip">95014</field>
|
||||||
<field model="res.country" name="country_id" search="[('name','=','United States')]"/>
|
<field model="res.country" name="country_id" search="[('name','=','United States')]"/>
|
||||||
<field name="street">10200 S. De Anza Blvd</field>
|
<field name="street">10200 S. De Anza Blvd</field>
|
||||||
|
<field name="email">info@seagate.com</field>
|
||||||
|
<field name="phone">+1 408 256987</field>
|
||||||
<field name="type">default</field>
|
<field name="type">default</field>
|
||||||
<field name="partner_id" ref="res_partner_seagate"/>
|
<field name="partner_id" ref="res_partner_seagate"/>
|
||||||
</record>
|
</record>
|
||||||
|
@ -573,7 +670,12 @@
|
||||||
|
|
||||||
<record id="res_partner_address_brussels0" model="res.partner.address">
|
<record id="res_partner_address_brussels0" model="res.partner.address">
|
||||||
<field eval="'Brussels'" name="city"/>
|
<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="partner_id" ref="res_partner_vickingdirect0"/>
|
||||||
<field name="country_id" ref="base.be"/>
|
<field name="country_id" ref="base.be"/>
|
||||||
</record>
|
</record>
|
||||||
|
@ -583,6 +685,7 @@
|
||||||
<field eval="'Kainuu'" name="city"/>
|
<field eval="'Kainuu'" name="city"/>
|
||||||
<field eval="'Roger Pecker'" name="name"/>
|
<field eval="'Roger Pecker'" name="name"/>
|
||||||
<field name="partner_id" ref="res_partner_woodywoodpecker0"/>
|
<field name="partner_id" ref="res_partner_woodywoodpecker0"/>
|
||||||
|
<field eval="'(+358).9.589 689'" name="phone"/>
|
||||||
<field name="country_id" ref="base.fi"/>
|
<field name="country_id" ref="base.fi"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
@ -618,10 +721,13 @@
|
||||||
|
|
||||||
<record id="res_partner_address_ericdubois0" model="res.partner.address">
|
<record id="res_partner_address_ericdubois0" model="res.partner.address">
|
||||||
<field eval="'Mons'" name="city"/>
|
<field eval="'Mons'" name="city"/>
|
||||||
|
<field eval="'Eric Dubois'" name="name"/>
|
||||||
<field eval="'7000'" name="zip"/>
|
<field eval="'7000'" name="zip"/>
|
||||||
<field name="partner_id" ref="res_partner_ericdubois0"/>
|
<field name="partner_id" ref="res_partner_ericdubois0"/>
|
||||||
<field name="country_id" ref="base.be"/>
|
<field name="country_id" ref="base.be"/>
|
||||||
<field eval="'Chaussée de Binche, 27'" name="street"/>
|
<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>
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ from osv import fields,osv
|
||||||
from osv.orm import browse_record
|
from osv.orm import browse_record
|
||||||
from service import security
|
from service import security
|
||||||
from tools.translate import _
|
from tools.translate import _
|
||||||
|
import openerp.exceptions
|
||||||
|
|
||||||
class groups(osv.osv):
|
class groups(osv.osv):
|
||||||
_name = "res.groups"
|
_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)
|
# 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):
|
def write(self, cr, uid, ids, values, context=None):
|
||||||
if not hasattr(ids, '__iter__'):
|
if not hasattr(ids, '__iter__'):
|
||||||
|
@ -437,14 +438,14 @@ class users(osv.osv):
|
||||||
if passwd == tools.config['admin_passwd']:
|
if passwd == tools.config['admin_passwd']:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
raise security.ExceptionNoTb('AccessDenied')
|
raise openerp.exceptions.AccessDenied()
|
||||||
|
|
||||||
def check(self, db, uid, passwd):
|
def check(self, db, uid, passwd):
|
||||||
"""Verifies that the given (uid, password) pair is authorized for the database ``db`` and
|
"""Verifies that the given (uid, password) pair is authorized for the database ``db`` and
|
||||||
raise an exception if it is not."""
|
raise an exception if it is not."""
|
||||||
if not passwd:
|
if not passwd:
|
||||||
# empty passwords disallowed for obvious security reasons
|
# empty passwords disallowed for obvious security reasons
|
||||||
raise security.ExceptionNoTb('AccessDenied')
|
raise openerp.exceptions.AccessDenied()
|
||||||
if self._uid_cache.get(db, {}).get(uid) == passwd:
|
if self._uid_cache.get(db, {}).get(uid) == passwd:
|
||||||
return
|
return
|
||||||
cr = pooler.get_db(db).cursor()
|
cr = pooler.get_db(db).cursor()
|
||||||
|
@ -453,7 +454,7 @@ class users(osv.osv):
|
||||||
(int(uid), passwd, True))
|
(int(uid), passwd, True))
|
||||||
res = cr.fetchone()[0]
|
res = cr.fetchone()[0]
|
||||||
if not res:
|
if not res:
|
||||||
raise security.ExceptionNoTb('AccessDenied')
|
raise openerp.exceptions.AccessDenied()
|
||||||
if self._uid_cache.has_key(db):
|
if self._uid_cache.has_key(db):
|
||||||
ulist = self._uid_cache[db]
|
ulist = self._uid_cache[db]
|
||||||
ulist[uid] = passwd
|
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))
|
cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd))
|
||||||
res = cr.fetchone()
|
res = cr.fetchone()
|
||||||
if not res:
|
if not res:
|
||||||
raise security.ExceptionNoTb('Bad username or password')
|
raise openerp.exceptions.AccessDenied()
|
||||||
return res[0]
|
return res[0]
|
||||||
finally:
|
finally:
|
||||||
cr.close()
|
cr.close()
|
||||||
|
@ -481,7 +482,7 @@ class users(osv.osv):
|
||||||
password is not used to authenticate requests.
|
password is not used to authenticate requests.
|
||||||
|
|
||||||
:return: True
|
: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
|
:raise: except_osv when new password is not set or empty
|
||||||
"""
|
"""
|
||||||
self.check(cr.dbname, uid, old_passwd)
|
self.check(cr.dbname, uid, old_passwd)
|
||||||
|
@ -553,7 +554,7 @@ class users_implied(osv.osv):
|
||||||
_inherit = 'res.users'
|
_inherit = 'res.users'
|
||||||
|
|
||||||
def create(self, cr, uid, values, context=None):
|
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)
|
user_id = super(users_implied, self).create(cr, uid, values, context)
|
||||||
if groups:
|
if groups:
|
||||||
# delegate addition of groups to add implied 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)
|
self._process_values_groups(cr, uid, values, context)
|
||||||
return super(users_view, self).write(cr, uid, ids, 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:
|
if not fields:
|
||||||
group_fields, fields = [], self.fields_get(cr, uid, context).keys()
|
group_fields, fields = [], self.fields_get(cr, uid, context).keys()
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -42,28 +42,32 @@
|
||||||
<field name="groups_id" eval="[(6,0, [ref('group_system'), ref('group_erp_manager')])]"/>
|
<field name="groups_id" eval="[(6,0, [ref('group_system'), ref('group_erp_manager')])]"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record model="ir.rule" id="res_widget_user_rule">
|
</data>
|
||||||
<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">
|
<data noupdate="1">
|
||||||
<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">
|
<record model="ir.rule" id="res_widget_user_rule">
|
||||||
<field name="name">Multi_company_default company</field>
|
<field name="name">res.widget.user rule</field>
|
||||||
<field name="model_id" ref="model_multi_company_default"/>
|
<field name="model_id" ref="model_res_widget_user"/>
|
||||||
<field name="global" eval="True"/>
|
<field name="global" eval="True"/>
|
||||||
<field name="domain_force">[('company_id','child_of',[user.company_id.id])]</field>
|
<field name="domain_force">['|', ('user_id','=',user.id),('user_id','=',False)]</field>
|
||||||
</record>
|
</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>
|
</data>
|
||||||
</openerp>
|
</openerp>
|
||||||
|
|
|
@ -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_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_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_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_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_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
|
"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_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_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_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
|
||||||
|
|
|
|
@ -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:
|
|
@ -144,45 +144,3 @@
|
||||||
!python {model: res.partner.category}: |
|
!python {model: res.partner.category}: |
|
||||||
self.pool._init = True
|
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"
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
|
@ -177,6 +177,251 @@
|
||||||
res_ids = self.search(cr, uid, [('company_id.partner_id', 'not in', [])])
|
res_ids = self.search(cr, uid, [('company_id.partner_id', 'not in', [])])
|
||||||
res_ids.sort()
|
res_ids.sort()
|
||||||
assert res_ids == all_ids, "Searching against empty set failed, returns %r" % res_ids
|
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.
|
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')]
|
domain = [('x','in',['y','z']),('a.v','=','e'),'|','|',('a','=','b'),'!',('c','>','d'),('e','!=','f'),('g','=','h')]
|
||||||
norm_domain = ['&','&','&'] + domain
|
norm_domain = ['&','&','&'] + domain
|
||||||
assert norm_domain == expression.normalize(domain), "Non-normalized domains should be properly normalized"
|
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')
|
||||||
|
|
||||||
|
|
|
@ -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
|
and provide real Python variables, e.g. addons_paths is really a list
|
||||||
of paths.
|
of paths.
|
||||||
|
|
||||||
|
To initialize properly this module, openerp.tools.config.parse_config()
|
||||||
|
must be used.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import deprecation
|
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:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
|
@ -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:
|
|
@ -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:
|
|
@ -21,6 +21,7 @@
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
import openerp.modules
|
import openerp.modules
|
||||||
|
import logging
|
||||||
|
|
||||||
def is_initialized(cr):
|
def is_initialized(cr):
|
||||||
""" Check if a database has been initialized for the ORM.
|
""" 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')
|
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)
|
base_sql_file = openerp.tools.misc.file_open(f)
|
||||||
try:
|
try:
|
||||||
cr.execute(base_sql_file.read())
|
cr.execute(base_sql_file.read())
|
||||||
|
@ -118,4 +123,14 @@ def create_categories(cr, categories):
|
||||||
categories = categories[1:]
|
categories = categories[1:]
|
||||||
return p_id
|
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:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
|
@ -58,16 +58,16 @@ class Graph(dict):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def add_node(self, name, deps):
|
def add_node(self, name, info):
|
||||||
max_depth, father = 0, None
|
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:
|
if n.depth >= max_depth:
|
||||||
father = n
|
father = n
|
||||||
max_depth = n.depth
|
max_depth = n.depth
|
||||||
if father:
|
if father:
|
||||||
return father.add_child(name)
|
return father.add_child(name, info)
|
||||||
else:
|
else:
|
||||||
return Node(name, self)
|
return Node(name, self, info)
|
||||||
|
|
||||||
def update_from_db(self, cr):
|
def update_from_db(self, cr):
|
||||||
if not len(self):
|
if not len(self):
|
||||||
|
@ -120,7 +120,7 @@ class Graph(dict):
|
||||||
continue
|
continue
|
||||||
later.clear()
|
later.clear()
|
||||||
current.remove(package)
|
current.remove(package)
|
||||||
node = self.add_node(package, deps)
|
node = self.add_node(package, info)
|
||||||
node.data = info
|
node.data = info
|
||||||
for kind in ('init', 'demo', 'update'):
|
for kind in ('init', 'demo', 'update'):
|
||||||
if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force:
|
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):
|
class Singleton(object):
|
||||||
def __new__(cls, name, graph):
|
def __new__(cls, name, graph, info):
|
||||||
if name in graph:
|
if name in graph:
|
||||||
inst = graph[name]
|
inst = graph[name]
|
||||||
else:
|
else:
|
||||||
inst = object.__new__(cls)
|
inst = object.__new__(cls)
|
||||||
inst.name = name
|
inst.name = name
|
||||||
|
inst.info = info
|
||||||
graph[name] = inst
|
graph[name] = inst
|
||||||
return inst
|
return inst
|
||||||
|
|
||||||
|
@ -167,19 +168,21 @@ class Singleton(object):
|
||||||
class Node(Singleton):
|
class Node(Singleton):
|
||||||
""" One module in the modules dependency graph.
|
""" 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
|
self.graph = graph
|
||||||
if not hasattr(self, 'children'):
|
if not hasattr(self, 'children'):
|
||||||
self.children = []
|
self.children = []
|
||||||
if not hasattr(self, 'depth'):
|
if not hasattr(self, 'depth'):
|
||||||
self.depth = 0
|
self.depth = 0
|
||||||
|
|
||||||
def add_child(self, name):
|
def add_child(self, name, info):
|
||||||
node = Node(name, self.graph)
|
node = Node(name, self.graph, info)
|
||||||
node.depth = self.depth + 1
|
node.depth = self.depth + 1
|
||||||
if node not in self.children:
|
if node not in self.children:
|
||||||
self.children.append(node)
|
self.children.append(node)
|
||||||
|
|
|
@ -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)
|
logger.info('module %s: loading objects', package.name)
|
||||||
migrations.migrate_module(package, 'pre')
|
migrations.migrate_module(package, 'pre')
|
||||||
register_module_classes(package.name)
|
register_module_classes(package.name)
|
||||||
models = pool.instanciate(package.name, cr)
|
models = pool.load(cr, package)
|
||||||
loaded_modules.append(package.name)
|
loaded_modules.append(package.name)
|
||||||
if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
|
if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
|
||||||
init_module_models(cr, package.name, models)
|
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()
|
# This is a brand new pool, just created in pooler.get_db_and_pool()
|
||||||
pool = pooler.get_pool(cr.dbname)
|
pool = pooler.get_pool(cr.dbname)
|
||||||
|
|
||||||
processed_modules = [] # for cleanup step after install
|
|
||||||
loaded_modules = [] # to avoid double loading
|
|
||||||
report = tools.assertion_report()
|
report = tools.assertion_report()
|
||||||
if 'base' in tools.config['update'] or 'all' in tools.config['update']:
|
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'))
|
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:
|
if not graph:
|
||||||
logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)')
|
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)'))
|
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']:
|
if tools.config['load_language']:
|
||||||
for lang in tools.config['load_language'].split(','):
|
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)""")
|
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():
|
for (model, name) in cr.fetchall():
|
||||||
model_obj = pool.get(model)
|
model_obj = pool.get(model)
|
||||||
if model_obj and not isinstance(model_obj, osv.osv.osv_memory):
|
if model_obj and not model_obj.is_transient():
|
||||||
logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name))
|
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
|
# Temporary warning while we remove access rights on osv_memory objects, as they have
|
||||||
# been replaced by owner-only access rights
|
# 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""")
|
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():
|
for (model, name) in cr.fetchall():
|
||||||
model_obj = pool.get(model)
|
model_obj = pool.get(model)
|
||||||
if isinstance(model_obj, osv.osv.osv_memory) and not isinstance(model_obj, osv.osv.osv):
|
if model_obj and model_obj.is_transient():
|
||||||
logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name))
|
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")
|
cr.execute("SELECT model from ir_model")
|
||||||
for (model,) in cr.fetchall():
|
for (model,) in cr.fetchall():
|
||||||
|
@ -356,7 +356,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
|
||||||
if obj:
|
if obj:
|
||||||
obj._check_removed_columns(cr, log=True)
|
obj._check_removed_columns(cr, log=True)
|
||||||
else:
|
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
|
# Cleanup orphan records
|
||||||
pool.get('ir.model.data')._process_end(cr, 1, processed_modules)
|
pool.get('ir.model.data')._process_end(cr, 1, processed_modules)
|
||||||
|
|
|
@ -31,7 +31,6 @@ import openerp.osv as osv
|
||||||
import openerp.tools as tools
|
import openerp.tools as tools
|
||||||
import openerp.tools.osutil as osutil
|
import openerp.tools.osutil as osutil
|
||||||
from openerp.tools.safe_eval import safe_eval as eval
|
from openerp.tools.safe_eval import safe_eval as eval
|
||||||
import openerp.pooler as pooler
|
|
||||||
from openerp.tools.translate import _
|
from openerp.tools.translate import _
|
||||||
|
|
||||||
import openerp.netsvc as netsvc
|
import openerp.netsvc as netsvc
|
||||||
|
@ -58,6 +57,11 @@ loaded = []
|
||||||
logger = netsvc.Logger()
|
logger = netsvc.Logger()
|
||||||
|
|
||||||
def initialize_sys_path():
|
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
|
global ad_paths
|
||||||
|
|
||||||
if 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['license'] = info.get('license') or 'AGPL-3'
|
||||||
info.setdefault('installable', True)
|
info.setdefault('installable', True)
|
||||||
info.setdefault('active', False)
|
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',
|
for kind in ['data', 'demo', 'test',
|
||||||
'init_xml', 'update_xml', 'demo_xml']:
|
'init_xml', 'update_xml', 'demo_xml']:
|
||||||
info.setdefault(kind, [])
|
info.setdefault(kind, [])
|
||||||
|
@ -271,7 +277,6 @@ def init_module_models(cr, module_name, obj_list):
|
||||||
TODO better explanation of _auto_init and init.
|
TODO better explanation of _auto_init and init.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.notifyChannel('init', netsvc.LOG_INFO,
|
logger.notifyChannel('init', netsvc.LOG_INFO,
|
||||||
'module %s: creating or updating database tables' % module_name)
|
'module %s: creating or updating database tables' % module_name)
|
||||||
todo = []
|
todo = []
|
||||||
|
@ -290,23 +295,22 @@ def init_module_models(cr, module_name, obj_list):
|
||||||
t[1](cr, *t[2])
|
t[1](cr, *t[2])
|
||||||
cr.commit()
|
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):
|
#sys.meta_path.append(MyImportHook())
|
||||||
""" 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()
|
|
||||||
|
|
||||||
|
|
||||||
def register_module_classes(m):
|
def register_module_classes(m):
|
||||||
""" Register module named m, if not already registered.
|
""" Register module named m, if not already registered.
|
||||||
|
|
||||||
This will load the module and register all of its models. (Actually, the
|
This loads the module and register all of its models, thanks to either
|
||||||
explicit constructor call of each of the models inside the module will
|
the MetaModel metaclass, or the explicit instantiation of the model.
|
||||||
register them.)
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -326,7 +330,7 @@ def register_module_classes(m):
|
||||||
try:
|
try:
|
||||||
zip_mod_path = mod_path + '.zip'
|
zip_mod_path = mod_path + '.zip'
|
||||||
if not os.path.isfile(zip_mod_path):
|
if not os.path.isfile(zip_mod_path):
|
||||||
load_module(m)
|
__import__(m)
|
||||||
else:
|
else:
|
||||||
zimp = zipimport.zipimporter(zip_mod_path)
|
zimp = zipimport.zipimporter(zip_mod_path)
|
||||||
zimp.load_module(m)
|
zimp.load_module(m)
|
||||||
|
|
|
@ -22,10 +22,16 @@
|
||||||
""" Models registries.
|
""" Models registries.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
import openerp.sql_db
|
import openerp.sql_db
|
||||||
import openerp.osv.orm
|
import openerp.osv.orm
|
||||||
|
import openerp.cron
|
||||||
|
import openerp.tools
|
||||||
|
import openerp.modules.db
|
||||||
|
import openerp.tools.config
|
||||||
|
|
||||||
class Registry(object):
|
class Registry(object):
|
||||||
""" Model registry for a particular database.
|
""" Model registry for a particular database.
|
||||||
|
@ -44,6 +50,14 @@ class Registry(object):
|
||||||
self.db_name = db_name
|
self.db_name = db_name
|
||||||
self.db = openerp.sql_db.db_connect(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):
|
def do_parent_store(self, cr):
|
||||||
for o in self._init_parent:
|
for o in self._init_parent:
|
||||||
self.get(o)._parent_store_compute(cr)
|
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 a model for a given name or raise KeyError if it doesn't exist."""
|
||||||
return self.models[model_name]
|
return self.models[model_name]
|
||||||
|
|
||||||
def instanciate(self, module, cr):
|
def load(self, cr, module):
|
||||||
""" Instanciate all the classes of a given module for a particular db."""
|
""" 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 = []
|
res = []
|
||||||
|
|
||||||
# Instantiate registered classes (via metamodel discovery or via explicit
|
# Instantiate registered classes (via the MetaModel automatic discovery
|
||||||
# constructor call), and add them to the pool.
|
# or via explicit constructor call), and add them to the pool.
|
||||||
for cls in openerp.osv.orm.MetaModel.module_to_models.get(module, []):
|
for cls in openerp.osv.orm.MetaModel.module_to_models.get(module.name, []):
|
||||||
res.append(cls.createInstance(self, cr))
|
res.append(cls.create_instance(self, cr))
|
||||||
|
|
||||||
return res
|
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):
|
def clear_caches(self):
|
||||||
""" Clear the caches
|
""" Clear the caches
|
||||||
|
|
||||||
This clears the caches associated to methods decorated with
|
This clears the caches associated to methods decorated with
|
||||||
``tools.ormcache`` or ``tools.ormcache_multi`` for all the models.
|
``tools.ormcache`` or ``tools.ormcache_multi`` for all the models.
|
||||||
"""
|
"""
|
||||||
for model in self.models.itervalues():
|
for model in self.models.itervalues():
|
||||||
model.clear_caches()
|
model.clear_caches()
|
||||||
|
|
||||||
|
|
||||||
class RegistryManager(object):
|
class RegistryManager(object):
|
||||||
""" Model registries manager.
|
""" Model registries manager.
|
||||||
|
|
||||||
|
@ -94,24 +122,22 @@ class RegistryManager(object):
|
||||||
registries (essentially database connection/model registry pairs).
|
registries (essentially database connection/model registry pairs).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Mapping between db name and model registry.
|
# Mapping between db name and model registry.
|
||||||
# Accessed through the methods below.
|
# Accessed through the methods below.
|
||||||
registries = {}
|
registries = {}
|
||||||
|
registries_lock = threading.RLock()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, db_name, force_demo=False, status=None, update_module=False,
|
def get(cls, db_name, force_demo=False, status=None, update_module=False,
|
||||||
pooljobs=True):
|
pooljobs=True):
|
||||||
""" Return a registry for a given database name."""
|
""" Return a registry for a given database name."""
|
||||||
|
with cls.registries_lock:
|
||||||
if db_name in cls.registries:
|
if db_name in cls.registries:
|
||||||
registry = cls.registries[db_name]
|
registry = cls.registries[db_name]
|
||||||
else:
|
else:
|
||||||
registry = cls.new(db_name, force_demo, status,
|
registry = cls.new(db_name, force_demo, status,
|
||||||
update_module, pooljobs)
|
update_module, pooljobs)
|
||||||
return registry
|
return registry
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def new(cls, db_name, force_demo=False, status=None,
|
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.
|
The (possibly) previous registry for that database name is discarded.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import openerp.modules
|
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
|
# Initializing a registry will call general code which will in turn
|
||||||
# call registries.get (this object) to obtain the registry being
|
# call registries.get (this object) to obtain the registry being
|
||||||
# initialized. Make it available in the registries dictionary then
|
# initialized. Make it available in the registries dictionary then
|
||||||
# remove it if an exception is raised.
|
# remove it if an exception is raised.
|
||||||
cls.delete(db_name)
|
cls.delete(db_name)
|
||||||
cls.registries[db_name] = registry
|
cls.registries[db_name] = registry
|
||||||
try:
|
try:
|
||||||
# This should be a method on Registry
|
# This should be a method on Registry
|
||||||
openerp.modules.load_modules(registry.db, force_demo, status, update_module)
|
openerp.modules.load_modules(registry.db, force_demo, status, update_module)
|
||||||
except Exception:
|
except Exception:
|
||||||
del cls.registries[db_name]
|
del cls.registries[db_name]
|
||||||
raise
|
raise
|
||||||
|
|
||||||
cr = registry.db.cursor()
|
cr = registry.db.cursor()
|
||||||
try:
|
try:
|
||||||
registry.do_parent_store(cr)
|
registry.do_parent_store(cr)
|
||||||
registry.get('ir.actions.report.xml').register_all(cr)
|
registry.get('ir.actions.report.xml').register_all(cr)
|
||||||
cr.commit()
|
cr.commit()
|
||||||
finally:
|
finally:
|
||||||
cr.close()
|
cr.close()
|
||||||
|
|
||||||
if pooljobs:
|
if pooljobs:
|
||||||
registry.get('ir.cron').restart(registry.db.dbname)
|
registry.schedule_cron_jobs()
|
||||||
|
|
||||||
return registry
|
|
||||||
|
|
||||||
|
return registry
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def delete(cls, db_name):
|
def delete(cls, db_name):
|
||||||
""" Delete the registry linked to a given database. """
|
"""Delete the registry linked to a given database.
|
||||||
if db_name in cls.registries:
|
|
||||||
del cls.registries[db_name]
|
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
|
@classmethod
|
||||||
def clear_caches(cls, db_name):
|
def clear_caches(cls, db_name):
|
||||||
""" Clear the caches
|
"""Clear caches
|
||||||
|
|
||||||
This clears the caches associated to methods decorated with
|
This clears the caches associated to methods decorated with
|
||||||
``tools.ormcache`` or ``tools.ormcache_multi`` for all the models
|
``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)``
|
This method is given to spare you a ``RegistryManager.get(db_name)``
|
||||||
that would loads the given database if it was not already loaded.
|
that would loads the given database if it was not already loaded.
|
||||||
"""
|
"""
|
||||||
if db_name in cls.registries:
|
with cls.registries_lock:
|
||||||
cls.registries[db_name].clear_caches()
|
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:
|
|
@ -21,7 +21,6 @@
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
import heapq
|
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import os
|
import os
|
||||||
|
@ -31,12 +30,14 @@ import socket
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
import types
|
import types
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
|
||||||
# TODO modules that import netsvc only for things from loglevels must be changed to use loglevels.
|
# TODO modules that import netsvc only for things from loglevels must be changed to use loglevels.
|
||||||
from loglevels import *
|
from loglevels import *
|
||||||
import tools
|
import tools
|
||||||
|
import openerp
|
||||||
|
|
||||||
def close_socket(sock):
|
def close_socket(sock):
|
||||||
""" Closes a socket instance cleanly
|
""" Closes a socket instance cleanly
|
||||||
|
@ -60,20 +61,22 @@ def close_socket(sock):
|
||||||
#.apidoc title: Common Services: netsvc
|
#.apidoc title: Common Services: netsvc
|
||||||
#.apidoc module-mods: member-order: bysource
|
#.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):
|
class Service(object):
|
||||||
""" Base class for *Local* services
|
""" Base class for *Local* services
|
||||||
|
|
||||||
Functionality here is trusted, no authentication.
|
Functionality here is trusted, no authentication.
|
||||||
"""
|
"""
|
||||||
_services = {}
|
_services = {}
|
||||||
def __init__(self, name, audience=''):
|
def __init__(self, name):
|
||||||
Service._services[name] = self
|
Service._services[name] = self
|
||||||
self.__name = name
|
self.__name = name
|
||||||
self._methods = {}
|
|
||||||
|
|
||||||
def joinGroup(self, name):
|
|
||||||
raise Exception("No group for local services")
|
|
||||||
#GROUPS.setdefault(name, {})[self.__name] = self
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def exists(cls, name):
|
def exists(cls, name):
|
||||||
|
@ -84,75 +87,39 @@ class Service(object):
|
||||||
if cls.exists(name):
|
if cls.exists(name):
|
||||||
cls._services.pop(name)
|
cls._services.pop(name)
|
||||||
|
|
||||||
def exportMethod(self, method):
|
def LocalService(name):
|
||||||
if callable(method):
|
# Special case for addons support, will be removed in a few days when addons
|
||||||
self._methods[method.__name__] = method
|
# 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):
|
return Service._services[name]
|
||||||
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)
|
|
||||||
|
|
||||||
class ExportService(object):
|
class ExportService(object):
|
||||||
""" Proxy for exported services.
|
""" 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
|
Note that this class has no direct proxy, capable of calling
|
||||||
eservice.method(). Rather, the proxy should call
|
eservice.method(). Rather, the proxy should call
|
||||||
dispatch(method,auth,params)
|
dispatch(method, params)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_services = {}
|
_services = {}
|
||||||
_groups = {}
|
|
||||||
_logger = logging.getLogger('web-services')
|
_logger = logging.getLogger('web-services')
|
||||||
|
|
||||||
def __init__(self, name, audience=''):
|
def __init__(self, name):
|
||||||
ExportService._services[name] = self
|
ExportService._services[name] = self
|
||||||
self.__name = name
|
self.__name = name
|
||||||
self._logger.debug("Registered an exported service: %s" % name)
|
self._logger.debug("Registered an exported service: %s" % name)
|
||||||
|
|
||||||
def joinGroup(self, name):
|
|
||||||
ExportService._groups.setdefault(name, {})[self.__name] = self
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def getService(cls,name):
|
def getService(cls,name):
|
||||||
return cls._services[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)
|
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)
|
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
|
#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
|
#These are the sequences need to get colored ouput
|
||||||
|
@ -244,83 +211,6 @@ def init_alternative_logger():
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
logger.setLevel(logging.ERROR)
|
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:
|
class Server:
|
||||||
""" Generic interface for all servers with an event loop etc.
|
""" Generic interface for all servers with an event loop etc.
|
||||||
Override this to impement http, net-rpc etc. servers.
|
Override this to impement http, net-rpc etc. servers.
|
||||||
|
@ -403,11 +293,6 @@ class Server:
|
||||||
def _close_socket(self):
|
def _close_socket(self):
|
||||||
close_socket(self.socket)
|
close_socket(self.socket)
|
||||||
|
|
||||||
class OpenERPDispatcherException(Exception):
|
|
||||||
def __init__(self, exception, traceback):
|
|
||||||
self.exception = exception
|
|
||||||
self.traceback = traceback
|
|
||||||
|
|
||||||
def replace_request_password(args):
|
def replace_request_password(args):
|
||||||
# password is always 3rd argument in a request, we replace it in RPC logs
|
# 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...
|
# 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)
|
logger.log(channel, indent+line)
|
||||||
indent=indent_after
|
indent=indent_after
|
||||||
|
|
||||||
class OpenERPDispatcher:
|
def dispatch_rpc(service_name, method, params):
|
||||||
def log(self, title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""):
|
""" 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)
|
log(title, msg, channel=channel, depth=depth, fn=fn)
|
||||||
def dispatch(self, service_name, method, params):
|
try:
|
||||||
try:
|
logger = logging.getLogger('result')
|
||||||
auth = getattr(self, 'auth_provider', None)
|
start_time = end_time = 0
|
||||||
logger = logging.getLogger('result')
|
if logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
|
||||||
start_time = end_time = 0
|
_log('service', tuple(replace_request_password(params)), depth=None, fn='%s.%s'%(service_name,method))
|
||||||
if logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
|
if logger.isEnabledFor(logging.DEBUG_RPC):
|
||||||
self.log('service', tuple(replace_request_password(params)), depth=None, fn='%s.%s'%(service_name,method))
|
start_time = time.time()
|
||||||
if logger.isEnabledFor(logging.DEBUG_RPC):
|
result = ExportService.getService(service_name).dispatch(method, params)
|
||||||
start_time = time.time()
|
if logger.isEnabledFor(logging.DEBUG_RPC):
|
||||||
result = ExportService.getService(service_name).dispatch(method, auth, params)
|
end_time = time.time()
|
||||||
if logger.isEnabledFor(logging.DEBUG_RPC):
|
if not logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
|
||||||
end_time = time.time()
|
_log('service (%.3fs)' % (end_time - start_time), tuple(replace_request_password(params)), depth=1, fn='%s.%s'%(service_name,method))
|
||||||
if not logger.isEnabledFor(logging.DEBUG_RPC_ANSWER):
|
_log('execution time', '%.3fs' % (end_time - start_time), channel=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))
|
_log('result', result, channel=logging.DEBUG_RPC_ANSWER)
|
||||||
self.log('execution time', '%.3fs' % (end_time - start_time), channel=logging.DEBUG_RPC_ANSWER)
|
return result
|
||||||
self.log('result', result, channel=logging.DEBUG_RPC_ANSWER)
|
except openerp.exceptions.AccessError:
|
||||||
return result
|
raise
|
||||||
except Exception, e:
|
except openerp.exceptions.AccessDenied:
|
||||||
self.log('exception', tools.exception_to_unicode(e))
|
raise
|
||||||
tb = getattr(e, 'traceback', sys.exc_info())
|
except openerp.exceptions.Warning:
|
||||||
tb_s = "".join(traceback.format_exception(*tb))
|
raise
|
||||||
if tools.config['debug_mode'] and isinstance(tb[2], types.TracebackType):
|
except openerp.exceptions.DeferredException, e:
|
||||||
import pdb
|
_log('exception', tools.exception_to_unicode(e))
|
||||||
pdb.post_mortem(tb[2])
|
post_mortem(e.traceback)
|
||||||
raise OpenERPDispatcherException(e, tb_s)
|
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:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
|
@ -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
|
from openerp.tools import flatten, reverse_enumerate
|
||||||
import fields
|
import fields
|
||||||
|
import openerp.modules
|
||||||
|
from openerp.osv.orm import MAGIC_COLUMNS
|
||||||
|
|
||||||
#.apidoc title: Domain Expressions
|
#.apidoc title: Domain Expressions
|
||||||
|
|
||||||
|
# Domain operators.
|
||||||
NOT_OPERATOR = '!'
|
NOT_OPERATOR = '!'
|
||||||
OR_OPERATOR = '|'
|
OR_OPERATOR = '|'
|
||||||
AND_OPERATOR = '&'
|
AND_OPERATOR = '&'
|
||||||
|
DOMAIN_OPERATORS = (NOT_OPERATOR, OR_OPERATOR, AND_OPERATOR)
|
||||||
|
|
||||||
TRUE_DOMAIN = [(1,'=',1)]
|
# List of available term operators. It is also possible to use the '<>'
|
||||||
FALSE_DOMAIN = [(0,'=',1)]
|
# 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):
|
def normalize(domain):
|
||||||
"""Returns a normalized version of ``domain_expr``, where all implicit '&' operators
|
"""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}
|
op_arity = {NOT_OPERATOR: 1, AND_OPERATOR: 2, OR_OPERATOR: 2}
|
||||||
for token in domain:
|
for token in domain:
|
||||||
if expected == 0: # more than expected, like in [A, B]
|
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
|
expected = 1
|
||||||
result.append(token)
|
result.append(token)
|
||||||
if isinstance(token, (list,tuple)): # domain term
|
if isinstance(token, (list, tuple)): # domain term
|
||||||
expected -= 1
|
expected -= 1
|
||||||
else:
|
else:
|
||||||
expected += op_arity.get(token, 0) - 1
|
expected += op_arity.get(token, 0) - 1
|
||||||
|
@ -57,7 +186,8 @@ def normalize(domain):
|
||||||
|
|
||||||
def combine(operator, unit, zero, domains):
|
def combine(operator, unit, zero, domains):
|
||||||
"""Returns a new domain expression where all domain components from ``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
|
: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
|
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``.
|
combined with any domain ``x`` via ``operator``, yields ``z``.
|
||||||
E.g. [(1,'=',1)] is the typical zero for OR_OPERATOR: as soon as
|
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.
|
you see it in a domain component the resulting domain is the zero.
|
||||||
|
:param domains: a list of normalized domains.
|
||||||
"""
|
"""
|
||||||
result = []
|
result = []
|
||||||
count = 0
|
count = 0
|
||||||
|
@ -84,13 +215,130 @@ def combine(operator, unit, zero, domains):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def AND(domains):
|
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)
|
return combine(AND_OPERATOR, TRUE_DOMAIN, FALSE_DOMAIN, domains)
|
||||||
|
|
||||||
def OR(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)
|
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):
|
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
|
For more info: http://christophe-simonis-at-tiny.blogspot.com/2008/08/new-new-domain-notation.html
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
def __init__(self, cr, uid, exp, table, context):
|
||||||
def _is_operator(cls, element):
|
self.has_unaccent = openerp.modules.registry.RegistryManager.get(cr.dbname).has_unaccent
|
||||||
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
|
|
||||||
self.__field_tables = {} # used to store the table to use for the sql generation. key = index of the leaf
|
self.__field_tables = {} # used to store the table to use for the sql generation. key = index of the leaf
|
||||||
self.__all_tables = set()
|
self.__all_tables = set()
|
||||||
self.__joins = []
|
self.__joins = []
|
||||||
self.__main_table = None # 'root' table. set by parse()
|
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
|
@property
|
||||||
def exp(self):
|
def exp(self):
|
||||||
return self.__exp[:]
|
return self.__exp[:]
|
||||||
|
|
||||||
def parse(self, cr, uid, table, context):
|
def parse(self, cr, uid, exp, table, context):
|
||||||
""" transform the leafs of the expression """
|
""" transform the leaves of the expression """
|
||||||
if not self.__exp:
|
self.__exp = exp
|
||||||
return self
|
self.__main_table = table
|
||||||
|
self.__all_tables.add(table)
|
||||||
|
|
||||||
def _rec_get(ids, table, parent=None, left='id', prefix=''):
|
def child_of_domain(left, ids, left_model, parent=None, prefix=''):
|
||||||
if table._parent_store and (not table.pool._init):
|
"""Returns a domain implementing the child_of operator for [(left,child_of,ids)],
|
||||||
# TODO: Improve where joins are implemented for many with '.', replace by:
|
either as a range using the parent_left/right tree lookup fields (when available),
|
||||||
# doms += ['&',(prefix+'.parent_left','<',o.parent_right),(prefix+'.parent_left','>=',o.parent_left)]
|
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 = []
|
doms = []
|
||||||
for o in table.browse(cr, uid, ids, context=context):
|
for o in left_model.browse(cr, uid, ids, context=context):
|
||||||
if doms:
|
if doms:
|
||||||
doms.insert(0, OR_OPERATOR)
|
doms.insert(0, OR_OPERATOR)
|
||||||
doms += [AND_OPERATOR, ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)]
|
doms += [AND_OPERATOR, ('parent_left', '<', o.parent_right), ('parent_left', '>=', o.parent_left)]
|
||||||
if prefix:
|
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
|
return doms
|
||||||
else:
|
else:
|
||||||
def rg(ids, table, parent):
|
def recursive_children(ids, model, parent_field):
|
||||||
if not ids:
|
if not ids:
|
||||||
return []
|
return []
|
||||||
ids2 = table.search(cr, uid, [(parent, 'in', ids)], context=context)
|
ids2 = model.search(cr, uid, [(parent_field, 'in', ids)], context=context)
|
||||||
return ids + rg(ids2, table, parent)
|
return ids + recursive_children(ids2, model, parent_field)
|
||||||
return [(left, 'in', rg(ids, table, parent or table._parent_name))]
|
return [(left, 'in', recursive_children(ids, left_model, parent or left_model._parent_name))]
|
||||||
|
|
||||||
def child_of_right_to_ids(value):
|
def to_ids(value, field_obj):
|
||||||
""" Normalize a single id, or a string, or a list of ids to a list of ids.
|
"""Normalize a single id or name, or a list of those, into a list of ids"""
|
||||||
|
names = []
|
||||||
This function is always used with _rec_get() above, so it should be
|
|
||||||
called directly from _rec_get instead of repeatedly before _rec_get.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if isinstance(value, basestring):
|
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)):
|
elif isinstance(value, (int, long)):
|
||||||
return [value]
|
return [value]
|
||||||
else:
|
return list(value)
|
||||||
return list(value)
|
|
||||||
|
|
||||||
self.__main_table = table
|
|
||||||
self.__all_tables.add(table)
|
|
||||||
|
|
||||||
i = -1
|
i = -1
|
||||||
while i + 1<len(self.__exp):
|
while i + 1<len(self.__exp):
|
||||||
i += 1
|
i += 1
|
||||||
e = self.__exp[i]
|
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
|
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
|
left, operator, right = e
|
||||||
operator = operator.lower()
|
|
||||||
working_table = table
|
working_table = table # The table containing the field (the name provided in the left operand)
|
||||||
main_table = table
|
field_path = left.split('.', 1)
|
||||||
fargs = left.split('.', 1)
|
|
||||||
if fargs[0] in table._inherit_fields:
|
# 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:
|
while True:
|
||||||
field = main_table._columns.get(fargs[0], False)
|
field = working_table._columns.get(field_path[0])
|
||||||
if field:
|
if field:
|
||||||
working_table = main_table
|
|
||||||
self.__field_tables[i] = working_table
|
self.__field_tables[i] = working_table
|
||||||
break
|
break
|
||||||
working_table = main_table.pool.get(main_table._inherit_fields[fargs[0]][0])
|
next_table = working_table.pool.get(working_table._inherit_fields[field_path[0]][0])
|
||||||
if working_table not in self.__all_tables:
|
if next_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.__joins.append('%s."%s"=%s."%s"' % (next_table._table, 'id', working_table._table, working_table._inherits[next_table._name]))
|
||||||
self.__all_tables.add(working_table)
|
self.__all_tables.add(next_table)
|
||||||
main_table = working_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 not field:
|
||||||
if left == 'id' and operator == 'child_of':
|
if left == 'id' and operator == 'child_of':
|
||||||
ids2 = child_of_right_to_ids(right)
|
ids2 = to_ids(right, table)
|
||||||
dom = _rec_get(ids2, working_table)
|
dom = child_of_domain(left, ids2, working_table)
|
||||||
self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
|
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
|
continue
|
||||||
|
|
||||||
field_obj = table.pool.get(field._obj)
|
field_obj = table.pool.get(field._obj)
|
||||||
if len(fargs) > 1:
|
if len(field_path) > 1:
|
||||||
if field._type == 'many2one':
|
if field._type == 'many2one':
|
||||||
right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context)
|
right = field_obj.search(cr, uid, [(field_path[1], operator, right)], context=context)
|
||||||
if right == []:
|
self.__exp[i] = (field_path[0], 'in', right)
|
||||||
self.__exp[i] = ( 'id', '=', 0 )
|
|
||||||
else:
|
|
||||||
self.__exp[i] = (fargs[0], 'in', right)
|
|
||||||
# Making search easier when there is a left operand as field.o2m or field.m2m
|
# Making search easier when there is a left operand as field.o2m or field.m2m
|
||||||
if field._type in ['many2many','one2many']:
|
if field._type in ['many2many', 'one2many']:
|
||||||
right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context)
|
right = field_obj.search(cr, uid, [(field_path[1], operator, right)], context=context)
|
||||||
right1 = table.search(cr, uid, [(fargs[0],'in', right)], context=context)
|
right1 = table.search(cr, uid, [(field_path[0], 'in', right)], context=context)
|
||||||
if right1 == []:
|
self.__exp[i] = ('id', 'in', right1)
|
||||||
self.__exp[i] = ( 'id', '=', 0 )
|
|
||||||
else:
|
|
||||||
self.__exp[i] = ('id', 'in', right1)
|
|
||||||
|
|
||||||
if not isinstance(field,fields.property):
|
if not isinstance(field, fields.property):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if field._properties and not field.store:
|
if field._properties and not field.store:
|
||||||
|
@ -249,16 +473,16 @@ class expression(object):
|
||||||
if not field._fnct_search:
|
if not field._fnct_search:
|
||||||
# the function field doesn't provide a search function and doesn't store
|
# 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
|
# 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:
|
else:
|
||||||
subexp = field.search(cr, uid, table, left, [self.__exp[i]], context=context)
|
subexp = field.search(cr, uid, table, left, [self.__exp[i]], context=context)
|
||||||
if not subexp:
|
if not subexp:
|
||||||
self.__exp[i] = self.__DUMMY_LEAF
|
self.__exp[i] = TRUE_LEAF
|
||||||
else:
|
else:
|
||||||
# we assume that the expression is valid
|
# we assume that the expression is valid
|
||||||
# we create a dummy leaf for forcing the parsing of the resulting expression
|
# we create a dummy leaf for forcing the parsing of the resulting expression
|
||||||
self.__exp[i] = AND_OPERATOR
|
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):
|
for j, se in enumerate(subexp):
|
||||||
self.__exp.insert(i + 2 + j, se)
|
self.__exp.insert(i + 2 + j, se)
|
||||||
# else, the value of the field is store in the database, so we search on it
|
# 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':
|
elif field._type == 'one2many':
|
||||||
# Applying recursivity on field(one2many)
|
# Applying recursivity on field(one2many)
|
||||||
if operator == 'child_of':
|
if operator == 'child_of':
|
||||||
ids2 = child_of_right_to_ids(right)
|
ids2 = to_ids(right, field_obj)
|
||||||
if field._obj != working_table._name:
|
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:
|
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:]
|
self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -282,7 +506,7 @@ class expression(object):
|
||||||
if ids2:
|
if ids2:
|
||||||
operator = 'in'
|
operator = 'in'
|
||||||
else:
|
else:
|
||||||
if not isinstance(right,list):
|
if not isinstance(right, list):
|
||||||
ids2 = [right]
|
ids2 = [right]
|
||||||
else:
|
else:
|
||||||
ids2 = right
|
ids2 = right
|
||||||
|
@ -290,22 +514,16 @@ class expression(object):
|
||||||
if operator in ['like','ilike','in','=']:
|
if operator in ['like','ilike','in','=']:
|
||||||
#no result found with given search criteria
|
#no result found with given search criteria
|
||||||
call_null = False
|
call_null = False
|
||||||
self.__exp[i] = ('id','=',0)
|
self.__exp[i] = FALSE_LEAF
|
||||||
else:
|
|
||||||
call_null = True
|
|
||||||
operator = 'in' # operator changed because ids are directly related to main object
|
|
||||||
else:
|
else:
|
||||||
call_null = False
|
ids2 = select_from_where(cr, field._fields_id, field_obj._table, 'id', ids2, operator)
|
||||||
o2m_op = 'in'
|
if ids2:
|
||||||
if operator in ['not like','not ilike','not in','<>','!=']:
|
call_null = False
|
||||||
o2m_op = 'not in'
|
self.__exp[i] = ('id', 'in', ids2)
|
||||||
self.__exp[i] = ('id', o2m_op, self.__execute_recursive_in(cr, field._fields_id, field_obj._table, 'id', ids2, operator, field._type))
|
|
||||||
|
|
||||||
if call_null:
|
if call_null:
|
||||||
o2m_op = 'not in'
|
o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
|
||||||
if operator in ['not like','not ilike','not in','<>','!=']:
|
self.__exp[i] = ('id', o2m_op, select_distinct_from_where_not_null(cr, field._fields_id, field_obj._table))
|
||||||
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])
|
|
||||||
|
|
||||||
elif field._type == 'many2many':
|
elif field._type == 'many2many':
|
||||||
#FIXME
|
#FIXME
|
||||||
|
@ -313,10 +531,10 @@ class expression(object):
|
||||||
def _rec_convert(ids):
|
def _rec_convert(ids):
|
||||||
if field_obj == table:
|
if field_obj == table:
|
||||||
return ids
|
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)
|
ids2 = to_ids(right, field_obj)
|
||||||
dom = _rec_get(ids2, field_obj)
|
dom = child_of_domain('id', ids2, field_obj)
|
||||||
ids2 = field_obj.search(cr, uid, dom, context=context)
|
ids2 = field_obj.search(cr, uid, dom, context=context)
|
||||||
self.__exp[i] = ('id', 'in', _rec_convert(ids2))
|
self.__exp[i] = ('id', 'in', _rec_convert(ids2))
|
||||||
else:
|
else:
|
||||||
|
@ -335,34 +553,28 @@ class expression(object):
|
||||||
if operator in ['like','ilike','in','=']:
|
if operator in ['like','ilike','in','=']:
|
||||||
#no result found with given search criteria
|
#no result found with given search criteria
|
||||||
call_null_m2m = False
|
call_null_m2m = False
|
||||||
self.__exp[i] = ('id','=',0)
|
self.__exp[i] = FALSE_LEAF
|
||||||
else:
|
else:
|
||||||
call_null_m2m = True
|
|
||||||
operator = 'in' # operator changed because ids are directly related to main object
|
operator = 'in' # operator changed because ids are directly related to main object
|
||||||
else:
|
else:
|
||||||
call_null_m2m = False
|
call_null_m2m = False
|
||||||
m2m_op = 'in'
|
m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
|
||||||
if operator in ['not like','not ilike','not in','<>','!=']:
|
self.__exp[i] = ('id', m2m_op, select_from_where(cr, field._id1, field._rel, field._id2, res_ids, operator) or [0])
|
||||||
m2m_op = 'not in'
|
|
||||||
|
|
||||||
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:
|
if call_null_m2m:
|
||||||
m2m_op = 'not in'
|
m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'
|
||||||
if operator in ['not like','not ilike','not in','<>','!=']:
|
self.__exp[i] = ('id', m2m_op, select_distinct_from_where_not_null(cr, field._id1, field._rel))
|
||||||
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])
|
|
||||||
|
|
||||||
elif field._type == 'many2one':
|
elif field._type == 'many2one':
|
||||||
if operator == 'child_of':
|
if operator == 'child_of':
|
||||||
ids2 = child_of_right_to_ids(right)
|
ids2 = to_ids(right, field_obj)
|
||||||
self.__operator = 'in'
|
|
||||||
if field._obj != working_table._name:
|
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:
|
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:]
|
self.__exp = self.__exp[:i] + dom + self.__exp[i+1:]
|
||||||
else:
|
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:
|
if context is None:
|
||||||
context = {}
|
context = {}
|
||||||
c = context.copy()
|
c = context.copy()
|
||||||
|
@ -370,46 +582,35 @@ class expression(object):
|
||||||
#Special treatment to ill-formed domains
|
#Special treatment to ill-formed domains
|
||||||
operator = ( operator in ['<','>','<=','>='] ) and 'in' or operator
|
operator = ( operator in ['<','>','<=','>='] ) and 'in' or operator
|
||||||
|
|
||||||
dict_op = {'not in':'!=','in':'=','=':'in','!=':'not in','<>':'not in'}
|
dict_op = {'not in':'!=','in':'=','=':'in','!=':'not in'}
|
||||||
if isinstance(right,tuple):
|
if isinstance(right, tuple):
|
||||||
right = list(right)
|
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]
|
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]
|
operator = dict_op[operator]
|
||||||
res_ids = field_obj.name_search(cr, uid, right, [], operator, limit=None, context=c)
|
res_ids = [x[0] for x in field_obj.name_search(cr, uid, right, [], operator, limit=None, context=c)]
|
||||||
if not res_ids:
|
if operator in NEGATIVE_TERM_OPERATORS:
|
||||||
return ('id','=',0)
|
res_ids.append(False) # TODO this should not be appended if False was in 'right'
|
||||||
else:
|
return (left, 'in', res_ids)
|
||||||
right = map(lambda x: x[0], res_ids)
|
|
||||||
return (left, 'in', right)
|
|
||||||
|
|
||||||
m2o_str = False
|
m2o_str = False
|
||||||
if right:
|
if right:
|
||||||
if isinstance(right, basestring): # and not isinstance(field, fields.related):
|
if isinstance(right, basestring): # and not isinstance(field, fields.related):
|
||||||
m2o_str = True
|
m2o_str = True
|
||||||
elif isinstance(right,(list,tuple)):
|
elif isinstance(right, (list, tuple)):
|
||||||
m2o_str = True
|
m2o_str = True
|
||||||
for ele in right:
|
for ele in right:
|
||||||
if not isinstance(ele, basestring):
|
if not isinstance(ele, basestring):
|
||||||
m2o_str = False
|
m2o_str = False
|
||||||
break
|
break
|
||||||
|
if m2o_str:
|
||||||
|
self.__exp[i] = _get_expression(field_obj, cr, uid, left, right, operator, context=context)
|
||||||
elif right == []:
|
elif right == []:
|
||||||
m2o_str = False
|
pass # Handled by __leaf_to_sql().
|
||||||
if operator in ('not in', '!=', '<>'):
|
else: # right is False
|
||||||
# (many2one not in []) should return all records
|
pass # Handled by __leaf_to_sql().
|
||||||
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)
|
|
||||||
|
|
||||||
if m2o_str:
|
|
||||||
self.__exp[i] = _get_expression(field_obj,cr, uid, left, right, operator, context=context)
|
|
||||||
else:
|
else:
|
||||||
# other field type
|
# other field type
|
||||||
# add the time part to datetime field when it's not there:
|
# 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])
|
self.__exp[i] = tuple(self.__exp[i])
|
||||||
|
|
||||||
if field.translate:
|
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
|
right = '%%%s%%' % right
|
||||||
|
|
||||||
operator = operator == '=like' and 'like' or operator
|
subselect = '( SELECT res_id' \
|
||||||
|
|
||||||
query1 = '( SELECT res_id' \
|
|
||||||
' FROM ir_translation' \
|
' FROM ir_translation' \
|
||||||
' WHERE name = %s' \
|
' WHERE name = %s' \
|
||||||
' AND lang = %s' \
|
' AND lang = %s' \
|
||||||
' AND type = %s'
|
' AND type = %s'
|
||||||
instr = ' %s'
|
instr = ' %s'
|
||||||
#Covering in,not in operators with operands (%s,%s) ,etc.
|
#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))
|
instr = ','.join(['%s'] * len(right))
|
||||||
query1 += ' AND value ' + operator + ' ' +" (" + instr + ")" \
|
subselect += ' AND value ' + sql_operator + ' ' +" (" + instr + ")" \
|
||||||
') UNION (' \
|
') UNION (' \
|
||||||
' SELECT id' \
|
' SELECT id' \
|
||||||
' FROM "' + working_table._table + '"' \
|
' FROM "' + working_table._table + '"' \
|
||||||
' WHERE "' + left + '" ' + operator + ' ' +" (" + instr + "))"
|
' WHERE "' + left + '" ' + sql_operator + ' ' +" (" + instr + "))"
|
||||||
else:
|
else:
|
||||||
query1 += ' AND value ' + operator + instr + \
|
subselect += ' AND value ' + sql_operator + instr + \
|
||||||
') UNION (' \
|
') UNION (' \
|
||||||
' SELECT id' \
|
' SELECT id' \
|
||||||
' FROM "' + working_table._table + '"' \
|
' 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',
|
context.get('lang', False) or 'en_US',
|
||||||
'model',
|
'model',
|
||||||
right,
|
right,
|
||||||
right,
|
right,
|
||||||
]
|
]
|
||||||
|
|
||||||
self.__exp[i] = ('id', 'inselect', (query1, query2))
|
self.__exp[i] = ('id', 'inselect', (subselect, params))
|
||||||
return self
|
|
||||||
|
|
||||||
def __leaf_to_sql(self, leaf, table):
|
def __leaf_to_sql(self, leaf, table):
|
||||||
if leaf == self.__DUMMY_LEAF:
|
|
||||||
return ('(1=1)', [])
|
|
||||||
left, operator, right = leaf
|
left, operator, right = leaf
|
||||||
|
|
||||||
if operator == 'inselect':
|
# final sanity checks - should never fail
|
||||||
query = '(%s.%s in (%s))' % (table._table, left, right[0])
|
assert operator in (TERM_OPERATORS + ('inselect',)), \
|
||||||
params = right[1]
|
"Invalid operator %r in domain term %r" % (operator, leaf)
|
||||||
elif operator in ['in', 'not in']:
|
assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in table._all_columns \
|
||||||
params = right and right[:] or []
|
or left in MAGIC_COLUMNS, "Invalid field %r in domain term %r" % (left, leaf)
|
||||||
len_before = len(params)
|
|
||||||
for i in range(len_before)[::-1]:
|
|
||||||
if params[i] == False:
|
|
||||||
del params[i]
|
|
||||||
|
|
||||||
len_after = len(params)
|
if leaf == TRUE_LEAF:
|
||||||
check_nulls = len_after != len_before
|
query = 'TRUE'
|
||||||
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:
|
|
||||||
params = []
|
params = []
|
||||||
|
|
||||||
if right == False and (leaf[0] in table._columns) and table._columns[leaf[0]]._type=="boolean" and (operator == '='):
|
elif leaf == FALSE_LEAF:
|
||||||
query = '(%s.%s IS NULL or %s.%s = false )' % (table._table, left,table._table, left)
|
query = 'FALSE'
|
||||||
elif (((right == False) and (type(right)==bool)) or (right is None)) and (operator == '='):
|
params = []
|
||||||
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
|
|
||||||
|
|
||||||
else:
|
elif operator == 'inselect':
|
||||||
if left == 'id':
|
query = '(%s."%s" in (%s))' % (table._table, left, right[0])
|
||||||
query = '%s.id %s %%s' % (table._table, operator)
|
params = right[1]
|
||||||
params = right
|
|
||||||
else:
|
|
||||||
like = operator in ('like', 'ilike', 'not like', 'not ilike')
|
|
||||||
|
|
||||||
op = {'=like':'like','=ilike':'ilike'}.get(operator,operator)
|
elif operator in ['in', 'not in']:
|
||||||
if left in table._columns:
|
# Two cases: right is a boolean or a list. The boolean case is an
|
||||||
format = like and '%s' or table._columns[left]._symbol_set[0]
|
# abuse and handled for backward compatibility.
|
||||||
query = '(%s.%s %s %s)' % (table._table, left, op, format)
|
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:
|
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 check_nulls and operator == 'in':
|
||||||
if like:
|
query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left)
|
||||||
if isinstance(right, str):
|
elif not check_nulls and operator == 'not in':
|
||||||
str_utf8 = right
|
query = '(%s OR %s."%s" IS NULL)' % (query, table._table, left)
|
||||||
elif isinstance(right, unicode):
|
elif check_nulls and operator == 'not in':
|
||||||
str_utf8 = right.encode('utf-8')
|
query = '(%s AND %s."%s" IS NOT NULL)' % (query, table._table, left) # needed only for TRUE.
|
||||||
else:
|
else: # Must not happen
|
||||||
str_utf8 = str(right)
|
raise ValueError("Invalid domain term %r" % (leaf,))
|
||||||
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:
|
elif right == False and (left in table._columns) and table._columns[left]._type=="boolean" and (operator == '='):
|
||||||
query = '(%s OR %s IS NULL)' % (query, left)
|
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):
|
if isinstance(params, basestring):
|
||||||
params = [params]
|
params = [params]
|
||||||
|
@ -555,25 +789,26 @@ class expression(object):
|
||||||
def to_sql(self):
|
def to_sql(self):
|
||||||
stack = []
|
stack = []
|
||||||
params = []
|
params = []
|
||||||
|
# Process the domain from right to left, using a stack, to generate a SQL expression.
|
||||||
for i, e in reverse_enumerate(self.__exp):
|
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)
|
table = self.__field_tables.get(i, self.__main_table)
|
||||||
q, p = self.__leaf_to_sql(e, table)
|
q, p = self.__leaf_to_sql(e, table)
|
||||||
params.insert(0, p)
|
params.insert(0, p)
|
||||||
stack.append(q)
|
stack.append(q)
|
||||||
|
elif e == NOT_OPERATOR:
|
||||||
|
stack.append('(NOT (%s))' % (stack.pop(),))
|
||||||
else:
|
else:
|
||||||
if e == NOT_OPERATOR:
|
ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '}
|
||||||
stack.append('(NOT (%s))' % (stack.pop(),))
|
q1 = stack.pop()
|
||||||
else:
|
q2 = stack.pop()
|
||||||
ops = {AND_OPERATOR: ' AND ', OR_OPERATOR: ' OR '}
|
stack.append('(%s %s %s)' % (q1, ops[e], q2,))
|
||||||
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)
|
joins = ' AND '.join(self.__joins)
|
||||||
if joins:
|
if joins:
|
||||||
query = '(%s) AND (%s)' % (joins, query)
|
query = '(%s) AND %s' % (joins, query)
|
||||||
return (query, flatten(params))
|
return (query, flatten(params))
|
||||||
|
|
||||||
def get_tables(self):
|
def get_tables(self):
|
||||||
|
|
|
@ -55,7 +55,7 @@ def _symbol_set(symb):
|
||||||
|
|
||||||
class _column(object):
|
class _column(object):
|
||||||
""" Base of all fields, a database column
|
""" Base of all fields, a database column
|
||||||
|
|
||||||
An instance of this object is a *description* of a database column. It will
|
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
|
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.
|
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_set = (_symbol_c, _symbol_f)
|
||||||
_symbol_get = None
|
_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.
|
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.help = args.get('help', '')
|
||||||
self.priority = priority
|
self.priority = priority
|
||||||
self.change_default = change_default
|
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.translate = translate
|
||||||
self._domain = domain
|
self._domain = domain
|
||||||
self._context = context
|
self._context = context
|
||||||
|
@ -112,12 +112,6 @@ class _column(object):
|
||||||
def set(self, cr, obj, id, name, value, user=None, context=None):
|
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))
|
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):
|
def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
|
||||||
raise Exception(_('undefined get method !'))
|
raise Exception(_('undefined get method !'))
|
||||||
|
|
||||||
|
@ -126,9 +120,6 @@ class _column(object):
|
||||||
res = obj.read(cr, uid, ids, [name], context=context)
|
res = obj.read(cr, uid, ids, [name], context=context)
|
||||||
return [x[name] for x in res]
|
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
|
# Simple fields
|
||||||
|
@ -269,7 +260,7 @@ class binary(_column):
|
||||||
_column.__init__(self, string=string, **args)
|
_column.__init__(self, string=string, **args)
|
||||||
self.filters = filters
|
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:
|
if not context:
|
||||||
context = {}
|
context = {}
|
||||||
if not values:
|
if not values:
|
||||||
|
@ -293,9 +284,6 @@ class binary(_column):
|
||||||
res[i] = val
|
res[i] = val
|
||||||
return res
|
return res
|
||||||
|
|
||||||
get = get_memory
|
|
||||||
|
|
||||||
|
|
||||||
class selection(_column):
|
class selection(_column):
|
||||||
_type = 'selection'
|
_type = 'selection'
|
||||||
|
|
||||||
|
@ -355,30 +343,6 @@ class many2one(_column):
|
||||||
_column.__init__(self, string=string, **args)
|
_column.__init__(self, string=string, **args)
|
||||||
self._obj = obj
|
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):
|
def get(self, cr, obj, ids, name, user=None, context=None, values=None):
|
||||||
if context is None:
|
if context is None:
|
||||||
context = {}
|
context = {}
|
||||||
|
@ -447,55 +411,6 @@ class one2many(_column):
|
||||||
#one2many can't be used as condition for defaults
|
#one2many can't be used as condition for defaults
|
||||||
assert(self.change_default != True)
|
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):
|
def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
|
||||||
if context is None:
|
if context is None:
|
||||||
context = {}
|
context = {}
|
||||||
|
@ -578,14 +493,34 @@ class one2many(_column):
|
||||||
# (6, ?, ids) set a list of links
|
# (6, ?, ids) set a list of links
|
||||||
#
|
#
|
||||||
class many2many(_column):
|
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_read = False
|
||||||
_classic_write = False
|
_classic_write = False
|
||||||
_prefetch = False
|
_prefetch = False
|
||||||
_type = 'many2many'
|
_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)
|
_column.__init__(self, string=string, **args)
|
||||||
self._obj = obj
|
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 !'\
|
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))
|
'You used %s, which is not a valid SQL table name.')% (string,rel))
|
||||||
self._rel = rel
|
self._rel = rel
|
||||||
|
@ -593,7 +528,30 @@ class many2many(_column):
|
||||||
self._id2 = id2
|
self._id2 = id2
|
||||||
self._limit = limit
|
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:
|
if not context:
|
||||||
context = {}
|
context = {}
|
||||||
if not values:
|
if not values:
|
||||||
|
@ -606,7 +564,8 @@ class many2many(_column):
|
||||||
if offset:
|
if offset:
|
||||||
warnings.warn("Specifying offset at a many2many.get() may produce unpredictable results.",
|
warnings.warn("Specifying offset at a many2many.get() may produce unpredictable results.",
|
||||||
DeprecationWarning, stacklevel=2)
|
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
|
# 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)
|
# domains supposed by dynamic and evaluated on client-side only (thus ignored here)
|
||||||
|
@ -636,11 +595,11 @@ class many2many(_column):
|
||||||
%(order_by)s \
|
%(order_by)s \
|
||||||
%(limit)s \
|
%(limit)s \
|
||||||
OFFSET %(offset)d' \
|
OFFSET %(offset)d' \
|
||||||
% {'rel': self._rel,
|
% {'rel': rel,
|
||||||
'from_c': from_c,
|
'from_c': from_c,
|
||||||
'tbl': obj._table,
|
'tbl': obj._table,
|
||||||
'id1': self._id1,
|
'id1': id1,
|
||||||
'id2': self._id2,
|
'id2': id2,
|
||||||
'where_c': where_c,
|
'where_c': where_c,
|
||||||
'limit': limit_str,
|
'limit': limit_str,
|
||||||
'order_by': order_by,
|
'order_by': order_by,
|
||||||
|
@ -651,31 +610,32 @@ class many2many(_column):
|
||||||
res[r[1]].append(r[0])
|
res[r[1]].append(r[0])
|
||||||
return res
|
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:
|
if not context:
|
||||||
context = {}
|
context = {}
|
||||||
if not values:
|
if not values:
|
||||||
return
|
return
|
||||||
obj = obj.pool.get(self._obj)
|
rel, id1, id2 = self._sql_names(model)
|
||||||
|
obj = model.pool.get(self._obj)
|
||||||
for act in values:
|
for act in values:
|
||||||
if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
|
if not (isinstance(act, list) or isinstance(act, tuple)) or not act:
|
||||||
continue
|
continue
|
||||||
if act[0] == 0:
|
if act[0] == 0:
|
||||||
idnew = obj.create(cr, user, act[2], context=context)
|
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:
|
elif act[0] == 1:
|
||||||
obj.write(cr, user, [act[1]], act[2], context=context)
|
obj.write(cr, user, [act[1]], act[2], context=context)
|
||||||
elif act[0] == 2:
|
elif act[0] == 2:
|
||||||
obj.unlink(cr, user, [act[1]], context=context)
|
obj.unlink(cr, user, [act[1]], context=context)
|
||||||
elif act[0] == 3:
|
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:
|
elif act[0] == 4:
|
||||||
# following queries are in the same transaction - so should be relatively safe
|
# 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():
|
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:
|
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:
|
elif act[0] == 6:
|
||||||
|
|
||||||
d1, d2,tables = obj.pool.get('ir.rule').domain_get(cr, user, obj._name, context=context)
|
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)
|
d1 = ' and ' + ' and '.join(d1)
|
||||||
else:
|
else:
|
||||||
d1 = ''
|
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]:
|
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
|
# 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):
|
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)
|
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):
|
def get_nice_size(value):
|
||||||
size = 0
|
size = 0
|
||||||
|
@ -801,8 +735,8 @@ class function(_column):
|
||||||
|
|
||||||
Implements the function field.
|
Implements the function field.
|
||||||
|
|
||||||
:param orm_template model: model to which the field belongs (should be ``self`` for
|
:param orm model: model to which the field belongs (should be ``self`` for
|
||||||
a model method)
|
a model method)
|
||||||
:param field_name(s): name of the field to compute, or if ``multi`` is provided,
|
:param field_name(s): name of the field to compute, or if ``multi`` is provided,
|
||||||
list of field names to compute.
|
list of field names to compute.
|
||||||
:type field_name(s): str | [str]
|
:type field_name(s): str | [str]
|
||||||
|
@ -865,8 +799,8 @@ class function(_column):
|
||||||
|
|
||||||
Callable that implements the ``write`` operation for the function field.
|
Callable that implements the ``write`` operation for the function field.
|
||||||
|
|
||||||
:param orm_template model: model to which the field belongs (should be ``self`` for
|
:param orm model: model to which the field belongs (should be ``self`` for
|
||||||
a model method)
|
a model method)
|
||||||
:param int id: the identifier of the object to write on
|
:param int id: the identifier of the object to write on
|
||||||
:param str field_name: name of the field to set
|
:param str field_name: name of the field to set
|
||||||
:param fnct_inv_arg: arbitrary value passed when declaring the function field
|
: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
|
a search criterion based on the function field into a new domain based only on
|
||||||
columns that are stored in the database.
|
columns that are stored in the database.
|
||||||
|
|
||||||
:param orm_template model: model to which the field belongs (should be ``self`` for
|
:param orm model: model to which the field belongs (should be ``self`` for
|
||||||
a model method)
|
a model method)
|
||||||
:param orm_template model_again: same value as ``model`` (seriously! this is for backwards
|
:param orm model_again: same value as ``model`` (seriously! this is for backwards
|
||||||
compatibility)
|
compatibility)
|
||||||
:param str field_name: name of the field to search on
|
:param str field_name: name of the field to search on
|
||||||
:param list criterion: domain component specifying the search criterion on the field.
|
:param list criterion: domain component specifying the search criterion on the field.
|
||||||
:rtype: list
|
:rtype: list
|
||||||
|
@ -935,7 +869,7 @@ class function(_column):
|
||||||
corresponding records in the source model (whose field values
|
corresponding records in the source model (whose field values
|
||||||
need to be recomputed).
|
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
|
:param list trigger_ids: ids of the records of trigger_model that were
|
||||||
modified
|
modified
|
||||||
:rtype: list
|
:rtype: list
|
||||||
|
@ -1064,14 +998,11 @@ class function(_column):
|
||||||
result[id] = self.postprocess(cr, uid, obj, name, result[id], context)
|
result[id] = self.postprocess(cr, uid, obj, name, result[id], context)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
get_memory = get
|
|
||||||
|
|
||||||
def set(self, cr, obj, id, name, value, user=None, context=None):
|
def set(self, cr, obj, id, name, value, user=None, context=None):
|
||||||
if not context:
|
if not context:
|
||||||
context = {}
|
context = {}
|
||||||
if self._fnct_inv:
|
if self._fnct_inv:
|
||||||
self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
|
self._fnct_inv(obj, cr, user, id, name, value, self._fnct_inv_arg, context)
|
||||||
set_memory = set
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Related fields
|
# Related fields
|
||||||
|
@ -1295,7 +1226,7 @@ class property(function):
|
||||||
|
|
||||||
def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
|
def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
|
||||||
prop = obj.pool.get('ir.property')
|
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)
|
default_val = self._get_defaults(obj, cr, uid, prop_names, context)
|
||||||
|
|
||||||
# build the dictionary that will be returned
|
# 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
|
:attr parent_column: the name of the column containing the m2o
|
||||||
relationship to the parent model that contains
|
relationship to the parent model that contains
|
||||||
this column, None for local columns.
|
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.name = name
|
||||||
self.column = column
|
self.column = column
|
||||||
self.parent_model = parent_model
|
self.parent_model = parent_model
|
||||||
self.parent_column = parent_column
|
self.parent_column = parent_column
|
||||||
|
self.original_parent = original_parent
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
||||||
|
|
1321
openerp/osv/orm.py
|
@ -21,31 +21,29 @@
|
||||||
|
|
||||||
#.apidoc title: Objects Services (OSV)
|
#.apidoc title: Objects Services (OSV)
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from psycopg2 import IntegrityError, errorcodes
|
||||||
|
|
||||||
import orm
|
import orm
|
||||||
|
import openerp
|
||||||
import openerp.netsvc as netsvc
|
import openerp.netsvc as netsvc
|
||||||
import openerp.pooler as pooler
|
import openerp.pooler as pooler
|
||||||
import openerp.sql_db as sql_db
|
import openerp.sql_db as sql_db
|
||||||
import logging
|
|
||||||
from psycopg2 import IntegrityError, errorcodes
|
|
||||||
from openerp.tools.func import wraps
|
from openerp.tools.func import wraps
|
||||||
from openerp.tools.translate import translate
|
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):
|
service = None
|
||||||
def __init__(self, name, value, exc_type='warning'):
|
|
||||||
self.name = name
|
|
||||||
self.exc_type = exc_type
|
|
||||||
self.value = value
|
|
||||||
self.args = (exc_type, name)
|
|
||||||
|
|
||||||
|
class object_proxy():
|
||||||
class object_proxy(netsvc.Service):
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.logger = logging.getLogger('web-services')
|
self.logger = logging.getLogger('web-services')
|
||||||
netsvc.Service.__init__(self, 'object_proxy', audience='')
|
global service
|
||||||
self.exportMethod(self.exec_workflow)
|
service = self
|
||||||
self.exportMethod(self.execute)
|
|
||||||
|
|
||||||
def check(f):
|
def check(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
|
@ -119,14 +117,14 @@ class object_proxy(netsvc.Service):
|
||||||
except orm.except_orm, inst:
|
except orm.except_orm, inst:
|
||||||
if inst.name == 'AccessError':
|
if inst.name == 'AccessError':
|
||||||
self.logger.debug("AccessError", exc_info=True)
|
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:
|
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:
|
except IntegrityError, inst:
|
||||||
osv_pool = pooler.get_pool(dbname)
|
osv_pool = pooler.get_pool(dbname)
|
||||||
for key in osv_pool._sql_error.keys():
|
for key in osv_pool._sql_error.keys():
|
||||||
if key in inst[0]:
|
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])
|
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):
|
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')
|
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)
|
msg += _('\n\n[object with reference: %s - %s]') % (model_name, model)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self.abortResponse(1, _('Integrity Error'), 'warning', msg)
|
netsvc.abort_response(1, _('Integrity Error'), 'warning', msg)
|
||||||
else:
|
else:
|
||||||
self.abortResponse(1, _('Integrity Error'), 'warning', inst[0])
|
netsvc.abort_response(1, _('Integrity Error'), 'warning', inst[0])
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception("Uncaught exception")
|
self.logger.exception("Uncaught exception")
|
||||||
raise
|
raise
|
||||||
|
@ -198,17 +196,10 @@ class object_proxy(netsvc.Service):
|
||||||
cr.close()
|
cr.close()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
# deprecated - for backward compatibility.
|
||||||
class osv_memory(orm.orm_memory):
|
osv = Model
|
||||||
""" Deprecated class. """
|
osv_memory = TransientModel
|
||||||
__metaclass__ = MetaModel
|
osv_abstract = AbstractModel # ;-)
|
||||||
_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.
|
|
||||||
|
|
||||||
|
|
||||||
def start_object_proxy():
|
def start_object_proxy():
|
||||||
|
|
|
@ -34,11 +34,6 @@ def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False,
|
||||||
return registry.db, registry
|
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):
|
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."""
|
"""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)
|
registry = RegistryManager.new(db_name, force_demo, status, update_module, True)
|
||||||
|
|
|
@ -136,16 +136,15 @@ class report_custom(report_int):
|
||||||
ids = self.pool.get(report.model_id.model).search(cr, uid, [])
|
ids = self.pool.get(report.model_id.model).search(cr, uid, [])
|
||||||
datas['ids'] = ids
|
datas['ids'] = ids
|
||||||
|
|
||||||
service = netsvc.LocalService("object_proxy")
|
|
||||||
report_id = datas['report_id']
|
report_id = datas['report_id']
|
||||||
report = service.execute(cr.dbname, uid, 'ir.report.custom', 'read', [report_id], context=context)[0]
|
report = self.pool.get('ir.report.custom').read(cr, uid, [report_id], context=context)[0]
|
||||||
fields = service.execute(cr.dbname, uid, 'ir.report.custom.fields', 'read', report['fields_child0'], context=context)
|
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'])
|
fields.sort(lambda x,y : x['sequence'] - y['sequence'])
|
||||||
|
|
||||||
if report['field_parent']:
|
if report['field_parent']:
|
||||||
parent_field = service.execute(cr.dbname, uid, 'ir.model.fields', 'read', [report['field_parent'][0]],['model'])
|
parent_field = self.pool.get('ir.model.fields').read(cr, uid, [report['field_parent'][0]], ['model'])
|
||||||
model_name = service.execute(cr.dbname, uid, 'ir.model', 'read', [report['model_id'][0]], ['model'],context=context)[0]['model']
|
model_name = self.pool.get('ir.model').read(cr, uid, [report['model_id'][0]], ['model'], context=context)[0]['model']
|
||||||
|
|
||||||
fct = {}
|
fct = {}
|
||||||
fct['id'] = lambda x : x
|
fct['id'] = lambda x : x
|
||||||
|
@ -160,9 +159,7 @@ class report_custom(report_int):
|
||||||
field_child = f['field_child'+str(i)]
|
field_child = f['field_child'+str(i)]
|
||||||
if field_child:
|
if field_child:
|
||||||
row.append(
|
row.append(
|
||||||
service.execute(cr.dbname, uid,
|
self.pool.get('ir.model.fields').read(cr, uid, [field_child[0]], ['name'], context=context)[0]['name']
|
||||||
'ir.model.fields', 'read', [field_child[0]],
|
|
||||||
['name'], context=context)[0]['name']
|
|
||||||
)
|
)
|
||||||
if f['fc'+str(i)+'_operande']:
|
if f['fc'+str(i)+'_operande']:
|
||||||
fct_name = 'id'
|
fct_name = 'id'
|
||||||
|
@ -346,7 +343,7 @@ class report_custom(report_int):
|
||||||
|
|
||||||
|
|
||||||
def _create_lines(self, cr, uid, ids, report, fields, results, context):
|
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()
|
pdf_string = cStringIO.StringIO()
|
||||||
can = canvas.init(fname=pdf_string, format='pdf')
|
can = canvas.init(fname=pdf_string, format='pdf')
|
||||||
|
|
||||||
|
@ -376,7 +373,7 @@ class report_custom(report_int):
|
||||||
for f in fields:
|
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])
|
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:
|
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':
|
if type[0]['ttype'] == 'date':
|
||||||
date_idx = idx
|
date_idx = idx
|
||||||
fct[idx] = process_date[report['frequency']]
|
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):
|
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()
|
pdf_string = cStringIO.StringIO()
|
||||||
can = canvas.init(fname=pdf_string, format='pdf')
|
can = canvas.init(fname=pdf_string, format='pdf')
|
||||||
|
|
||||||
|
@ -475,7 +472,7 @@ class report_custom(report_int):
|
||||||
for f in fields:
|
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])
|
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:
|
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':
|
if type[0]['ttype'] == 'date':
|
||||||
date_idx = idx
|
date_idx = idx
|
||||||
fct[idx] = process_date[report['frequency']]
|
fct[idx] = process_date[report['frequency']]
|
||||||
|
|